aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-01-08 13:30:29 +0100
committerFlorian Dold <florian@dold.me>2021-01-08 13:30:29 +0100
commit8921a5e8f2f47c113eeeaa1bf14937c5b6cfb0ac (patch)
tree956d493e976b9316b23332cab6e3057933db2a3a
parent324f44ae6954ef7a75a67838a7f0cbf2a6dc6d76 (diff)
implement import of backup recovery document
-rw-r--r--packages/taler-wallet-cli/src/index.ts38
-rw-r--r--packages/taler-wallet-core/src/operations/backup.ts154
-rw-r--r--packages/taler-wallet-core/src/types/backupTypes.ts57
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts30
-rw-r--r--packages/taler-wallet-core/src/types/walletTypes.ts24
-rw-r--r--packages/taler-wallet-core/src/wallet.ts12
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 87e0e00d1..87a51f30d 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 f67d32e50..72fdf7aa1 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 caab92cf8..56b50d71c 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 1c9f546d9..551495a68 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 1b962e1c4..235ea11f1 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 0b2b4d639..56e3d82d1 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.
*/