import { open, seal, type EncryptionKeyring } from "@wrnexus/encryption"; import type { AuthSecretProtector } from "./types.ts"; const PURPOSE_PREFIX = "wrn-auth-secret:v1:"; type AuthSecretPurpose = "totp" | "oauth-access" | "oauth-refresh"; function bindPurpose(value: string, purpose: AuthSecretPurpose): string { return `${PURPOSE_PREFIX}${purpose}\0${value}`; } function revealBoundValue(value: string, purpose: AuthSecretPurpose): string { if (!value.startsWith(PURPOSE_PREFIX)) { // Backward compatibility for ciphertext written before purpose binding was introduced. return value; } const separator = value.indexOf("\0", PURPOSE_PREFIX.length); if (separator < 0) throw new Error("WRN-AUTH-SECRET-PAYLOAD"); const storedPurpose = value.slice(PURPOSE_PREFIX.length, separator); if (storedPurpose !== purpose) throw new Error("WRN-AUTH-SECRET-PURPOSE"); return value.slice(separator + 1); } /** * Protect TOTP and OAuth secrets with the versioned @wrnexus/encryption keyring. * Rotated keys continue to decrypt old records while new writes use the active key. * New payloads are bound to their purpose so encrypted values cannot be swapped * between TOTP, OAuth access-token, and OAuth refresh-token fields. */ export function createAuthSecretProtector(keyring: EncryptionKeyring): AuthSecretProtector { return { async protect(value, purpose) { return seal(bindPurpose(value, purpose), keyring); }, async reveal(value, purpose) { return revealBoundValue(await open(value, keyring), purpose); }, }; }