gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/03: backup WIP


From: gnunet
Subject: [taler-wallet-core] 01/03: backup WIP
Date: Wed, 02 Dec 2020 21:56:23 +0100

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

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

commit 89f1a281fea66b986fc0a003dc10446f6ed6e4a2
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Dec 2 14:55:04 2020 +0100

    backup WIP
---
 packages/taler-wallet-android/src/index.ts         |   7 +
 packages/taler-wallet-core/package.json            |   1 +
 .../src/crypto/workers/cryptoApi.ts                |   5 +
 .../src/crypto/workers/cryptoImplementation.ts     |  18 +
 packages/taler-wallet-core/src/db.ts               |  21 +-
 .../taler-wallet-core/src/headless/NodeHttpLib.ts  |  56 +--
 .../taler-wallet-core/src/operations/backup.ts     | 402 +++++++++++++++++++++
 .../taler-wallet-core/src/types/backupTypes.ts     | 215 +++++++++++
 packages/taler-wallet-core/src/types/dbTypes.ts    |  48 ++-
 packages/taler-wallet-core/src/types/schemacore.ts |  58 ---
 .../taler-wallet-core/src/types/walletTypes.ts     |   9 +
 packages/taler-wallet-core/src/util/http.ts        |  20 +
 packages/taler-wallet-core/src/util/query.ts       |   4 +-
 packages/taler-wallet-core/src/wallet.ts           |  13 +
 .../src/browserHttpLib.ts                          |  25 +-
 pnpm-lock.yaml                                     |   6 +
 16 files changed, 804 insertions(+), 104 deletions(-)

diff --git a/packages/taler-wallet-android/src/index.ts 
b/packages/taler-wallet-android/src/index.ts
index 07d15d58..bfda8ab7 100644
--- a/packages/taler-wallet-android/src/index.ts
+++ b/packages/taler-wallet-android/src/index.ts
@@ -38,6 +38,8 @@ import {
   WalletNotification,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
   WALLET_MERCHANT_PROTOCOL_VERSION,
+  bytesToString,
+  stringToBytes,
 } from "taler-wallet-core";
 
 import fs from "fs";
@@ -57,6 +59,10 @@ export class AndroidHttpLib implements HttpRequestLibrary {
 
   constructor(private sendMessage: (m: string) => void) {}
 
+  fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+    return this.nodeHttpLib.fetch(url, opt);
+  }
+
   get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
     if (this.useNfcTunnel) {
       const myId = this.requestId++;
@@ -120,6 +126,7 @@ export class AndroidHttpLib implements HttpRequestLibrary {
         requestMethod: "FIXME",
         json: async () => JSON.parse(msg.responseText),
         text: async () => msg.responseText,
+        bytes: async () => { throw Error("bytes() not supported for tunnel 
response") },
       };
       p.resolve(resp);
     } else {
diff --git a/packages/taler-wallet-core/package.json 
b/packages/taler-wallet-core/package.json
index 72f9f379..62e4c898 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -58,6 +58,7 @@
     "@types/node": "^14.14.7",
     "axios": "^0.21.0",
     "big-integer": "^1.6.48",
+    "fflate": "^0.3.10",
     "idb-bridge": "workspace:*",
     "source-map-support": "^0.5.19",
     "tslib": "^2.0.3"
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
index 286de5a1..29f3b02b 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
@@ -42,6 +42,7 @@ import {
   PlanchetCreationResult,
   PlanchetCreationRequest,
   DepositInfo,
+  MakeSyncSignatureRequest,
 } from "../../types/walletTypes";
 
 import * as timer from "../../util/timer";
@@ -455,4 +456,8 @@ export class CryptoApi {
   benchmark(repetitions: number): Promise<BenchmarkResult> {
     return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions);
   }
+
+  makeSyncSignature(req: MakeSyncSignatureRequest): Promise<string> {
+    return this.doRpc<string>("makeSyncSignature", 3, req);
+  }
 }
diff --git 
a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
index 46ac7c8a..41836fdf 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -43,6 +43,7 @@ import {
   PlanchetCreationResult,
   PlanchetCreationRequest,
   DepositInfo,
+  MakeSyncSignatureRequest,
 } from "../../types/walletTypes";
 import { AmountJson, Amounts } from "../../util/amounts";
 import * as timer from "../../util/timer";
@@ -85,6 +86,7 @@ enum SignaturePurpose {
   WALLET_COIN_LINK = 1204,
   EXCHANGE_CONFIRM_RECOUP = 1039,
   EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+  SYNC_BACKUP_UPLOAD = 1450,
 }
 
 function amountToBuffer(amount: AmountJson): Uint8Array {
@@ -589,4 +591,20 @@ export class CryptoImplementation {
       },
     };
   }
+
+  makeSyncSignature(req: MakeSyncSignatureRequest): string {
+    const hNew = decodeCrock(req.newHash);
+    let hOld: Uint8Array;
+    if (req.oldHash) {
+      hOld = decodeCrock(req.oldHash);
+    } else {
+      hOld = new Uint8Array(64);
+    }
+    const sigBlob = new 
SignaturePurposeBuilder(SignaturePurpose.SYNC_BACKUP_UPLOAD)
+      .put(hOld)
+      .put(hNew)
+      .build();
+    const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
+    return encodeCrock(uploadSig);
+  }
 }
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index ecc5509d..6f5b6b45 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1,7 +1,12 @@
 import { Stores } from "./types/dbTypes";
 import { openDatabase, Database, Store, Index } from "./util/query";
