gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: implement backup encryption, some more CLI co


From: gnunet
Subject: [taler-wallet-core] 02/02: implement backup encryption, some more CLI commands
Date: Thu, 07 Jan 2021 18:56:17 +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 265034104241eabffab32693f3a5a1af85cd7749
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Jan 7 18:56:09 2021 +0100

    implement backup encryption, some more CLI commands
---
 packages/taler-wallet-cli/src/index.ts             |  53 ++++++++
 .../src/crypto/primitives/nacl-fast.ts             |  12 +-
 .../taler-wallet-core/src/operations/backup.ts     | 140 ++++++++++++++++++++-
 .../taler-wallet-core/src/types/backupTypes.ts     |   1 +
 packages/taler-wallet-core/src/wallet.ts           |  25 ++++
 5 files changed, 221 insertions(+), 10 deletions(-)

diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index f4970e73..87e0e00d 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -409,6 +409,29 @@ backupCli.subcommand("exportPlain", 
"export-plain").action(async (args) => {
   });
 });
 
+backupCli
+  .subcommand("export", "export")
+  .requiredArgument("filename", clk.STRING, {
+    help: "backup filename",
+  })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      const backup = await wallet.exportBackupEncrypted();
+      fs.writeFileSync(args.export.filename, backup);
+    });
+  });
+
+backupCli
+  .subcommand("import", "import")
+  .requiredArgument("filename", clk.STRING, {
+    help: "backup filename",
+  })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      const backupEncBlob = fs.readFileSync(args.import.filename);
+      await wallet.importBackupEncrypted(backupEncBlob);
+    });
+  });
 
 backupCli.subcommand("importPlain", "import-plain").action(async (args) => {
   await withWallet(args, async (wallet) => {
@@ -417,6 +440,36 @@ backupCli.subcommand("importPlain", 
"import-plain").action(async (args) => {
   });
 });
 
+backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => {
+  await withWallet(args, async (wallet) => {
+    const recoveryJson = await wallet.getBackupRecovery();
+    console.log(JSON.stringify(recoveryJson, undefined, 2));
+  });
+});
+
+backupCli.subcommand("run", "run").action(async (args) => {
+  await withWallet(args, async (wallet) => {
+    await wallet.runBackupCycle();
+  });
+});
+
+backupCli
+  .subcommand("recoveryLoad", "load-recovery")
+  .action(async (args) => {});
+
+backupCli.subcommand("status", "status").action(async (args) => {});
+
+backupCli
+  .subcommand("addProvider", "add-provider")
+  .requiredArgument("url", clk.STRING)
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      wallet.addBackupProvider({
+        backupProviderBaseUrl: args.addProvider.url,
+      });
+    });
+  });
+
 const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
   help:
     "Subcommands for advanced operations (only use if you know what you're 
doing!).",
diff --git a/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts 
b/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts
index ceb60146..acaebf54 100644
--- a/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts
+++ b/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts
@@ -2990,7 +2990,11 @@ export function sign_ed25519_pk_to_curve25519(
   return x25519_pk;
 }
 
