gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (f2492cac -> 269022a5)


From: gnunet
Subject: [taler-wallet-core] branch master updated (f2492cac -> 269022a5)
Date: Mon, 18 Oct 2021 21:48:50 +0200

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

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

    from f2492cac Fix key-rotation test case.
     new 684b1efa anastasis-webui: prevent webpack warning
     new 1b425294 bump manifest
     new b1034801 reducer implementation WIP
     new 3a69f274 move declarations into anastasis-core
     new 269022a5 move some more crypto to taler-util package

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/anastasis-core/package.json               |   8 +-
 packages/anastasis-core/src/crypto.ts              | 193 +++++-
 packages/anastasis-core/src/index.ts               | 714 ++++++++++++++++++++-
 packages/anastasis-core/src/provider-types.ts      |  74 +++
 packages/anastasis-core/src/reducer-types.ts       | 241 +++++++
 packages/anastasis-core/tsconfig.json              |   2 +-
 packages/anastasis-webui/package.json              |   1 +
 .../src/hooks/use-anastasis-reducer.ts             | 190 ++----
 packages/anastasis-webui/src/routes/home/index.tsx |  24 +-
 packages/anastasis-webui/src/sw.js                 |   4 -
 packages/anastasis-webui/tsconfig.json             | 110 ++--
 packages/taler-util/src/index.ts                   |   7 +-
 packages/taler-util/src/kdf.ts                     |  19 +-
 packages/taler-util/src/nacl-fast.ts               |   5 +-
 packages/taler-util/src/talerCrypto.test.ts        |   2 +-
 packages/taler-util/src/talerCrypto.ts             |  57 +-
 .../src/crypto/workers/cryptoImplementation.ts     |  93 +--
 packages/taler-wallet-core/src/operations/pay.ts   |   2 +
 .../taler-wallet-core/src/util/contractTerms.ts    |   8 +-
 packages/taler-wallet-webextension/manifest.json   |   4 +-
 pnpm-lock.yaml                                     |  39 +-
 21 files changed, 1482 insertions(+), 315 deletions(-)
 create mode 100644 packages/anastasis-core/src/provider-types.ts
 create mode 100644 packages/anastasis-core/src/reducer-types.ts
 delete mode 100644 packages/anastasis-webui/src/sw.js

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..32cf470c 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -1,15 +1,48 @@
 import {
+  bytesToString,
   canonicalJson,
   decodeCrock,
   encodeCrock,
+  getRandomBytes,
+  kdf,
+  kdfKw,
+  secretbox,
+  crypto_sign_keyPair_fromSeed,
   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">;
+export type EddsaPublicKey = Flavor<string, "EddsaPublicKey">;
+export type EddsaPrivateKey = Flavor<string, "EddsaPrivateKey">;
+/**
+ * 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 +57,151 @@ export async function userIdentifierDerive(
   return encodeCrock(result);
 }
 
-// interface Keypair {
-//   pub: string;
-//   priv: string;
-// }
+export interface AccountKeyPair {
+  priv: EddsaPrivateKey;
+  pub: EddsaPublicKey;
+}
+
+export function accountKeypairDerive(userId: UserIdentifier): AccountKeyPair {
+  // FIXME: the KDF invocation looks fishy, but that's what the C code 
presently does.
+  const d = kdfKw({
+    outputLength: 32,
+    ikm: stringToBytes("ver"),
+    salt: decodeCrock(userId),
+  });
+  // FIXME: This bit twiddling seems wrong/unnecessary.
+  d[0] &= 248;
+  d[31] &= 127;
+  d[31] |= 64;
+  const pair = crypto_sign_keyPair_fromSeed(d);
+  return {
+    priv: encodeCrock(pair.secretKey),
+    pub: encodeCrock(pair.publicKey),
+  };
+}
+
+export async function encryptRecoveryDocument(
+  userId: UserIdentifier,
+  recoveryDoc: any,
+): Promise<OpaqueData> {
+  const plaintext = stringToBytes(JSON.stringify(recoveryDoc));
+  const nonce = encodeCrock(getRandomBytes(nonceSize));
+  return anastasisEncrypt(
+    nonce,
+    asOpaque(userId),
+    encodeCrock(plaintext),
+    "erd",
+  );
+}
+
+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;
+}
+
+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]));
+}
+
+const asOpaque = (x: string): OpaqueData => x;
+const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
+const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
 
-// async function accountKeypairDerive(): Promise<Keypair> {}
+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),
+  );
+}
 
-// async function secureAnswerHash(
-//   answer: string,
-//   truthUuid: string,
-//   questionSalt: string,
-// ): Promise<string> {}
+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..8921433b 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -1,14 +1,710 @@
-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 {
+  accountKeypairDerive,
+  coreSecretEncrypt,
+  encryptKeyshare,
+  encryptRecoveryDocument,
+  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],
+        // FIXME: ...
+        uuid: [],
+        salt: undefined as any,
+      };
+    }),
+  };
+
+  for (const prov of state.policy_providers!) {
+    const uid = uidMap[prov.provider_url]
+    const acctKeypair = accountKeypairDerive(uid);
+    const encRecoveryDoc = await encryptRecoveryDocument(uid, rd);
+    // FIXME: Upload recovery document.
+    const resp = await fetch(
+      new URL(`policy/${acctKeypair.pub}`, prov.provider_url).href,
+      {
+        method: "POST",
+        body: decodeCrock(encRecoveryDoc),
+      },
+    );
+  }
+
+  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,
diff --git a/packages/anastasis-webui/package.json 
b/packages/anastasis-webui/package.json
index 5093ad2e..78d8671b 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -22,6 +22,7 @@
   },
   "dependencies": {
     "@gnu-taler/taler-util": "workspace:^0.8.3",
+    "anastasis-core": "workspace:^0.0.1",
     "preact": "^10.3.1",
     "preact-render-to-string": "^5.1.4",
     "preact-router": "^3.2.1"
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts 
b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index 110ec016..be68ba6e 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -1,144 +1,16 @@
 import { TalerErrorCode } from "@gnu-taler/taler-util";
+import { BackupStates, getBackupStartState, getRecoveryStartState, 
RecoveryStates, reduceAction, ReducerState } from "anastasis-core";
 import { useState } from "preact/hooks";
 
-export type ReducerState =
-  | ReducerStateBackup
-  | ReducerStateRecovery
-  | ReducerStateError;
-
-export interface ReducerStateBackup {
-  recovery_state: undefined;
-  backup_state: BackupStates;
-  code: undefined;
-  continents: any;
-  countries: any;
-  identity_attributes?: { [n: string]: string };
-  authentication_providers: any;
-  authentication_methods?: AuthMethod[];
-  required_attributes: any;
-  secret_name?: string;
-  policies?: {
-    methods: {
-      authentication_method: number;
-      provider: string;
-    }[];
-  }[];
-  success_details: {
-    [provider_url: string]: {
-      policy_version: number;
-    };
-  };
-  payments?: string[];
-  policy_payment_requests?: {
-    payto: string;
-    provider: string;
-  }[];
-
-  core_secret?: {
-    mime: string;
-    value: string;
-  };
-}
-
-export interface AuthMethod {
-  type: string;
-  instructions: string;
-  challenge: string;
-}
-
-export interface ChallengeInfo {
-  cost: string;
-  instructions: string;
-  type: string;
-  uuid: 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;
-}
+const reducerBaseUrl = "http://localhost:5000/";;
+let remoteReducer = true;
 
 interface AnastasisState {
   reducerState: ReducerState | undefined;
   currentError: any;
 }
 
-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",
-}
-
-const reducerBaseUrl = "http://localhost:5000/";;
-
-async function getBackupStartState(): Promise<ReducerState> {
+async function getBackupStartStateRemote(): Promise<ReducerState> {
   let resp: Response;
 
   try {
@@ -159,7 +31,7 @@ async function getBackupStartState(): Promise<ReducerState> {
   }
 }
 
-async function getRecoveryStartState(): Promise<ReducerState> {
+async function getRecoveryStartStateRemote(): Promise<ReducerState> {
   let resp: Response;
   try {
     resp = await fetch(new URL("start-recovery", reducerBaseUrl).href);
@@ -179,7 +51,7 @@ async function getRecoveryStartState(): 
Promise<ReducerState> {
   }
 }
 
-async function reduceState(
+async function reduceStateRemote(
   state: any,
   action: string,
   args: any,
@@ -235,10 +107,23 @@ export interface AnastasisReducerApi {
   runTransaction(f: (h: ReducerTransactionHandle) => Promise<void>): void;
 }
 
+function storageGet(key: string): string | null {
+  if (typeof localStorage === "object") {
+    return localStorage.getItem(key);
+  }
+  return null;
+}
+
+function storageSet(key: string, value: any): void {
+  if (typeof localStorage === "object") {
+    return localStorage.setItem(key, value);
+  }
+}
+
 function restoreState(): any {
   let state: any;
   try {
-    let s = localStorage.getItem("anastasisReducerState");
+    let s = storageGet("anastasisReducerState");
     if (s === "undefined") {
       state = undefined;
     } else if (s) {
@@ -261,7 +146,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
 
   const setAnastasisState = (newState: AnastasisState) => {
     try {
-      localStorage.setItem(
+      storageSet(
         "anastasisReducerState",
         JSON.stringify(newState.reducerState),
       );
@@ -273,7 +158,12 @@ export function useAnastasisReducer(): AnastasisReducerApi 
{
 
   async function doTransition(action: string, args: any) {
     console.log("reducing with", action, args);
-    const s = await reduceState(anastasisState.reducerState, action, args);
+    let s: ReducerState;
+    if (remoteReducer) {
+      s = await reduceStateRemote(anastasisState.reducerState, action, args);
+    } else {
+      s = await reduceAction(anastasisState.reducerState!, action, args);
+    }
     console.log("got new state from reducer", s);
     if (s.code) {
       setAnastasisState({ ...anastasisState, currentError: s });
@@ -290,7 +180,12 @@ export function useAnastasisReducer(): AnastasisReducerApi 
{
     currentReducerState: anastasisState.reducerState,
     currentError: anastasisState.currentError,
     async startBackup() {
-      const s = await getBackupStartState();
+      let s: ReducerState;
+      if (remoteReducer) {
+        s = await getBackupStartStateRemote();
+      } else {
+        s = await getBackupStartState();
+      }
       if (s.code !== undefined) {
         setAnastasisState({
           ...anastasisState,
@@ -305,7 +200,12 @@ export function useAnastasisReducer(): AnastasisReducerApi 
{
       }
     },
     async startRecover() {
-      const s = await getRecoveryStartState();
+      let s: ReducerState;
+      if (remoteReducer) {
+        s = await getRecoveryStartStateRemote();
+      } else {
+        s = await getRecoveryStartState();
+      }
       if (s.code !== undefined) {
         setAnastasisState({
           ...anastasisState,
@@ -381,12 +281,14 @@ export function useAnastasisReducer(): 
AnastasisReducerApi {
 class ReducerTxImpl implements ReducerTransactionHandle {
   constructor(public transactionState: ReducerState) {}
   async transition(action: string, args: any): Promise<ReducerState> {
+    let s: ReducerState;
+    if (remoteReducer) {
+      s = await reduceStateRemote(this.transactionState, action, args);
+    } else {
+      s = await reduceAction(this.transactionState, action, args);
+    }
     console.log("making transition in transaction", action);
-    this.transactionState = await reduceState(
-      this.transactionState,
-      action,
-      args,
-    );
+    this.transactionState = s;
     // Abort transaction as soon as we transition into an error state.
     if (this.transactionState.code !== undefined) {
       throw Error("transition resulted in error");
diff --git a/packages/anastasis-webui/src/routes/home/index.tsx 
b/packages/anastasis-webui/src/routes/home/index.tsx
index b1d017f3..1351775b 100644
--- a/packages/anastasis-webui/src/routes/home/index.tsx
+++ b/packages/anastasis-webui/src/routes/home/index.tsx
@@ -5,6 +5,15 @@ import {
   encodeCrock,
   stringToBytes,
 } from "@gnu-taler/taler-util";
+import {
+  AuthMethod,
+  BackupStates,
+  ChallengeFeedback,
+  ChallengeInfo,
+  RecoveryStates,
+  ReducerStateBackup,
+  ReducerStateRecovery,
+} from "anastasis-core";
 import {
   FunctionalComponent,
   ComponentChildren,
@@ -14,13 +23,6 @@ import {
 import { useState, useContext, useRef, useLayoutEffect } from "preact/hooks";
 import {
   AnastasisReducerApi,
-  AuthMethod,
-  BackupStates,
-  ChallengeFeedback,
-  ChallengeInfo,
-  RecoveryStates,
-  ReducerStateBackup,
-  ReducerStateRecovery,
   useAnastasisReducer,
 } from "../../hooks/use-anastasis-reducer";
 import style from "./style.css";
@@ -511,8 +513,8 @@ const AnastasisClientImpl: FunctionalComponent = () => {
         </p>
         <p>The backup is stored by the following providers:</p>
         <ul>
-          {Object.keys(backupState.success_details).map((x, i) => {
-            const sd = backupState.success_details[x];
+          {Object.keys(backupState.success_details!).map((x, i) => {
+            const sd = backupState.success_details![x];
             return (
               <li>
                 {x} (Policy version {sd.policy_version})
@@ -835,11 +837,11 @@ function AuthenticationEditor(props: 
AuthenticationEditorProps) {
     undefined,
   );
   const { reducer, backupState } = props;
-  const providers = backupState.authentication_providers;
+  const providers = backupState.authentication_providers!;
   const authAvailableSet = new Set<string>();
   for (const provKey of Object.keys(providers)) {
     const p = providers[provKey];
-    if (p.methods) {
+    if ("http_status" in p && (!("error_code" in p)) && p.methods) {
       for (const meth of p.methods) {
         authAvailableSet.add(meth.type);
       }
diff --git a/packages/anastasis-webui/src/sw.js 
b/packages/anastasis-webui/src/sw.js
deleted file mode 100644
index 9071b03f..00000000
--- a/packages/anastasis-webui/src/sw.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
-
-// setupRouting();
-// setupPrecaching(getFiles());
diff --git a/packages/anastasis-webui/tsconfig.json 
b/packages/anastasis-webui/tsconfig.json
index 14d4d047..e2491daa 100644
--- a/packages/anastasis-webui/tsconfig.json
+++ b/packages/anastasis-webui/tsconfig.json
@@ -1,60 +1,68 @@
 {
-    "compilerOptions": {
-        /* Basic Options */
-        "target": "ES5",                          /* Specify ECMAScript target 
version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
-        "module": "ESNext",                       /* Specify module code 
generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
-        // "lib": [],                             /* Specify library files to 
be included in the compilation:  */
-        "allowJs": true,                          /* Allow javascript files to 
be compiled. */
-        // "checkJs": true,                       /* Report errors in .js 
files. */
-        "jsx": "react",                           /* Specify JSX code 
generation: 'preserve', 'react-native', or 'react'. */
-        "jsxFactory": "h",                        /* Specify the JSX factory 
function to use when targeting react JSX emit, e.g. React.createElement or h. */
-        // "declaration": true,                   /* Generates corresponding 
'.d.ts' file. */
-        // "sourceMap": true,                     /* Generates corresponding 
'.map' file. */
-        // "outFile": "./",                       /* Concatenate and emit 
output to single file. */
-        // "outDir": "./",                        /* Redirect output structure 
to the directory. */
-        // "rootDir": "./",                       /* Specify the root 
directory of input files. Use to control the output directory structure with 
--outDir. */
-        // "removeComments": true,                /* Do not emit comments to 
output. */
-        "noEmit": true,                           /* Do not emit outputs. */
-        // "importHelpers": true,                 /* Import emit helpers from 
'tslib'. */
-        // "downlevelIteration": true,            /* Provide full support for 
iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. 
*/
-        // "isolatedModules": true,               /* Transpile each file as a 
separate module (similar to 'ts.transpileModule'). */
+  "compilerOptions": {
+    /* Basic Options */
+    "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 
'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
+    "module": "ESNext" /* Specify module code generation: 'none', commonjs', 
'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
+    // "lib": [],                             /* Specify library files to be 
included in the compilation:  */
+    "allowJs": true /* Allow javascript files to be compiled. */,
+    // "checkJs": true,                       /* Report errors in .js files. */
+    "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', 
or 'react'. */,
+    "jsxFactory": "h" /* Specify the JSX factory function to use when 
targeting react JSX emit, e.g. React.createElement or h. */,
+    // "declaration": true,                   /* Generates corresponding 
'.d.ts' file. */
+    // "sourceMap": true,                     /* Generates corresponding 
'.map' file. */
+    // "outFile": "./",                       /* Concatenate and emit output 
to single file. */
+    // "outDir": "./",                        /* Redirect output structure to 
the directory. */
+    // "rootDir": "./",                       /* Specify the root directory of 
input files. Use to control the output directory structure with --outDir. */
+    // "removeComments": true,                /* Do not emit comments to 
output. */
+    "noEmit": true /* Do not emit outputs. */,
+    // "importHelpers": true,                 /* Import emit helpers from 
'tslib'. */
+    // "downlevelIteration": true,            /* Provide full support for 
iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. 
*/
+    // "isolatedModules": true,               /* Transpile each file as a 
separate module (similar to 'ts.transpileModule'). */
 
-        /* Strict Type-Checking Options */
-        "strict": true,                           /* Enable all strict 
type-checking options. */
-        // "noImplicitAny": true,                 /* Raise error on 
expressions and declarations with an implied 'any' type. */
-        // "strictNullChecks": true,              /* Enable strict null 
checks. */
-        // "noImplicitThis": true,                /* Raise error on 'this' 
expressions with an implied 'any' type. */
-        // "alwaysStrict": true,                  /* Parse in strict mode and 
emit "use strict" for each source file. */
+    /* Strict Type-Checking Options */
+    "strict": true /* Enable all strict type-checking options. */,
+    // "noImplicitAny": true,                 /* Raise error on expressions 
and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,              /* Enable strict null checks. */
+    // "noImplicitThis": true,                /* Raise error on 'this' 
expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                  /* Parse in strict mode and emit 
"use strict" for each source file. */
 
-        /* Additional Checks */
-        // "noUnusedLocals": true,                /* Report errors on unused 
locals. */
-        // "noUnusedParameters": true,            /* Report errors on unused 
parameters. */
-        // "noImplicitReturns": true,             /* Report error when not all 
code paths in function return a value. */
-        // "noFallthroughCasesInSwitch": true,    /* Report errors for 
fallthrough cases in switch statement. */
+    /* Additional Checks */
+    // "noUnusedLocals": true,                /* Report errors on unused 
locals. */
+    // "noUnusedParameters": true,            /* Report errors on unused 
parameters. */
+    // "noImplicitReturns": true,             /* Report error when not all 
code paths in function return a value. */
+    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough 
cases in switch statement. */
 
-        /* Module Resolution Options */
-        "moduleResolution": "node",               /* Specify module resolution 
strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
-        "esModuleInterop": true,                  /* */
-        // "baseUrl": "./",                       /* Base directory to resolve 
non-absolute module names. */
-        // "paths": {},                           /* A series of entries which 
re-map imports to lookup locations relative to the 'baseUrl'. */
-        // "rootDirs": [],                        /* List of root folders 
whose combined content represents the structure of the project at runtime. */
-        // "typeRoots": [],                       /* List of folders to 
include type definitions from. */
-        // "types": [],                           /* Type declaration files to 
be included in compilation. */
-        // "allowSyntheticDefaultImports": true,  /* Allow default imports 
from modules with no default export. This does not affect code emit, just 
typechecking. */
-        // "preserveSymlinks": true,              /* Do not resolve the real 
path of symlinks. */
+    /* Module Resolution Options */
+    "moduleResolution": "node" /* Specify module resolution strategy: 'node' 
(Node.js) or 'classic' (TypeScript pre-1.6). */,
+    "esModuleInterop": true /* */,
+    // "baseUrl": "./",                       /* Base directory to resolve 
non-absolute module names. */
+    // "paths": {},                           /* A series of entries which 
re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                        /* List of root folders whose 
combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                       /* List of folders to include 
type definitions from. */
+    // "types": [],                           /* Type declaration files to be 
included in compilation. */
+    // "allowSyntheticDefaultImports": true,  /* Allow default imports from 
modules with no default export. This does not affect code emit, just 
typechecking. */
+    // "preserveSymlinks": true,              /* Do not resolve the real path 
of symlinks. */
 
-        /* Source Map Options */
-        // "sourceRoot": "./",                    /* Specify the location 
where debugger should locate TypeScript files instead of source locations. */
-        // "mapRoot": "./",                       /* Specify the location 
where debugger should locate map files instead of generated locations. */
-        // "inlineSourceMap": true,               /* Emit a single file with 
source maps instead of having a separate file. */
-        // "inlineSources": true,                 /* Emit the source alongside 
the sourcemaps within a single file; requires '--inlineSourceMap' or 
'--sourceMap' to be set. */
+    /* Source Map Options */
+    // "sourceRoot": "./",                    /* Specify the location where 
debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "./",                       /* Specify the location where 
debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,               /* Emit a single file with 
source maps instead of having a separate file. */
+    // "inlineSources": true,                 /* Emit the source alongside the 
sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' 
to be set. */
 
-        /* Experimental Options */
-        // "experimentalDecorators": true,        /* Enables experimental 
support for ES7 decorators. */
-        // "emitDecoratorMetadata": true,         /* Enables experimental 
support for emitting type metadata for decorators. */
+    /* Experimental Options */
+    // "experimentalDecorators": true,        /* Enables experimental support 
for ES7 decorators. */
+    // "emitDecoratorMetadata": true,         /* Enables experimental support 
for emitting type metadata for decorators. */
 
-        /* Advanced Options */
-        "skipLibCheck": true                      /* Skip type checking of 
declaration files. */
+    /* Advanced Options */
+    "skipLibCheck": true /* Skip type checking of declaration files. */
+  },
+  "references": [
+    {
+      "path": "../taler-util/"
     },
-    "include": ["src/**/*", "tests/**/*"]
+    {
+      "path": "../anastasis-core/"
+    }
+  ],
+  "include": ["src/**/*", "tests/**/*"]
 }
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index ccb917f6..4ad75295 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -22,4 +22,9 @@ export * from "./url.js";
 export { fnutil } from "./fnutils.js";
 export * from "./kdf.js";
 export * from "./talerCrypto.js";
