Phase 3a red: auth.unwrapAuth tests and stub
Tests for the password-only decryption chain that follows a successful login (SRP or email OTP, with or without 2FA). The unwrap covers: password -> KEK (Argon2id) -> masterKey (secretbox) -> secretKey (secretbox) -> tokenBytes (sealed box) -> base64url token Each test builds a synthetic AuthorizationResponse using libsodium directly and asserts unwrapAuth recovers the inputs byte for byte. The test file also functions as the canonical description of the protocol. Adds src/auth/types.ts with KeyAttributes, SRPAttributes, AuthorizationResponse, and LoginChallenge declarations matching the README's API reference. src/auth/unwrap.ts is the throwing stub; the real implementation lands next.
This commit is contained in:
53
src/auth/types.ts
Normal file
53
src/auth/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// Base64-encoded binary, in either standard or URL-safe form. Decoded with
|
||||
// crypto.fromBase64 which accepts either variant.
|
||||
export type Base64 = string;
|
||||
|
||||
// The blob the server returns alongside the auth token after a successful
|
||||
// login. Together with the user's password, this is everything quack needs
|
||||
// to derive the master key, secret key, and auth token.
|
||||
export interface KeyAttributes {
|
||||
kekSalt: Base64;
|
||||
encryptedKey: Base64;
|
||||
keyDecryptionNonce: Base64;
|
||||
publicKey: Base64;
|
||||
encryptedSecretKey: Base64;
|
||||
secretKeyDecryptionNonce: Base64;
|
||||
memLimit: number;
|
||||
opsLimit: number;
|
||||
masterKeyEncryptedWithRecoveryKey?: Base64;
|
||||
masterKeyDecryptionNonce?: Base64;
|
||||
recoveryKeyEncryptedWithMasterKey?: Base64;
|
||||
recoveryKeyDecryptionNonce?: Base64;
|
||||
}
|
||||
|
||||
// Server-issued attributes needed to start an SRP-6a handshake. Returned
|
||||
// from GET /users/srp/attributes?email=<email>.
|
||||
export interface SRPAttributes {
|
||||
srpUserID: string;
|
||||
srpSalt: Base64;
|
||||
memLimit: number;
|
||||
opsLimit: number;
|
||||
kekSalt: Base64;
|
||||
isEmailMFAEnabled: boolean;
|
||||
}
|
||||
|
||||
// The body of a successful login response. Exactly one of the following
|
||||
// situations applies, and the caller dispatches on the populated fields:
|
||||
// - both keyAttributes and encryptedToken present: login is complete
|
||||
// - twoFactorSessionID present: caller must submit a TOTP code
|
||||
// - passkeySessionID present: caller must complete a passkey ceremony
|
||||
export interface AuthorizationResponse {
|
||||
id: number;
|
||||
keyAttributes?: KeyAttributes;
|
||||
encryptedToken?: Base64;
|
||||
twoFactorSessionID?: string;
|
||||
passkeySessionID?: string;
|
||||
}
|
||||
|
||||
// Discriminated union returned by the high-level login functions to tell
|
||||
// the caller what to do next.
|
||||
export type LoginChallenge =
|
||||
| { kind: "complete"; response: AuthorizationResponse }
|
||||
| { kind: "totp"; sessionID: string }
|
||||
| { kind: "passkey"; sessionID: string }
|
||||
| { kind: "emailOTP" };
|
||||
17
src/auth/unwrap.ts
Normal file
17
src/auth/unwrap.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Stub: see the README "Development workflow" section for TDD policy.
|
||||
|
||||
import type { AuthorizationResponse } from "./types.js";
|
||||
|
||||
export interface UnwrapResult {
|
||||
masterKey: Uint8Array;
|
||||
secretKey: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const unwrapAuth = async (
|
||||
_response: AuthorizationResponse,
|
||||
_password: string,
|
||||
): Promise<UnwrapResult> => {
|
||||
throw new Error("auth.unwrapAuth not implemented");
|
||||
};
|
||||
Reference in New Issue
Block a user