summaryrefslogtreecommitdiff
path: root/packages/anastasis-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-11-03 13:34:57 +0100
committerFlorian Dold <florian@dold.me>2021-11-03 13:34:57 +0100
commit04356cd23fef76d2020338d2b2b394095fdc2b14 (patch)
tree2c3081305fcdbc248a462d26fca37a54baa27a2f /packages/anastasis-core
parentab6fd6c8c72ac674648ef66d7bcec01f7a232410 (diff)
downloadwallet-core-04356cd23fef76d2020338d2b2b394095fdc2b14.tar.gz
wallet-core-04356cd23fef76d2020338d2b2b394095fdc2b14.tar.bz2
wallet-core-04356cd23fef76d2020338d2b2b394095fdc2b14.zip
anastasis: refactor feedback types
Diffstat (limited to 'packages/anastasis-core')
-rw-r--r--packages/anastasis-core/src/challenge-feedback-types.ts149
-rw-r--r--packages/anastasis-core/src/index.ts150
-rw-r--r--packages/anastasis-core/src/reducer-types.ts35
3 files changed, 249 insertions, 85 deletions
diff --git a/packages/anastasis-core/src/challenge-feedback-types.ts b/packages/anastasis-core/src/challenge-feedback-types.ts
new file mode 100644
index 000000000..d6a2e3e80
--- /dev/null
+++ b/packages/anastasis-core/src/challenge-feedback-types.ts
@@ -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;
+}
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index db99db610..859dd083b 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -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);
- // FIXME: This isn't correct for non-question truth responses.
- url.searchParams.set(
- "response",
- await secureAnswerHash(ta.answer, truth.uuid, truth.truth_salt),
- );
+ if (solveRequest) {
+ // FIXME: This isn't correct for non-question truth responses.
+ url.searchParams.set(
+ "response",
+ await secureAnswerHash(solveRequest.answer, truth.uuid, truth.truth_salt),
+ );
+ }
const resp = await fetch(url.href, {
headers: {
@@ -710,48 +709,79 @@ 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;
- }
+ if (resp.status === HttpStatusCode.Ok) {
+ const answerSalt =
+ solveRequest && truth.escrow_type === "question"
+ ? solveRequest.answer
+ : undefined;
- const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined;
+ const userId = await userIdentifierDerive(
+ state.identity_attributes,
+ truth.provider_salt,
+ );
- const userId = await userIdentifierDerive(
- state.identity_attributes,
- truth.provider_salt,
- );
+ const respBody = new Uint8Array(await resp.arrayBuffer());
+ const keyShare = await decryptKeyShare(
+ encodeCrock(respBody),
+ userId,
+ answerSalt,
+ );
- const respBody = new Uint8Array(await resp.arrayBuffer());
- const keyShare = await decryptKeyShare(
- encodeCrock(respBody),
- userId,
- answerSalt,
- );
+ const recoveredKeyShares = {
+ ...(state.recovered_key_shares ?? {}),
+ [truth.uuid]: keyShare,
+ };
- const recoveredKeyShares = {
- ...(state.recovered_key_shares ?? {}),
- [truth.uuid]: keyShare,
- };
+ const challengeFeedback: { [x: string]: ChallengeFeedback } = {
+ ...state.challenge_feedback,
+ [truth.uuid]: {
+ state: ChallengeFeedbackStatus.Solved,
+ },
+ };
- const challengeFeedback = {
- ...state.challenge_feedback,
- [truth.uuid]: {
- state: "solved",
- },
- };
+ const newState: ReducerStateRecovery = {
+ ...state,
+ recovery_state: RecoveryStates.ChallengeSelecting,
+ challenge_feedback: challengeFeedback,
+ recovered_key_shares: recoveredKeyShares,
+ };
- const newState: ReducerStateRecovery = {
- ...state,
- recovery_state: RecoveryStates.ChallengeSelecting,
- challenge_feedback: challengeFeedback,
- recovered_key_shares: recoveredKeyShares,
- };
+ 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 tryRecoverSecret(newState);
+ 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(
diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts
index 948268704..69feb6b64 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -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");