From 04356cd23fef76d2020338d2b2b394095fdc2b14 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 3 Nov 2021 13:34:57 +0100 Subject: anastasis: refactor feedback types --- .../anastasis-core/src/challenge-feedback-types.ts | 149 ++++++++++++++++++++ packages/anastasis-core/src/index.ts | 150 ++++++++++++--------- packages/anastasis-core/src/reducer-types.ts | 35 +++-- 3 files changed, 249 insertions(+), 85 deletions(-) create mode 100644 packages/anastasis-core/src/challenge-feedback-types.ts (limited to 'packages/anastasis-core/src') diff --git a/packages/anastasis-core/src/challenge-feedback-types.ts b/packages/anastasis-core/src/challenge-feedback-types.ts new file mode 100644 index 000000000..d6a2e3e80 --- /dev/null +++ b/packages/anastasis-core/src/challenge-feedback-types.ts @@ -0,0 +1,149 @@ +import { AmountString, HttpStatusCode } from "@gnu-taler/taler-util"; + +export enum ChallengeFeedbackStatus { + Solved = "solved", + ServerFailure = "server-failure", + TruthUnknown = "truth-unknown", + Redirect = "redirect", + Payment = "payment", + Pending = "pending", + Message = "message", + Unsupported = "unsupported", + RateLimitExceeded = "rate-limit-exceeded", + AuthIban = "auth-iban", +} + +export type ChallengeFeedback = + | ChallengeFeedbackSolved + | ChallengeFeedbackPending + | ChallengeFeedbackPayment + | ChallengeFeedbackServerFailure + | ChallengeFeedbackRateLimitExceeded + | ChallengeFeedbackTruthUnknown + | ChallengeFeedbackRedirect + | ChallengeFeedbackMessage + | ChallengeFeedbackUnsupported + | ChallengeFeedbackAuthIban; + +/** + * Challenge has been solved and the key share has + * been retrieved. + */ +export interface ChallengeFeedbackSolved { + state: ChallengeFeedbackStatus.Solved; +} + +/** + * The challenge given by the server is unsupported + * by the current anastasis client. + */ +export interface ChallengeFeedbackUnsupported { + state: ChallengeFeedbackStatus.Unsupported; + http_status: HttpStatusCode; + /** + * Human-readable identifier of the unsupported method. + */ + unsupported_method: string; +} + +/** + * The user tried to answer too often with a wrong answer. + */ +export interface ChallengeFeedbackRateLimitExceeded { + state: ChallengeFeedbackStatus.RateLimitExceeded; +} + +/** + * Instructions for performing authentication via an + * IBAN bank transfer. + */ +export interface ChallengeFeedbackAuthIban { + state: ChallengeFeedbackStatus.AuthIban; + + /** + * Amount that should be transfered for a successful authentication. + */ + challenge_amount: AmountString; + + /** + * Account that should be credited. + */ + credit_iban: string; + + /** + * Creditor name. + */ + business_name: string; + + /** + * Unstructured remittance information that should + * be contained in the bank transfer. + */ + wire_transfer_subject: string; +} + +/** + * Challenge still needs to be solved. + */ +export interface ChallengeFeedbackPending { + state: ChallengeFeedbackStatus.Pending; +} + +/** + * Human-readable response from the provider + * after the user failed to solve the challenge + * correctly. + */ +export interface ChallengeFeedbackMessage { + state: ChallengeFeedbackStatus.Message; + message: string; +} + +/** + * The server experienced a temporary failure. + */ +export interface ChallengeFeedbackServerFailure { + state: ChallengeFeedbackStatus.ServerFailure; + http_status: HttpStatusCode | 0; + + /** + * Taler-style error response, if available. + */ + error_response?: any; +} + +/** + * The truth is unknown to the provider. There + * is no reason to continue trying to solve any + * challenges in the policy. + */ +export interface ChallengeFeedbackTruthUnknown { + state: ChallengeFeedbackStatus.TruthUnknown; +} + +/** + * The user should be asked to go to a URL + * to complete the authentication there. + */ +export interface ChallengeFeedbackRedirect { + state: ChallengeFeedbackStatus.Redirect; + http_status: number; + redirect_url: string; +} + +/** + * A payment is required before the user can + * even attempt to solve the challenge. + */ +export interface ChallengeFeedbackPayment { + state: ChallengeFeedbackStatus.Payment; + + taler_pay_uri: string; + + provider: string; + + /** + * FIXME: Why is this required?! + */ + payment_secret: string; +} diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index db99db610..859dd083b 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -11,10 +11,9 @@ import { Duration, eddsaSign, encodeCrock, - getDurationRemaining, getRandomBytes, - getTimestampNow, hash, + HttpStatusCode, j2s, Logger, stringToBytes, @@ -91,6 +90,7 @@ import { import { unzlibSync, zlibSync } from "fflate"; import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js"; import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js"; +import { ChallengeFeedback, ChallengeFeedbackStatus } from "./challenge-feedback-types.js"; const { fetch } = fetchPonyfill({}); @@ -291,7 +291,6 @@ async function backupEnterUserAttributes( return newState; } - /** * Truth data as stored in the reducer. */ @@ -551,6 +550,7 @@ async function uploadSecret( return { ...state, + core_secret: undefined, backup_state: BackupStates.BackupFinished, success_details: successDetails, }; @@ -684,25 +684,24 @@ async function tryRecoverSecret( return { ...state }; } -async function solveChallenge( +/** + * Request a truth, optionally with a challenge solution + * provided by the user. + */ +async function requestTruth( state: ReducerStateRecovery, - ta: ActionArgsSolveChallengeRequest, + truth: EscrowMethod, + solveRequest?: ActionArgsSolveChallengeRequest, ): Promise { - 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), - ); + if (solveRequest) { + // FIXME: This isn't correct for non-question truth responses. + url.searchParams.set( + "response", + await secureAnswerHash(solveRequest.answer, truth.uuid, truth.truth_salt), + ); + } const resp = await fetch(url.href, { headers: { @@ -710,48 +709,79 @@ async function solveChallenge( }, }); - if (resp.status !== 200) { - return { - code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, - hint: "got non-200 response", - http_status: resp.status, - } as ReducerStateError; - } + if (resp.status === HttpStatusCode.Ok) { + const answerSalt = + solveRequest && truth.escrow_type === "question" + ? solveRequest.answer + : undefined; - const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined; + const userId = await userIdentifierDerive( + state.identity_attributes, + truth.provider_salt, + ); - 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 respBody = new Uint8Array(await resp.arrayBuffer()); - const keyShare = await decryptKeyShare( - encodeCrock(respBody), - userId, - answerSalt, - ); + const recoveredKeyShares = { + ...(state.recovered_key_shares ?? {}), + [truth.uuid]: keyShare, + }; - const recoveredKeyShares = { - ...(state.recovered_key_shares ?? {}), - [truth.uuid]: keyShare, - }; + const challengeFeedback: { [x: string]: ChallengeFeedback } = { + ...state.challenge_feedback, + [truth.uuid]: { + state: ChallengeFeedbackStatus.Solved, + }, + }; - const challengeFeedback = { - ...state.challenge_feedback, - [truth.uuid]: { - state: "solved", - }, - }; + const newState: ReducerStateRecovery = { + ...state, + recovery_state: RecoveryStates.ChallengeSelecting, + challenge_feedback: challengeFeedback, + recovered_key_shares: recoveredKeyShares, + }; - const newState: ReducerStateRecovery = { - ...state, - recovery_state: RecoveryStates.ChallengeSelecting, - challenge_feedback: challengeFeedback, - recovered_key_shares: recoveredKeyShares, - }; + return tryRecoverSecret(newState); + } + + if (resp.status === HttpStatusCode.Forbidden) { + return { + ...state, + recovery_state: RecoveryStates.ChallengeSolving, + challenge_feedback: { + ...state.challenge_feedback, + [truth.uuid]: { + state: ChallengeFeedbackStatus.Message, + message: "Challenge should be solved", + }, + }, + }; + } - return tryRecoverSecret(newState); + return { + code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, + hint: "got unexpected /truth/ response status", + http_status: resp.status, + } as ReducerStateError; +} + +async function solveChallenge( + state: ReducerStateRecovery, + ta: ActionArgsSolveChallengeRequest, +): Promise { + const recDoc: RecoveryDocument = state.verbatim_recovery_document!; + const truth = recDoc.escrow_methods.find( + (x) => x.uuid === state.selected_challenge_uuid, + ); + if (!truth) { + throw Error("truth for challenge not found"); + } + return requestTruth(state, truth, ta); } async function recoveryEnterUserAttributes( @@ -776,19 +806,7 @@ async function selectChallenge( 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, - }, - }); - - return { - ...state, - recovery_state: RecoveryStates.ChallengeSolving, - selected_challenge_uuid: ta.uuid, - }; + return requestTruth({ ...state, selected_challenge_uuid: ta.uuid }, truth); } async function backupSelectContinent( diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index 948268704..69feb6b64 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -8,6 +8,7 @@ import { codecForTimestamp, Timestamp, } from "@gnu-taler/taler-util"; +import { ChallengeFeedback } from "./challenge-feedback-types.js"; import { KeyShare } from "./crypto.js"; import { RecoveryDocument } from "./recovery-document-types.js"; @@ -185,10 +186,6 @@ export interface ReducerStateRecovery { authentication_providers?: { [url: string]: AuthenticationProviderStatus }; } -export interface ChallengeFeedback { - state: string; -} - export interface ReducerStateError { backup_state?: undefined; recovery_state?: undefined; @@ -311,21 +308,10 @@ export interface ActionArgSelectCountry { currencies: string[]; } -export const codecForActionArgSelectCountry = () => - buildCodecForObject() - .property("country_code", codecForString()) - .property("currencies", codecForList(codecForString())) - .build("ActionArgSelectCountry"); - export interface ActionArgsSelectChallenge { uuid: string; } -export const codecForActionArgSelectChallenge = () => - buildCodecForObject() - .property("uuid", codecForString()) - .build("ActionArgSelectChallenge"); - export type ActionArgsSolveChallengeRequest = SolveChallengeAnswerRequest; export interface SolveChallengeAnswerRequest { @@ -341,6 +327,10 @@ export interface ActionArgsAddPolicy { policy: PolicyMember[]; } +export interface ActionArgsUpdateExpiration { + expiration: Timestamp; +} + export const codecForPolicyMember = () => buildCodecForObject() .property("authentication_method", codecForNumber()) @@ -352,11 +342,18 @@ export const codecForActionArgsAddPolicy = () => .property("policy", codecForList(codecForPolicyMember())) .build("ActionArgsAddPolicy"); -export interface ActionArgsUpdateExpiration { - expiration: Timestamp; -} - export const codecForActionArgsUpdateExpiration = () => buildCodecForObject() .property("expiration", codecForTimestamp) .build("ActionArgsUpdateExpiration"); + +export const codecForActionArgSelectChallenge = () => + buildCodecForObject() + .property("uuid", codecForString()) + .build("ActionArgSelectChallenge"); + +export const codecForActionArgSelectCountry = () => + buildCodecForObject() + .property("country_code", codecForString()) + .property("currencies", codecForList(codecForString())) + .build("ActionArgSelectCountry"); -- cgit v1.2.3