Files
accounted/lib/auth/oauth-flow-crypto.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

36 lines
1.8 KiB
TypeScript

import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
/**
* At-rest encryption for the secrets an oauth_flows row holds between
* /authorize and /callback: the PKCE verifier, and during the two-minute
* brand handoff the provider's authorization code or error text.
*
* Same construction as the provider OAuth handoff (PR #2305): AES-256-GCM
* under a purpose-scoped derivation of the server-only service-role secret,
* which every deployment already has, with the row identity as additional
* authenticated data so a ciphertext cannot be moved between rows or
* columns. Plaintext or unreadable values never decrypt successfully.
*/
function getKey(): Buffer {
const secret = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!secret) throw new Error('SUPABASE_SERVICE_ROLE_KEY is required')
return createHash('sha256').update('oauth-flows:v1:' + secret).digest()
}
export function encryptOAuthFlowValue(plaintext: string, context: string): string {
const iv = randomBytes(12)
const cipher = createCipheriv('aes-256-gcm', getKey(), iv)
cipher.setAAD(Buffer.from(context))
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
return 'v1:' + Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString('base64url')
}
export function decryptOAuthFlowValue(ciphertext: string, context: string): string {
if (!ciphertext.startsWith('v1:')) throw new Error('Invalid OAuth flow ciphertext')
const combined = Buffer.from(ciphertext.slice(3), 'base64url')
const decipher = createDecipheriv('aes-256-gcm', getKey(), combined.subarray(0, 12))
decipher.setAAD(Buffer.from(context))
decipher.setAuthTag(combined.subarray(12, 28))
return Buffer.concat([decipher.update(combined.subarray(28)), decipher.final()]).toString('utf8')
}