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:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user