-export { randomBytes, secretbox, secretbox_open } from "./nacl-fast.js";
+export {
+  randomBytes,
+  secretbox,
+  secretbox_open,
+  crypto_sign_keyPair_fromSeed,
+} from "./nacl-fast.js";
diff --git a/packages/taler-util/src/kdf.ts b/packages/taler-util/src/kdf.ts
index af4d0503..7710de90 100644
--- a/packages/taler-util/src/kdf.ts
+++ b/packages/taler-util/src/kdf.ts
@@ -59,15 +59,30 @@ export function hmacSha256(key: Uint8Array, message: 
Uint8Array): Uint8Array {
   return hmac(sha256, 64, key, message);
 }
 
+/**
+ * HMAC-SHA512-SHA256 (see RFC 5869).
+ */
+export function kdfKw(args: {
+  outputLength: number;
+  ikm: Uint8Array;
+  salt?: Uint8Array;
+  info?: Uint8Array;
+}) {
+  return kdf(args.outputLength, args.ikm, args.salt, args.info);
+}
+
 export function kdf(
   outputLength: number,
   ikm: Uint8Array,
-  salt: Uint8Array,
-  info: Uint8Array,
+  salt?: Uint8Array,
+  info?: Uint8Array,
 ): Uint8Array {
+  salt = salt ?? new Uint8Array(64);
   // extract
   const prk = hmacSha512(salt, ikm);
 
+  info = info ?? new Uint8Array(0);
+
   // expand
   const N = Math.ceil(outputLength / 32);
   const output = new Uint8Array(N * 32);
diff --git a/packages/taler-util/src/nacl-fast.ts 
b/packages/taler-util/src/nacl-fast.ts
index 909c6a60..6e721f32 100644
--- a/packages/taler-util/src/nacl-fast.ts
+++ b/packages/taler-util/src/nacl-fast.ts
@@ -2894,7 +2894,6 @@ export function x25519_edwards_keyPair_fromSecretKey(
     throw new Error("bad secret key size");
   }
   d.set(secretKey, 0);
-  //crypto_hash(d, secretKey, 32);
 
   d[0] &= 248;
   d[31] &= 127;
@@ -2906,7 +2905,7 @@ export function x25519_edwards_keyPair_fromSecretKey(
   return pk;
 }
 
-export function sign_keyPair_fromSecretKey(
+export function crypto_sign_keyPair_fromSecretKey(
   secretKey: Uint8Array,
 ): {
   publicKey: Uint8Array;
@@ -2920,7 +2919,7 @@ export function sign_keyPair_fromSecretKey(
   return { publicKey: pk, secretKey: new Uint8Array(secretKey) };
 }
 
-export function sign_keyPair_fromSeed(
+export function crypto_sign_keyPair_fromSeed(
   seed: Uint8Array,
 ): {
   publicKey: Uint8Array;
diff --git a/packages/taler-util/src/talerCrypto.test.ts 
b/packages/taler-util/src/talerCrypto.test.ts
index ffd1d25c..1e3ceef6 100644
--- a/packages/taler-util/src/talerCrypto.test.ts
+++ b/packages/taler-util/src/talerCrypto.test.ts
@@ -69,7 +69,7 @@ test("taler-exchange-tvg eddsa key", (t) => {
   const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40";
   const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0";
 
-  const pair = nacl.sign_keyPair_fromSeed(decodeCrock(priv));
+  const pair = nacl.crypto_sign_keyPair_fromSeed(decodeCrock(priv));
   t.deepEqual(encodeCrock(pair.publicKey), pub);
 });
 
diff --git a/packages/taler-util/src/talerCrypto.ts 
b/packages/taler-util/src/talerCrypto.ts
index efa92a95..536c4dc4 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -126,7 +126,7 @@ export function decodeCrock(encoded: string): Uint8Array {
 }
 
 export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
-  const pair = nacl.sign_keyPair_fromSeed(eddsaPriv);
+  const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
   return pair.publicKey;
 }
 
@@ -353,7 +353,7 @@ export function hash(d: Uint8Array): Uint8Array {
 }
 
 export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
-  const pair = nacl.sign_keyPair_fromSeed(eddsaPriv);
+  const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
   return nacl.sign_detached(msg, pair.secretKey);
 }
 
@@ -447,3 +447,56 @@ export function setupRefreshTransferPub(
     ecdhePub: ecdheGetPublic(out),
   };
 }
+
+export enum TalerSignaturePurpose {
+  MERCHANT_TRACK_TRANSACTION = 1103,
+  WALLET_RESERVE_WITHDRAW = 1200,
+  WALLET_COIN_DEPOSIT = 1201,
+  MASTER_DENOMINATION_KEY_VALIDITY = 1025,
+  MASTER_WIRE_FEES = 1028,
+  MASTER_WIRE_DETAILS = 1030,
+  WALLET_COIN_MELT = 1202,
+  TEST = 4242,
+  MERCHANT_PAYMENT_OK = 1104,
+  MERCHANT_CONTRACT = 1101,
+  WALLET_COIN_RECOUP = 1203,
+  WALLET_COIN_LINK = 1204,
+  EXCHANGE_CONFIRM_RECOUP = 1039,
+  EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+  ANASTASIS_POLICY_UPLOAD = 1400,
+  ANASTASIS_POLICY_DOWNLOAD = 1401,
+  SYNC_BACKUP_UPLOAD = 1450,
+}
+
+export class SignaturePurposeBuilder {
+  private chunks: Uint8Array[] = [];
+
+  constructor(private purposeNum: number) {}
+
+  put(bytes: Uint8Array): SignaturePurposeBuilder {
+    this.chunks.push(Uint8Array.from(bytes));
+    return this;
+  }
+
+  build(): Uint8Array {
+    let payloadLen = 0;
+    for (const c of this.chunks) {
+      payloadLen += c.byteLength;
+    }
+    const buf = new ArrayBuffer(4 + 4 + payloadLen);
+    const u8buf = new Uint8Array(buf);
+    let p = 8;
+    for (const c of this.chunks) {
+      u8buf.set(c, p);
+      p += c.byteLength;
+    }
+    const dvbuf = new DataView(buf);
+    dvbuf.setUint32(0, payloadLen + 4 + 4);
+    dvbuf.setUint32(4, this.purposeNum);
+    return u8buf;
+  }
+}
+
+export function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
+  return new SignaturePurposeBuilder(purposeNum);
+}
diff --git 
a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
index 4ca0576b..c42ece77 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -34,7 +34,14 @@ import {
   CoinSourceType,
 } from "../../db.js";
 
-import { CoinDepositPermission, RecoupRequest, RefreshPlanchetInfo } from 
"@gnu-taler/taler-util";
+import {
+  buildSigPS,
+  CoinDepositPermission,
+  RecoupRequest,
+  RefreshPlanchetInfo,
+  SignaturePurposeBuilder,
+  TalerSignaturePurpose,
+} from "@gnu-taler/taler-util";
 // FIXME: These types should be internal to the wallet!
 import {
   BenchmarkResult,
@@ -80,24 +87,6 @@ import bigint from "big-integer";
 
 const logger = new Logger("cryptoImplementation.ts");
 
-enum SignaturePurpose {
-  MERCHANT_TRACK_TRANSACTION = 1103,
-  WALLET_RESERVE_WITHDRAW = 1200,
-  WALLET_COIN_DEPOSIT = 1201,
-  MASTER_DENOMINATION_KEY_VALIDITY = 1025,
-  MASTER_WIRE_FEES = 1028,
-  MASTER_WIRE_DETAILS = 1030,
-  WALLET_COIN_MELT = 1202,
-  TEST = 4242,
-  MERCHANT_PAYMENT_OK = 1104,
-  MERCHANT_CONTRACT = 1101,
-  WALLET_COIN_RECOUP = 1203,
-  WALLET_COIN_LINK = 1204,
-  EXCHANGE_CONFIRM_RECOUP = 1039,
-  EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
-  SYNC_BACKUP_UPLOAD = 1450,
-}
-
 function amountToBuffer(amount: AmountJson): Uint8Array {
   const buffer = new ArrayBuffer(8 + 4 + 12);
   const dvbuf = new DataView(buffer);
@@ -139,38 +128,6 @@ function timestampRoundedToBuffer(ts: Timestamp): 
Uint8Array {
   return new Uint8Array(b);
 }
 
-class SignaturePurposeBuilder {
-  private chunks: Uint8Array[] = [];
-
-  constructor(private purposeNum: number) { }
-
-  put(bytes: Uint8Array): SignaturePurposeBuilder {
-    this.chunks.push(Uint8Array.from(bytes));
-    return this;
-  }
-
-  build(): Uint8Array {
-    let payloadLen = 0;
-    for (const c of this.chunks) {
-      payloadLen += c.byteLength;
-    }
-    const buf = new ArrayBuffer(4 + 4 + payloadLen);
-    const u8buf = new Uint8Array(buf);
-    let p = 8;
-    for (const c of this.chunks) {
-      u8buf.set(c, p);
-      p += c.byteLength;
-    }
-    const dvbuf = new DataView(buf);
-    dvbuf.setUint32(0, payloadLen + 4 + 4);
-    dvbuf.setUint32(4, this.purposeNum);
-    return u8buf;
-  }
-}
-
-function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
-  return new SignaturePurposeBuilder(purposeNum);
-}
 export class CryptoImplementation {
   static enableTracing = false;
 
@@ -192,7 +149,9 @@ export class CryptoImplementation {
     const denomPubHash = hash(denomPub);
     const evHash = hash(ev);
 
-    const withdrawRequest = 
buildSigPS(SignaturePurpose.WALLET_RESERVE_WITHDRAW)
+    const withdrawRequest = buildSigPS(
+      TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
+    )
       .put(reservePub)
       .put(amountToBuffer(amountWithFee))
       .put(denomPubHash)
@@ -236,7 +195,7 @@ export class CryptoImplementation {
   }
 
   signTrackTransaction(req: SignTrackTransactionRequest): string {
-    const p = buildSigPS(SignaturePurpose.MERCHANT_TRACK_TRANSACTION)
+    const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION)
       .put(decodeCrock(req.contractTermsHash))
       .put(decodeCrock(req.wireHash))
       .put(decodeCrock(req.merchantPub))
@@ -249,7 +208,7 @@ export class CryptoImplementation {
    * Create and sign a message to recoup a coin.
    */
   createRecoupRequest(coin: CoinRecord): RecoupRequest {
-    const p = buildSigPS(SignaturePurpose.WALLET_COIN_RECOUP)
+    const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
       .put(decodeCrock(coin.coinPub))
       .put(decodeCrock(coin.denomPubHash))
       .put(decodeCrock(coin.blindingKey))
@@ -276,7 +235,7 @@ export class CryptoImplementation {
     contractHash: string,
     merchantPub: string,
   ): boolean {
-    const p = buildSigPS(SignaturePurpose.MERCHANT_PAYMENT_OK)
+    const p = buildSigPS(TalerSignaturePurpose.MERCHANT_PAYMENT_OK)
       .put(decodeCrock(contractHash))
       .build();
     const sigBytes = decodeCrock(sig);
@@ -288,7 +247,7 @@ export class CryptoImplementation {
    * Check if a wire fee is correctly signed.
    */
   isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
-    const p = buildSigPS(SignaturePurpose.MASTER_WIRE_FEES)
+    const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES)
       .put(hash(stringToBytes(type + "\0")))
       .put(timestampRoundedToBuffer(wf.startStamp))
       .put(timestampRoundedToBuffer(wf.endStamp))
@@ -304,7 +263,7 @@ export class CryptoImplementation {
    * Check if the signature of a denomination is valid.
    */
   isValidDenom(denom: DenominationRecord, masterPub: string): boolean {
-    const p = buildSigPS(SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
+    const p = 
buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
       .put(decodeCrock(masterPub))
       .put(timestampRoundedToBuffer(denom.stampStart))
       .put(timestampRoundedToBuffer(denom.stampExpireWithdraw))
@@ -334,7 +293,9 @@ export class CryptoImplementation {
       stringToBytes(paytoUri + "\0"),
       new Uint8Array(0),
     );
-    const p = buildSigPS(SignaturePurpose.MASTER_WIRE_DETAILS).put(h).build();
+    const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
+      .put(h)
+      .build();
     return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
   }
 
@@ -344,7 +305,7 @@ export class CryptoImplementation {
     merchantPub: string,
   ): boolean {
     const cthDec = decodeCrock(contractTermsHash);
-    const p = buildSigPS(SignaturePurpose.MERCHANT_CONTRACT)
+    const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT)
       .put(cthDec)
       .build();
     return eddsaVerify(p, decodeCrock(sig), decodeCrock(merchantPub));
@@ -364,8 +325,8 @@ export class CryptoImplementation {
   eddsaGetPublic(key: string): { priv: string; pub: string } {
     return {
       priv: key,
-      pub: encodeCrock(eddsaGetPublic(decodeCrock(key)))
-    }
+      pub: encodeCrock(eddsaGetPublic(decodeCrock(key))),
+    };
   }
 
   /**
@@ -392,7 +353,7 @@ export class CryptoImplementation {
    * and deposit permissions for each given coin.
    */
   signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
-    const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
+    const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
       .put(decodeCrock(depositInfo.contractTermsHash))
       .put(decodeCrock(depositInfo.wireInfoHash))
       .put(decodeCrock(depositInfo.denomPubHash))
@@ -503,7 +464,7 @@ export class CryptoImplementation {
     }
 
     const sessionHash = sessionHc.finish();
-    const confirmData = buildSigPS(SignaturePurpose.WALLET_COIN_MELT)
+    const confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
       .put(sessionHash)
       .put(decodeCrock(meltCoinDenomPubHash))
       .put(amountToBuffer(valueWithFee))
@@ -549,7 +510,7 @@ export class CryptoImplementation {
     coinEv: string,
   ): string {
     const coinEvHash = hash(decodeCrock(coinEv));
-    const coinLink = buildSigPS(SignaturePurpose.WALLET_COIN_LINK)
+    const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK)
       .put(decodeCrock(newDenomHash))
       .put(decodeCrock(transferPub))
       .put(coinEvHash)
@@ -622,9 +583,7 @@ export class CryptoImplementation {
     } else {
       hOld = new Uint8Array(64);
     }
-    const sigBlob = new SignaturePurposeBuilder(
-      SignaturePurpose.SYNC_BACKUP_UPLOAD,
-    )
+    const sigBlob = buildSigPS(TalerSignaturePurpose.SYNC_BACKUP_UPLOAD)
       .put(hOld)
       .put(hNew)
       .build();
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 079ffcd9..8fad5599 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -763,6 +763,8 @@ async function processDownloadProposalImpl(
     proposalResp.contract_terms,
   );
 
+  logger.info(`Contract terms hash: ${contractTermsHash}`);
+
   let parsedContractTerms: ContractTerms;
 
   try {
diff --git a/packages/taler-wallet-core/src/util/contractTerms.ts 
b/packages/taler-wallet-core/src/util/contractTerms.ts
index 652ef707..b064079e 100644
--- a/packages/taler-wallet-core/src/util/contractTerms.ts
+++ b/packages/taler-wallet-core/src/util/contractTerms.ts
@@ -14,7 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { canonicalJson } from "@gnu-taler/taler-util";
+import { canonicalJson, Logger } from "@gnu-taler/taler-util";
 import { kdf } from "@gnu-taler/taler-util";
 import {
   decodeCrock,
@@ -24,6 +24,8 @@ import {
   stringToBytes,
 } from "@gnu-taler/taler-util";
 
+const logger = new Logger("contractTerms.ts");
+
 export namespace ContractTermsUtil {
   export type PathPredicate = (path: string[]) => boolean;
 
@@ -222,6 +224,8 @@ export namespace ContractTermsUtil {
   export function hashContractTerms(contractTerms: unknown): string {
     const cleaned = scrub(contractTerms);
     const canon = canonicalJson(cleaned) + "\0";
-    return encodeCrock(hash(stringToBytes(canon)));
+    const bytes = stringToBytes(canon);
+    logger.info(`contract terms before hashing: ${encodeCrock(bytes)}`);
+    return encodeCrock(hash(bytes));
   }
 }
diff --git a/packages/taler-wallet-webextension/manifest.json 
b/packages/taler-wallet-webextension/manifest.json
index e864fe08..c87d0c0f 100644
--- a/packages/taler-wallet-webextension/manifest.json
+++ b/packages/taler-wallet-webextension/manifest.json
@@ -4,8 +4,8 @@
   "name": "GNU Taler Wallet (git)",
   "description": "Privacy preserving and transparent payments",
   "author": "GNU Taler Developers",
-  "version": "0.8.0.9",
-  "version_name": "0.8.1-dev.9",
+  "version": "0.8.0.10",
+  "version_name": "0.8.1-dev.10",
 
   "minimum_chrome_version": "51",
   "minimum_opera_version": "36",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 66ab99b3..84fcccce 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,11 +16,15 @@ importers:
     specifiers:
       '@gnu-taler/taler-util': workspace:^0.8.3
       ava: ^3.15.0
+      fetch-ponyfill: ^7.1.0
       hash-wasm: ^4.9.0
+      node-fetch: ^3.0.0
       typescript: ^4.4.3
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
+      fetch-ponyfill: 7.1.0
       hash-wasm: 4.9.0
+      node-fetch: 3.0.0
     devDependencies:
       ava: 3.15.0
       typescript: 4.4.3
@@ -32,6 +36,7 @@ importers:
       '@types/jest': ^26.0.8
       '@typescript-eslint/eslint-plugin': ^2.25.0
       '@typescript-eslint/parser': ^2.25.0
+      anastasis-core: workspace:^0.0.1
       enzyme: ^3.11.0
       enzyme-adapter-preact-pure: ^3.1.0
       eslint: ^6.8.0
@@ -46,6 +51,7 @@ importers:
       typescript: ^3.7.5
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
+      anastasis-core: link:../anastasis-core
       preact: 10.5.14
       preact-render-to-string: 5.1.19_preact@10.5.14
       preact-router: 3.2.1_preact@10.5.14
@@ -9701,6 +9707,11 @@ packages:
       assert-plus: 1.0.0
     dev: true
 
+  /data-uri-to-buffer/3.0.1:
+    resolution: {integrity: 
sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
+    engines: {node: '>= 6'}
+    dev: false
+
   /data-urls/1.1.0:
     resolution: {integrity: 
sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==}
     dependencies:
@@ -11267,6 +11278,19 @@ packages:
       bser: 2.1.1
     dev: true
 
+  /fetch-blob/3.1.2:
+    resolution: {integrity: 
sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==}
+    engines: {node: ^12.20 || >= 14.13}
+    dependencies:
+      web-streams-polyfill: 3.1.1
+    dev: false
+
+  /fetch-ponyfill/7.1.0:
+    resolution: {integrity: 
sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==}
+    dependencies:
+      node-fetch: 2.6.1
+    dev: false
+
   /fflate/0.6.0:
     resolution: {integrity: 
sha512-u4AdW/Xx7iinDhYQuS0B0vvbUX7JWXO07jEvYUlbNZvtoiDLkDvHR17LSwxhbawjZVDXczzLHAQUDSllISm4/A==}
     dev: false
@@ -11658,6 +11682,7 @@ packages:
     resolution: {integrity: 
sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
+    requiresBuild: true
     dev: true
     optional: true
 
@@ -15032,7 +15057,14 @@ packages:
   /node-fetch/2.6.1:
     resolution: {integrity: 
sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==}
     engines: {node: 4.x || >=6.0.0}
-    dev: true
+
+  /node-fetch/3.0.0:
+    resolution: {integrity: 
sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dependencies:
+      data-uri-to-buffer: 3.0.1
+      fetch-blob: 3.1.2
+    dev: false
 
   /node-forge/0.10.0:
     resolution: {integrity: 
sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==}
@@ -20331,6 +20363,11 @@ packages:
     resolution: {integrity: 
sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
     dev: true
 
+  /web-streams-polyfill/3.1.1:
+    resolution: {integrity: 
sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==}
+    engines: {node: '>= 8'}
+    dev: false
+
   /webidl-conversions/4.0.2:
     resolution: {integrity: 
sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
     dev: 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]