diff options
author | Florian Dold <florian@dold.me> | 2021-10-21 18:51:19 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-10-21 18:51:19 +0200 |
commit | 3740010117df56c0ab8cfa97c983d9cf0143daf1 (patch) | |
tree | e290a211f9e76af226e69a30012f3d2079b93829 /packages/anastasis-core/src/index.ts | |
parent | 0ee669f52341a8331394a1e9892264c0ef0bb7d7 (diff) | |
download | wallet-core-3740010117df56c0ab8cfa97c983d9cf0143daf1.tar.xz |
anastasis: make recovery work, at least for security questions
Diffstat (limited to 'packages/anastasis-core/src/index.ts')
-rw-r--r-- | packages/anastasis-core/src/index.ts | 201 |
1 files changed, 191 insertions, 10 deletions
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index b8fedf006..b4e911ffb 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -26,7 +26,8 @@ import { ActionArgEnterSecret, ActionArgEnterSecretName, ActionArgEnterUserAttributes, - ActionArgSelectChallenge, + ActionArgsSelectChallenge, + ActionArgsSolveChallengeRequest, AuthenticationProviderStatus, AuthenticationProviderStatusOk, AuthMethod, @@ -66,6 +67,9 @@ import { userIdentifierDerive, typedArrayConcat, decryptRecoveryDocument, + decryptKeyShare, + KeyShare, + coreSecretRecover, } from "./crypto.js"; import { unzlibSync, zlibSync } from "fflate"; import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js"; @@ -626,8 +630,10 @@ async function downloadPolicy( const providerUrls = Object.keys(state.authentication_providers ?? {}); let foundRecoveryInfo: RecoveryInternalData | undefined = undefined; let recoveryDoc: RecoveryDocument | undefined = undefined; - const newProviderStatus: { [url: string]: AuthenticationProviderStatus } = {}; + const newProviderStatus: { [url: string]: AuthenticationProviderStatusOk } = + {}; const userAttributes = state.identity_attributes!; + // FIXME: Shouldn't we also store the status of bad providers? for (const url of providerUrls) { const pi = await getProviderInfo(url); if ("error_code" in pi || !("http_status" in pi)) { @@ -635,6 +641,12 @@ async function downloadPolicy( continue; } newProviderStatus[url] = pi; + } + for (const url of providerUrls) { + const pi = newProviderStatus[url]; + if (!pi) { + continue; + } const userId = await userIdentifierDerive(userAttributes, pi.salt); const acctKeypair = accountKeypairDerive(userId); const resp = await fetch(new URL(`policy/${acctKeypair.pub}`, url).href); @@ -670,7 +682,7 @@ async function downloadPolicy( } const recoveryInfo: RecoveryInformation = { challenges: recoveryDoc.escrow_methods.map((x) => { - console.log("providers", state.authentication_providers); + console.log("providers", newProviderStatus); const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk; return { cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!, @@ -692,9 +704,124 @@ async function downloadPolicy( recovery_state: RecoveryStates.SecretSelecting, recovery_document: foundRecoveryInfo, recovery_information: recoveryInfo, + verbatim_recovery_document: recoveryDoc, }; } +/** + * Try to reconstruct the secret from the available shares. + * + * Returns the state unmodified if not enough key shares are available yet. + */ +async function tryRecoverSecret( + state: ReducerStateRecovery, +): Promise<ReducerStateRecovery | ReducerStateError> { + const rd = state.verbatim_recovery_document!; + for (const p of rd.policies) { + const keyShares: KeyShare[] = []; + let missing = false; + for (const truthUuid of p.uuids) { + const ks = (state.recovered_key_shares ?? {})[truthUuid]; + if (!ks) { + missing = true; + break; + } + keyShares.push(ks); + } + + if (missing) { + continue; + } + + const policyKey = await policyKeyDerive(keyShares, p.salt); + const coreSecretBytes = await coreSecretRecover({ + encryptedCoreSecret: rd.encrypted_core_secret, + encryptedMasterKey: p.master_key, + policyKey, + }); + + return { + ...state, + recovery_state: RecoveryStates.RecoveryFinished, + selected_challenge_uuid: undefined, + core_secret: JSON.parse(bytesToString(decodeCrock(coreSecretBytes))), + }; + } + return { ...state }; +} + +async function solveChallenge( + state: ReducerStateRecovery, + ta: ActionArgsSolveChallengeRequest, +): Promise<ReducerStateRecovery | ReducerStateError> { + const recDoc: RecoveryDocument = state.verbatim_recovery_document!; + const truth = recDoc.escrow_methods.find( + (x) => x.uuid === state.selected_challenge_uuid, + ); + if (!truth) { + throw "truth for challenge not found"; + } + + const url = new URL(`/truth/${truth.uuid}`, truth.url); + + // FIXME: This isn't correct for non-question truth responses. + url.searchParams.set( + "response", + await secureAnswerHash(ta.answer, truth.uuid, truth.truth_salt), + ); + + const resp = await fetch(url.href, { + headers: { + "Anastasis-Truth-Decryption-Key": truth.truth_key, + }, + }); + + console.log(resp); + + if (resp.status !== 200) { + return { + code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, + hint: "got non-200 response", + http_status: resp.status, + } as ReducerStateError; + } + + const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined; + + const userId = await userIdentifierDerive( + state.identity_attributes, + truth.provider_salt, + ); + + const respBody = new Uint8Array(await resp.arrayBuffer()); + const keyShare = await decryptKeyShare( + encodeCrock(respBody), + userId, + answerSalt, + ); + + const recoveredKeyShares = { + ...(state.recovered_key_shares ?? {}), + [truth.uuid]: keyShare, + }; + + const challengeFeedback = { + ...state.challenge_feedback, + [truth.uuid]: { + state: "solved", + }, + }; + + const newState: ReducerStateRecovery = { + ...state, + recovery_state: RecoveryStates.ChallengeSelecting, + challenge_feedback: challengeFeedback, + recovered_key_shares: recoveredKeyShares, + }; + + return tryRecoverSecret(newState); +} + async function recoveryEnterUserAttributes( state: ReducerStateRecovery, attributes: Record<string, string>, @@ -707,6 +834,33 @@ async function recoveryEnterUserAttributes( return downloadPolicy(st); } +async function selectChallenge( + state: ReducerStateRecovery, + ta: ActionArgsSelectChallenge, +): Promise<ReducerStateRecovery | ReducerStateError> { + const recDoc: RecoveryDocument = state.verbatim_recovery_document!; + const truth = recDoc.escrow_methods.find((x) => x.uuid === ta.uuid); + if (!truth) { + throw "truth for challenge not found"; + } + + const url = new URL(`/truth/${truth.uuid}`, truth.url); + + const resp = await fetch(url.href, { + headers: { + "Anastasis-Truth-Decryption-Key": truth.truth_key, + }, + }); + + console.log(resp); + + return { + ...state, + recovery_state: RecoveryStates.ChallengeSolving, + selected_challenge_uuid: ta.uuid, + }; +} + export async function reduceAction( state: ReducerState, action: string, @@ -989,17 +1143,22 @@ export async function reduceAction( if (state.recovery_state === RecoveryStates.ChallengeSelecting) { if (action === "select_challenge") { - const ta: ActionArgSelectChallenge = args; - return { - ...state, - recovery_state: RecoveryStates.ChallengeSolving, - selected_challenge_uuid: ta.uuid, - }; + const ta: ActionArgsSelectChallenge = args; + return selectChallenge(state, ta); } else if (action === "back") { return { ...state, recovery_state: RecoveryStates.SecretSelecting, }; + } else if (action === "next") { + const s2 = await tryRecoverSecret(state); + if (s2.recovery_state === RecoveryStates.RecoveryFinished) { + return s2; + } + return { + code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, + hint: "Not enough challenges solved", + }; } else { return { code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, @@ -1010,12 +1169,34 @@ export async function reduceAction( if (state.recovery_state === RecoveryStates.ChallengeSolving) { if (action === "back") { - const ta: ActionArgSelectChallenge = args; + const ta: ActionArgsSelectChallenge = args; + return { + ...state, + selected_challenge_uuid: undefined, + recovery_state: RecoveryStates.ChallengeSelecting, + }; + } else if (action === "solve_challenge") { + const ta: ActionArgsSolveChallengeRequest = args; + return solveChallenge(state, ta); + } else { + return { + code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, + hint: `Unsupported action '${action}'`, + }; + } + } + + if (state.recovery_state === RecoveryStates.RecoveryFinished) { + if (action === "back") { + const ta: ActionArgsSelectChallenge = args; return { ...state, selected_challenge_uuid: undefined, recovery_state: RecoveryStates.ChallengeSelecting, }; + } else if (action === "solve_challenge") { + const ta: ActionArgsSolveChallengeRequest = args; + return solveChallenge(state, ta); } else { return { code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, |