Files
WRNexusJS/packages/auth/src/protector.ts
T
2026-07-29 12:51:10 +05:30

40 lines
1.5 KiB
TypeScript

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);
},
};
}