gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 03/05: reducer implementation WIP


From: gnunet
Subject: [taler-wallet-core] 03/05: reducer implementation WIP
Date: Mon, 18 Oct 2021 21:48:53 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

commit b1034801d124b53cbb683e4a430ac00c7979bca1
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Oct 18 19:18:34 2021 +0200

    reducer implementation WIP
---
 packages/anastasis-core/package.json          |   8 +-
 packages/anastasis-core/src/crypto.ts         | 152 +++++-
 packages/anastasis-core/src/index.ts          | 701 +++++++++++++++++++++++++-
 packages/anastasis-core/src/provider-types.ts |  74 +++
 packages/anastasis-core/src/reducer-types.ts  | 241 +++++++++
 packages/anastasis-core/tsconfig.json         |   2 +-
 6 files changed, 1154 insertions(+), 24 deletions(-)

diff --git a/packages/anastasis-core/package.json 
b/packages/anastasis-core/package.json
index acc46f7c..f4b611ed 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -2,7 +2,9 @@
   "name": "anastasis-core",
   "version": "0.0.1",
   "description": "",
-  "main": "index.js",
+  "main": "./lib/index.js",
+  "module": "./lib/index.js",
+  "types": "./lib/index.d.ts",
   "scripts": {
     "prepare": "tsc",
     "compile": "tsc",
@@ -20,7 +22,9 @@
   },
   "dependencies": {
     "@gnu-taler/taler-util": "workspace:^0.8.3",
-    "hash-wasm": "^4.9.0"
+    "fetch-ponyfill": "^7.1.0",
+    "hash-wasm": "^4.9.0",
+    "node-fetch": "^3.0.0"
   },
   "ava": {
     "files": [
diff --git a/packages/anastasis-core/src/crypto.ts 
b/packages/anastasis-core/src/crypto.ts
index c20d323a..5da3a4cc 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -1,15 +1,44 @@
 import {
+  bytesToString,
   canonicalJson,
   decodeCrock,
   encodeCrock,
+  getRandomBytes,
+  kdf,
+  secretbox,
   stringToBytes,
 } from "@gnu-taler/taler-util";
 import { argon2id } from "hash-wasm";
 
+export type Flavor<T, FlavorT> = T & { _flavor?: FlavorT };
+export type FlavorP<T, FlavorT, S extends number> = T & {
+  _flavor?: FlavorT;
+  _size?: S;
+};
+
+export type UserIdentifier = Flavor<string, "UserIdentifier">;
+export type ServerSalt = Flavor<string, "ServerSalt">;
+export type PolicySalt = Flavor<string, "PolicySalt">;
+export type PolicyKey = FlavorP<string, "PolicyKey", 64>;
+export type KeyShare = Flavor<string, "KeyShare">;
+export type EncryptedKeyShare = Flavor<string, "EncryptedKeyShare">;
+export type EncryptedTruth = Flavor<string, "EncryptedTruth">;
+export type EncryptedCoreSecret = Flavor<string, "EncryptedCoreSecret">;
+export type EncryptedMasterKey = Flavor<string, "EncryptedMasterKey">;
+/**
+ * Truth key, found in the recovery document.
+ */
+export type TruthKey = Flavor<string, "TruthKey">;
+export type EncryptionNonce = Flavor<string, "EncryptionNonce">;
+export type OpaqueData = Flavor<string, "OpaqueData">;
+
+const nonceSize = 24;
+const masterKeySize = 64;
+
 export async function userIdentifierDerive(
   idData: any,
-  serverSalt: string,
-): Promise<string> {
+  serverSalt: ServerSalt,
+): Promise<UserIdentifier> {
   const canonIdData = canonicalJson(idData);
   const hashInput = stringToBytes(canonIdData);
   const result = await argon2id({
@@ -24,15 +53,114 @@ export async function userIdentifierDerive(
   return encodeCrock(result);
 }
 
-// interface Keypair {
-//   pub: string;
-//   priv: string;
-// }
+function taConcat(chunks: Uint8Array[]): Uint8Array {
+  let payloadLen = 0;
+  for (const c of chunks) {
+    payloadLen += c.byteLength;
+  }
+  const buf = new ArrayBuffer(payloadLen);
+  const u8buf = new Uint8Array(buf);
+  let p = 0;
+  for (const c of chunks) {
+    u8buf.set(c, p);
+    p += c.byteLength;
+  }
+  return u8buf;
+}
 
-// async function accountKeypairDerive(): Promise<Keypair> {}
+export async function policyKeyDerive(
+  keyShares: KeyShare[],
+  policySalt: PolicySalt,
+): Promise<PolicyKey> {
+  const chunks = keyShares.map((x) => decodeCrock(x));
+  const polKey = kdf(
+    64,
+    taConcat(chunks),
+    decodeCrock(policySalt),
+    new Uint8Array(0),
+  );
+  return encodeCrock(polKey);
+}
+
+async function deriveKey(
+  keySeed: OpaqueData,
+  nonce: EncryptionNonce,
+  salt: string,
+): Promise<Uint8Array> {
+  return kdf(32, decodeCrock(keySeed), stringToBytes(salt), 
decodeCrock(nonce));
+}
+
+async function anastasisEncrypt(
+  nonce: EncryptionNonce,
+  keySeed: OpaqueData,
+  plaintext: OpaqueData,
+  salt: string,
+): Promise<OpaqueData> {
+  const key = await deriveKey(keySeed, nonce, salt);
+  const nonceBuf = decodeCrock(nonce);
+  const cipherText = secretbox(decodeCrock(plaintext), decodeCrock(nonce), 
key);
+  return encodeCrock(taConcat([nonceBuf, cipherText]));
+}
 
-// async function secureAnswerHash(
-//   answer: string,
-//   truthUuid: string,
-//   questionSalt: string,
-// ): Promise<string> {}
+const asOpaque = (x: string): OpaqueData => x;
+const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
+const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
+
+export async function encryptKeyshare(
+  keyShare: KeyShare,
+  userId: UserIdentifier,
+  answerSalt?: string,
+): Promise<EncryptedKeyShare> {
+  const s = answerSalt ?? "eks";
+  const nonce = encodeCrock(getRandomBytes(24));
+  return asEncryptedKeyShare(
+    await anastasisEncrypt(nonce, asOpaque(userId), asOpaque(keyShare), s),
+  );
+}
+
+export async function encryptTruth(
+  nonce: EncryptionNonce,
+  truthEncKey: TruthKey,
+  truth: OpaqueData,
+): Promise<EncryptedTruth> {
+  const salt = "ect";
+  return asEncryptedTruth(
+    await anastasisEncrypt(nonce, asOpaque(truthEncKey), truth, salt),
+  );
+}
+
+export interface CoreSecretEncResult {
+  encCoreSecret: EncryptedCoreSecret;
+  encMasterKeys: EncryptedMasterKey[];
+}
+
+export async function coreSecretEncrypt(
+  policyKeys: PolicyKey[],
+  coreSecret: OpaqueData,
+): Promise<CoreSecretEncResult> {
+  const masterKey = getRandomBytes(masterKeySize);
+  const nonce = encodeCrock(getRandomBytes(nonceSize));
+  const coreSecretEncSalt = "cse";
+  const masterKeyEncSalt = "emk";
+  const encCoreSecret = (await anastasisEncrypt(
+    nonce,
+    encodeCrock(masterKey),
+    coreSecret,
+    coreSecretEncSalt,
+  )) as string;
+  const encMasterKeys: EncryptedMasterKey[] = [];
+  for (let i = 0; i < policyKeys.length; i++) {
+    const polNonce = encodeCrock(getRandomBytes(nonceSize));
+    const encMasterKey = await anastasisEncrypt(
+      polNonce,
+      asOpaque(policyKeys[i]),
+      encodeCrock(masterKey),
+      masterKeyEncSalt,
+    );
+    encMasterKeys.push(encMasterKey as string);
+  }
+  return {
+    encCoreSecret,
+    encMasterKeys,
+  };
+}
diff --git a/packages/anastasis-core/src/index.ts 
b/packages/anastasis-core/src/index.ts
index 7a14440a..f33a0be4 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -1,14 +1,697 @@
-import { md5, sha1, sha512, sha3 } from 'hash-wasm';
+import {
+  AmountString,
+  codecForGetExchangeWithdrawalInfo,
+  decodeCrock,
+  encodeCrock,
+  getRandomBytes,
+  TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import { anastasisData } from "./anastasis-data.js";
+import {
+  EscrowConfigurationResponse,
+  TruthUploadRequest,
+} from "./provider-types.js";
+import {
+  ActionArgAddAuthentication,
+  ActionArgDeleteAuthentication,
+  ActionArgDeletePolicy,
+  ActionArgEnterSecret,
+  ActionArgEnterSecretName,
+  ActionArgEnterUserAttributes,
+  AuthenticationProviderStatus,
+  AuthenticationProviderStatusOk,
+  AuthMethod,
+  BackupStates,
+  ContinentInfo,
+  CountryInfo,
+  MethodSpec,
+  Policy,
+  PolicyProvider,
+  RecoveryStates,
+  ReducerState,
+  ReducerStateBackup,
+  ReducerStateBackupUserAttributesCollecting,
+  ReducerStateError,
+  ReducerStateRecovery,
+} from "./reducer-types.js";
+import fetchPonyfill from "fetch-ponyfill";
+import {
+  coreSecretEncrypt,
+  encryptKeyshare,
+  encryptTruth,
+  PolicyKey,
+  policyKeyDerive,
+  UserIdentifier,
+  userIdentifierDerive,
+} from "./crypto.js";
 
-async function run() {
-  console.log('MD5:', await md5('demo'));
+const { fetch, Request, Response, Headers } = fetchPonyfill({});
 
-  const int8Buffer = new Uint8Array([0, 1, 2, 3]);
-  console.log('SHA1:', await sha1(int8Buffer));
-  console.log('SHA512:', await sha512(int8Buffer));
+export * from "./reducer-types.js";
 
-  const int32Buffer = new Uint32Array([1056, 641]);
-  console.log('SHA3-256:', await sha3(int32Buffer, 256));
+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.
+   */
+  uuid: 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 (see /truth/ API below).
+  // 16 bytes base32-crock encoded.
+  uuid: string;
+
+  // 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: string;
+
+  // Salt used to encrypt the truth on the Anastasis server.
+  salt: string;
+
+  // 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).
+  // (Q: as string in base32 encoding?)
+  // (Q: what is the mime-type of this value?)
+  //
+  // The plaintext challenge is not revealed to the
+  // Anastasis server.
+  instructions: string;
+}
+
+function getContinents(): ContinentInfo[] {
+  const continentSet = new Set<string>();
+  const continents: ContinentInfo[] = [];
+  for (const country of anastasisData.countriesList.countries) {
+    if (continentSet.has(country.continent)) {
+      continue;
+    }
+    continentSet.add(country.continent);
+    continents.push({
+      ...{ name_i18n: country.continent_i18n },
+      name: country.continent,
+    });
+  }
+  return continents;
+}
+
+function getCountries(continent: string): CountryInfo[] {
+  return anastasisData.countriesList.countries.filter(
+    (x) => x.continent === continent,
+  );
+}
+
+export async function getBackupStartState(): Promise<ReducerStateBackup> {
+  return {
+    backup_state: BackupStates.ContinentSelecting,
+    continents: getContinents(),
+  };
+}
+
+export async function getRecoveryStartState(): Promise<ReducerStateRecovery> {
+  return {
+    recovery_state: RecoveryStates.ContinentSelecting,
+    continents: getContinents(),
+  };
+}
+
+async function backupSelectCountry(
+  state: ReducerStateBackup,
+  countryCode: string,
+  currencies: string[],
+): Promise<ReducerStateError | ReducerStateBackupUserAttributesCollecting> {
+  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,
+    backup_state: BackupStates.UserAttributesCollecting,
+    selected_country: countryCode,
+    currencies,
+    required_attributes: ra,
+    authentication_providers: providers,
+  };
+}
+
+async function getProviderInfo(
+  providerBaseUrl: string,
+): Promise<AuthenticationProviderStatus> {
+  // FIXME: Use a reasonable timeout here.
+  let resp: Response;
+  try {
+    resp = await fetch(new URL("config", providerBaseUrl).href);
+  } catch (e) {
+    return {
+      code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
+      hint: "request to provider failed",
+    };
+  }
+  if (resp.status !== 200) {
+    return {
+      code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
+      hint: "unexpected status",
+      http_status: resp.status,
+    };
+  }
+  try {
+    const jsonResp: EscrowConfigurationResponse = await resp.json();
+    return {
+      http_status: 200,
+      annual_fee: jsonResp.annual_fee,
+      business_name: jsonResp.business_name,
+      currency: jsonResp.currency,
+      liability_limit: jsonResp.liability_limit,
+      methods: jsonResp.methods.map((x) => ({
+        type: x.type,
+        usage_fee: x.cost,
+      })),
+      salt: jsonResp.server_salt,
+      storage_limit_in_megabytes: jsonResp.storage_limit_in_megabytes,
+      truth_upload_fee: jsonResp.truth_upload_fee,
+    } as AuthenticationProviderStatusOk;
+  } catch (e) {
+    return {
+      code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
+      hint: "provider did not return JSON",
+    };
+  }
+}
+
+async function backupEnterUserAttributes(
+  state: ReducerStateBackup,
+  attributes: Record<string, string>,
+): Promise<ReducerStateBackup> {
+  const providerUrls = Object.keys(state.authentication_providers ?? {});
+  const newProviders = state.authentication_providers ?? {};
+  for (const url of providerUrls) {
+    newProviders[url] = await getProviderInfo(url);
+  }
+  const newState = {
+    ...state,
+    backup_state: BackupStates.AuthenticationsEditing,
+    authentication_providers: newProviders,
+    identity_attributes: attributes,
+  };
+  return newState;
 }
 
-run();
\ No newline at end of file
+interface PolicySelectionResult {
+  policies: Policy[];
+  policy_providers: PolicyProvider[];
+}
+
+type MethodSelection = number[];
+
+function enumerateSelections(n: number, m: number): MethodSelection[] {
+  const selections: MethodSelection[] = [];
+  const a = new Array(n);
+  const sel = (i: number) => {
+    if (i === n) {
+      selections.push([...a]);
+      return;
+    }
+    const start = i == 0 ? 0 : a[i - 1] + 1;
+    for (let j = start; j < m; j++) {
+      a[i] = j;
+      sel(i + 1);
+    }
+  };
+  sel(0);
+  return selections;
+}
+
+/**
+ * Provider information used during provider/method mapping.
+ */
+interface ProviderInfo {
+  url: string;
+  methodCost: Record<string, AmountString>;
+}
+
+/**
+ * Assign providers to a method selection.
+ */
+function assignProviders(
+  methods: AuthMethod[],
+  providers: ProviderInfo[],
+  methodSelection: number[],
+): Policy | undefined {
+  const selectedProviders: string[] = [];
+  for (const mi of methodSelection) {
+    const m = methods[mi];
+    let found = false;
+    for (const prov of providers) {
+      if (prov.methodCost[m.type]) {
+        selectedProviders.push(prov.url);
+        found = true;
+        break;
+      }
+    }
+    if (!found) {
+      /* No provider found for this method */
+      return undefined;
+    }
+  }
+  return {
+    methods: methodSelection.map((x, i) => {
+      return {
+        authentication_method: x,
+        provider: selectedProviders[i],
+      };
+    }),
+  };
+}
+
+function suggestPolicies(
+  methods: AuthMethod[],
+  providers: ProviderInfo[],
+): PolicySelectionResult {
+  const numMethods = methods.length;
+  if (numMethods === 0) {
+    throw Error("no methods");
+  }
+  let numSel: number;
+  if (numMethods <= 2) {
+    numSel = numMethods;
+  } else if (numMethods <= 4) {
+    numSel = numMethods - 1;
+  } else if (numMethods <= 6) {
+    numSel = numMethods - 2;
+  } else if (numMethods == 7) {
+    numSel = numMethods - 3;
+  } else {
+    numSel = 4;
+  }
+  const policies: Policy[] = [];
+  const selections = enumerateSelections(numSel, numMethods);
+  console.log("selections", selections);
+  for (const sel of selections) {
+    const p = assignProviders(methods, providers, sel);
+    if (p) {
+      policies.push(p);
+    }
+  }
+  return {
+    policies,
+    policy_providers: providers.map((x) => ({
+      provider_url: x.url,
+    })),
+  };
+}
+
+/**
+ * Truth data as stored in the reducer.
+ */
+interface TruthMetaData {
+  uuid: string;
+
+  key_share: string;
+
+  policy_index: number;
+
+  pol_method_index: number;
+
+  /**
+   * Nonce used for encrypting the truth.
+   */
+  nonce: string;
+
+  /**
+   * Key that the truth (i.e. secret question answer, email address, mobile 
number, ...)
+   * is encrypted with when stored at the provider.
+   */
+  truth_key: string;
+
+  /**
+   * Truth-specific salt.
+   */
+  salt: string;
+}
+
+async function uploadSecret(
+  state: ReducerStateBackup,
+): Promise<ReducerStateBackup | ReducerStateError> {
+  const policies = state.policies!;
+  const secretName = state.secret_name!;
+  const coreSecret = state.core_secret?.value!;
+  // Truth key is `${methodIndex}/${providerUrl}`
+  const truthMetadataMap: Record<string, TruthMetaData> = {};
+  const policyKeys: PolicyKey[] = [];
+
+  for (let policyIndex = 0; policyIndex < policies.length; policyIndex++) {
+    const pol = policies[policyIndex];
+    const policySalt = encodeCrock(getRandomBytes(64));
+    const keyShares: string[] = [];
+    for (let methIndex = 0; methIndex < pol.methods.length; methIndex++) {
+      const meth = pol.methods[methIndex];
+      const truthKey = `${meth.authentication_method}:${meth.provider}`;
+      if (truthMetadataMap[truthKey]) {
+        continue;
+      }
+      const keyShare = encodeCrock(getRandomBytes(32));
+      keyShares.push(keyShare);
+      const tm: TruthMetaData = {
+        key_share: keyShare,
+        nonce: encodeCrock(getRandomBytes(24)),
+        salt: encodeCrock(getRandomBytes(16)),
+        truth_key: encodeCrock(getRandomBytes(32)),
+        uuid: encodeCrock(getRandomBytes(32)),
+        pol_method_index: methIndex,
+        policy_index: policyIndex,
+      };
+      truthMetadataMap[truthKey] = tm;
+    }
+    const policyKey = await policyKeyDerive(keyShares, policySalt);
+    policyKeys.push(policyKey);
+  }
+
+  const csr = await coreSecretEncrypt(policyKeys, coreSecret);
+
+  const uidMap: Record<string, UserIdentifier> = {};
+  for (const prov of state.policy_providers!) {
+    const provider = state.authentication_providers![
+      prov.provider_url
+    ] as AuthenticationProviderStatusOk;
+    uidMap[prov.provider_url] = await userIdentifierDerive(
+      state.identity_attributes!,
+      provider.salt,
+    );
+  }
+
+  const escrowMethods: EscrowMethod[] = [];
+
+  for (const truthKey of Object.keys(truthMetadataMap)) {
+    const tm = truthMetadataMap[truthKey];
+    const pol = state.policies![tm.policy_index];
+    const meth = pol.methods[tm.pol_method_index];
+    const authMethod =
+      state.authentication_methods![meth.authentication_method];
+    const provider = state.authentication_providers![
+      meth.provider
+    ] as AuthenticationProviderStatusOk;
+    const encryptedTruth = await encryptTruth(
+      tm.nonce,
+      tm.truth_key,
+      authMethod.challenge,
+    );
+    const uid = uidMap[meth.provider];
+    const encryptedKeyShare = await encryptKeyshare(tm.key_share, uid, 
tm.salt);
+    console.log(
+      "encrypted key share len",
+      decodeCrock(encryptedKeyShare).length,
+    );
+    const tur: TruthUploadRequest = {
+      encrypted_truth: encryptedTruth,
+      key_share_data: encryptedKeyShare,
+      storage_duration_years: 5 /* FIXME */,
+      type: authMethod.type,
+      truth_mime: authMethod.mime_type,
+    };
+    const resp = await fetch(new URL(`truth/${tm.uuid}`, meth.provider).href, {
+      method: "POST",
+      headers: {
+        "content-type": "application/json",
+      },
+      body: JSON.stringify(tur),
+    });
+
+    escrowMethods.push({
+      escrow_type: authMethod.type,
+      instructions: authMethod.instructions,
+      provider_salt: provider.salt,
+      salt: tm.salt,
+      truth_key: tm.truth_key,
+      url: meth.provider,
+      uuid: tm.uuid,
+    });
+  }
+
+  // FIXME: We need to store the truth metadata in
+  // the state, since it's possible that we'll run into
+  // a provider that requests a payment.
+
+  const rd: RecoveryDocument = {
+    secret_name: secretName,
+    encrypted_core_secret: csr.encCoreSecret,
+    escrow_methods: escrowMethods,
+    policies: policies.map((x, i) => {
+      return {
+        master_key: csr.encMasterKeys[i],
+        uuid: [],
+        salt: 
+      };
+    }),
+  };
+
+  for (const prov of state.policy_providers!) {
+    // FIXME: Upload recovery document.
+  }
+
+  return {
+    code: 123,
+    hint: "not implemented",
+  };
+}
+
+export async function reduceAction(
+  state: ReducerState,
+  action: string,
+  args: any,
+): Promise<ReducerState> {
+  console.log(`ts reducer: handling action ${action}`);
+  if (state.backup_state === BackupStates.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,
+        backup_state: BackupStates.CountrySelecting,
+        countries: getCountries(continent),
+        selected_continent: continent,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+  if (state.backup_state === BackupStates.CountrySelecting) {
+    if (action === "back") {
+      return {
+        ...state,
+        backup_state: BackupStates.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 backupSelectCountry(state, countryCode, currencies);
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+  if (state.backup_state === BackupStates.UserAttributesCollecting) {
+    if (action === "back") {
+      return {
+        ...state,
+        backup_state: BackupStates.CountrySelecting,
+      };
+    } else if (action === "enter_user_attributes") {
+      const ta = args as ActionArgEnterUserAttributes;
+      return backupEnterUserAttributes(state, ta.identity_attributes);
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+  if (state.backup_state === BackupStates.AuthenticationsEditing) {
+    if (action === "back") {
+      return {
+        ...state,
+        backup_state: BackupStates.UserAttributesCollecting,
+      };
+    } else if (action === "add_authentication") {
+      const ta = args as ActionArgAddAuthentication;
+      return {
+        ...state,
+        authentication_methods: [
+          ...(state.authentication_methods ?? []),
+          ta.authentication_method,
+        ],
+      };
+    } else if (action === "delete_authentication") {
+      const ta = args as ActionArgDeleteAuthentication;
+      const m = state.authentication_methods ?? [];
+      m.splice(ta.authentication_method, 1);
+      return {
+        ...state,
+        authentication_methods: m,
+      };
+    } else if (action === "next") {
+      const methods = state.authentication_methods ?? [];
+      const providers: ProviderInfo[] = [];
+      for (const provUrl of Object.keys(state.authentication_providers ?? {})) 
{
+        const prov = state.authentication_providers![provUrl];
+        if ("error_code" in prov) {
+          continue;
+        }
+        if (!("http_status" in prov && prov.http_status === 200)) {
+          continue;
+        }
+        const methodCost: Record<string, AmountString> = {};
+        for (const meth of prov.methods) {
+          methodCost[meth.type] = meth.usage_fee;
+        }
+        providers.push({
+          methodCost,
+          url: provUrl,
+        });
+      }
+      const pol = suggestPolicies(methods, providers);
+      console.log("policies", pol);
+      return {
+        ...state,
+        backup_state: BackupStates.PoliciesReviewing,
+        ...pol,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+  if (state.backup_state === BackupStates.PoliciesReviewing) {
+    if (action === "back") {
+      return {
+        ...state,
+        backup_state: BackupStates.AuthenticationsEditing,
+      };
+    } else if (action === "delete_policy") {
+      const ta = args as ActionArgDeletePolicy;
+      const policies = [...(state.policies ?? [])];
+      policies.splice(ta.policy_index, 1);
+      return {
+        ...state,
+        policies,
+      };
+    } else if (action === "next") {
+      return {
+        ...state,
+        backup_state: BackupStates.SecretEditing,
+      };
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+  if (state.backup_state === BackupStates.SecretEditing) {
+    if (action === "back") {
+      return {
+        ...state,
+        backup_state: BackupStates.PoliciesReviewing,
+      };
+    } else if (action === "enter_secret_name") {
+      const ta = args as ActionArgEnterSecretName;
+      return {
+        ...state,
+        secret_name: ta.name,
+      };
+    } else if (action === "enter_secret") {
+      const ta = args as ActionArgEnterSecret;
+      return {
+        ...state,
+        expiration: ta.expiration,
+        core_secret: {
+          mime: ta.secret.mime ?? "text/plain",
+          value: ta.secret.value,
+        },
+      };
+    } else if (action === "next") {
+      return uploadSecret(state);
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+  return {
+    code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+    hint: "Reducer action invalid",
+  };
+}
diff --git a/packages/anastasis-core/src/provider-types.ts 
b/packages/anastasis-core/src/provider-types.ts
new file mode 100644
index 00000000..b477c09b
--- /dev/null
+++ b/packages/anastasis-core/src/provider-types.ts
@@ -0,0 +1,74 @@
+import { AmountString } from "@gnu-taler/taler-util";
+
+export interface EscrowConfigurationResponse {
+  // Protocol identifier, clarifies that this is an Anastasis provider.
+  name: "anastasis";
+
+  // libtool-style representation of the Exchange protocol version, see
+  // 
https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+  // The format is "current:revision:age".
+  version: string;
+
+  // Currency in which this provider processes payments.
+  currency: string;
+
+  // Supported authorization methods.
+  methods: AuthorizationMethodConfig[];
+
+  // Maximum policy upload size supported.
+  storage_limit_in_megabytes: number;
+
+  // Payment required to maintain an account to store policy documents for a 
year.
+  // Users can pay more, in which case the storage time will go up 
proportionally.
+  annual_fee: AmountString;
+
+  // Payment required to upload truth.  To be paid per upload.
+  truth_upload_fee: AmountString;
+
+  // Limit on the liability that the provider is offering with
+  // respect to the services provided.
+  liability_limit: AmountString;
+
+  // Salt value with 128 bits of entropy.
+  // Different providers
+  // will use different high-entropy salt values. The resulting
+  // **provider salt** is then used in various operations to ensure
+  // cryptographic operations differ by provider.  A provider must
+  // never change its salt value.
+  server_salt: string;
+
+  business_name: string;
+}
+
+export interface AuthorizationMethodConfig {
+  // Name of the authorization method.
+  type: string;
+
+  // Fee for accessing key share using this method.
+  cost: AmountString;
+}
+
+export interface TruthUploadRequest {
+  // Contains the information of an interface EncryptedKeyShare, but simply
+  // as one binary block (in Crockford Base32 encoding for JSON).
+  key_share_data: string;
+
+  // Key share method, i.e. "security question", "SMS", "e-mail", ...
+  type: string;
+
+  // Variable-size truth. After decryption,
+  // this contains the ground truth, i.e. H(challenge answer),
+  // phone number, e-mail address, picture, fingerprint, ...
+  // **base32 encoded**.
+  //
+  // The nonce of the HKDF for this encryption must include the
+  // string "ECT".
+  encrypted_truth: string; //bytearray
+
+  // MIME type of truth, i.e. text/ascii, image/jpeg, etc.
+  truth_mime?: string;
+
+  // For how many years from now would the client like us to
+  // store the truth?
+  storage_duration_years: number;
+}
diff --git a/packages/anastasis-core/src/reducer-types.ts 
b/packages/anastasis-core/src/reducer-types.ts
new file mode 100644
index 00000000..0d1754bd
--- /dev/null
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -0,0 +1,241 @@
+import { Duration } from "@gnu-taler/taler-util";
+
+export type ReducerState =
+  | ReducerStateBackup
+  | ReducerStateRecovery
+  | ReducerStateError;
+
+export interface ContinentInfo {
+  name: string;
+}
+
+export interface CountryInfo {
+  code: string;
+  name: string;
+  continent: string;
+  currency: string;
+}
+
+export interface Policy {
+  methods: {
+    authentication_method: number;
+    provider: string;
+  }[];
+}
+
+export interface PolicyProvider {
+  provider_url: string;
+}
+
+export interface ReducerStateBackup {
+  recovery_state?: undefined;
+  backup_state: BackupStates;
+  code?: undefined;
+  currencies?: string[];
+  continents?: ContinentInfo[];
+  countries?: any;
+  identity_attributes?: { [n: string]: string };
+  authentication_providers?: { [url: string]: AuthenticationProviderStatus };
+  authentication_methods?: AuthMethod[];
+  required_attributes?: any;
+  selected_continent?: string;
+  selected_country?: string;
+  secret_name?: string;
+  policies?: Policy[];
+  /**
+   * Policy providers are providers that we checked to be functional
+   * and that are actually used in policies.
+   */
+  policy_providers?: PolicyProvider[];
+  success_details?: {
+    [provider_url: string]: {
+      policy_version: number;
+    };
+  };
+  payments?: string[];
+  policy_payment_requests?: {
+    payto: string;
+    provider: string;
+  }[];
+
+  core_secret?: {
+    mime: string;
+    value: string;
+  };
+
+  expiration?: Duration;
+}
+
+export interface AuthMethod {
+  type: string;
+  instructions: string;
+  challenge: string;
+  mime_type?: string;
+}
+
+export interface ChallengeInfo {
+  cost: string;
+  instructions: string;
+  type: string;
+  uuid: string;
+}
+
+export interface UserAttributeSpec {
+  label: string;
+  name: string;
+  type: string;
+  uuid: string;
+  widget: string;
+}
+
+export interface ReducerStateRecovery {
+  backup_state?: undefined;
+  recovery_state: RecoveryStates;
+  code?: undefined;
+
+  identity_attributes?: { [n: string]: string };
+
+  continents?: any;
+  countries?: any;
+  required_attributes?: any;
+
+  recovery_information?: {
+    challenges: ChallengeInfo[];
+    policies: {
+      /**
+       * UUID of the associated challenge.
+       */
+      uuid: string;
+    }[][];
+  };
+
+  recovery_document?: {
+    secret_name: string;
+    provider_url: string;
+    version: number;
+  };
+
+  selected_challenge_uuid?: string;
+
+  challenge_feedback?: { [uuid: string]: ChallengeFeedback };
+
+  core_secret?: {
+    mime: string;
+    value: string;
+  };
+
+  authentication_providers?: {
+    [url: string]: {
+      business_name: string;
+    };
+  };
+
+  recovery_error?: any;
+}
+
+export interface ChallengeFeedback {
+  state: string;
+}
+
+export interface ReducerStateError {
+  backup_state?: undefined;
+  recovery_state?: undefined;
+  code: number;
+  hint?: string;
+  message?: string;
+}
+
+export enum BackupStates {
+  ContinentSelecting = "CONTINENT_SELECTING",
+  CountrySelecting = "COUNTRY_SELECTING",
+  UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
+  AuthenticationsEditing = "AUTHENTICATIONS_EDITING",
+  PoliciesReviewing = "POLICIES_REVIEWING",
+  SecretEditing = "SECRET_EDITING",
+  TruthsPaying = "TRUTHS_PAYING",
+  PoliciesPaying = "POLICIES_PAYING",
+  BackupFinished = "BACKUP_FINISHED",
+}
+
+export enum RecoveryStates {
+  ContinentSelecting = "CONTINENT_SELECTING",
+  CountrySelecting = "COUNTRY_SELECTING",
+  UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
+  SecretSelecting = "SECRET_SELECTING",
+  ChallengeSelecting = "CHALLENGE_SELECTING",
+  ChallengePaying = "CHALLENGE_PAYING",
+  ChallengeSolving = "CHALLENGE_SOLVING",
+  RecoveryFinished = "RECOVERY_FINISHED",
+}
+
+export interface MethodSpec {
+  type: string;
+  usage_fee: string;
+}
+
+// FIXME: This should be tagged!
+export type AuthenticationProviderStatusEmpty = {};
+
+export interface AuthenticationProviderStatusOk {
+  annual_fee: string;
+  business_name: string;
+  currency: string;
+  http_status: 200;
+  liability_limit: string;
+  salt: string;
+  storage_limit_in_megabytes: number;
+  truth_upload_fee: string;
+  methods: MethodSpec[];
+}
+
+export interface AuthenticationProviderStatusError {
+  http_status: number;
+  error_code: number;
+}
+
+export type AuthenticationProviderStatus =
+  | AuthenticationProviderStatusEmpty
+  | AuthenticationProviderStatusError
+  | AuthenticationProviderStatusOk;
+
+export interface ReducerStateBackupUserAttributesCollecting
+  extends ReducerStateBackup {
+  backup_state: BackupStates.UserAttributesCollecting;
+  selected_country: string;
+  currencies: string[];
+  required_attributes: UserAttributeSpec[];
+  authentication_providers: { [url: string]: AuthenticationProviderStatus };
+}
+
+export interface ActionArgEnterUserAttributes {
+  identity_attributes: Record<string, string>;
+}
+
+export interface ActionArgAddAuthentication {
+  authentication_method: {
+    type: string;
+    instructions: string;
+    challenge: string;
+    mime?: string;
+  };
+}
+
+export interface ActionArgDeleteAuthentication {
+  authentication_method: number;
+}
+
+export interface ActionArgDeletePolicy {
+  policy_index: number;
+}
+
+export interface ActionArgEnterSecretName {
+  name: string;
+}
+
+export interface ActionArgEnterSecret {
+  secret: {
+    value: string;
+    mime?: string;
+  };
+  expiration: Duration;
+}
diff --git a/packages/anastasis-core/tsconfig.json 
b/packages/anastasis-core/tsconfig.json
index 34027c4a..b5476273 100644
--- a/packages/anastasis-core/tsconfig.json
+++ b/packages/anastasis-core/tsconfig.json
@@ -6,7 +6,7 @@
     "module": "ESNext",
     "moduleResolution": "node",
     "sourceMap": true,
-    "lib": ["es6"],
+    "lib": ["es6", "DOM"],
     "noImplicitReturns": true,
     "noFallthroughCasesInSwitch": true,
     "strict": true,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]