summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/anastasis-core/src/index.ts232
-rw-r--r--packages/anastasis-core/src/reducer-types.ts2
-rw-r--r--packages/anastasis-webui/src/hooks/async.ts21
-rw-r--r--packages/taler-util/src/taler-error-codes.ts98
4 files changed, 235 insertions, 118 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<string> {
+ 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<ReducerStateRecovery | ReducerStateError> {
- const url = new URL(`/truth/${truth.uuid}`, 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 url = new URL(`/truth/${truth.uuid}/solve`, truth.url);
+
+ 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<S, T> {
handler: (s: S, args: T) => Promise<S | ReducerStateError>;
}
-interface Transition<S, T> {
- [x: string]: TransitionImpl<S, T>;
+interface Transition<S> {
+ [x: string]: TransitionImpl<S, any>;
}
function transition<S, T>(
action: string,
argCodec: Codec<T>,
handler: (s: S, args: T) => Promise<S | ReducerStateError>,
-): Transition<S, T> {
+): Transition<S> {
return {
[action]: {
argCodec,
@@ -1160,7 +1162,7 @@ function transition<S, T>(
function transitionBackupJump(
action: string,
st: BackupStates,
-): Transition<ReducerStateBackup, void> {
+): Transition<ReducerStateBackup> {
return {
[action]: {
argCodec: codecForAny(),
@@ -1172,7 +1174,7 @@ function transitionBackupJump(
function transitionRecoveryJump(
action: string,
st: RecoveryStates,
-): Transition<ReducerStateRecovery, void> {
+): Transition<ReducerStateRecovery> {
return {
[action]: {
argCodec: codecForAny(),
@@ -1440,7 +1442,7 @@ async function updateSecretExpiration(
const backupTransitions: Record<
BackupStates,
- Transition<ReducerStateBackup, any>
+ Transition<ReducerStateBackup>
> = {
[BackupStates.ContinentSelecting]: {
...transition(
@@ -1511,7 +1513,7 @@ const backupTransitions: Record<
const recoveryTransitions: Record<
RecoveryStates,
- Transition<ReducerStateRecovery, any>
+ Transition<ReducerStateRecovery>
> = {
[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<T> {
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<T>(
fn?: (...args: any) => Promise<T>,
{ slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
@@ -42,11 +53,15 @@ export function useAsync<T>(
const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(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<T>(
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
@@ -221,6 +221,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -396,6 +403,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -1082,6 +1103,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).
* (A value of 0 indicates that the error is generated client-side).