-export function secretbox(msg: Uint8Array, nonce: Uint8Array, key: Uint8Array) 
{
+export function secretbox(
+  msg: Uint8Array,
+  nonce: Uint8Array,
+  key: Uint8Array,
+): Uint8Array {
   checkArrayTypes(msg, nonce, key);
   checkLengths(key, nonce);
   var m = new Uint8Array(crypto_secretbox_ZEROBYTES + msg.length);
@@ -3005,15 +3009,15 @@ export function secretbox_open(
   box: Uint8Array,
   nonce: Uint8Array,
   key: Uint8Array,
-) {
+): Uint8Array | undefined {
   checkArrayTypes(box, nonce, key);
   checkLengths(key, nonce);
   var c = new Uint8Array(crypto_secretbox_BOXZEROBYTES + box.length);
   var m = new Uint8Array(c.length);
   for (var i = 0; i < box.length; i++)
     c[i + crypto_secretbox_BOXZEROBYTES] = box[i];
-  if (c.length < 32) return null;
-  if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return null;
+  if (c.length < 32) return undefined;
+  if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return 
undefined;
   return m.subarray(crypto_secretbox_ZEROBYTES);
 }
 
diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
index 4f736c3d..5108dccf 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -74,6 +74,7 @@ import {
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
 import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
 import {
+  bytesToString,
   decodeCrock,
   eddsaGetPublic,
   EddsaKeyPair,
@@ -102,11 +103,13 @@ import {
   readSuccessResponseJsonOrThrow,
 } from "../util/http";
 import { Logger } from "../util/logging";
-import { gzipSync } from "fflate";
+import { gunzipSync, gzipSync } from "fflate";
 import { kdf } from "../crypto/primitives/kdf";
 import { initRetryInfo } from "../util/retries";
 import { RefreshReason } from "../types/walletTypes";
 import { CryptoApi } from "../crypto/workers/cryptoApi";
+import { secretbox, secretbox_open } from "../crypto/primitives/nacl-fast";
+import { str } from "../i18n";
 
 interface WalletBackupConfState {
   deviceId: string;
@@ -588,10 +591,54 @@ export async function exportBackup(
   );
 }
 
+function concatArrays(xs: Uint8Array[]): Uint8Array {
+  let len = 0;
+  for (const x of xs) {
+    len += x.byteLength;
+  }
+  const out = new Uint8Array(len);
+  let offset = 0;
+  for (const x of xs) {
+    out.set(x, offset);
+    offset += x.length;
+  }
+  return out;
+}
+
+const magic = "TLRWBK01";
+
+/**
+ * Encrypt the backup.
+ *
+ * Blob format:
+ * Magic "TLRWBK01" (8 bytes)
+ * Nonce (24 bytes)
+ * Compressed JSON blob (rest)
+ */
 export async function encryptBackup(
   config: WalletBackupConfState,
   blob: WalletBackupContentV1,
 ): Promise<Uint8Array> {
+  const chunks: Uint8Array[] = [];
+  chunks.push(stringToBytes(magic));
+  const nonceStr = config.lastBackupNonce;
+  checkLogicInvariant(!!nonceStr);
+  const nonce = decodeCrock(nonceStr).slice(0, 24);
+  chunks.push(nonce);
+  const backupJsonContent = canonicalJson(blob);
+  logger.trace("backup JSON size", backupJsonContent.length);
+  const compressedContent = gzipSync(stringToBytes(backupJsonContent));
+  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");
 }
 
@@ -778,7 +825,10 @@ async function getDenomSelStateFromBackup(
   exchangeBaseUrl: string,
   sel: BackupDenomSel,
 ): Promise<DenomSelectionState> {
-  const d0 = await tx.get(Stores.denominations, [exchangeBaseUrl, 
sel[0].denom_pub_hash]);
+  const d0 = await tx.get(Stores.denominations, [
+    exchangeBaseUrl,
+    sel[0].denom_pub_hash,
+  ]);
   checkBackupInvariant(!!d0);
   const selectedDenoms: {
     denomPubHash: string;
@@ -787,16 +837,20 @@ async function getDenomSelStateFromBackup(
   let totalCoinValue = Amounts.getZero(d0.value.currency);
   let totalWithdrawCost = Amounts.getZero(d0.value.currency);
   for (const s of sel) {
-    const d = await tx.get(Stores.denominations, [exchangeBaseUrl, 
s.denom_pub_hash]);
+    const d = await tx.get(Stores.denominations, [
+      exchangeBaseUrl,
+      s.denom_pub_hash,
+    ]);
     checkBackupInvariant(!!d);
     totalCoinValue = Amounts.add(totalCoinValue, d.value).amount;
-    totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, 
d.feeWithdraw).amount;
+    totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, d.feeWithdraw)
+      .amount;
   }
   return {
     selectedDenoms,
     totalCoinValue,
     totalWithdrawCost,
-  }
+  };
 }
 
 export async function importBackup(
@@ -1407,6 +1461,15 @@ function deriveAccountKeyPair(
   };
 }
 
+function deriveBlobSecret(bc: WalletBackupConfState): Uint8Array {
+  return kdf(
+    32,
+    decodeCrock(bc.walletRootPriv),
+    stringToBytes("taler-sync-blob-secret-salt"),
+    stringToBytes("taler-sync-blob-secret-info"),
+  );
+}
+
 /**
  * Do one backup cycle that consists of:
  * 1. Exporting a backup and try to upload it.
@@ -1566,6 +1629,71 @@ export async function importBackupPlain(
 /**
  * Get information about the current state of wallet backups.
  */
-export function getBackupInfo(ws: InternalWalletState): Promise<BackupInfo> {
+export async function getBackupInfo(
+  ws: InternalWalletState,
+): Promise<BackupInfo> {
   throw Error("not implemented");
 }
+
+export interface BackupRecovery {
+  walletRootPriv: string;
+  providers: {
+    url: string;
+  }[];
+}
+
+/**
+ * Get information about the current state of wallet backups.
+ */
+export async function getBackupRecovery(
+  ws: InternalWalletState,
+): Promise<BackupRecovery> {
+  const bs = await provideBackupState(ws);
+  const providers = await ws.db.iter(Stores.backupProviders).toArray();
+  return {
+    providers: providers
+      .filter((x) => x.active)
+      .map((x) => {
+        return {
+          url: x.baseUrl,
+        };
+      }),
+    walletRootPriv: bs.walletRootPriv,
+  };
+}
+
+export async function exportBackupEncrypted(
+  ws: InternalWalletState,
+): Promise<Uint8Array> {
+  await provideBackupState(ws);
+  const blob = await exportBackup(ws);
+  const bs = await ws.db.runWithWriteTransaction(
+    [Stores.config],
+    async (tx) => {
+      return await getWalletBackupState(ws, tx);
+    },
+  );
+  return encryptBackup(bs, blob);
+}
+
+export async function importBackupEncrypted(
+  ws: InternalWalletState,
+  data: Uint8Array,
+): Promise<void> {
+  const backupConfig = await provideBackupState(ws);
+  const rMagic = bytesToString(data.slice(0, 8));
+  if (rMagic != magic) {
+    throw Error("invalid backup file (magic tag mismatch)");
+  }
+
+  const nonce = data.slice(8, 8 + 24);
+  const box = data.slice(8 + 24);
+  const secret = deriveBlobSecret(backupConfig);
+  const dataCompressed = secretbox_open(box, nonce, secret);
+  if (!dataCompressed) {
+    throw Error("decryption failed");
+  }
+  const blob = JSON.parse(bytesToString(gunzipSync(dataCompressed)));
+  const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
+  await importBackup(ws, blob, cryptoData);
+}
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
index f7bd8784..32ff8c52 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -40,6 +40,7 @@
  *     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
  *
  * Questions:
  * 1. What happens when two backups are merged that have
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index b917246f..0b2b4d63 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -162,6 +162,11 @@ import {
   runBackupCycle,
   exportBackup,
   importBackupPlain,
+  exportBackupEncrypted,
+  importBackupEncrypted,
+  BackupRecovery,
+  getBackupRecovery,
+  AddBackupProviderRequest,
 } from "./operations/backup";
 
 const builtinCurrencies: CurrencyRecord[] = [
@@ -942,6 +947,26 @@ export class Wallet {
     return importBackupPlain(this.ws, backup);
   }
 
+  async exportBackupEncrypted() {
+    return exportBackupEncrypted(this.ws);
+  }
+
+  async importBackupEncrypted(backup: Uint8Array) {
+    return importBackupEncrypted(this.ws, backup);
+  }
+
+  async getBackupRecovery(): Promise<BackupRecovery> {
+    return getBackupRecovery(this.ws);
+  }
+
+  async addBackupProvider(req: AddBackupProviderRequest): Promise<void> {
+    return addBackupProvider(this.ws, req);
+  }
+
+  async runBackupCycle(): Promise<void> {
+    return runBackupCycle(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]