-import { IDBFactory, IDBDatabase, IDBObjectStore, IDBTransaction } from 
"idb-bridge";
-import { Logger } from './util/logging';
+import {
+  IDBFactory,
+  IDBDatabase,
+  IDBObjectStore,
+  IDBTransaction,
+} from "idb-bridge";
+import { Logger } from "./util/logging";
 
 /**
  * Name of the Taler database.  This is effectively the major
@@ -18,7 +23,7 @@ const TALER_DB_NAME = "taler-wallet-prod-v1";
  * backwards-compatible way or object stores and indices
  * are added.
  */
-export const WALLET_DB_MINOR_VERSION = 2;
+export const WALLET_DB_MINOR_VERSION = 3;
 
 const logger = new Logger("db.ts");
 
@@ -43,7 +48,9 @@ export function openTalerDatabase(
           const s = db.createObjectStore(si.name, si.storeParams);
           for (const indexName in si as any) {
             if ((si as any)[indexName] instanceof Index) {
-              const ii: Index<string, string, any, any> = (si as 
any)[indexName];
+              const ii: Index<string, string, any, any> = (si as any)[
+                indexName
+              ];
               s.createIndex(ii.indexName, ii.keyPath, ii.options);
             }
           }
@@ -59,7 +66,8 @@ export function openTalerDatabase(
       if ((Stores as any)[n] instanceof Store) {
         const si: Store<string, any> = (Stores as any)[n];
         let s: IDBObjectStore;
-        if ((si.storeParams?.versionAdded ?? 1) > oldVersion) {
+        const storeVersionAdded = si.storeParams?.versionAdded ?? 1;
+        if (storeVersionAdded > oldVersion) {
           s = db.createObjectStore(si.name, si.storeParams);
         } else {
           s = upgradeTransaction.objectStore(si.name);
@@ -67,7 +75,8 @@ export function openTalerDatabase(
         for (const indexName in si as any) {
           if ((si as any)[indexName] instanceof Index) {
             const ii: Index<string, string, any, any> = (si as any)[indexName];
-            if ((ii.options?.versionAdded ?? 0) > oldVersion) {
+            const indexVersionAdded = ii.options?.versionAdded ?? 0;
+            if (indexVersionAdded > oldVersion || storeVersionAdded > 
oldVersion) {
               s.createIndex(ii.indexName, ii.keyPath, ii.options);
             }
           }
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts 
b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
index ed4e0e1e..5eefb24f 100644
--- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
+++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
@@ -31,6 +31,7 @@ import { OperationFailedError, makeErrorDetails } from 
"../operations/errors";
 import { TalerErrorCode } from "../TalerErrorCode";
 import { URL } from "../util/url";
 import { Logger } from "../util/logging";
+import { bytesToString } from '../crypto/talerCrypto';
 
 const logger = new Logger("NodeHttpLib.ts");
 
@@ -48,12 +49,10 @@ export class NodeHttpLib implements HttpRequestLibrary {
     this.throttlingEnabled = enabled;
   }
 
-  private async req(
-    method: "POST" | "GET",
-    url: string,
-    body: any,
-    opt?: HttpRequestOptions,
-  ): Promise<HttpResponse> {
+  async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+    const method = opt?.method ?? "GET";
+    let body = opt?.body;
+
     const parsedUrl = new URL(url);
     if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
       throw OperationFailedError.fromCode(
@@ -75,7 +74,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
       resp = await Axios({
         method,
         url: url,
-        responseType: "text",
+        responseType: "arraybuffer",
         headers: opt?.headers,
         validateStatus: () => true,
         transformResponse: (x) => x,
@@ -93,26 +92,18 @@ export class NodeHttpLib implements HttpRequestLibrary {
       );
     }
 
-    const respText = resp.data;
-    if (typeof respText !== "string") {
-      throw new OperationFailedError(
-        makeErrorDetails(
-          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-          "unexpected response type",
-          {
-            httpStatusCode: resp.status,
-            requestUrl: url,
-            requestMethod: method,
-          },
-        ),
-      );
+    const makeText = async(): Promise<string> => {
+      const respText = new Uint8Array(resp.data);
+      return bytesToString(respText);
     }
+
     const makeJson = async (): Promise<any> => {
       let responseJson;
+      const respText = await makeText();
       try {
         responseJson = JSON.parse(respText);
       } catch (e) {
-        logger.trace(`invalid json: '${respText}'`);
+        logger.trace(`invalid json: '${resp.data}'`);
         throw new OperationFailedError(
           makeErrorDetails(
             TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
@@ -141,6 +132,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
       }
       return responseJson;
     };
+    const makeBytes = async () => {
+      if (!(resp.data instanceof ArrayBuffer)) {
+        throw Error("expected array buffer");
+      }
+      const buf = resp.data;
+      return buf;
+    };
     const headers = new Headers();
     for (const hn of Object.keys(resp.headers)) {
       headers.set(hn, resp.headers[hn]);
@@ -150,13 +148,17 @@ export class NodeHttpLib implements HttpRequestLibrary {
       requestMethod: method,
       headers,
       status: resp.status,
-      text: async () => resp.data,
+      text: makeText,
       json: makeJson,
+      bytes: makeBytes,
     };
-  }
 
+  }
   async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
-    return this.req("GET", url, undefined, opt);
+    return this.fetch(url, {
+      method: "GET",
+      ...opt,
+    });
   }
 
   async postJson(
@@ -164,6 +166,10 @@ export class NodeHttpLib implements HttpRequestLibrary {
     body: any,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse> {
-    return this.req("POST", url, body, opt);
+    return this.fetch(url, {
+      method: "POST",
+      body,
+      ...opt,
+    });
   }
 }
diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
new file mode 100644
index 00000000..dbcb3337
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -0,0 +1,402 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of wallet backups (export/import/upload) and sync
+ * server management.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { InternalWalletState } from "./state";
+import {
+  BackupCoin,
+  BackupCoinSource,
+  BackupCoinSourceType,
+  BackupExchangeData,
+  WalletBackupContentV1,
+} from "../types/backupTypes";
+import { TransactionHandle } from "../util/query";
+import {
+  CoinSourceType,
+  CoinStatus,
+  ConfigRecord,
+  Stores,
+} from "../types/dbTypes";
+import { checkDbInvariant } from "../util/invariants";
+import { Amounts, codecForAmountString } from "../util/amounts";
+import {
+  decodeCrock,
+  eddsaGetPublic,
+  EddsaKeyPair,
+  encodeCrock,
+  getRandomBytes,
+  hash,
+  stringToBytes,
+} from "../crypto/talerCrypto";
+import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
+import { Timestamp } from "../util/time";
+import { URL } from "../util/url";
+import { AmountString } from "../types/talerTypes";
+import {
+  buildCodecForObject,
+  Codec,
+  codecForNumber,
+  codecForString,
+} from "../util/codec";
+import {
+  HttpResponseStatus,
+  readSuccessResponseJsonOrThrow,
+} from "../util/http";
+import { Logger } from "../util/logging";
+import { gzipSync } from "fflate";
+import { sign_keyPair_fromSeed } from "../crypto/primitives/nacl-fast";
+import { kdf } from "../crypto/primitives/kdf";
+
+interface WalletBackupConfState {
+  walletRootPub: string;
+  walletRootPriv: string;
+  clock: number;
+  lastBackupHash?: string;
+  lastBackupNonce?: string;
+}
+
+const WALLET_BACKUP_STATE_KEY = "walletBackupState";
+
+const logger = new Logger("operations/backup.ts");
+
+async function provideBackupState(
+  ws: InternalWalletState,
+): Promise<WalletBackupConfState> {
+  const bs: ConfigRecord<WalletBackupConfState> | undefined = await ws.db.get(
+    Stores.config,
+    WALLET_BACKUP_STATE_KEY,
+  );
+  if (bs) {
+    return bs.value;
+  }
+  // We need to generate the key outside of the transaction
+  // due to how IndexedDB works.
+  const k = await ws.cryptoApi.createEddsaKeypair();
+  return await ws.db.runWithWriteTransaction([Stores.config], async (tx) => {
+    let backupStateEntry:
+      | ConfigRecord<WalletBackupConfState>
+      | undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
+    if (!backupStateEntry) {
+      backupStateEntry = {
+        key: WALLET_BACKUP_STATE_KEY,
+        value: {
+          walletRootPub: k.pub,
+          walletRootPriv: k.priv,
+          clock: 0,
+          lastBackupHash: undefined,
+        },
+      };
+      await tx.put(Stores.config, backupStateEntry);
+    }
+    return backupStateEntry.value;
+  });
+}
+
+async function getWalletBackupState(
+  ws: InternalWalletState,
+  tx: TransactionHandle<typeof Stores.config>,
+): Promise<WalletBackupConfState> {
+  let bs = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
+  checkDbInvariant(!!bs, "wallet backup state should be in DB");
+  return bs.value;
+}
+
+export async function exportBackup(
+  ws: InternalWalletState,
+): Promise<WalletBackupContentV1> {
+  await provideBackupState(ws);
+  return ws.db.runWithWriteTransaction(
+    [Stores.config, Stores.exchanges, Stores.coins],
+    async (tx) => {
+      const bs = await getWalletBackupState(ws, tx);
+
+      const exchanges: BackupExchangeData[] = [];
+      const coins: BackupCoin[] = [];
+
+      await tx.iter(Stores.exchanges).forEach((ex) => {
+        if (!ex.details) {
+          return;
+        }
+        exchanges.push({
+          exchangeBaseUrl: ex.baseUrl,
+          exchangeMasterPub: ex.details?.masterPublicKey,
+          termsOfServiceAcceptedEtag: ex.termsOfServiceAcceptedEtag,
+        });
+      });
+
+      await tx.iter(Stores.coins).forEach((coin) => {
+        let bcs: BackupCoinSource;
+        switch (coin.coinSource.type) {
+          case CoinSourceType.Refresh:
+            bcs = {
+              type: BackupCoinSourceType.Refresh,
+              oldCoinPub: coin.coinSource.oldCoinPub,
+            };
+            break;
+          case CoinSourceType.Tip:
+            bcs = {
+              type: BackupCoinSourceType.Tip,
+              coinIndex: coin.coinSource.coinIndex,
+              walletTipId: coin.coinSource.walletTipId,
+            };
+            break;
+          case CoinSourceType.Withdraw:
+            bcs = {
+              type: BackupCoinSourceType.Withdraw,
+              coinIndex: coin.coinSource.coinIndex,
+              reservePub: coin.coinSource.reservePub,
+              withdrawalGroupId: coin.coinSource.withdrawalGroupId,
+            };
+            break;
+        }
+
+        coins.push({
+          exchangeBaseUrl: coin.exchangeBaseUrl,
+          blindingKey: coin.blindingKey,
+          coinPriv: coin.coinPriv,
+          coinPub: coin.coinPub,
+          coinSource: bcs,
+          currentAmount: Amounts.stringify(coin.currentAmount),
+          fresh: coin.status === CoinStatus.Fresh,
+        });
+      });
+
+      const backupBlob: WalletBackupContentV1 = {
+        schemaId: "gnu-taler-wallet-backup",
+        schemaVersion: 1,
+        clock: bs.clock,
+        coins: coins,
+        exchanges: exchanges,
+        planchets: [],
+        refreshSessions: [],
+        reserves: [],
+        walletRootPub: bs.walletRootPub,
+      };
+
+      // If the backup changed, we increment our clock.
+
+      let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
+      if (h != bs.lastBackupHash) {
+        backupBlob.clock = ++bs.clock;
+        bs.lastBackupHash = encodeCrock(
+          hash(stringToBytes(canonicalJson(backupBlob))),
+        );
+        bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
+        await tx.put(Stores.config, {
+          key: WALLET_BACKUP_STATE_KEY,
+          value: bs,
+        });
+      }
+
+      return backupBlob;
+    },
+  );
+}
+
+export interface BackupRequest {
+  backupBlob: any;
+}
+
+export async function encryptBackup(
+  config: WalletBackupConfState,
+  blob: WalletBackupContentV1,
+): Promise<Uint8Array> {
+  throw Error("not implemented");
+}
+
+export function importBackup(
+  ws: InternalWalletState,
+  backupRequest: BackupRequest,
+): Promise<void> {
+  throw Error("not implemented");
+}
+
+function deriveAccountKeyPair(
+  bc: WalletBackupConfState,
+  providerUrl: string,
+): EddsaKeyPair {
+  const privateKey = kdf(
+    32,
+    decodeCrock(bc.walletRootPriv),
+    stringToBytes("taler-sync-account-key-salt"),
+    stringToBytes(providerUrl),
+  );
+
+  return {
+    eddsaPriv: privateKey,
+    eddsaPub: eddsaGetPublic(privateKey),
+  };
+}
+
+/**
+ * Do one backup cycle that consists of:
+ * 1. Exporting a backup and try to upload it.
+ *    Stop if this step succeeds.
+ * 2. Download, verify and import backups from connected sync accounts.
+ * 3. Upload the updated backup blob.
+ */
+export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
+  const providers = await ws.db.iter(Stores.backupProviders).toArray();
+  const backupConfig = await provideBackupState(ws);
+
+  logger.trace("got backup providers", providers);
+  const backupJsonContent = canonicalJson(await exportBackup(ws));
+  logger.trace("backup JSON size", backupJsonContent.length);
+  const compressedContent = gzipSync(stringToBytes(backupJsonContent));
+  logger.trace("backup compressed JSON size", compressedContent.length);
+
+  const h = hash(compressedContent);
+
+  for (const provider of providers) {
+    const accountKeyPair = deriveAccountKeyPair(backupConfig, 
provider.baseUrl);
+    logger.trace(`trying to upload backup to ${provider.baseUrl}`);
+
+    const syncSig = await ws.cryptoApi.makeSyncSignature({
+      newHash: encodeCrock(h),
+      oldHash: provider.lastBackupHash,
+      accountPriv: encodeCrock(accountKeyPair.eddsaPriv),
+    });
+
+    logger.trace(`sync signature is ${syncSig}`);
+
+    const accountBackupUrl = new URL(
+      `/backups/${encodeCrock(accountKeyPair.eddsaPub)}`,
+      provider.baseUrl,
+    );
+
+    const resp = await ws.http.fetch(accountBackupUrl.href, {
+      method: "POST",
+      body: compressedContent,
+      headers: {
+        "content-type": "application/octet-stream",
+        "sync-signature": syncSig,
+        "if-none-match": encodeCrock(h),
+      },
+    });
+
+    logger.trace(`response status: ${resp.status}`);
+
+    if (resp.status === HttpResponseStatus.PaymentRequired) {
+      logger.trace("payment required for backup");
+      logger.trace(`headers: ${j2s(resp.headers)}`)
+      return;
+    }
+
+    if (resp.status === HttpResponseStatus.Ok) {
+      return;
+    }
+
+    logger.trace(`response body: ${j2s(await resp.json())}`);
+  }
+}
+
+interface SyncTermsOfServiceResponse {
+  // maximum backup size supported
+  storage_limit_in_megabytes: number;
+
+  // Fee for an account, per year.
+  annual_fee: AmountString;
+
+  // protocol version supported by the server,
+  // for now always "0.0".
+  version: string;
+}
+
+const codecForSyncTermsOfServiceResponse = (): Codec<
+  SyncTermsOfServiceResponse
+> =>
+  buildCodecForObject<SyncTermsOfServiceResponse>()
+    .property("storage_limit_in_megabytes", codecForNumber())
+    .property("annual_fee", codecForAmountString())
+    .property("version", codecForString())
+    .build("SyncTermsOfServiceResponse");
+
+export interface AddBackupProviderRequest {
+  backupProviderBaseUrl: string;
+}
+
+export const codecForAddBackupProviderRequest = (): Codec<
+  AddBackupProviderRequest
+> =>
+  buildCodecForObject<AddBackupProviderRequest>()
+    .property("backupProviderBaseUrl", codecForString())
+    .build("AddBackupProviderRequest");
+
+export async function addBackupProvider(
+  ws: InternalWalletState,
+  req: AddBackupProviderRequest,
+): Promise<void> {
+  await provideBackupState(ws);
+  const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
+  const oldProv = await ws.db.get(Stores.backupProviders, canonUrl);
+  if (oldProv) {
+    return;
+  }
+  const termsUrl = new URL("terms", canonUrl);
+  const resp = await ws.http.get(termsUrl.href);
+  const terms = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForSyncTermsOfServiceResponse(),
+  );
+  await ws.db.put(Stores.backupProviders, {
+    active: true,
+    annualFee: terms.annual_fee,
+    baseUrl: canonUrl,
+    storageLimitInMegabytes: terms.storage_limit_in_megabytes,
+    supportedProtocolVersion: terms.version,
+  });
+}
+
+export async function removeBackupProvider(
+  syncProviderBaseUrl: string,
+): Promise<void> {}
+
+export async function restoreFromRecoverySecret(): Promise<void> {}
+
+/**
+ * Information about one provider.
+ *
+ * We don't store the account key here,
+ * as that's derived from the wallet root key.
+ */
+export interface ProviderInfo {
+  syncProviderBaseUrl: string;
+  lastRemoteClock: number;
+  lastBackup?: Timestamp;
+}
+
+export interface BackupInfo {
+  walletRootPub: string;
+  deviceId: string;
+  lastLocalClock: number;
+  providers: ProviderInfo[];
+}
+
+/**
+ * Get information about the current state of wallet backups.
+ */
+export function getBackupInfo(ws: InternalWalletState): Promise<BackupInfo> {
+  throw Error("not implemented");
+}
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
new file mode 100644
index 00000000..72d0486b
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -0,0 +1,215 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+/**
+ * Type declarations for backup.
+ *
+ * Contains some redundancy with the other type declarations,
+ * as the backup schema must be very stable.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+type BackupAmountString = string;
+
+/**
+ * Content of the backup.
+ *
+ * The contents of the wallet must be serialized in a deterministic
+ * way across implementations, so that the normalized backup content
+ * JSON is identical when the wallet's content is identical.
+ */
+export interface WalletBackupContentV1 {
+  schemaId: "gnu-taler-wallet-backup";
+
+  schemaVersion: 1;
+
+  /**
+   * Monotonically increasing clock of the wallet,
+   * used to determine causality when merging backups.
+   */
+  clock: number;
+
+  walletRootPub: string;
+
+  /**
+   * Per-exchange data sorted by exchange master public key.
+   */
+  exchanges: BackupExchangeData[];
+
+  reserves: ReserveBackupData[];
+
+  coins: BackupCoin[];
+
+  planchets: BackupWithdrawalPlanchet[];
+
+  refreshSessions: BackupRefreshSession[];
+}
+
+export interface BackupRefreshSession {
+
+}
+
+
+export interface BackupReserve {
+  reservePub: string;
+  reservePriv: string;
+  /**
+   * The exchange base URL.
+   */
+  exchangeBaseUrl: string;
+
+  bankConfirmUrl?: string;
+
+  /**
+   * Wire information (as payto URI) for the bank account that
+   * transfered funds for this reserve.
+   */
+  senderWire?: string;
+}
+
+export interface ReserveBackupData {
+  /**
+   * The reserve public key.
+   */
+  reservePub: string;
+
+  /**
+   * The reserve private key.
+   */
+  reservePriv: string;
+
+  /**
+   * The exchange base URL.
+   */
+  exchangeBaseUrl: string;
+
+  instructedAmount: string;
+
+  /**
+   * Wire information (as payto URI) for the bank account that
+   * transfered funds for this reserve.
+   */
+  senderWire?: string;
+}
+
+export interface BackupExchangeData {
+  exchangeBaseUrl: string;
+  exchangeMasterPub: string;
+
+  /**
+   * ETag for last terms of service download.
+   */
+  termsOfServiceAcceptedEtag: string | undefined;
+}
+
+
+export interface BackupWithdrawalPlanchet {
+  coinSource: BackupWithdrawCoinSource | BackupTipCoinSource;
+  blindingKey: string;
+  coinPriv: string;
+  coinPub: string;
+  denomPubHash: string;
+
+  /**
+   * Base URL that identifies the exchange from which we are getting the
+   * coin.
+   */
+  exchangeBaseUrl: string;
+}
+
+
+export enum BackupCoinSourceType {
+  Withdraw = "withdraw",
+  Refresh = "refresh",
+  Tip = "tip",
+}
+
+export interface BackupWithdrawCoinSource {
+  type: BackupCoinSourceType.Withdraw;
+  withdrawalGroupId: string;
+
+  /**
+   * Index of the coin in the withdrawal session.
+   */
+  coinIndex: number;
+
+  /**
+   * Reserve public key for the reserve we got this coin from.
+   */
+  reservePub: string;
+}
+
+export interface BackupRefreshCoinSource {
+  type: BackupCoinSourceType.Refresh;
+  oldCoinPub: string;
+}
+
+export interface BackupTipCoinSource {
+  type: BackupCoinSourceType.Tip;
+  walletTipId: string;
+  coinIndex: number;
+}
+
+export type BackupCoinSource =
+  | BackupWithdrawCoinSource
+  | BackupRefreshCoinSource
+  | BackupTipCoinSource;
+
+/**
+ * Coin that has been withdrawn and might have been
+ * (partially) spent.
+ */
+export interface BackupCoin {
+  /**
+   * Public key of the coin.
+   */
+  coinPub: string;
+
+  /**
+   * Private key of the coin.
+   */
+  coinPriv: string;
+
+  /**
+   * Where did the coin come from (withdrawal/refresh/tip)?
+   * Used for recouping coins.
+   */
+  coinSource: BackupCoinSource;
+
+  /**
+   * Is the coin still fresh
+   */
+  fresh: boolean;
+
+  /**
+   * Blinding key used when withdrawing the coin.
+   * Potentionally used again during payback.
+   */
+  blindingKey: string;
+
+  /**
+   * Amount that's left on the coin.
+   */
+  currentAmount: BackupAmountString;
+
+  /**
+   * Base URL that identifies the exchange from which we got the
+   * coin.
+   */
+  exchangeBaseUrl: string;
+}
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 349713eb..26400dd3 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -31,6 +31,7 @@ import {
   MerchantInfo,
   Product,
   InternationalizedString,
+  AmountString,
 } from "./talerTypes";
 
 import { Index, Store } from "../util/query";
@@ -706,6 +707,10 @@ export enum CoinSourceType {
 
 export interface WithdrawCoinSource {
   type: CoinSourceType.Withdraw;
+
+  /**
+   * Can be the empty string for orphaned coins.
+   */
   withdrawalGroupId: string;
 
   /**
@@ -1395,9 +1400,9 @@ export interface PurchaseRecord {
  * Configuration key/value entries to configure
  * the wallet.
  */
-export interface ConfigRecord {
+export interface ConfigRecord<T> {
   key: string;
-  value: any;
+  value: T;
 }
 
 export interface DenominationSelectionInfo {
@@ -1531,6 +1536,30 @@ export enum ImportPayloadType {
   CoreSchema = "core-schema",
 }
 
+export interface BackupProviderRecord {
+  baseUrl: string;
+
+  supportedProtocolVersion: string;
+
+  annualFee: AmountString;
+
+  storageLimitInMegabytes: number;
+
+  active: boolean;
+
+  /**
+   * Hash of the last backup that we already
+   * merged.
+   */
+  lastBackupHash?: string;
+
+  /**
+   * Clock of the last backup that we already
+   * merged.
+   */
+  lastBackupClock?: number;
+}
+
 class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
   constructor() {
     super("exchanges", { keyPath: "baseUrl" });
@@ -1609,7 +1638,7 @@ class CurrenciesStore extends Store<"currencies", 
CurrencyRecord> {
   }
 }
 
-class ConfigStore extends Store<"config", ConfigRecord> {
+class ConfigStore extends Store<"config", ConfigRecord<any>> {
   constructor() {
     super("config", { keyPath: "key" });
   }
@@ -1690,6 +1719,18 @@ class BankWithdrawUrisStore extends Store<
   }
 }
 
+
+/**
+ */
+class BackupProvidersStore extends Store<
+  "backupProviders",
+  BackupProviderRecord
+> {
+  constructor() {
+    super("backupProviders", { keyPath: "baseUrl", versionAdded: 3 });
+  }
+}
+
 /**
  * The stores and indices for the wallet database.
  */
@@ -1716,4 +1757,5 @@ export const Stores = {
   withdrawalGroups: new WithdrawalGroupsStore(),
   planchets: new PlanchetsStore(),
   bankWithdrawUris: new BankWithdrawUrisStore(),
+  backupProviders: new BackupProvidersStore(),
 };
diff --git a/packages/taler-wallet-core/src/types/schemacore.ts 
b/packages/taler-wallet-core/src/types/schemacore.ts
deleted file mode 100644
index 820f68d1..00000000
--- a/packages/taler-wallet-core/src/types/schemacore.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Core of the wallet's schema, used for painless export, import
- * and schema migration.
- *
- * If this schema is extended, it must be extended in a completely
- * backwards-compatible way.
- */
-
-interface CoreCoin {
-  exchangeBaseUrl: string;
-  coinPub: string;
-  coinPriv: string;
-  amountRemaining: string;
-}
-
-interface CorePurchase {
-  noncePub: string;
-  noncePriv: string;
-  paySig: string;
-  contractTerms: any;
-}
-
-interface CoreReserve {
-  reservePub: string;
-  reservePriv: string;
-  exchangeBaseUrl: string;
-}
-
-interface SchemaCore {
-  coins: CoreCoin[];
-  purchases: CorePurchase[];
-
-  /**
-   * Schema version (of full schema) of wallet that exported the core schema.
-   */
-  versionExporter: number;
-
-  /**
-   * Schema version of the database that has been exported to the core schema
-   */
-  versionSourceDatabase: number;
-}
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts 
b/packages/taler-wallet-core/src/types/walletTypes.ts
index 7940497a..ab7d3b4d 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -885,6 +885,15 @@ export const withdrawTestBalanceDefaults = {
   exchangeBaseUrl: "https://exchange.test.taler.net/";,
 };
 
+/**
+ * Request to the crypto worker to make a sync signature.
+ */
+export interface MakeSyncSignatureRequest {
+  accountPriv: string;
+  oldHash: string | undefined;
+  newHash: string;
+}
+
 export const codecForWithdrawTestBalance = (): Codec<
   WithdrawTestBalanceRequest
 > =>
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 1a2459f7..1ec9c2f5 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -17,6 +17,8 @@
 /**
  * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
  * Allows for easy mocking for test cases.
+ * 
+ * The API is inspired by the HTML5 fetch API.
  */
 
 /**
@@ -47,16 +49,20 @@ export interface HttpResponse {
   headers: Headers;
   json(): Promise<any>;
   text(): Promise<string>;
+  bytes(): Promise<ArrayBuffer>;
 }
 
 export interface HttpRequestOptions {
+  method?: "POST" | "PUT" | "GET";
   headers?: { [name: string]: string };
   timeout?: Duration;
+  body?: string | ArrayBuffer | ArrayBufferView;
 }
 
 export enum HttpResponseStatus {
   Ok = 200,
   Gone = 210,
+  PaymentRequired = 402,
 }
 
 /**
@@ -82,6 +88,12 @@ export class Headers {
       this.headerMap.set(normalizedName, value);
     }
   }
+
+  toJSON(): any {
+    const m: Record<string, string> = {};
+    this.headerMap.forEach((v, k) => m[k] = v);
+    return m;
+  }
 }
 
 /**
@@ -104,6 +116,14 @@ export interface HttpRequestLibrary {
     body: any,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse>;
+
+  /**
+   * Make an HTTP POST request with a JSON body.
+   */
+  fetch(
+    url: string,
+    opt?: HttpRequestOptions,
+  ): Promise<HttpResponse>;
 }
 
 type TalerErrorResponse = {
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 08572fbd..beb14cad 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -595,8 +595,8 @@ export class Database {
   }
 
   async put<St extends Store<string, any>>(
-    store: St extends Store<infer N, infer R> ? Store<N, R> : never,
-    value: St extends Store<any, infer R> ? R : never,
+    store: St,
+    value: StoreContent<St>,
     key?: any,
   ): Promise<any> {
     const tx = this.db.transaction([store.name], "readwrite");
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 1140a13c..4491a167 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -152,6 +152,7 @@ import {
   testPay,
 } from "./operations/testing";
 import { TalerErrorCode } from ".";
+import { addBackupProvider, codecForAddBackupProviderRequest, runBackupCycle, 
exportBackup } from './operations/backup';
 
 const builtinCurrencies: CurrencyRecord[] = [
   {
@@ -1074,6 +1075,18 @@ export class Wallet {
         await this.acceptTip(req.walletTipId);
         return {};
       }
+      case "exportBackup": {
+        return exportBackup(this.ws);
+      }
+      case "addBackupProvider": {
+        const req = codecForAddBackupProviderRequest().decode(payload);
+        await addBackupProvider(this.ws, req);
+        return {};
+      }
+      case "runBackupCycle": {
+        await runBackupCycle(this.ws);
+        return {};
+      }
     }
     throw OperationFailedError.fromCode(
       TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts 
b/packages/taler-wallet-webextension/src/browserHttpLib.ts
index 96484bc9..bfc85563 100644
--- a/packages/taler-wallet-webextension/src/browserHttpLib.ts
+++ b/packages/taler-wallet-webextension/src/browserHttpLib.ts
@@ -34,12 +34,9 @@ const logger = new Logger("browserHttpLib");
  * browser's XMLHttpRequest.
  */
 export class BrowserHttpLib implements HttpRequestLibrary {
-  private req(
-    method: string,
-    url: string,
-    requestBody?: any,
-    options?: HttpRequestOptions,
-  ): Promise<HttpResponse> {
+  fetch(url: string, options?: HttpRequestOptions): Promise<HttpResponse> {
+    const method = options?.method ?? "GET";
+    let requestBody = options?.body;
     return new Promise<HttpResponse>((resolve, reject) => {
       const myRequest = new XMLHttpRequest();
       myRequest.open(method, url);
@@ -48,7 +45,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
           myRequest.setRequestHeader(headerName, options.headers[headerName]);
         }
       }
-      myRequest.setRequestHeader;
+      myRequest.responseType = "arraybuffer";
       if (requestBody) {
         myRequest.send(requestBody);
       } else {
@@ -130,6 +127,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
             requestMethod: method,
             json: makeJson,
             text: async () => myRequest.responseText,
+            bytes: async () => myRequest.response,
           };
           resolve(resp);
         }
@@ -138,15 +136,22 @@ export class BrowserHttpLib implements HttpRequestLibrary 
{
   }
 
   get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
-    return this.req("GET", url, undefined, opt);
+    return this.fetch(url, {
+      method: "GET",
+      ...opt,
+    });
   }
 
   postJson(
     url: string,
-    body: unknown,
+    body: any,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse> {
-    return this.req("POST", url, JSON.stringify(body), opt);
+    return this.fetch(url, {
+      method: "POST",
+      body,
+      ...opt,
+    });
   }
 
   stop(): void {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e233d539..a81089d8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -124,6 +124,7 @@ importers:
       '@types/node': 14.14.7
       axios: 0.21.0
       big-integer: 1.6.48
+      fflate: 0.3.10
       idb-bridge: 'link:../idb-bridge'
       source-map-support: 0.5.19
       tslib: 2.0.3
@@ -167,6 +168,7 @@ importers:
       eslint-plugin-react: ^7.21.5
       eslint-plugin-react-hooks: ^4.2.0
       esm: ^3.2.25
+      fflate: ^0.3.10
       idb-bridge: 'workspace:*'
       jed: ^1.1.1
       nyc: ^15.1.0
@@ -2338,6 +2340,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==
+  /fflate/0.3.10:
+    dev: false
+    resolution:
+      integrity: 
sha512-s5j69APkUPPbzdI20Ix4pPtQP+1Qi58YcFRpE7aO/P1kEywUYjbl2RjZRVEMdnySO9pr4MB0BHPbxkiahrtD/Q==
   /figures/3.2.0:
     dependencies:
       escape-string-regexp: 1.0.5

-- 
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]