From f33d9dad4782c81df9056e81da0e1a4174ae3298 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 6 Apr 2022 13:19:34 +0200 Subject: [PATCH] anastasis: use new truth API --- packages/anastasis-core/src/index.ts | 230 ++++++++++--------- packages/anastasis-core/src/reducer-types.ts | 2 - packages/anastasis-webui/src/hooks/async.ts | 21 +- packages/taler-util/src/taler-error-codes.ts | 98 ++++++++ 4 files changed, 234 insertions(+), 117 deletions(-) diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index d1afc706b..a355eaa54 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -867,6 +867,51 @@ async function pollChallenges( return state; } +async function getResponseHash( + truth: EscrowMethod, + solveRequest: ActionArgsSolveChallengeRequest, +): Promise { + let respHash: string; + switch (truth.escrow_type) { + case ChallengeType.Question: { + if ("answer" in solveRequest) { + respHash = await secureAnswerHash( + solveRequest.answer, + truth.uuid, + truth.truth_salt, + ); + } else { + throw Error("unsupported answer request"); + } + break; + } + case ChallengeType.Email: + case ChallengeType.Sms: + case ChallengeType.Post: + case ChallengeType.Iban: + case ChallengeType.Totp: { + if ("answer" in solveRequest) { + const s = solveRequest.answer.trim().replace(/^A-/, ""); + let pin: number; + try { + pin = Number.parseInt(s); + } catch (e) { + throw Error("invalid pin format"); + } + respHash = await pinAnswerHash(pin); + } else if ("pin" in solveRequest) { + respHash = await pinAnswerHash(solveRequest.pin); + } else { + throw Error("unsupported answer request"); + } + break; + } + default: + throw Error(`unsupported challenge type "${truth.escrow_type}""`); + } + return respHash; +} + /** * Request a truth, optionally with a challenge solution * provided by the user. @@ -874,61 +919,26 @@ async function pollChallenges( async function requestTruth( state: ReducerStateRecovery, truth: EscrowMethod, - solveRequest?: ActionArgsSolveChallengeRequest, + solveRequest: ActionArgsSolveChallengeRequest, ): Promise { - const url = new URL(`/truth/${truth.uuid}`, truth.url); + const url = new URL(`/truth/${truth.uuid}/solve`, truth.url); - if (solveRequest) { - logger.info(`handling solve request ${j2s(solveRequest)}`); - let respHash: string; - switch (truth.escrow_type) { - case ChallengeType.Question: { - if ("answer" in solveRequest) { - respHash = await secureAnswerHash( - solveRequest.answer, - truth.uuid, - truth.truth_salt, - ); - } else { - throw Error("unsupported answer request"); - } - break; - } - case ChallengeType.Email: - case ChallengeType.Sms: - case ChallengeType.Post: - case ChallengeType.Iban: - case ChallengeType.Totp: { - if ("answer" in solveRequest) { - const s = solveRequest.answer.trim().replace(/^A-/, ""); - let pin: number; - try { - pin = Number.parseInt(s); - } catch (e) { - throw Error("invalid pin format"); - } - respHash = await pinAnswerHash(pin); - } else if ("pin" in solveRequest) { - respHash = await pinAnswerHash(solveRequest.pin); - } else { - throw Error("unsupported answer request"); - } - break; - } - default: - throw Error(`unsupported challenge type "${truth.escrow_type}""`); - } - url.searchParams.set("response", respHash); - } + const hresp = await getResponseHash(truth, solveRequest); const resp = await fetch(url.href, { + method: "POST", headers: { - "Anastasis-Truth-Decryption-Key": truth.truth_key, + Accept: "application/json", + "Content-Type": "application/json", }, + body: JSON.stringify({ + truth_decryption_key: truth.truth_key, + h_response: hresp, + }), }); logger.info( - `got GET /truth response from ${truth.url}, http status ${resp.status}`, + `got POST /truth/.../solve response from ${truth.url}, http status ${resp.status}`, ); if (resp.status === HttpStatusCode.Ok) { @@ -975,66 +985,6 @@ async function requestTruth( return tryRecoverSecret(newState); } - if (resp.status === HttpStatusCode.Forbidden) { - const body = await resp.json(); - if ( - body.code === TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED - ) { - return { - ...state, - recovery_state: RecoveryStates.ChallengeSolving, - challenge_feedback: { - ...state.challenge_feedback, - [truth.uuid]: { - state: ChallengeFeedbackStatus.Pending, - }, - }, - }; - } - return { - ...state, - recovery_state: RecoveryStates.ChallengeSolving, - challenge_feedback: { - ...state.challenge_feedback, - [truth.uuid]: { - state: ChallengeFeedbackStatus.Message, - message: body.hint ?? "Challenge should be solved", - }, - }, - }; - } - - if (resp.status === HttpStatusCode.Accepted) { - const body = await resp.json(); - logger.info(`got body ${j2s(body)}`); - if (body.method === "iban") { - const b = body as IbanExternalAuthResponse; - return { - ...state, - recovery_state: RecoveryStates.ChallengeSolving, - challenge_feedback: { - ...state.challenge_feedback, - [truth.uuid]: { - state: ChallengeFeedbackStatus.AuthIban, - answer_code: b.answer_code, - business_name: b.details.business_name, - challenge_amount: b.details.challenge_amount, - credit_iban: b.details.credit_iban, - wire_transfer_subject: b.details.wire_transfer_subject, - details: b.details, - method: "iban", - }, - }, - }; - } else { - return { - code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, - hint: "unknown external authentication method", - http_status: resp.status, - } as ReducerStateError; - } - } - return { code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, hint: "got unexpected /truth/ response status", @@ -1053,6 +1003,7 @@ async function solveChallenge( if (!truth) { throw Error("truth for challenge not found"); } + return requestTruth(state, truth, ta); } @@ -1096,7 +1047,58 @@ async function selectChallenge( throw "truth for challenge not found"; } - return requestTruth({ ...state, selected_challenge_uuid: ta.uuid }, truth); + const url = new URL(`/truth/${truth.uuid}/challenge`, truth.url); + + if (truth.escrow_type === ChallengeType.Question) { + return { + ...state, + recovery_state: RecoveryStates.ChallengeSolving, + selected_challenge_uuid: truth.uuid, + challenge_feedback: { + ...state.challenge_feedback, + [truth.uuid]: { + state: ChallengeFeedbackStatus.Pending, + }, + }, + }; + } + + const resp = await fetch(url.href, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + truth_decryption_key: truth.truth_key, + }), + }); + + logger.info( + `got GET /truth/.../challenge response from ${truth.url}, http status ${resp.status}`, + ); + + if (resp.status === HttpStatusCode.Ok) { + return { + ...state, + recovery_state: RecoveryStates.ChallengeSolving, + selected_challenge_uuid: truth.uuid, + challenge_feedback: { + ...state.challenge_feedback, + [truth.uuid]: { + state: ChallengeFeedbackStatus.Pending, + }, + }, + }; + } + + // FIXME: look at response, include in challenge_feedback! + + return { + code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, + hint: "got unexpected /truth/.../challenge response status", + http_status: resp.status, + } as ReducerStateError; } async function backupSelectContinent( @@ -1140,15 +1142,15 @@ interface TransitionImpl { handler: (s: S, args: T) => Promise; } -interface Transition { - [x: string]: TransitionImpl; +interface Transition { + [x: string]: TransitionImpl; } function transition( action: string, argCodec: Codec, handler: (s: S, args: T) => Promise, -): Transition { +): Transition { return { [action]: { argCodec, @@ -1160,7 +1162,7 @@ function transition( function transitionBackupJump( action: string, st: BackupStates, -): Transition { +): Transition { return { [action]: { argCodec: codecForAny(), @@ -1172,7 +1174,7 @@ function transitionBackupJump( function transitionRecoveryJump( action: string, st: RecoveryStates, -): Transition { +): Transition { return { [action]: { argCodec: codecForAny(), @@ -1440,7 +1442,7 @@ async function updateSecretExpiration( const backupTransitions: Record< BackupStates, - Transition + Transition > = { [BackupStates.ContinentSelecting]: { ...transition( @@ -1511,7 +1513,7 @@ const backupTransitions: Record< const recoveryTransitions: Record< RecoveryStates, - Transition + Transition > = { [RecoveryStates.ContinentSelecting]: { ...transition( diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index 2a869fe47..4682eddb7 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -6,9 +6,7 @@ import { codecForNumber, codecForString, codecForTimestamp, - Duration, TalerProtocolTimestamp, - AbsoluteTime, } from "@gnu-taler/taler-util"; import { ChallengeFeedback } from "./challenge-feedback-types.js"; import { KeyShare } from "./crypto.js"; diff --git a/packages/anastasis-webui/src/hooks/async.ts b/packages/anastasis-webui/src/hooks/async.ts index 0fc197554..5235e1e3e 100644 --- a/packages/anastasis-webui/src/hooks/async.ts +++ b/packages/anastasis-webui/src/hooks/async.ts @@ -18,7 +18,7 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { useState } from "preact/hooks"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; // import { cancelPendingRequest } from "./backend"; export interface Options { @@ -34,6 +34,17 @@ export interface AsyncOperationApi { error: string | undefined; } +export function useIsMounted() { + const isMountedRef = useRef(true); + const isMounted = useCallback(() => isMountedRef.current, []); + + useEffect(() => { + return () => void (isMountedRef.current = false); + }, []); + + return isMounted; +} + export function useAsync( fn?: (...args: any) => Promise, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }, @@ -42,11 +53,15 @@ export function useAsync( const [isLoading, setLoading] = useState(false); const [error, setError] = useState(undefined); const [isSlow, setSlow] = useState(false); + const isMounted = useIsMounted(); const request = async (...args: any) => { if (!fn) return; setLoading(true); const handler = setTimeout(() => { + if (!isMounted()) { + return; + } setSlow(true); }, tooLong); @@ -54,6 +69,10 @@ export function useAsync( console.log("calling async", args); const result = await fn(...args); console.log("async back", result); + if (!isMounted()) { + // Possibly calling fn(...) resulted in the component being unmounted. + return; + } setData(result); } catch (error) { setError(error); diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts index 8ea97f7e7..539289862 100644 --- a/packages/taler-util/src/taler-error-codes.ts +++ b/packages/taler-util/src/taler-error-codes.ts @@ -220,6 +220,13 @@ export enum TalerErrorCode { */ GENERIC_FAILED_COMPUTE_JSON_HASH = 61, + /** + * The service could not compute an amount. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_FAILED_COMPUTE_AMOUNT = 62, + /** * The HTTP server had insufficient memory to parse the request. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). @@ -395,6 +402,20 @@ export enum TalerErrorCode { */ EXCHANGE_GENERIC_CLOCK_SKEW = 1020, + /** + * The specified amount for the coin is higher than the value of the denomination of the coin. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE = 1021, + + /** + * The exchange was not properly configured with global fees. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_GLOBAL_FEES_MISSING = 1022, + /** * The exchange did not find information about the specified transaction in the database. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). @@ -1081,6 +1102,83 @@ export enum TalerErrorCode { */ EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817, + /** + * The purse was previously created with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA = 1850, + + /** + * The purse was previously created with a different contract. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED = 1851, + + /** + * A coin signature for a deposit into the purse is invalid. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID = 1852, + + /** + * The purse expiration time is in the past. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW = 1853, + + /** + * The purse expiration time is "never". + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER = 1854, + + /** + * The purse signature over the purse meta data is invalid. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID = 1855, + + /** + * The signature over the encrypted contract is invalid. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID = 1856, + + /** + * The signature from the exchange over the confirmation is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID = 1857, + + /** + * The coin was previously deposited with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA = 1858, + + /** + * The encrypted contract was previously uploaded with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA = 1859, + + /** + * The deposited amount is less than the purse fee. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE = 1860, + /** * The auditor signature over the denomination meta data is invalid. * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).