aboutsummaryrefslogtreecommitdiff
path: root/packages/anastasis-core/src/index.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-10-21 13:11:17 +0200
committerFlorian Dold <florian@dold.me>2021-10-21 13:11:33 +0200
commit0ee669f52341a8331394a1e9892264c0ef0bb7d7 (patch)
tree5a4d1a02ad6acd0dd04edde2dc032160c299700f /packages/anastasis-core/src/index.ts
parentcf25f5698e9f3a3b36930e545f7cce9829fb08f6 (diff)
downloadwallet-core-0ee669f52341a8331394a1e9892264c0ef0bb7d7.tar.xz
reducer WIP, user error boundaries in UI
Diffstat (limited to 'packages/anastasis-core/src/index.ts')
-rw-r--r--packages/anastasis-core/src/index.ts329
1 files changed, 263 insertions, 66 deletions
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index c99bd5b44..b8fedf006 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -2,6 +2,8 @@ import {
AmountString,
buildSigPS,
bytesToString,
+ Codec,
+ codecForAny,
decodeCrock,
eddsaSign,
encodeCrock,
@@ -24,6 +26,7 @@ import {
ActionArgEnterSecret,
ActionArgEnterSecretName,
ActionArgEnterUserAttributes,
+ ActionArgSelectChallenge,
AuthenticationProviderStatus,
AuthenticationProviderStatusOk,
AuthMethod,
@@ -33,6 +36,8 @@ import {
MethodSpec,
Policy,
PolicyProvider,
+ RecoveryInformation,
+ RecoveryInternalData,
RecoveryStates,
ReducerState,
ReducerStateBackup,
@@ -60,78 +65,15 @@ import {
UserIdentifier,
userIdentifierDerive,
typedArrayConcat,
+ decryptRecoveryDocument,
} from "./crypto.js";
-import { zlibSync } from "fflate";
+import { unzlibSync, zlibSync } from "fflate";
+import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
const { fetch, Request, Response, Headers } = fetchPonyfill({});
export * from "./reducer-types.js";
-interface RecoveryDocument {
- // Human-readable name of the secret
- secret_name?: string;
-
- // Encrypted core secret.
- encrypted_core_secret: string; // bytearray of undefined length
-
- // List of escrow providers and selected authentication method.
- escrow_methods: EscrowMethod[];
-
- // List of possible decryption policies.
- policies: DecryptionPolicy[];
-}
-
-interface DecryptionPolicy {
- // Salt included to encrypt master key share when
- // using this decryption policy.
- salt: string;
-
- /**
- * Master key, AES-encrypted with key derived from
- * salt and keyshares revealed by the following list of
- * escrow methods identified by UUID.
- */
- master_key: string;
-
- /**
- * List of escrow methods identified by their UUID.
- */
- uuids: string[];
-}
-
-interface EscrowMethod {
- /**
- * URL of the escrow provider (including possibly this Anastasis server).
- */
- url: string;
-
- /**
- * Type of the escrow method (e.g. security question, SMS etc.).
- */
- escrow_type: string;
-
- // UUID of the escrow method.
- // 16 bytes base32-crock encoded.
- uuid: TruthUuid;
-
- // Key used to encrypt the Truth this EscrowMethod is related to.
- // Client has to provide this key to the server when using /truth/.
- truth_key: TruthKey;
-
- /**
- * Salt to hash the security question answer if applicable.
- */
- truth_salt: TruthSalt;
-
- // Salt from the provider to derive the user ID
- // at this provider.
- provider_salt: string;
-
- // The instructions to give to the user (i.e. the security question
- // if this is challenge-response).
- instructions: string;
-}
-
function getContinents(): ContinentInfo[] {
const continentSet = new Set<string>();
const continents: ContinentInfo[] = [];
@@ -203,6 +145,41 @@ async function backupSelectCountry(
};
}
+async function recoverySelectCountry(
+ state: ReducerStateRecovery,
+ countryCode: string,
+ currencies: string[],
+): Promise<ReducerStateError | ReducerStateRecovery> {
+ const country = anastasisData.countriesList.countries.find(
+ (x) => x.code === countryCode,
+ );
+ if (!country) {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: "invalid country selected",
+ };
+ }
+
+ const providers: { [x: string]: {} } = {};
+ for (const prov of anastasisData.providersList.anastasis_provider) {
+ if (currencies.includes(prov.currency)) {
+ providers[prov.url] = {};
+ }
+ }
+
+ const ra = (anastasisData.countryDetails as any)[countryCode]
+ .required_attributes;
+
+ return {
+ ...state,
+ recovery_state: RecoveryStates.UserAttributesCollecting,
+ selected_country: countryCode,
+ currencies,
+ required_attributes: ra,
+ authentication_providers: providers,
+ };
+}
+
async function getProviderInfo(
providerBaseUrl: string,
): Promise<AuthenticationProviderStatus> {
@@ -436,6 +413,13 @@ async function compressRecoveryDoc(rd: any): Promise<Uint8Array> {
return typedArrayConcat([new Uint8Array(sizeHeaderBuf), zippedDoc]);
}
+async function uncompressRecoveryDoc(zippedRd: Uint8Array): Promise<any> {
+ const header = zippedRd.slice(0, 4);
+ const data = zippedRd.slice(4);
+ const res = unzlibSync(data);
+ return JSON.parse(bytesToString(res));
+}
+
async function uploadSecret(
state: ReducerStateBackup,
): Promise<ReducerStateBackup | ReducerStateError> {
@@ -632,6 +616,97 @@ async function uploadSecret(
};
}
+/**
+ * Download policy based on current user attributes and selected
+ * version in the state.
+ */
+async function downloadPolicy(
+ state: ReducerStateRecovery,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+ const providerUrls = Object.keys(state.authentication_providers ?? {});
+ let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
+ let recoveryDoc: RecoveryDocument | undefined = undefined;
+ const newProviderStatus: { [url: string]: AuthenticationProviderStatus } = {};
+ const userAttributes = state.identity_attributes!;
+ for (const url of providerUrls) {
+ const pi = await getProviderInfo(url);
+ if ("error_code" in pi || !("http_status" in pi)) {
+ // Could not even get /config of the provider
+ continue;
+ }
+ newProviderStatus[url] = pi;
+ const userId = await userIdentifierDerive(userAttributes, pi.salt);
+ const acctKeypair = accountKeypairDerive(userId);
+ const resp = await fetch(new URL(`policy/${acctKeypair.pub}`, url).href);
+ if (resp.status !== 200) {
+ continue;
+ }
+ const body = await resp.arrayBuffer();
+ const bodyDecrypted = await decryptRecoveryDocument(
+ userId,
+ encodeCrock(body),
+ );
+ const rd: RecoveryDocument = await uncompressRecoveryDoc(
+ decodeCrock(bodyDecrypted),
+ );
+ console.log("rd", rd);
+ let policyVersion = 0;
+ try {
+ policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
+ } catch (e) {}
+ foundRecoveryInfo = {
+ provider_url: url,
+ secret_name: rd.secret_name ?? "<unknown>",
+ version: policyVersion,
+ };
+ recoveryDoc = rd;
+ break;
+ }
+ if (!foundRecoveryInfo || !recoveryDoc) {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED,
+ hint: "No backups found at any provider for your identity information.",
+ };
+ }
+ const recoveryInfo: RecoveryInformation = {
+ challenges: recoveryDoc.escrow_methods.map((x) => {
+ console.log("providers", state.authentication_providers);
+ const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
+ return {
+ cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
+ instructions: x.instructions,
+ type: x.escrow_type,
+ uuid: x.uuid,
+ };
+ }),
+ policies: recoveryDoc.policies.map((x) => {
+ return x.uuids.map((m) => {
+ return {
+ uuid: m,
+ };
+ });
+ }),
+ };
+ return {
+ ...state,
+ recovery_state: RecoveryStates.SecretSelecting,
+ recovery_document: foundRecoveryInfo,
+ recovery_information: recoveryInfo,
+ };
+}
+
+async function recoveryEnterUserAttributes(
+ state: ReducerStateRecovery,
+ attributes: Record<string, string>,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+ // FIXME: validate attributes
+ const st: ReducerStateRecovery = {
+ ...state,
+ identity_attributes: attributes,
+ };
+ return downloadPolicy(st);
+}
+
export async function reduceAction(
state: ReducerState,
action: string,
@@ -827,6 +902,128 @@ export async function reduceAction(
};
}
}
+
+ if (state.recovery_state === RecoveryStates.ContinentSelecting) {
+ if (action === "select_continent") {
+ const continent: string = args.continent;
+ if (typeof continent !== "string") {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: "continent required",
+ };
+ }
+ return {
+ ...state,
+ recovery_state: RecoveryStates.CountrySelecting,
+ countries: getCountries(continent),
+ selected_continent: continent,
+ };
+ } else {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: `Unsupported action '${action}'`,
+ };
+ }
+ }
+
+ if (state.recovery_state === RecoveryStates.CountrySelecting) {
+ if (action === "back") {
+ return {
+ ...state,
+ recovery_state: RecoveryStates.ContinentSelecting,
+ countries: undefined,
+ };
+ } else if (action === "select_country") {
+ const countryCode = args.country_code;
+ if (typeof countryCode !== "string") {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: "country_code required",
+ };
+ }
+ const currencies = args.currencies;
+ return recoverySelectCountry(state, countryCode, currencies);
+ } else {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: `Unsupported action '${action}'`,
+ };
+ }
+ }
+
+ if (state.recovery_state === RecoveryStates.UserAttributesCollecting) {
+ if (action === "back") {
+ return {
+ ...state,
+ recovery_state: RecoveryStates.CountrySelecting,
+ };
+ } else if (action === "enter_user_attributes") {
+ const ta = args as ActionArgEnterUserAttributes;
+ return recoveryEnterUserAttributes(state, ta.identity_attributes);
+ } else {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: `Unsupported action '${action}'`,
+ };
+ }
+ }
+
+ if (state.recovery_state === RecoveryStates.SecretSelecting) {
+ if (action === "back") {
+ return {
+ ...state,
+ recovery_state: RecoveryStates.UserAttributesCollecting,
+ };
+ } else if (action === "next") {
+ return {
+ ...state,
+ recovery_state: RecoveryStates.ChallengeSelecting,
+ };
+ } else {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: `Unsupported action '${action}'`,
+ };
+ }
+ }
+
+ if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
+ if (action === "select_challenge") {
+ const ta: ActionArgSelectChallenge = args;
+ return {
+ ...state,
+ recovery_state: RecoveryStates.ChallengeSolving,
+ selected_challenge_uuid: ta.uuid,
+ };
+ } else if (action === "back") {
+ return {
+ ...state,
+ recovery_state: RecoveryStates.SecretSelecting,
+ };
+ } else {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: `Unsupported action '${action}'`,
+ };
+ }
+ }
+
+ if (state.recovery_state === RecoveryStates.ChallengeSolving) {
+ if (action === "back") {
+ const ta: ActionArgSelectChallenge = args;
+ return {
+ ...state,
+ selected_challenge_uuid: undefined,
+ recovery_state: RecoveryStates.ChallengeSelecting,
+ };
+ } else {
+ return {
+ code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+ hint: `Unsupported action '${action}'`,
+ };
+ }
+ }
+
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "Reducer action invalid",