functions/api/_hmac.js 2.0 K raw
1
function base64UrlEncode(buffer) {
2
  const bytes = new Uint8Array(buffer)
3
  let binary = ''
4
  for (const byte of bytes) {
5
    binary += String.fromCharCode(byte)
6
  }
7
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
8
}
9
10
function base64UrlDecode(str) {
11
  const padded = str.replace(/-/g, '+').replace(/_/g, '/') + '=='.slice(0, (4 - (str.length % 4)) % 4)
12
  const binary = atob(padded)
13
  const bytes = new Uint8Array(binary.length)
14
  for (let i = 0; i < binary.length; i++) {
15
    bytes[i] = binary.charCodeAt(i)
16
  }
17
  return bytes.buffer
18
}
19
20
async function getHmacKey(secret) {
21
  return crypto.subtle.importKey(
22
    'raw',
23
    new TextEncoder().encode(secret),
24
    { name: 'HMAC', hash: 'SHA-256' },
25
    false,
26
    ['sign', 'verify']
27
  )
28
}
29
30
export async function createToken(secret) {
31
  const nonce = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)))
32
  const issuedAt = Date.now()
33
  const payload = base64UrlEncode(new TextEncoder().encode(JSON.stringify({ nonce, issuedAt })))
34
  const key = await getHmacKey(secret)
35
  const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload))
36
  return { token: payload + '.' + base64UrlEncode(signature), nonce, issuedAt }
37
}
38
39
export async function verifyToken(tokenString, secret) {
40
  if (typeof tokenString !== 'string') return null
41
  const parts = tokenString.split('.')
42
  if (parts.length !== 2) return null
43
44
  const [payloadB64, signatureB64] = parts
45
46
  try {
47
    const key = await getHmacKey(secret)
48
    const signatureBuffer = base64UrlDecode(signatureB64)
49
    const valid = await crypto.subtle.verify(
50
      'HMAC',
51
      key,
52
      signatureBuffer,
53
      new TextEncoder().encode(payloadB64)
54
    )
55
    if (!valid) return null
56
57
    const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadB64))
58
    const payload = JSON.parse(payloadJson)
59
    if (typeof payload.nonce !== 'string' || typeof payload.issuedAt !== 'number') return null
60
61
    return payload
62
  } catch {
63
    return null
64
  }
65
}