The implementation is exactly the decryption chain documented in the test file: deriveKEK -> decryptBox(masterKey) -> decryptBox(secretKey) -> decryptSealed(token) -> toBase64URL. Errors from the underlying crypto primitives propagate; the only added validation is the up-front check that the response actually contains both keyAttributes and encryptedToken (caller bug if not). Also re-exports the auth/unwrap and auth/types public surface from src/index.ts. All 38 tests pass; make check and make docker are green.
65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
import {
|
|
decryptBox,
|
|
decryptSealed,
|
|
deriveKEK,
|
|
fromBase64,
|
|
toBase64URL,
|
|
} from "../crypto/index.js";
|
|
import type { AuthorizationResponse } from "./types.js";
|
|
|
|
export interface UnwrapResult {
|
|
masterKey: Uint8Array;
|
|
secretKey: Uint8Array;
|
|
publicKey: Uint8Array;
|
|
// URL-safe-no-padding base64 of the decrypted token bytes; this is the
|
|
// exact value to put in the X-Auth-Token header on subsequent calls.
|
|
token: string;
|
|
}
|
|
|
|
// Given a successful AuthorizationResponse and the user's password,
|
|
// derive the KEK, decrypt the master key, decrypt the secret key, and
|
|
// open the sealed auth token. See test/auth/unwrap.test.ts for the
|
|
// complete protocol description.
|
|
export const unwrapAuth = async (
|
|
response: AuthorizationResponse,
|
|
password: string,
|
|
): Promise<UnwrapResult> => {
|
|
if (!response.keyAttributes) {
|
|
throw new Error(
|
|
"unwrapAuth: response.keyAttributes is required (login may be incomplete: TOTP or passkey pending)",
|
|
);
|
|
}
|
|
if (!response.encryptedToken) {
|
|
throw new Error(
|
|
"unwrapAuth: response.encryptedToken is required (login may be incomplete: TOTP or passkey pending)",
|
|
);
|
|
}
|
|
|
|
const ka = response.keyAttributes;
|
|
const kek = await deriveKEK(
|
|
password,
|
|
fromBase64(ka.kekSalt),
|
|
ka.opsLimit,
|
|
ka.memLimit,
|
|
);
|
|
const masterKey = decryptBox(
|
|
fromBase64(ka.encryptedKey),
|
|
fromBase64(ka.keyDecryptionNonce),
|
|
kek,
|
|
);
|
|
const secretKey = decryptBox(
|
|
fromBase64(ka.encryptedSecretKey),
|
|
fromBase64(ka.secretKeyDecryptionNonce),
|
|
masterKey,
|
|
);
|
|
const publicKey = fromBase64(ka.publicKey);
|
|
const tokenBytes = decryptSealed(
|
|
fromBase64(response.encryptedToken),
|
|
publicKey,
|
|
secretKey,
|
|
);
|
|
const token = toBase64URL(tokenBytes);
|
|
|
|
return { masterKey, secretKey, publicKey, token };
|
|
};
|