anastasis: use new truth API

This commit is contained in:
Florian Dold 2022-04-06 13:19:34 +02:00
parent a30a547ac5
commit f33d9dad47
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 234 additions and 117 deletions

View File

@ -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);
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<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(

View File

@ -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";

View File

@ -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);

View File

@ -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).