diff options
author | Florian Dold <florian@dold.me> | 2022-04-13 21:40:56 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-04-13 23:46:34 +0200 |
commit | 4e1fe5eb10a5db44c86becbcef66daacc408239d (patch) | |
tree | 66c619c46a2228dc00ed78b200db6d1be395125b /packages/anastasis-core | |
parent | bd76b5d76f0e03d2d511b6cc0aae4636a2ec52c9 (diff) | |
download | wallet-core-4e1fe5eb10a5db44c86becbcef66daacc408239d.tar.xz |
anastasis-webui: updated challenge feedback
Diffstat (limited to 'packages/anastasis-core')
-rw-r--r-- | packages/anastasis-core/src/challenge-feedback-types.ts | 109 | ||||
-rw-r--r-- | packages/anastasis-core/src/index.ts | 139 | ||||
-rw-r--r-- | packages/anastasis-core/src/provider-types.ts | 90 | ||||
-rw-r--r-- | packages/anastasis-core/src/reducer-types.ts | 2 |
4 files changed, 245 insertions, 95 deletions
diff --git a/packages/anastasis-core/src/challenge-feedback-types.ts b/packages/anastasis-core/src/challenge-feedback-types.ts index 0770d9296..30f42288f 100644 --- a/packages/anastasis-core/src/challenge-feedback-types.ts +++ b/packages/anastasis-core/src/challenge-feedback-types.ts @@ -1,29 +1,48 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ import { AmountString, HttpStatusCode } from "@gnu-taler/taler-util"; export enum ChallengeFeedbackStatus { Solved = "solved", + CodeInFile = "code-in-file", + CodeSent = "code-sent", ServerFailure = "server-failure", TruthUnknown = "truth-unknown", - Redirect = "redirect", - Payment = "payment", - Pending = "pending", - Message = "message", + TalerPayment = "taler-payment", Unsupported = "unsupported", RateLimitExceeded = "rate-limit-exceeded", AuthIban = "auth-iban", + IncorrectAnswer = "incorrect-answer", } export type ChallengeFeedback = | ChallengeFeedbackSolved - | ChallengeFeedbackPending - | ChallengeFeedbackPayment + | ChallengeFeedbackCodeInFile + | ChallengeFeedbackCodeSent + | ChallengeFeedbackIncorrectAnswer + | ChallengeFeedbackTalerPaymentRequired | ChallengeFeedbackServerFailure | ChallengeFeedbackRateLimitExceeded | ChallengeFeedbackTruthUnknown - | ChallengeFeedbackRedirect - | ChallengeFeedbackMessage | ChallengeFeedbackUnsupported - | ChallengeFeedbackAuthIban; + | ChallengeFeedbackBankTransferRequired; /** * Challenge has been solved and the key share has @@ -33,13 +52,29 @@ export interface ChallengeFeedbackSolved { state: ChallengeFeedbackStatus.Solved; } +export interface ChallengeFeedbackIncorrectAnswer { + state: ChallengeFeedbackStatus.IncorrectAnswer; +} + +export interface ChallengeFeedbackCodeInFile { + state: ChallengeFeedbackStatus.CodeInFile; + filename: string; + display_hint: string; +} + +export interface ChallengeFeedbackCodeSent { + state: ChallengeFeedbackStatus.CodeSent; + display_hint: string; + address_hint: string; +} + /** * 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. */ @@ -57,7 +92,7 @@ export interface ChallengeFeedbackRateLimitExceeded { * Instructions for performing authentication via an * IBAN bank transfer. */ -export interface ChallengeFeedbackAuthIban { +export interface ChallengeFeedbackBankTransferRequired { state: ChallengeFeedbackStatus.AuthIban; /** @@ -68,12 +103,12 @@ export interface ChallengeFeedbackAuthIban { /** * Account that should be credited. */ - credit_iban: string; + target_iban: string; /** * Creditor name. */ - business_name: string; + target_business_name: string; /** * Unstructured remittance information that should @@ -81,41 +116,7 @@ export interface ChallengeFeedbackAuthIban { */ wire_transfer_subject: string; - /** - * FIXME: This field is only present for compatibility with - * the C reducer test suite. - */ - method: "iban"; - answer_code: number; - - /** - * FIXME: This field is only present for compatibility with - * the C reducer test suite. - */ - details: { - challenge_amount: AmountString; - credit_iban: string; - business_name: string; - 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; } /** @@ -141,21 +142,11 @@ export interface ChallengeFeedbackTruthUnknown { } /** - * 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; +export interface ChallengeFeedbackTalerPaymentRequired { + state: ChallengeFeedbackStatus.TalerPayment; taler_pay_uri: string; diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index 98aba2ce6..055f3fb62 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -25,6 +25,7 @@ import { } from "@gnu-taler/taler-util"; import { anastasisData } from "./anastasis-data.js"; import { + codecForChallengeInstructionMessage, EscrowConfigurationResponse, RecoveryMetaResponse, TruthUploadRequest, @@ -363,9 +364,10 @@ async function getTruthValue( case "email": case "totp": case "iban": + case "post": return authMethod.challenge; default: - throw Error("unknown auth type"); + throw Error(`unknown auth type '${authMethod.type}'`); } } @@ -947,17 +949,27 @@ async function requestTruth( const hresp = await getResponseHash(truth, solveRequest); - 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, - h_response: hresp, - }), - }); + let resp: Response; + + try { + resp = await fetch(url.href, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + truth_decryption_key: truth.truth_key, + h_response: hresp, + }), + }); + } catch (e) { + return { + reducer_type: "error", + code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, + hint: "network error", + } as ReducerStateError; + } logger.info( `got POST /truth/.../solve response from ${truth.url}, http status ${resp.status}`, @@ -1007,6 +1019,19 @@ async function requestTruth( return tryRecoverSecret(newState); } + if (resp.status === HttpStatusCode.Forbidden) { + const challengeFeedback: { [x: string]: ChallengeFeedback } = { + ...state.challenge_feedback, + [truth.uuid]: { + state: ChallengeFeedbackStatus.IncorrectAnswer, + }, + }; + return { + ...state, + challenge_feedback: challengeFeedback, + }; + } + return { reducer_type: "error", code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, @@ -1072,6 +1097,9 @@ async function selectChallenge( const url = new URL(`/truth/${truth.uuid}/challenge`, truth.url); + const newFeedback = { ...state.challenge_feedback }; + delete newFeedback[truth.uuid]; + switch (truth.escrow_type) { case ChallengeType.Question: case ChallengeType.Totp: { @@ -1079,51 +1107,93 @@ async function selectChallenge( ...state, recovery_state: RecoveryStates.ChallengeSolving, selected_challenge_uuid: truth.uuid, - challenge_feedback: { - ...state.challenge_feedback, - [truth.uuid]: { - state: ChallengeFeedbackStatus.Pending, - }, - }, + challenge_feedback: newFeedback, }; } } - 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, - }), - }); + let resp: Response; + + try { + resp = await fetch(url.href, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + truth_decryption_key: truth.truth_key, + }), + }); + } catch (e) { + const feedback: ChallengeFeedback = { + state: ChallengeFeedbackStatus.ServerFailure, + http_status: 0, + }; + return { + ...state, + recovery_state: RecoveryStates.ChallengeSelecting, + selected_challenge_uuid: truth.uuid, + challenge_feedback: { + ...state.challenge_feedback, + [truth.uuid]: feedback, + }, + }; + } logger.info( `got GET /truth/.../challenge response from ${truth.url}, http status ${resp.status}`, ); if (resp.status === HttpStatusCode.Ok) { + const respBodyJson = await resp.json(); + const instr = codecForChallengeInstructionMessage().decode(respBodyJson); + let feedback: ChallengeFeedback; + switch (instr.method) { + case "FILE_WRITTEN": { + feedback = { + state: ChallengeFeedbackStatus.CodeInFile, + display_hint: "TAN code is in file (for debugging)", + filename: instr.filename, + }; + break; + } + case "IBAN_WIRE": { + feedback = { + state: ChallengeFeedbackStatus.AuthIban, + answer_code: instr.answer_code, + target_business_name: instr.business_name, + challenge_amount: instr.amount, + target_iban: instr.credit_iban, + wire_transfer_subject: instr.wire_transfer_subject, + }; + break; + } + case "TAN_SENT": { + feedback = { + state: ChallengeFeedbackStatus.CodeSent, + address_hint: instr.tan_address_hint, + display_hint: "Code sent to address", + }; + } + } return { ...state, recovery_state: RecoveryStates.ChallengeSolving, selected_challenge_uuid: truth.uuid, challenge_feedback: { ...state.challenge_feedback, - [truth.uuid]: { - state: ChallengeFeedbackStatus.Pending, - }, + [truth.uuid]: feedback, }, }; } - // FIXME: look at response, include in challenge_feedback! + // FIXME: look at more error codes in response return { reducer_type: "error", code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, - hint: "got unexpected /truth/.../challenge response status", + hint: `got unexpected /truth/.../challenge response status (${resp.status})`, http_status: resp.status, } as ReducerStateError; } @@ -1727,8 +1797,9 @@ export async function reduceAction( } try { return await h.handler(state, parsedArgs); - } catch (e) { + } catch (e: any) { logger.error("action handler failed"); + logger.error(`${e?.stack ?? e}`); if (e instanceof ReducerError) { return { reducer_type: "error", diff --git a/packages/anastasis-core/src/provider-types.ts b/packages/anastasis-core/src/provider-types.ts index 72f2dc6e5..4da62fc04 100644 --- a/packages/anastasis-core/src/provider-types.ts +++ b/packages/anastasis-core/src/provider-types.ts @@ -16,6 +16,13 @@ import { AmountString, + buildCodecForObject, + buildCodecForUnion, + Codec, + codecForAmountString, + codecForConstString, + codecForNumber, + codecForString, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; @@ -122,3 +129,86 @@ export interface RecoveryMetaDataItem { // document was uploaded. upload_time: TalerProtocolTimestamp; } + +export type ChallengeInstructionMessage = + | FileChallengeInstructionMessage + | IbanChallengeInstructionMessage + | PinChallengeInstructionMessage; + +export interface IbanChallengeInstructionMessage { + // What kind of challenge is this? + method: "IBAN_WIRE"; + + // How much should be wired? + amount: AmountString; + + // What is the target IBAN? + credit_iban: string; + + // What is the receiver name? + business_name: string; + + // What is the expected wire transfer subject? + wire_transfer_subject: string; + + // What is the numeric code (also part of the + // wire transfer subject) to be hashed when + // solving the challenge? + answer_code: number; + + // Hint about the origin account that must be used. + debit_account_hint: string; +} + +export interface PinChallengeInstructionMessage { + // What kind of challenge is this? + method: "TAN_SENT"; + + // Where was the PIN code sent? Note that this + // address will most likely have been obscured + // to improve privacy. + tan_address_hint: string; +} + +export interface FileChallengeInstructionMessage { + // What kind of challenge is this? + method: "FILE_WRITTEN"; + + // Name of the file where the PIN code was written. + filename: string; +} + +export const codecForFileChallengeInstructionMessage = + (): Codec<FileChallengeInstructionMessage> => + buildCodecForObject<FileChallengeInstructionMessage>() + .property("method", codecForConstString("FILE_WRITTEN")) + .property("filename", codecForString()) + .build("FileChallengeInstructionMessage"); + +export const codecForPinChallengeInstructionMessage = + (): Codec<PinChallengeInstructionMessage> => + buildCodecForObject<PinChallengeInstructionMessage>() + .property("method", codecForConstString("TAN_SENT")) + .property("tan_address_hint", codecForString()) + .build("PinChallengeInstructionMessage"); + +export const codecForIbanChallengeInstructionMessage = + (): Codec<IbanChallengeInstructionMessage> => + buildCodecForObject<IbanChallengeInstructionMessage>() + .property("method", codecForConstString("IBAN_WIRE")) + .property("amount", codecForAmountString()) + .property("business_name", codecForString()) + .property("credit_iban", codecForString()) + .property("wire_transfer_subject", codecForString()) + .property("answer_code", codecForNumber()) + .property("debit_account_hint", codecForString()) + .build("IbanChallengeInstructionMessage"); + +export const codecForChallengeInstructionMessage = + (): Codec<ChallengeInstructionMessage> => + buildCodecForUnion<ChallengeInstructionMessage>() + .discriminateOn("method") + .alternative("FILE_WRITTEN", codecForFileChallengeInstructionMessage()) + .alternative("IBAN_WIRE", codecForIbanChallengeInstructionMessage()) + .alternative("TAN_SENT", codecForPinChallengeInstructionMessage()) + .build("ChallengeInstructionMessage"); diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index 5b5f40297..b7e3148cb 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -220,8 +220,6 @@ export interface ReducerStateRecovery { /** * Explicitly selected version by the user. - * FIXME: In the C reducer this is called "version". - * FIXME: rename to selected_secret / selected_policy? */ selected_version?: AggregatedPolicyMetaInfo; |