anastasis: refactor feedback types

This commit is contained in:
Florian Dold 2021-11-03 13:34:57 +01:00
parent ab6fd6c8c7
commit 04356cd23f
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 257 additions and 93 deletions

View File

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

View File

@ -11,10 +11,9 @@ import {
Duration, Duration,
eddsaSign, eddsaSign,
encodeCrock, encodeCrock,
getDurationRemaining,
getRandomBytes, getRandomBytes,
getTimestampNow,
hash, hash,
HttpStatusCode,
j2s, j2s,
Logger, Logger,
stringToBytes, stringToBytes,
@ -91,6 +90,7 @@ import {
import { unzlibSync, zlibSync } from "fflate"; import { unzlibSync, zlibSync } from "fflate";
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js"; import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js"; import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js";
import { ChallengeFeedback, ChallengeFeedbackStatus } from "./challenge-feedback-types.js";
const { fetch } = fetchPonyfill({}); const { fetch } = fetchPonyfill({});
@ -291,7 +291,6 @@ async function backupEnterUserAttributes(
return newState; return newState;
} }
/** /**
* Truth data as stored in the reducer. * Truth data as stored in the reducer.
*/ */
@ -551,6 +550,7 @@ async function uploadSecret(
return { return {
...state, ...state,
core_secret: undefined,
backup_state: BackupStates.BackupFinished, backup_state: BackupStates.BackupFinished,
success_details: successDetails, success_details: successDetails,
}; };
@ -684,25 +684,24 @@ async function tryRecoverSecret(
return { ...state }; return { ...state };
} }
async function solveChallenge( /**
* Request a truth, optionally with a challenge solution
* provided by the user.
*/
async function requestTruth(
state: ReducerStateRecovery, state: ReducerStateRecovery,
ta: ActionArgsSolveChallengeRequest, truth: EscrowMethod,
solveRequest?: ActionArgsSolveChallengeRequest,
): Promise<ReducerStateRecovery | ReducerStateError> { ): Promise<ReducerStateRecovery | ReducerStateError> {
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); const url = new URL(`/truth/${truth.uuid}`, truth.url);
if (solveRequest) {
// FIXME: This isn't correct for non-question truth responses. // FIXME: This isn't correct for non-question truth responses.
url.searchParams.set( url.searchParams.set(
"response", "response",
await secureAnswerHash(ta.answer, truth.uuid, truth.truth_salt), await secureAnswerHash(solveRequest.answer, truth.uuid, truth.truth_salt),
); );
}
const resp = await fetch(url.href, { const resp = await fetch(url.href, {
headers: { headers: {
@ -710,15 +709,11 @@ async function solveChallenge(
}, },
}); });
if (resp.status !== 200) { if (resp.status === HttpStatusCode.Ok) {
return { const answerSalt =
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, solveRequest && truth.escrow_type === "question"
hint: "got non-200 response", ? solveRequest.answer
http_status: resp.status, : undefined;
} as ReducerStateError;
}
const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined;
const userId = await userIdentifierDerive( const userId = await userIdentifierDerive(
state.identity_attributes, state.identity_attributes,
@ -737,10 +732,10 @@ async function solveChallenge(
[truth.uuid]: keyShare, [truth.uuid]: keyShare,
}; };
const challengeFeedback = { const challengeFeedback: { [x: string]: ChallengeFeedback } = {
...state.challenge_feedback, ...state.challenge_feedback,
[truth.uuid]: { [truth.uuid]: {
state: "solved", state: ChallengeFeedbackStatus.Solved,
}, },
}; };
@ -752,6 +747,41 @@ async function solveChallenge(
}; };
return tryRecoverSecret(newState); 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 {
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<ReducerStateRecovery | ReducerStateError> {
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( async function recoveryEnterUserAttributes(
@ -776,19 +806,7 @@ async function selectChallenge(
throw "truth for challenge not found"; throw "truth for challenge not found";
} }
const url = new URL(`/truth/${truth.uuid}`, truth.url); return requestTruth({ ...state, selected_challenge_uuid: ta.uuid }, truth);
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,
};
} }
async function backupSelectContinent( async function backupSelectContinent(

View File

@ -8,6 +8,7 @@ import {
codecForTimestamp, codecForTimestamp,
Timestamp, Timestamp,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { ChallengeFeedback } from "./challenge-feedback-types.js";
import { KeyShare } from "./crypto.js"; import { KeyShare } from "./crypto.js";
import { RecoveryDocument } from "./recovery-document-types.js"; import { RecoveryDocument } from "./recovery-document-types.js";
@ -185,10 +186,6 @@ export interface ReducerStateRecovery {
authentication_providers?: { [url: string]: AuthenticationProviderStatus }; authentication_providers?: { [url: string]: AuthenticationProviderStatus };
} }
export interface ChallengeFeedback {
state: string;
}
export interface ReducerStateError { export interface ReducerStateError {
backup_state?: undefined; backup_state?: undefined;
recovery_state?: undefined; recovery_state?: undefined;
@ -311,21 +308,10 @@ export interface ActionArgSelectCountry {
currencies: string[]; currencies: string[];
} }
export const codecForActionArgSelectCountry = () =>
buildCodecForObject<ActionArgSelectCountry>()
.property("country_code", codecForString())
.property("currencies", codecForList(codecForString()))
.build("ActionArgSelectCountry");
export interface ActionArgsSelectChallenge { export interface ActionArgsSelectChallenge {
uuid: string; uuid: string;
} }
export const codecForActionArgSelectChallenge = () =>
buildCodecForObject<ActionArgsSelectChallenge>()
.property("uuid", codecForString())
.build("ActionArgSelectChallenge");
export type ActionArgsSolveChallengeRequest = SolveChallengeAnswerRequest; export type ActionArgsSolveChallengeRequest = SolveChallengeAnswerRequest;
export interface SolveChallengeAnswerRequest { export interface SolveChallengeAnswerRequest {
@ -341,6 +327,10 @@ export interface ActionArgsAddPolicy {
policy: PolicyMember[]; policy: PolicyMember[];
} }
export interface ActionArgsUpdateExpiration {
expiration: Timestamp;
}
export const codecForPolicyMember = () => export const codecForPolicyMember = () =>
buildCodecForObject<PolicyMember>() buildCodecForObject<PolicyMember>()
.property("authentication_method", codecForNumber()) .property("authentication_method", codecForNumber())
@ -352,11 +342,18 @@ export const codecForActionArgsAddPolicy = () =>
.property("policy", codecForList(codecForPolicyMember())) .property("policy", codecForList(codecForPolicyMember()))
.build("ActionArgsAddPolicy"); .build("ActionArgsAddPolicy");
export interface ActionArgsUpdateExpiration {
expiration: Timestamp;
}
export const codecForActionArgsUpdateExpiration = () => export const codecForActionArgsUpdateExpiration = () =>
buildCodecForObject<ActionArgsUpdateExpiration>() buildCodecForObject<ActionArgsUpdateExpiration>()
.property("expiration", codecForTimestamp) .property("expiration", codecForTimestamp)
.build("ActionArgsUpdateExpiration"); .build("ActionArgsUpdateExpiration");
export const codecForActionArgSelectChallenge = () =>
buildCodecForObject<ActionArgsSelectChallenge>()
.property("uuid", codecForString())
.build("ActionArgSelectChallenge");
export const codecForActionArgSelectCountry = () =>
buildCodecForObject<ActionArgSelectCountry>()
.property("country_code", codecForString())
.property("currencies", codecForList(codecForString()))
.build("ActionArgSelectCountry");