diff --git a/README.md b/README.md index f045495..7b3159a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/auth/unwrap.ts b/src/auth/unwrap.ts index 78ca171..1a894df 100644 --- a/src/auth/unwrap.ts +++ b/src/auth/unwrap.ts @@ -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 => { - 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 }; }; diff --git a/src/index.ts b/src/index.ts index 2e47a88..073f505 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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";