Phase 3a green: implement auth.unwrapAuth

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.
This commit is contained in:
2026-05-11 00:59:43 -07:00
parent 6386a0ec9f
commit 78fdabe54a
3 changed files with 64 additions and 7 deletions

View File

@@ -611,8 +611,10 @@ Phase 3: SRP + auth
- [ ] `beginLogin(email, password)` returning a `LoginChallenge`
- [ ] `requestEmailOTP` and `submitEmailOTP` for accounts without SRP
- [ ] `submitTOTP(sessionID, code)`
- [ ] `unwrapAuth(response, password)` returning master key, secret key, public
key, and decrypted token
- [x] `unwrapAuth(response, password)` returning master key, secret key, public
key, and decrypted token (URL-safe-no-padding base64)
- [x] `src/auth/types.ts` with `KeyAttributes`, `SRPAttributes`,
`AuthorizationResponse`, and `LoginChallenge`
- [ ] Tests against recorded HTTP fixtures
Phase 4: HTTP client + endpoints

View File

@@ -1,17 +1,64 @@
// Stub: see the README "Development workflow" section for TDD policy.
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,
response: AuthorizationResponse,
password: string,
): Promise<UnwrapResult> => {
throw new Error("auth.unwrapAuth not implemented");
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 };
};

View File

@@ -1 +1,9 @@
export const VERSION = "0.0.0";
export { unwrapAuth, type UnwrapResult } from "./auth/unwrap.js";
export type {
AuthorizationResponse,
KeyAttributes,
LoginChallenge,
SRPAttributes,
} from "./auth/types.js";