gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: backup cycle


From: gnunet
Subject: [taler-wallet-core] branch master updated: backup cycle
Date: Thu, 07 Jan 2021 19:50:59 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 324f44ae backup cycle
324f44ae is described below

commit 324f44ae6954ef7a75a67838a7f0cbf2a6dc6d76
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Jan 7 19:50:53 2021 +0100

    backup cycle
---
 .../taler-wallet-core/src/headless/NodeHttpLib.ts  |   2 +-
 .../taler-wallet-core/src/operations/backup.ts     | 136 ++++++++++++++++-----
 packages/taler-wallet-core/src/operations/pay.ts   |   2 +-
 .../taler-wallet-core/src/types/backupTypes.ts     |   2 +
 packages/taler-wallet-core/src/util/http.ts        |   3 +
 5 files changed, 111 insertions(+), 34 deletions(-)

diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts 
b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
index 68a54927..95626cc4 100644
--- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
+++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
@@ -133,7 +133,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
       return responseJson;
     };
     const makeBytes = async () => {
-      if (!(resp.data instanceof ArrayBuffer)) {
+      if (typeof resp.data.byteLength !== "number") {
         throw Error("expected array buffer");
       }
       const buf = resp.data;
diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
index 5108dccf..f67d32e5 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -101,22 +101,35 @@ import {
 import {
   HttpResponseStatus,
   readSuccessResponseJsonOrThrow,
+  throwUnexpectedRequestError,
 } from "../util/http";
 import { Logger } from "../util/logging";
 import { gunzipSync, gzipSync } from "fflate";
 import { kdf } from "../crypto/primitives/kdf";
 import { initRetryInfo } from "../util/retries";
-import { RefreshReason } from "../types/walletTypes";
+import {
+  ConfirmPayResultType,
+  PreparePayResultType,
+  RefreshReason,
+} from "../types/walletTypes";
 import { CryptoApi } from "../crypto/workers/cryptoApi";
 import { secretbox, secretbox_open } from "../crypto/primitives/nacl-fast";
 import { str } from "../i18n";
+import { confirmPay, preparePayForUri } from "./pay";
 
 interface WalletBackupConfState {
   deviceId: string;
   walletRootPub: string;
   walletRootPriv: string;
   clocks: { [device_id: string]: number };
-  lastBackupHash?: string;
+
+  /**
+   * Last hash of the canonicalized plain-text backup.
+   *
+   * Used to determine whether the wallet's content changed
+   * and we need to bump the clock.
+   */
+  lastBackupPlainHash?: string;
 
   /**
    * Timestamp stored in the last backup.
@@ -163,7 +176,7 @@ async function provideBackupState(
           clocks: { [deviceId]: 1 },
           walletRootPub: k.pub,
           walletRootPriv: k.priv,
-          lastBackupHash: undefined,
+          lastBackupPlainHash: undefined,
         },
       };
       await tx.put(Stores.config, backupStateEntry);
@@ -574,9 +587,9 @@ export async function exportBackup(
       // If the backup changed, we increment our clock.
 
       let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
-      if (h != bs.lastBackupHash) {
+      if (h != bs.lastBackupPlainHash) {
         backupBlob.clocks[bs.deviceId] = ++bs.clocks[bs.deviceId];
-        bs.lastBackupHash = encodeCrock(
+        bs.lastBackupPlainHash = encodeCrock(
           hash(stringToBytes(canonicalJson(backupBlob))),
         );
         bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
@@ -631,17 +644,9 @@ export async function encryptBackup(
   const secret = deriveBlobSecret(config);
   const encrypted = secretbox(compressedContent, nonce.slice(0, 24), secret);
   chunks.push(encrypted);
-  logger.trace(`enc: ${encodeCrock(encrypted)}`);
   return concatArrays(chunks);
 }
 
-export async function decryptBackup(
-  config: WalletBackupConfState,
-  box: Uint8Array,
-): Promise<WalletBackupContentV1> {
-  throw Error("not implemented");
-}
-
 interface CompletedCoin {
   coinPub: string;
   coinEvHash: string;
@@ -1482,19 +1487,18 @@ export async function runBackupCycle(ws: 
InternalWalletState): Promise<void> {
   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 backupJson = await exportBackup(ws);
 
-  const h = hash(compressedContent);
+  const encBackup = await encryptBackup(backupConfig, backupJson);
+
+  const currentBackupHash = hash(encBackup);
 
   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),
+      newHash: encodeCrock(currentBackupHash),
       oldHash: provider.lastBackupHash,
       accountPriv: encodeCrock(accountKeyPair.eddsaPriv),
     });
@@ -1508,11 +1512,16 @@ export async function runBackupCycle(ws: 
InternalWalletState): Promise<void> {
 
     const resp = await ws.http.fetch(accountBackupUrl.href, {
       method: "POST",
-      body: compressedContent,
+      body: encBackup,
       headers: {
         "content-type": "application/octet-stream",
         "sync-signature": syncSig,
-        "if-none-match": encodeCrock(h),
+        "if-none-match": encodeCrock(currentBackupHash),
+        ...(provider.lastBackupHash
+          ? {
+              "if-match": provider.lastBackupHash,
+            }
+          : {}),
       },
     });
 
@@ -1521,14 +1530,70 @@ export async function runBackupCycle(ws: 
InternalWalletState): Promise<void> {
     if (resp.status === HttpResponseStatus.PaymentRequired) {
       logger.trace("payment required for backup");
       logger.trace(`headers: ${j2s(resp.headers)}`);
-      return;
+      const talerUri = resp.headers.get("taler");
+      if (!talerUri) {
+        throw Error("no taler URI available to pay provider");
+      }
+      const res = await preparePayForUri(ws, talerUri);
+      let proposalId: string | undefined;
+      switch (res.status) {
+        case PreparePayResultType.InsufficientBalance:
+          // FIXME: record in provider state!
+          logger.warn("insufficient balance to pay for backup provider");
+          break;
+        case PreparePayResultType.PaymentPossible:
+        case PreparePayResultType.AlreadyConfirmed:
+          proposalId = res.proposalId;
+          break;
+      }
+      if (!proposalId) {
+        continue;
+      }
+      const confirmRes = await confirmPay(ws, proposalId);
+      switch (confirmRes.type) {
+        case ConfirmPayResultType.Pending:
+          logger.warn("payment not yet finished yet");
+          break;
+      }
     }
-
-    if (resp.status === HttpResponseStatus.Ok) {
-      return;
+    if (resp.status === HttpResponseStatus.NoContent) {
+      await ws.db.runWithWriteTransaction(
+        [Stores.backupProviders],
+        async (tx) => {
+          const prov = await tx.get(Stores.backupProviders, provider.baseUrl);
+          if (!prov) {
+            return;
+          }
+          prov.lastBackupHash = encodeCrock(currentBackupHash);
+          prov.lastBackupClock =
+            backupJson.clocks[backupJson.current_device_id];
+          await tx.put(Stores.backupProviders, prov);
+        },
+      );
+      continue;
+    }
+    if (resp.status === HttpResponseStatus.Conflict) {
+      logger.info("conflicting backup found");
+      const backupEnc = new Uint8Array(await resp.bytes());
+      const backupConfig = await provideBackupState(ws);
+      const blob = await decryptBackup(backupConfig, backupEnc);
+      const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
+      await importBackup(ws, blob, cryptoData);
+      await ws.db.runWithWriteTransaction(
+        [Stores.backupProviders],
+        async (tx) => {
+          const prov = await tx.get(Stores.backupProviders, provider.baseUrl);
+          if (!prov) {
+            return;
+          }
+          prov.lastBackupHash = encodeCrock(hash(backupEnc));
+          prov.lastBackupClock =
+            blob.clocks[blob.current_device_id];
+          await tx.put(Stores.backupProviders, prov);
+        },
+      );
+      logger.info("processed existing backup");
     }
-
-    logger.trace(`response body: ${j2s(await resp.json())}`);
   }
 }
 
@@ -1676,11 +1741,10 @@ export async function exportBackupEncrypted(
   return encryptBackup(bs, blob);
 }
 
-export async function importBackupEncrypted(
-  ws: InternalWalletState,
+export async function decryptBackup(
+  backupConfig: WalletBackupConfState,
   data: Uint8Array,
-): Promise<void> {
-  const backupConfig = await provideBackupState(ws);
+): Promise<WalletBackupContentV1> {
   const rMagic = bytesToString(data.slice(0, 8));
   if (rMagic != magic) {
     throw Error("invalid backup file (magic tag mismatch)");
@@ -1693,7 +1757,15 @@ export async function importBackupEncrypted(
   if (!dataCompressed) {
     throw Error("decryption failed");
   }
-  const blob = JSON.parse(bytesToString(gunzipSync(dataCompressed)));
+  return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
+}
+
+export async function importBackupEncrypted(
+  ws: InternalWalletState,
+  data: Uint8Array,
+): Promise<void> {
+  const backupConfig = await provideBackupState(ws);
+  const blob = await decryptBackup(backupConfig, data);
   const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
   await importBackup(ws, blob, cryptoData);
 }
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index e9d642d3..b75859f8 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -1255,7 +1255,7 @@ async function generateDepositPermissions(
 export async function confirmPay(
   ws: InternalWalletState,
   proposalId: string,
-  sessionIdOverride: string | undefined,
+  sessionIdOverride?: string,
 ): Promise<ConfirmPayResult> {
   logger.trace(
     `executing confirmPay with proposalId ${proposalId} and sessionIdOverride 
${sessionIdOverride}`,
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
index 32ff8c52..caab92cf 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -41,6 +41,8 @@
  * 11. Failed refunds do not have any information about why they failed.
  *     => This should go into the general "error reports"
  * 12. Tombstones for removed backup providers
+ * 13. Do we somehow need to model the mechanism for first only withdrawing
+ *     the amount to pay the backup provider?
  *
  * Questions:
  * 1. What happens when two backups are merged that have
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 4b0a8e75..43a0ab16 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -61,8 +61,11 @@ export interface HttpRequestOptions {
 
 export enum HttpResponseStatus {
   Ok = 200,
+  NoContent = 204,
   Gone = 210,
+  NotModified = 304,
   PaymentRequired = 402,
+  Conflict = 409,
 }
 
 /**

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