gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: implement import of backup re


From: gnunet
Subject: [taler-wallet-core] branch master updated: implement import of backup recovery document
Date: Fri, 08 Jan 2021 13:30:44 +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 8921a5e8 implement import of backup recovery document
8921a5e8 is described below

commit 8921a5e8f2f47c113eeeaa1bf14937c5b6cfb0ac
Author: Florian Dold <florian@dold.me>
AuthorDate: Fri Jan 8 13:30:29 2021 +0100

    implement import of backup recovery document
---
 packages/taler-wallet-cli/src/index.ts             |  38 ++++-
 .../taler-wallet-core/src/operations/backup.ts     | 154 ++++++++++++++++++---
 .../taler-wallet-core/src/types/backupTypes.ts     |  57 ++++----
 packages/taler-wallet-core/src/types/dbTypes.ts    |  30 +++-
 .../taler-wallet-core/src/types/walletTypes.ts     |  24 ++++
 packages/taler-wallet-core/src/wallet.ts           |  12 ++
 6 files changed, 262 insertions(+), 53 deletions(-)

diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index 87e0e00d..87a51f30 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -36,6 +36,8 @@ import {
   NodeThreadCryptoWorkerFactory,
   CryptoApi,
   rsaBlind,
+  RecoveryMergeStrategy,
+  stringToBytes,
 } from "taler-wallet-core";
 import * as clk from "./clk";
 import { deepStrictEqual } from "assert";
@@ -453,19 +455,49 @@ backupCli.subcommand("run", "run").action(async (args) => 
{
   });
 });
 
+backupCli.subcommand("status", "status").action(async (args) => {
+  await withWallet(args, async (wallet) => {
+    const status = await wallet.getBackupStatus();
+    console.log(JSON.stringify(status, undefined, 2));
+  });
+});
+
 backupCli
   .subcommand("recoveryLoad", "load-recovery")
-  .action(async (args) => {});
-
-backupCli.subcommand("status", "status").action(async (args) => {});
+  .maybeOption("strategy", ["--strategy"], clk.STRING, {
+    help:
+      "Strategy for resolving a conflict with the existing wallet key 
('theirs' or 'ours')",
+  })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      const data = JSON.parse(await read(process.stdin));
+      let strategy: RecoveryMergeStrategy | undefined;
+      const stratStr = args.recoveryLoad.strategy;
+      if (stratStr) {
+        if (stratStr === "theirs") {
+          strategy = RecoveryMergeStrategy.Theirs;
+        } else if (stratStr === "ours") {
+          strategy = RecoveryMergeStrategy.Theirs;
+        } else {
+          throw Error("invalid recovery strategy");
+        }
+      }
+      await wallet.loadBackupRecovery({
+        recovery: data,
+        strategy,
+      });
+    });
+  });
 
 backupCli
   .subcommand("addProvider", "add-provider")
   .requiredArgument("url", clk.STRING)
+  .flag("activate", ["--activate"])
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
       wallet.addBackupProvider({
         backupProviderBaseUrl: args.addProvider.url,
+        activate: args.addProvider.activate,
       });
     });
   });
diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
index f67d32e5..72fdf7aa 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -27,6 +27,7 @@
 import { InternalWalletState } from "./state";
 import {
   BackupBackupProvider,
+  BackupBackupProviderTerms,
   BackupCoin,
   BackupCoinSource,
   BackupCoinSourceType,
@@ -52,6 +53,7 @@ import {
 import { TransactionHandle } from "../util/query";
 import {
   AbortStatus,
+  BackupProviderStatus,
   CoinSource,
   CoinSourceType,
   CoinStatus,
@@ -110,6 +112,8 @@ import { initRetryInfo } from "../util/retries";
 import {
   ConfirmPayResultType,
   PreparePayResultType,
+  RecoveryLoadRequest,
+  RecoveryMergeStrategy,
   RefreshReason,
 } from "../types/walletTypes";
 import { CryptoApi } from "../crypto/workers/cryptoApi";
@@ -303,12 +307,18 @@ export async function exportBackup(
       });
 
       await tx.iter(Stores.backupProviders).forEach((bp) => {
+        let terms: BackupBackupProviderTerms | undefined;
+        if (bp.terms) {
+          terms = {
+            annual_fee: Amounts.stringify(bp.terms.annualFee),
+            storage_limit_in_megabytes: bp.terms.storageLimitInMegabytes,
+            supported_protocol_version: bp.terms.supportedProtocolVersion,
+          };
+        }
         backupBackupProviders.push({
-          annual_fee: Amounts.stringify(bp.annualFee),
+          terms,
           base_url: canonicalizeBaseUrl(bp.baseUrl),
-          pay_proposal_ids: [],
-          storage_limit_in_megabytes: bp.storageLimitInMegabytes,
-          supported_protocol_version: bp.supportedProtocolVersion,
+          pay_proposal_ids: bp.paymentProposalIds,
         });
       });
 
@@ -1256,7 +1266,13 @@ export async function importBackup(
             case "abort-refund":
               abortStatus = AbortStatus.AbortRefund;
               break;
+            case undefined:
+              abortStatus = AbortStatus.None;
+              break;
             default:
+              logger.warn(
+                `got backup purchase abort_status 
${j2s(backupPurchase.abort_status)}`,
+              );
               throw Error("not reachable");
           }
           const parsedContractTerms = codecForContractTerms().decode(
@@ -1484,11 +1500,9 @@ function deriveBlobSecret(bc: WalletBackupConfState): 
Uint8Array {
  */
 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 backupJson = await exportBackup(ws);
-
+  const backupConfig = await provideBackupState(ws);
   const encBackup = await encryptBackup(backupConfig, backupJson);
 
   const currentBackupHash = hash(encBackup);
@@ -1549,6 +1563,15 @@ export async function runBackupCycle(ws: 
InternalWalletState): Promise<void> {
       if (!proposalId) {
         continue;
       }
+      const p = proposalId;
+      await ws.db.runWithWriteTransaction([Stores.backupProviders], async (tx) 
=> {
+        const provRec = await tx.get(Stores.backupProviders, provider.baseUrl);
+        checkDbInvariant(!!provRec);
+        const ids = new Set(provRec.paymentProposalIds)
+        ids.add(p);
+        provRec.paymentProposalIds = Array.from(ids);
+        await tx.put(Stores.backupProviders, provRec);
+      });
       const confirmRes = await confirmPay(ws, proposalId);
       switch (confirmRes.type) {
         case ConfirmPayResultType.Pending:
@@ -1565,6 +1588,7 @@ export async function runBackupCycle(ws: 
InternalWalletState): Promise<void> {
             return;
           }
           prov.lastBackupHash = encodeCrock(currentBackupHash);
+          prov.lastBackupTimestamp = getTimestampNow();
           prov.lastBackupClock =
             backupJson.clocks[backupJson.current_device_id];
           await tx.put(Stores.backupProviders, prov);
@@ -1587,8 +1611,8 @@ export async function runBackupCycle(ws: 
InternalWalletState): Promise<void> {
             return;
           }
           prov.lastBackupHash = encodeCrock(hash(backupEnc));
-          prov.lastBackupClock =
-            blob.clocks[blob.current_device_id];
+          prov.lastBackupClock = blob.clocks[blob.current_device_id];
+          prov.lastBackupTimestamp = getTimestampNow();
           await tx.put(Stores.backupProviders, prov);
         },
       );
@@ -1620,6 +1644,11 @@ const codecForSyncTermsOfServiceResponse = (): Codec<
 
 export interface AddBackupProviderRequest {
   backupProviderBaseUrl: string;
+  /**
+   * Activate the provider.  Should only be done after
+   * the user has reviewed the provider.
+   */
+  activate?: boolean;
 }
 
 export const codecForAddBackupProviderRequest = (): Codec<
@@ -1637,6 +1666,10 @@ export async function addBackupProvider(
   const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
   const oldProv = await ws.db.get(Stores.backupProviders, canonUrl);
   if (oldProv) {
+    if (req.activate) {
+      oldProv.active = true;
+      await ws.db.put(Stores.backupProviders, oldProv);
+    }
     return;
   }
   const termsUrl = new URL("terms", canonUrl);
@@ -1646,11 +1679,14 @@ export async function addBackupProvider(
     codecForSyncTermsOfServiceResponse(),
   );
   await ws.db.put(Stores.backupProviders, {
-    active: true,
-    annualFee: terms.annual_fee,
+    active: !!req.activate,
+    terms: {
+      annualFee: terms.annual_fee,
+      storageLimitInMegabytes: terms.storage_limit_in_megabytes,
+      supportedProtocolVersion: terms.version,
+    },
+    paymentProposalIds: [],
     baseUrl: canonUrl,
-    storageLimitInMegabytes: terms.storage_limit_in_megabytes,
-    supportedProtocolVersion: terms.version,
   });
 }
 
@@ -1667,9 +1703,11 @@ export async function restoreFromRecoverySecret(): 
Promise<void> {}
  * as that's derived from the wallet root key.
  */
 export interface ProviderInfo {
+  active: boolean;
   syncProviderBaseUrl: string;
-  lastRemoteClock: number;
-  lastBackup?: Timestamp;
+  lastRemoteClock?: number;
+  lastBackupTimestamp?: Timestamp;
+  paymentProposalIds: string[];
 }
 
 export interface BackupInfo {
@@ -1697,7 +1735,20 @@ export async function importBackupPlain(
 export async function getBackupInfo(
   ws: InternalWalletState,
 ): Promise<BackupInfo> {
-  throw Error("not implemented");
+  const backupConfig = await provideBackupState(ws);
+  const providers = await ws.db.iter(Stores.backupProviders).toArray();
+  return {
+    deviceId: backupConfig.deviceId,
+    lastLocalClock: backupConfig.clocks[backupConfig.deviceId],
+    walletRootPub: backupConfig.walletRootPub,
+    providers: providers.map((x) => ({
+      active: x.active,
+      lastRemoteClock: x.lastBackupClock,
+      syncProviderBaseUrl: x.baseUrl,
+      lastBackupTimestamp: x.lastBackupTimestamp,
+      paymentProposalIds: x.paymentProposalIds,
+    })),
+  };
 }
 
 export interface BackupRecovery {
@@ -1727,6 +1778,77 @@ export async function getBackupRecovery(
   };
 }
 
+async function backupRecoveryTheirs(
+  ws: InternalWalletState,
+  br: BackupRecovery,
+) {
+  await ws.db.runWithWriteTransaction(
+    [Stores.config, Stores.backupProviders],
+    async (tx) => {
+      let backupStateEntry:
+        | ConfigRecord<WalletBackupConfState>
+        | undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
+      checkDbInvariant(!!backupStateEntry);
+      backupStateEntry.value.lastBackupNonce = undefined;
+      backupStateEntry.value.lastBackupTimestamp = undefined;
+      backupStateEntry.value.lastBackupCheckTimestamp = undefined;
+      backupStateEntry.value.lastBackupPlainHash = undefined;
+      backupStateEntry.value.walletRootPriv = br.walletRootPriv;
+      backupStateEntry.value.walletRootPub = encodeCrock(
+        eddsaGetPublic(decodeCrock(br.walletRootPriv)),
+      );
+      await tx.put(Stores.config, backupStateEntry);
+      for (const prov of br.providers) {
+        const existingProv = await tx.get(Stores.backupProviders, prov.url);
+        if (!existingProv) {
+          await tx.put(Stores.backupProviders, {
+            active: true,
+            baseUrl: prov.url,
+            paymentProposalIds: [],
+          });
+        }
+      }
+      const providers = await tx.iter(Stores.backupProviders).toArray();
+      for (const prov of providers) {
+        prov.lastBackupTimestamp = undefined;
+        prov.lastBackupHash = undefined;
+        prov.lastBackupClock = undefined;
+        await tx.put(Stores.backupProviders, prov);
+      }
+    },
+  );
+}
+
+async function backupRecoveryOurs(ws: InternalWalletState, br: BackupRecovery) 
{
+  throw Error("not implemented");
+}
+
+export async function loadBackupRecovery(
+  ws: InternalWalletState,
+  br: RecoveryLoadRequest,
+): Promise<void> {
+  const bs = await provideBackupState(ws);
+  const providers = await ws.db.iter(Stores.backupProviders).toArray();
+  let strategy = br.strategy;
+  if (
+    br.recovery.walletRootPriv != bs.walletRootPriv &&
+    providers.length > 0 &&
+    !strategy
+  ) {
+    throw Error(
+      "recovery load strategy must be specified for wallet with existing 
providers",
+    );
+  } else if (!strategy) {
+    // Default to using the new key if we don't have providers yet.
+    strategy = RecoveryMergeStrategy.Theirs;
+  }
+  if (strategy === RecoveryMergeStrategy.Theirs) {
+    return backupRecoveryTheirs(ws, br.recovery);
+  } else {
+    return backupRecoveryOurs(ws, br.recovery);
+  }
+}
+
 export async function exportBackupEncrypted(
   ws: InternalWalletState,
 ): Promise<Uint8Array> {
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
index caab92cf..56b50d71 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -21,27 +21,22 @@
  * as the backup schema must remain very stable and should be self-contained.
  *
  * Current limitations:
- * 1. Exchange/auditor trust isn't exported yet
- *    (see https://bugs.gnunet.org/view.php?id=6448)
- * 2. Reports to the auditor (cryptographic proofs and/or diagnostics) aren't 
exported yet
- * 3. "Ghost spends", where a coin is spent unexpectedly by another wallet
+ * 1. "Ghost spends", where a coin is spent unexpectedly by another wallet
  *    and a corresponding transaction (that is missing some details!) should
  *    be added to the transaction history, aren't implemented yet.
- * 4. Clocks for denom/coin selections aren't properly modeled yet.
+ * 2. Clocks for denom/coin selections aren't properly modeled yet.
  *    (Needed for re-denomination of withdrawal / re-selection of coins)
- * 5. Preferences about how currencies are to be displayed
+ * 3. Preferences about how currencies are to be displayed
  *    aren't exported yet (and not even implemented in wallet-core).
- * 6. Returning money to own bank account isn't supported/exported yet.
- * 7. Peer-to-peer payments aren't supported yet.
- * 8. Next update time / next refresh time isn't backed up yet.
- * 9. Coin/denom selections should be forgettable once that information
+ * 4. Returning money to own bank account isn't supported/exported yet.
+ * 5. Peer-to-peer payments aren't supported yet.
+ * 6. Next update time / next auto-refresh time isn't backed up yet.
+ * 7. Coin/denom selections should be forgettable once that information
  *    becomes irrelevant.
- * 10. Re-denominated payments/refreshes are not shown properly in the total
- *     payment cost.
- * 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
+ * 8. Re-denominated payments/refreshes are not shown properly in the total
+ *    payment cost.
+ * 9. Permanently failed operations aren't properly modeled yet
+ * 10. Do we somehow need to model the mechanism for first only withdrawing
  *     the amount to pay the backup provider?
  *
  * Questions:
@@ -299,15 +294,7 @@ export interface BackupTrustExchange {
   clock_removed?: ClockValue;
 }
 
-/**
- * Backup information about one backup storage provider.
- */
-export class BackupBackupProvider {
-  /**
-   * Canonicalized base URL of the provider.
-   */
-  base_url: string;
-
+export class BackupBackupProviderTerms {
   /**
    * Last known supported protocol version.
    */
@@ -322,6 +309,22 @@ export class BackupBackupProvider {
    * Last known storage limit.
    */
   storage_limit_in_megabytes: number;
+}
+
+/**
+ * Backup information about one backup storage provider.
+ */
+export class BackupBackupProvider {
+  /**
+   * Canonicalized base URL of the provider.
+   */
+  base_url: string;
+
+  /**
+   * Last known terms.  Might be unavailable in some situations, such
+   * as directly after restoring form a backup recovery document.
+   */
+  terms?: BackupBackupProviderTerms;
 
   /**
    * Proposal IDs for payments to this provider.
@@ -790,11 +793,11 @@ export interface BackupPurchase {
 
   /**
    * Total cost initially shown to the user.
-   * 
+   *
    * This includes the amount taken by the merchant, fees (wire/deposit) 
contributed
    * by the customer, refreshing fees, fees for withdraw-after-refresh and 
"trimmings"
    * of coins that are too small to spend.
-   * 
+   *
    * Note that in rare situations, this cost might not be accurate (e.g.
    * when the payment or refresh gets re-denominated).
    * We might show adjustments to this later, but currently we don't do so.
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 1c9f546d..551495a6 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -1426,20 +1426,30 @@ export enum ImportPayloadType {
   CoreSchema = "core-schema",
 }
 
+export enum BackupProviderStatus {
+  PaymentRequired = "payment-required",
+  Ready = "ready",
+}
+
 export interface BackupProviderRecord {
   baseUrl: string;
 
-  supportedProtocolVersion: string;
-
-  annualFee: AmountString;
-
-  storageLimitInMegabytes: number;
+  /**
+   * Terms of service of the provider.
+   * Might be unavailable in the DB in certain situations
+   * (such as loading a recovery document).
+   */
+  terms?: {
+    supportedProtocolVersion: string;
+    annualFee: AmountString;
+    storageLimitInMegabytes: number;  
+  };
 
   active: boolean;
 
   /**
-   * Hash of the last backup that we already
-   * merged.
+   * Hash of the last encrypted backup that we already merged
+   * or successfully uploaded ourselves.
    */
   lastBackupHash?: string;
 
@@ -1448,6 +1458,12 @@ export interface BackupProviderRecord {
    * merged.
    */
   lastBackupClock?: number;
+
+  lastBackupTimestamp?: Timestamp;
+
+  currentPaymentProposalId?: string;
+
+  paymentProposalIds: string[];
 }
 
 class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts 
b/packages/taler-wallet-core/src/types/walletTypes.ts
index 1b962e1c..235ea11f 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -56,6 +56,7 @@ import {
   ContractTerms,
 } from "./talerTypes";
 import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes";
+import { BackupRecovery } from "../operations/backup";
 
 /**
  * Response for the create reserve request to the wallet.
@@ -896,6 +897,29 @@ export interface MakeSyncSignatureRequest {
   newHash: string;
 }
 
+/**
+ * Strategy for loading recovery information.
+ */
+export enum RecoveryMergeStrategy {
+  /**
+   * Keep the local wallet root key, import and take over providers.
+   */
+  Ours = "ours",
+
+  /**
+   * Migrate to the wallet root key from the recovery information.
+   */
+  Theirs = "theirs",
+}
+
+/**
+ * Load recovery information into the wallet.
+ */
+export interface RecoveryLoadRequest {
+  recovery: BackupRecovery;
+  strategy?: RecoveryMergeStrategy;
+}
+
 export const codecForWithdrawTestBalance = (): Codec<
   WithdrawTestBalanceRequest
 > =>
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 0b2b4d63..56e3d82d 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -94,6 +94,7 @@ import {
   codecForAcceptTipRequest,
   codecForAbortPayWithRefundRequest,
   ApplyRefundResponse,
+  RecoveryLoadRequest,
 } from "./types/walletTypes";
 import { Logger } from "./util/logging";
 
@@ -167,6 +168,9 @@ import {
   BackupRecovery,
   getBackupRecovery,
   AddBackupProviderRequest,
+  getBackupInfo,
+  BackupInfo,
+  loadBackupRecovery,
 } from "./operations/backup";
 
 const builtinCurrencies: CurrencyRecord[] = [
@@ -959,6 +963,10 @@ export class Wallet {
     return getBackupRecovery(this.ws);
   }
 
+  async loadBackupRecovery(req: RecoveryLoadRequest): Promise<void> {
+    return loadBackupRecovery(this.ws, req);
+  }
+
   async addBackupProvider(req: AddBackupProviderRequest): Promise<void> {
     return addBackupProvider(this.ws, req);
   }
@@ -967,6 +975,10 @@ export class Wallet {
     return runBackupCycle(this.ws);
   }
 
+  async getBackupStatus(): Promise<BackupInfo> {
+    return getBackupInfo(this.ws);
+  }
+  
   /**
    * Implementation of the "wallet-core" API.
    */

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