anastasis: refactor feedback types
This commit is contained in:
parent
ab6fd6c8c7
commit
04356cd23f
149
packages/anastasis-core/src/challenge-feedback-types.ts
Normal file
149
packages/anastasis-core/src/challenge-feedback-types.ts
Normal 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;
|
||||
}
|
@ -11,10 +11,9 @@ import {
|
||||
Duration,
|
||||
eddsaSign,
|
||||
encodeCrock,
|
||||
getDurationRemaining,
|
||||
getRandomBytes,
|
||||
getTimestampNow,
|
||||
hash,
|
||||
HttpStatusCode,
|
||||
j2s,
|
||||
Logger,
|
||||
stringToBytes,
|
||||
@ -91,6 +90,7 @@ import {
|
||||
import { unzlibSync, zlibSync } from "fflate";
|
||||
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
|
||||
import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js";
|
||||
import { ChallengeFeedback, ChallengeFeedbackStatus } from "./challenge-feedback-types.js";
|
||||
|
||||
const { fetch } = fetchPonyfill({});
|
||||
|
||||
@ -291,7 +291,6 @@ async function backupEnterUserAttributes(
|
||||
return newState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Truth data as stored in the reducer.
|
||||
*/
|
||||
@ -551,6 +550,7 @@ async function uploadSecret(
|
||||
|
||||
return {
|
||||
...state,
|
||||
core_secret: undefined,
|
||||
backup_state: BackupStates.BackupFinished,
|
||||
success_details: successDetails,
|
||||
};
|
||||
@ -684,25 +684,24 @@ async function tryRecoverSecret(
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
async function solveChallenge(
|
||||
/**
|
||||
* Request a truth, optionally with a challenge solution
|
||||
* provided by the user.
|
||||
*/
|
||||
async function requestTruth(
|
||||
state: ReducerStateRecovery,
|
||||
ta: ActionArgsSolveChallengeRequest,
|
||||
truth: EscrowMethod,
|
||||
solveRequest?: 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 "truth for challenge not found";
|
||||
}
|
||||
|
||||
const url = new URL(`/truth/${truth.uuid}`, truth.url);
|
||||
|
||||
if (solveRequest) {
|
||||
// FIXME: This isn't correct for non-question truth responses.
|
||||
url.searchParams.set(
|
||||
"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, {
|
||||
headers: {
|
||||
@ -710,15 +709,11 @@ async function solveChallenge(
|
||||
},
|
||||
});
|
||||
|
||||
if (resp.status !== 200) {
|
||||
return {
|
||||
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
|
||||
hint: "got non-200 response",
|
||||
http_status: resp.status,
|
||||
} as ReducerStateError;
|
||||
}
|
||||
|
||||
const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined;
|
||||
if (resp.status === HttpStatusCode.Ok) {
|
||||
const answerSalt =
|
||||
solveRequest && truth.escrow_type === "question"
|
||||
? solveRequest.answer
|
||||
: undefined;
|
||||
|
||||
const userId = await userIdentifierDerive(
|
||||
state.identity_attributes,
|
||||
@ -737,10 +732,10 @@ async function solveChallenge(
|
||||
[truth.uuid]: keyShare,
|
||||
};
|
||||
|
||||
const challengeFeedback = {
|
||||
const challengeFeedback: { [x: string]: ChallengeFeedback } = {
|
||||
...state.challenge_feedback,
|
||||
[truth.uuid]: {
|
||||
state: "solved",
|
||||
state: ChallengeFeedbackStatus.Solved,
|
||||
},
|
||||
};
|
||||
|
||||
@ -752,6 +747,41 @@ async function solveChallenge(
|
||||
};
|
||||
|
||||
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(
|
||||
@ -776,19 +806,7 @@ async function selectChallenge(
|
||||
throw "truth for challenge not found";
|
||||
}
|
||||
|
||||
const url = new URL(`/truth/${truth.uuid}`, truth.url);
|
||||
|
||||
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,
|
||||
};
|
||||
return requestTruth({ ...state, selected_challenge_uuid: ta.uuid }, truth);
|
||||
}
|
||||
|
||||
async function backupSelectContinent(
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
codecForTimestamp,
|
||||
Timestamp,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { ChallengeFeedback } from "./challenge-feedback-types.js";
|
||||
import { KeyShare } from "./crypto.js";
|
||||
import { RecoveryDocument } from "./recovery-document-types.js";
|
||||
|
||||
@ -185,10 +186,6 @@ export interface ReducerStateRecovery {
|
||||
authentication_providers?: { [url: string]: AuthenticationProviderStatus };
|
||||
}
|
||||
|
||||
export interface ChallengeFeedback {
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface ReducerStateError {
|
||||
backup_state?: undefined;
|
||||
recovery_state?: undefined;
|
||||
@ -311,21 +308,10 @@ export interface ActionArgSelectCountry {
|
||||
currencies: string[];
|
||||
}
|
||||
|
||||
export const codecForActionArgSelectCountry = () =>
|
||||
buildCodecForObject<ActionArgSelectCountry>()
|
||||
.property("country_code", codecForString())
|
||||
.property("currencies", codecForList(codecForString()))
|
||||
.build("ActionArgSelectCountry");
|
||||
|
||||
export interface ActionArgsSelectChallenge {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export const codecForActionArgSelectChallenge = () =>
|
||||
buildCodecForObject<ActionArgsSelectChallenge>()
|
||||
.property("uuid", codecForString())
|
||||
.build("ActionArgSelectChallenge");
|
||||
|
||||
export type ActionArgsSolveChallengeRequest = SolveChallengeAnswerRequest;
|
||||
|
||||
export interface SolveChallengeAnswerRequest {
|
||||
@ -341,6 +327,10 @@ export interface ActionArgsAddPolicy {
|
||||
policy: PolicyMember[];
|
||||
}
|
||||
|
||||
export interface ActionArgsUpdateExpiration {
|
||||
expiration: Timestamp;
|
||||
}
|
||||
|
||||
export const codecForPolicyMember = () =>
|
||||
buildCodecForObject<PolicyMember>()
|
||||
.property("authentication_method", codecForNumber())
|
||||
@ -352,11 +342,18 @@ export const codecForActionArgsAddPolicy = () =>
|
||||
.property("policy", codecForList(codecForPolicyMember()))
|
||||
.build("ActionArgsAddPolicy");
|
||||
|
||||
export interface ActionArgsUpdateExpiration {
|
||||
expiration: Timestamp;
|
||||
}
|
||||
|
||||
export const codecForActionArgsUpdateExpiration = () =>
|
||||
buildCodecForObject<ActionArgsUpdateExpiration>()
|
||||
.property("expiration", codecForTimestamp)
|
||||
.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");
|
||||
|
Loading…
Reference in New Issue
Block a user