~ knotless Search/
↑↓ move↵ open⇧↵ all results

peterullrich.com/hatk

AT Protocol app framework

Forked from chadtmiller.com/hatk

0
0
main
99 lines · 2.9 KB Raw History
// DPoP key management (IndexedDB) and proof creation

import { randomString, sha256Base64Url, signJwt } from './crypto.js'

const DB_VERSION = 1
const KEY_STORE = 'dpop-keys'
const KEY_ID = 'dpop-key'

const dbPromises = new Map()

function openDatabase(namespace) {
  const existing = dbPromises.get(namespace)
  if (existing) return existing

  const promise = new Promise((resolve, reject) => {
    const req = indexedDB.open(`appview-oauth-${namespace}`, DB_VERSION)
    req.onerror = () => reject(req.error)
    req.onsuccess = () => resolve(req.result)
    req.onupgradeneeded = (e) => {
      const db = e.target.result
      if (!db.objectStoreNames.contains(KEY_STORE)) {
        db.createObjectStore(KEY_STORE, { keyPath: 'id' })
      }
    }
  })

  dbPromises.set(namespace, promise)
  return promise
}

async function getStoredKey(namespace) {
  const db = await openDatabase(namespace)
  return new Promise((resolve, reject) => {
    const tx = db.transaction(KEY_STORE, 'readonly')
    const req = tx.objectStore(KEY_STORE).get(KEY_ID)
    req.onsuccess = () => resolve(req.result || null)
    req.onerror = () => reject(req.error)
  })
}

async function storeKey(namespace, privateKey, publicJwk) {
  const db = await openDatabase(namespace)
  return new Promise((resolve, reject) => {
    const tx = db.transaction(KEY_STORE, 'readwrite')
    const req = tx.objectStore(KEY_STORE).put({
      id: KEY_ID,
      privateKey,
      publicJwk,
      createdAt: Date.now(),
    })
    req.onsuccess = () => resolve()
    req.onerror = () => reject(req.error)
  })
}

export async function getOrCreateDPoPKey(namespace) {
  const existing = await getStoredKey(namespace)
  if (existing) return existing

  const keyPair = await crypto.subtle.generateKey(
    { name: 'ECDSA', namedCurve: 'P-256' },
    false, // non-extractable private key
    ['sign'],
  )

  const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
  await storeKey(namespace, keyPair.privateKey, publicJwk)

  return { id: KEY_ID, privateKey: keyPair.privateKey, publicJwk, createdAt: Date.now() }
}

export async function clearDPoPKey(namespace) {
  const db = await openDatabase(namespace)
  return new Promise((resolve, reject) => {
    const tx = db.transaction(KEY_STORE, 'readwrite')
    const req = tx.objectStore(KEY_STORE).delete(KEY_ID)
    req.onsuccess = () => resolve()
    req.onerror = () => reject(req.error)
  })
}

export async function createDPoPProof(namespace, method, url, accessToken) {
  const keyData = await getOrCreateDPoPKey(namespace)
  const { kty, crv, x, y } = keyData.publicJwk

  const header = { alg: 'ES256', typ: 'dpop+jwt', jwk: { kty, crv, x, y } }
  const payload = {
    jti: randomString(16),
    htm: method,
    htu: url.split('?')[0],
    iat: Math.floor(Date.now() / 1000),
  }

  if (accessToken) {
    payload.ath = await sha256Base64Url(accessToken)
  }

  return signJwt(header, payload, keyData.privateKey)
}