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,
|
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(
|
||||||
|
@ -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");
|
||||||
|
Loading…
Reference in New Issue
Block a user