summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/backup.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/backup.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/backup.ts294
1 files changed, 289 insertions, 5 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup.ts b/packages/taler-wallet-core/src/operations/backup.ts
index fdccd23c1..b82e63ff2 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -60,6 +60,7 @@ import {
DenomSelectionState,
ExchangeUpdateStatus,
ExchangeWireInfo,
+ PayCoinSelection,
ProposalDownload,
ProposalStatus,
RefreshSessionRecord,
@@ -67,6 +68,8 @@ import {
ReserveBankInfo,
ReserveRecordStatus,
Stores,
+ WalletContractData,
+ WalletRefundItem,
} from "../types/dbTypes";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
@@ -77,6 +80,7 @@ import {
encodeCrock,
getRandomBytes,
hash,
+ rsaBlind,
stringToBytes,
} from "../crypto/talerCrypto";
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
@@ -102,6 +106,7 @@ import { 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";
interface WalletBackupConfState {
deviceId: string;
@@ -461,6 +466,8 @@ export async function exportBackup(
? undefined
: purch.abortStatus,
nonce_priv: purch.noncePriv,
+ merchant_sig: purch.download.contractData.merchantSig,
+ total_pay_cost: Amounts.stringify(purch.totalPayCost),
});
});
@@ -607,11 +614,77 @@ interface CompletedCoin {
interface BackupCryptoPrecomputedData {
denomPubToHash: Record<string, string>;
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
- proposalNoncePrivToProposalPub: { [priv: string]: string };
+ proposalNoncePrivToPub: { [priv: string]: string };
proposalIdToContractTermsHash: { [proposalId: string]: string };
reservePrivToPub: Record<string, string>;
}
+/**
+ * Compute cryptographic values for a backup blob.
+ *
+ * FIXME: Take data that we already know from the DB.
+ * FIXME: Move computations into crypto worker.
+ */
+async function computeBackupCryptoData(
+ cryptoApi: CryptoApi,
+ backupContent: WalletBackupContentV1,
+): Promise<BackupCryptoPrecomputedData> {
+ const cryptoData: BackupCryptoPrecomputedData = {
+ coinPrivToCompletedCoin: {},
+ denomPubToHash: {},
+ proposalIdToContractTermsHash: {},
+ proposalNoncePrivToPub: {},
+ reservePrivToPub: {},
+ };
+ for (const backupExchange of backupContent.exchanges) {
+ for (const backupDenom of backupExchange.denominations) {
+ for (const backupCoin of backupDenom.coins) {
+ const coinPub = encodeCrock(
+ eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
+ );
+ const blindedCoin = rsaBlind(
+ hash(decodeCrock(backupCoin.coin_priv)),
+ decodeCrock(backupCoin.blinding_key),
+ decodeCrock(backupDenom.denom_pub),
+ );
+ cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
+ coinEvHash: encodeCrock(hash(blindedCoin)),
+ coinPub,
+ }
+ }
+ cryptoData.denomPubToHash[backupDenom.denom_pub] = encodeCrock(
+ hash(decodeCrock(backupDenom.denom_pub)),
+ );
+ }
+ for (const backupReserve of backupExchange.reserves) {
+ cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
+ eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
+ );
+ }
+ }
+ for (const prop of backupContent.proposals) {
+ const contractTermsHash = await cryptoApi.hashString(
+ canonicalJson(prop.contract_terms_raw),
+ );
+ const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(prop.nonce_priv)));
+ cryptoData.proposalNoncePrivToPub[prop.nonce_priv] = noncePub;
+ cryptoData.proposalIdToContractTermsHash[
+ prop.proposal_id
+ ] = contractTermsHash;
+ }
+ for (const purch of backupContent.purchases) {
+ const contractTermsHash = await cryptoApi.hashString(
+ canonicalJson(purch.contract_terms_raw),
+ );
+ const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
+ cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
+ cryptoData.proposalIdToContractTermsHash[
+ purch.proposal_id
+ ] = contractTermsHash;
+ }
+ return cryptoData;
+}
+
function checkBackupInvariant(b: boolean, m?: string): asserts b {
if (!b) {
if (m) {
@@ -622,6 +695,88 @@ function checkBackupInvariant(b: boolean, m?: string): asserts b {
}
}
+/**
+ * Re-compute information about the coin selection for a payment.
+ */
+async function recoverPayCoinSelection(
+ tx: TransactionHandle<
+ typeof Stores.exchanges | typeof Stores.coins | typeof Stores.denominations
+ >,
+ contractData: WalletContractData,
+ backupPurchase: BackupPurchase,
+): Promise<PayCoinSelection> {
+ const coinPubs: string[] = backupPurchase.pay_coins.map((x) => x.coin_pub);
+ const coinContributions: AmountJson[] = backupPurchase.pay_coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ );
+
+ const coveredExchanges: Set<string> = new Set();
+
+ let totalWireFee: AmountJson = Amounts.getZero(contractData.amount.currency);
+ let totalDepositFees: AmountJson = Amounts.getZero(
+ contractData.amount.currency,
+ );
+
+ for (const coinPub of coinPubs) {
+ const coinRecord = await tx.get(Stores.coins, coinPub);
+ checkBackupInvariant(!!coinRecord);
+ const denom = await tx.get(Stores.denominations, [
+ coinRecord.exchangeBaseUrl,
+ coinRecord.denomPubHash,
+ ]);
+ checkBackupInvariant(!!denom);
+ totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
+
+ if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
+ const exchange = await tx.get(
+ Stores.exchanges,
+ coinRecord.exchangeBaseUrl,
+ );
+ checkBackupInvariant(!!exchange);
+ let wireFee: AmountJson | undefined;
+ const feesForType = exchange.wireInfo?.feesForType;
+ checkBackupInvariant(!!feesForType);
+ for (const fee of feesForType[contractData.wireMethod] || []) {
+ if (
+ fee.startStamp <= contractData.timestamp &&
+ fee.endStamp >= contractData.timestamp
+ ) {
+ wireFee = fee.wireFee;
+ break;
+ }
+ }
+ if (wireFee) {
+ totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
+ }
+ }
+ }
+
+ let customerWireFee: AmountJson;
+
+ const amortizedWireFee = Amounts.divide(
+ totalWireFee,
+ contractData.wireFeeAmortization,
+ );
+ if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
+ customerWireFee = amortizedWireFee;
+ } else {
+ customerWireFee = Amounts.getZero(contractData.amount.currency);
+ }
+
+ const customerDepositFees = Amounts.sub(
+ totalDepositFees,
+ contractData.maxDepositFee,
+ ).amount;
+
+ return {
+ coinPubs,
+ coinContributions,
+ paymentAmount: contractData.amount,
+ customerWireFees: customerWireFee,
+ customerDepositFees,
+ };
+}
+
function getDenomSelStateFromBackup(
tx: TransactionHandle<typeof Stores.denominations>,
sel: BackupDenomSel,
@@ -959,9 +1114,7 @@ export async function importBackup(
orderId: backupProposal.order_id,
noncePriv: backupProposal.nonce_priv,
noncePub:
- cryptoComp.proposalNoncePrivToProposalPub[
- backupProposal.nonce_priv
- ],
+ cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
proposalId: backupProposal.proposal_id,
repurchaseProposalId: backupProposal.repurchase_proposal_id,
retryInfo: initRetryInfo(false),
@@ -977,7 +1130,138 @@ export async function importBackup(
backupPurchase.proposal_id,
);
if (!existingPurchase) {
- await tx.put(Stores.purchases, {});
+ const refunds: { [refundKey: string]: WalletRefundItem } = {};
+ for (const backupRefund of backupPurchase.refunds) {
+ const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
+ const coin = await tx.get(Stores.coins, backupRefund.coin_pub);
+ checkBackupInvariant(!!coin);
+ const denom = await tx.get(Stores.denominations, [
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ checkBackupInvariant(!!denom);
+ const common = {
+ coinPub: backupRefund.coin_pub,
+ executionTime: backupRefund.execution_time,
+ obtainedTime: backupRefund.obtained_time,
+ refundAmount: Amounts.parseOrThrow(backupRefund.refund_amount),
+ refundFee: denom.feeRefund,
+ rtransactionId: backupRefund.rtransaction_id,
+ totalRefreshCostBound: Amounts.parseOrThrow(
+ backupRefund.total_refresh_cost_bound,
+ ),
+ };
+ switch (backupRefund.type) {
+ case BackupRefundState.Applied:
+ refunds[key] = {
+ type: RefundState.Applied,
+ ...common,
+ };
+ break;
+ case BackupRefundState.Failed:
+ refunds[key] = {
+ type: RefundState.Failed,
+ ...common,
+ };
+ break;
+ case BackupRefundState.Pending:
+ refunds[key] = {
+ type: RefundState.Pending,
+ ...common,
+ };
+ break;
+ }
+ }
+ let abortStatus: AbortStatus;
+ switch (backupPurchase.abort_status) {
+ case "abort-finished":
+ abortStatus = AbortStatus.AbortFinished;
+ break;
+ case "abort-refund":
+ abortStatus = AbortStatus.AbortRefund;
+ break;
+ default:
+ throw Error("not reachable");
+ }
+ const parsedContractTerms = codecForContractTerms().decode(
+ backupPurchase.contract_terms_raw,
+ );
+ const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
+ const contractTermsHash =
+ cryptoComp.proposalIdToContractTermsHash[
+ backupPurchase.proposal_id
+ ];
+ let maxWireFee: AmountJson;
+ if (parsedContractTerms.max_wire_fee) {
+ maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
+ } else {
+ maxWireFee = Amounts.getZero(amount.currency);
+ }
+ const download: ProposalDownload = {
+ contractData: {
+ amount,
+ contractTermsHash: contractTermsHash,
+ fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
+ merchantBaseUrl: parsedContractTerms.merchant_base_url,
+ merchantPub: parsedContractTerms.merchant_pub,
+ merchantSig: backupPurchase.merchant_sig,
+ orderId: parsedContractTerms.order_id,
+ summary: parsedContractTerms.summary,
+ autoRefund: parsedContractTerms.auto_refund,
+ maxWireFee,
+ payDeadline: parsedContractTerms.pay_deadline,
+ refundDeadline: parsedContractTerms.refund_deadline,
+ wireFeeAmortization:
+ parsedContractTerms.wire_fee_amortization || 1,
+ allowedAuditors: parsedContractTerms.auditors.map((x) => ({
+ auditorBaseUrl: x.url,
+ auditorPub: x.master_pub,
+ })),
+ allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
+ exchangeBaseUrl: x.url,
+ exchangePub: x.master_pub,
+ })),
+ timestamp: parsedContractTerms.timestamp,
+ wireMethod: parsedContractTerms.wire_method,
+ wireInfoHash: parsedContractTerms.h_wire,
+ maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
+ merchant: parsedContractTerms.merchant,
+ products: parsedContractTerms.products,
+ summaryI18n: parsedContractTerms.summary_i18n,
+ },
+ contractTermsRaw: backupPurchase.contract_terms_raw,
+ };
+ await tx.put(Stores.purchases, {
+ proposalId: backupPurchase.proposal_id,
+ noncePriv: backupPurchase.nonce_priv,
+ noncePub:
+ cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
+ lastPayError: undefined,
+ autoRefundDeadline: { t_ms: "never" },
+ refundStatusRetryInfo: initRetryInfo(false),
+ lastRefundStatusError: undefined,
+ timestampAccept: backupPurchase.timestamp_accept,
+ timestampFirstSuccessfulPay:
+ backupPurchase.timestamp_first_successful_pay,
+ timestampLastRefundStatus:
+ backupPurchase.timestamp_last_refund_status,
+ merchantPaySig: backupPurchase.merchant_pay_sig,
+ lastSessionId: undefined,
+ abortStatus,
+ // FIXME!
+ payRetryInfo: initRetryInfo(false),
+ download,
+ paymentSubmitPending: !backupPurchase.timestamp_first_successful_pay,
+ refundQueryRequested: false,
+ payCoinSelection: await recoverPayCoinSelection(
+ tx,
+ download.contractData,
+ backupPurchase,
+ ),
+ coinDepositPermissions: undefined,
+ totalPayCost: Amounts.parseOrThrow(backupPurchase.total_pay_cost),
+ refunds,
+ });
}
}