aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-01-07 18:56:09 +0100
committerFlorian Dold <florian@dold.me>2021-01-07 18:56:09 +0100
commit265034104241eabffab32693f3a5a1af85cd7749 (patch)
treebce9e6b418fb23b4ca561cf6c94be8c1a0d19345 /packages
parentb2e213bae6c8f9d6a7629f3b9a1e474fb33bdb24 (diff)
implement backup encryption, some more CLI commands
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-wallet-cli/src/index.ts53
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts12
-rw-r--r--packages/taler-wallet-core/src/operations/backup.ts140
-rw-r--r--packages/taler-wallet-core/src/types/backupTypes.ts1
-rw-r--r--packages/taler-wallet-core/src/wallet.ts25
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 f4970e73d..87e0e00d1 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 ceb601468..acaebf546 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 4f736c3df..5108dccfd 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 f7bd8784d..32ff8c52d 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 b917246fc..0b2b4d639 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.
*/