diff options
author | Florian Dold <florian@dold.me> | 2023-08-30 18:01:18 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-08-30 18:01:18 +0200 |
commit | a713d90c3c564408309d92223d383ecc9225924f (patch) | |
tree | 18142a4ce3d98df6e8a4945dbca23c47715eee6a /packages/taler-wallet-core | |
parent | 0a4782a0da631aba31dc0ecef7427df2467cc3e6 (diff) | |
download | wallet-core-a713d90c3c564408309d92223d383ecc9225924f.tar.xz |
wallet-core: remove old sync code, add stored backups skeleton
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 49 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup/export.ts | 586 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup/import.ts | 874 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup/index.ts | 188 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup/state.ts | 92 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet-api-types.ts | 42 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 32 |
7 files changed, 194 insertions, 1669 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index e68385267..1255e8c71 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -2769,6 +2769,24 @@ export const walletMetadataStore = { ), }; +export interface StoredBackupMeta { + name: string; +} + +export interface StoredBackupData { + name: string; + data: any; +} + +export const StoredBackupStores = { + backupMeta: describeStore( + "backupMeta", + describeContents<MetaConfigRecord>({ keyPath: "name" }), + {}, + ), + backupData: describeStore("backupData", describeContents<any>({}), {}), +}; + export interface DbDumpRecord { /** * Key, serialized with structuredEncapsulated. @@ -2831,6 +2849,7 @@ export async function exportSingleDb( return new Promise((resolve, reject) => { const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { + myDb.close(); resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of @@ -3211,6 +3230,36 @@ function onMetaDbUpgradeNeeded( ); } +function onStoredBackupsDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + StoredBackupStores, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +export async function openStoredBackupsDatabase( + idbFactory: IDBFactory, +): Promise<DbAccess<typeof StoredBackupStores>> { + const backupsDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_META_DB_NAME, + 1, + () => {}, + onStoredBackupsDbUpgradeNeeded, + ); + + const handle = new DbAccess(backupsDbHandle, StoredBackupStores); + return handle; +} + /** * Return a promise that resolves * to the taler wallet db. diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts deleted file mode 100644 index c9446a05f..000000000 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ /dev/null @@ -1,586 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Implementation of wallet backups (export/import/upload) and sync - * server management. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ -import { - AbsoluteTime, - Amounts, - BackupBackupProvider, - BackupBackupProviderTerms, - BackupCoin, - BackupCoinSource, - BackupCoinSourceType, - BackupDenomination, - BackupExchange, - BackupExchangeDetails, - BackupExchangeSignKey, - BackupExchangeWireFee, - BackupOperationStatus, - BackupPayInfo, - BackupProposalStatus, - BackupPurchase, - BackupRecoupGroup, - BackupRefreshGroup, - BackupRefreshOldCoin, - BackupRefreshSession, - BackupRefundItem, - BackupRefundState, - BackupTip, - BackupWgInfo, - BackupWgType, - BackupWithdrawalGroup, - BACKUP_VERSION_MAJOR, - BACKUP_VERSION_MINOR, - canonicalizeBaseUrl, - canonicalJson, - CoinStatus, - encodeCrock, - getRandomBytes, - hash, - Logger, - stringToBytes, - WalletBackupContentV1, - TalerPreciseTimestamp, -} from "@gnu-taler/taler-util"; -import { - CoinSourceType, - ConfigRecordKey, - DenominationRecord, - PurchaseStatus, - RefreshCoinStatus, - WithdrawalGroupStatus, - WithdrawalRecordType, -} from "../../db.js"; -import { InternalWalletState } from "../../internal-wallet-state.js"; -import { assertUnreachable } from "../../util/assertUnreachable.js"; -import { checkDbInvariant } from "../../util/invariants.js"; -import { getWalletBackupState, provideBackupState } from "./state.js"; - -const logger = new Logger("backup/export.ts"); - -export async function exportBackup( - ws: InternalWalletState, -): Promise<WalletBackupContentV1> { - await provideBackupState(ws); - return ws.db - .mktx((x) => [ - x.config, - x.exchanges, - x.exchangeDetails, - x.exchangeSignKeys, - x.coins, - x.contractTerms, - x.denominations, - x.purchases, - x.refreshGroups, - x.backupProviders, - x.rewards, - x.recoupGroups, - x.withdrawalGroups, - ]) - .runReadWrite(async (tx) => { - const bs = await getWalletBackupState(ws, tx); - - const backupExchangeDetails: BackupExchangeDetails[] = []; - const backupExchanges: BackupExchange[] = []; - const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {}; - const backupDenominationsByExchange: { - [url: string]: BackupDenomination[]; - } = {}; - const backupPurchases: BackupPurchase[] = []; - const backupRefreshGroups: BackupRefreshGroup[] = []; - const backupBackupProviders: BackupBackupProvider[] = []; - const backupTips: BackupTip[] = []; - const backupRecoupGroups: BackupRecoupGroup[] = []; - const backupWithdrawalGroups: BackupWithdrawalGroup[] = []; - - await tx.withdrawalGroups.iter().forEachAsync(async (wg) => { - let info: BackupWgInfo; - switch (wg.wgInfo.withdrawalType) { - case WithdrawalRecordType.BankIntegrated: - info = { - type: BackupWgType.BankIntegrated, - exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri, - taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri, - confirm_url: wg.wgInfo.bankInfo.confirmUrl, - timestamp_bank_confirmed: - wg.wgInfo.bankInfo.timestampBankConfirmed, - timestamp_reserve_info_posted: - wg.wgInfo.bankInfo.timestampReserveInfoPosted, - }; - break; - case WithdrawalRecordType.BankManual: - info = { - type: BackupWgType.BankManual, - }; - break; - case WithdrawalRecordType.PeerPullCredit: - info = { - type: BackupWgType.PeerPullCredit, - contract_priv: wg.wgInfo.contractPriv, - contract_terms: wg.wgInfo.contractTerms, - }; - break; - case WithdrawalRecordType.PeerPushCredit: - info = { - type: BackupWgType.PeerPushCredit, - contract_terms: wg.wgInfo.contractTerms, - }; - break; - case WithdrawalRecordType.Recoup: - info = { - type: BackupWgType.Recoup, - }; - break; - default: - assertUnreachable(wg.wgInfo); - } - backupWithdrawalGroups.push({ - raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount), - info, - timestamp_created: wg.timestampStart, - timestamp_finish: wg.timestampFinish, - withdrawal_group_id: wg.withdrawalGroupId, - secret_seed: wg.secretSeed, - exchange_base_url: wg.exchangeBaseUrl, - instructed_amount: Amounts.stringify(wg.instructedAmount), - effective_withdrawal_amount: Amounts.stringify( - wg.effectiveWithdrawalAmount, - ), - reserve_priv: wg.reservePriv, - restrict_age: wg.restrictAge, - // FIXME: proper status conversion! - operation_status: - wg.status == WithdrawalGroupStatus.Finished - ? BackupOperationStatus.Finished - : BackupOperationStatus.Pending, - selected_denoms_uid: wg.denomSelUid, - selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - }); - }); - - await tx.rewards.iter().forEach((tip) => { - backupTips.push({ - exchange_base_url: tip.exchangeBaseUrl, - merchant_base_url: tip.merchantBaseUrl, - merchant_tip_id: tip.merchantRewardId, - wallet_tip_id: tip.walletRewardId, - next_url: tip.next_url, - secret_seed: tip.secretSeed, - selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - timestamp_finished: tip.pickedUpTimestamp, - timestamp_accepted: tip.acceptedTimestamp, - timestamp_created: tip.createdTimestamp, - timestamp_expiration: tip.rewardExpiration, - tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw), - selected_denoms_uid: tip.denomSelUid, - }); - }); - - await tx.recoupGroups.iter().forEach((recoupGroup) => { - backupRecoupGroups.push({ - recoup_group_id: recoupGroup.recoupGroupId, - timestamp_created: recoupGroup.timestampStarted, - timestamp_finish: recoupGroup.timestampFinished, - coins: recoupGroup.coinPubs.map((x, i) => ({ - coin_pub: x, - recoup_finished: recoupGroup.recoupFinishedPerCoin[i], - })), - }); - }); - - await tx.backupProviders.iter().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({ - terms, - base_url: canonicalizeBaseUrl(bp.baseUrl), - pay_proposal_ids: bp.paymentProposalIds, - uids: bp.uids, - }); - }); - - await tx.coins.iter().forEach((coin) => { - let bcs: BackupCoinSource; - switch (coin.coinSource.type) { - case CoinSourceType.Refresh: - bcs = { - type: BackupCoinSourceType.Refresh, - old_coin_pub: coin.coinSource.oldCoinPub, - refresh_group_id: coin.coinSource.refreshGroupId, - }; - break; - case CoinSourceType.Reward: - bcs = { - type: BackupCoinSourceType.Reward, - coin_index: coin.coinSource.coinIndex, - wallet_tip_id: coin.coinSource.walletRewardId, - }; - break; - case CoinSourceType.Withdraw: - bcs = { - type: BackupCoinSourceType.Withdraw, - coin_index: coin.coinSource.coinIndex, - reserve_pub: coin.coinSource.reservePub, - withdrawal_group_id: coin.coinSource.withdrawalGroupId, - }; - break; - } - - const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []); - coins.push({ - blinding_key: coin.blindingKey, - coin_priv: coin.coinPriv, - coin_source: bcs, - fresh: coin.status === CoinStatus.Fresh, - spend_allocation: coin.spendAllocation - ? { - amount: coin.spendAllocation.amount, - id: coin.spendAllocation.id, - } - : undefined, - denom_sig: coin.denomSig, - }); - }); - - await tx.denominations.iter().forEach((denom) => { - const backupDenoms = (backupDenominationsByExchange[ - denom.exchangeBaseUrl - ] ??= []); - backupDenoms.push({ - coins: backupCoinsByDenom[denom.denomPubHash] ?? [], - denom_pub: denom.denomPub, - fee_deposit: Amounts.stringify(denom.fees.feeDeposit), - fee_refresh: Amounts.stringify(denom.fees.feeRefresh), - fee_refund: Amounts.stringify(denom.fees.feeRefund), - fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw), - is_offered: denom.isOffered, - is_revoked: denom.isRevoked, - master_sig: denom.masterSig, - stamp_expire_deposit: denom.stampExpireDeposit, - stamp_expire_legal: denom.stampExpireLegal, - stamp_expire_withdraw: denom.stampExpireWithdraw, - stamp_start: denom.stampStart, - value: Amounts.stringify(DenominationRecord.getValue(denom)), - list_issue_date: denom.listIssueDate, - }); - }); - - await tx.exchanges.iter().forEachAsync(async (ex) => { - const dp = ex.detailsPointer; - if (!dp) { - return; - } - backupExchanges.push({ - base_url: ex.baseUrl, - currency: dp.currency, - master_public_key: dp.masterPublicKey, - update_clock: dp.updateClock, - }); - }); - - await tx.exchangeDetails.iter().forEachAsync(async (ex) => { - // Only back up permanently added exchanges. - - const wi = ex.wireInfo; - const wireFees: BackupExchangeWireFee[] = []; - - Object.keys(wi.feesForType).forEach((x) => { - for (const f of wi.feesForType[x]) { - wireFees.push({ - wire_type: x, - closing_fee: Amounts.stringify(f.closingFee), - end_stamp: f.endStamp, - sig: f.sig, - start_stamp: f.startStamp, - wire_fee: Amounts.stringify(f.wireFee), - }); - } - }); - checkDbInvariant(ex.rowId != null); - const exchangeSk = - await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll( - ex.rowId, - ); - let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({ - key: x.signkeyPub, - master_sig: x.masterSig, - stamp_end: x.stampEnd, - stamp_expire: x.stampExpire, - stamp_start: x.stampStart, - })); - - backupExchangeDetails.push({ - base_url: ex.exchangeBaseUrl, - reserve_closing_delay: ex.reserveClosingDelay, - accounts: ex.wireInfo.accounts.map((x) => ({ - payto_uri: x.payto_uri, - master_sig: x.master_sig, - })), - auditors: ex.auditors.map((x) => ({ - auditor_pub: x.auditor_pub, - auditor_url: x.auditor_url, - denomination_keys: x.denomination_keys, - })), - master_public_key: ex.masterPublicKey, - currency: ex.currency, - protocol_version: ex.protocolVersionRange, - wire_fees: wireFees, - signing_keys: signingKeys, - global_fees: ex.globalFees.map((x) => ({ - accountFee: Amounts.stringify(x.accountFee), - historyFee: Amounts.stringify(x.historyFee), - purseFee: Amounts.stringify(x.purseFee), - endDate: x.endDate, - historyTimeout: x.historyTimeout, - signature: x.signature, - purseLimit: x.purseLimit, - purseTimeout: x.purseTimeout, - startDate: x.startDate, - })), - tos_accepted_etag: ex.tosAccepted?.etag, - tos_accepted_timestamp: ex.tosAccepted?.timestamp, - denominations: - backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [], - }); - }); - - const purchaseProposalIdSet = new Set<string>(); - - await tx.purchases.iter().forEachAsync(async (purch) => { - const refunds: BackupRefundItem[] = []; - purchaseProposalIdSet.add(purch.proposalId); - // for (const refundKey of Object.keys(purch.refunds)) { - // const ri = purch.refunds[refundKey]; - // const common = { - // coin_pub: ri.coinPub, - // execution_time: ri.executionTime, - // obtained_time: ri.obtainedTime, - // refund_amount: Amounts.stringify(ri.refundAmount), - // rtransaction_id: ri.rtransactionId, - // total_refresh_cost_bound: Amounts.stringify( - // ri.totalRefreshCostBound, - // ), - // }; - // switch (ri.type) { - // case RefundState.Applied: - // refunds.push({ type: BackupRefundState.Applied, ...common }); - // break; - // case RefundState.Failed: - // refunds.push({ type: BackupRefundState.Failed, ...common }); - // break; - // case RefundState.Pending: - // refunds.push({ type: BackupRefundState.Pending, ...common }); - // break; - // } - // } - - let propStatus: BackupProposalStatus; - switch (purch.purchaseStatus) { - case PurchaseStatus.Done: - case PurchaseStatus.PendingQueryingAutoRefund: - case PurchaseStatus.PendingQueryingRefund: - propStatus = BackupProposalStatus.Paid; - break; - case PurchaseStatus.PendingPayingReplay: - case PurchaseStatus.PendingDownloadingProposal: - case PurchaseStatus.DialogProposed: - case PurchaseStatus.PendingPaying: - propStatus = BackupProposalStatus.Proposed; - break; - case PurchaseStatus.DialogShared: - propStatus = BackupProposalStatus.Shared; - break; - case PurchaseStatus.FailedClaim: - case PurchaseStatus.AbortedIncompletePayment: - propStatus = BackupProposalStatus.PermanentlyFailed; - break; - case PurchaseStatus.AbortingWithRefund: - case PurchaseStatus.AbortedProposalRefused: - propStatus = BackupProposalStatus.Refused; - break; - case PurchaseStatus.RepurchaseDetected: - propStatus = BackupProposalStatus.Repurchase; - break; - default: { - const error = purch.purchaseStatus; - throw Error(`purchase status ${error} is not handled`); - } - } - - const payInfo = purch.payInfo; - let backupPayInfo: BackupPayInfo | undefined = undefined; - if (payInfo) { - backupPayInfo = { - pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({ - coin_pub: x, - contribution: Amounts.stringify( - payInfo.payCoinSelection.coinContributions[i], - ), - })), - total_pay_cost: Amounts.stringify(payInfo.totalPayCost), - pay_coins_uid: payInfo.payCoinSelectionUid, - }; - } - - let contractTermsRaw = undefined; - if (purch.download) { - const contractTermsRecord = await tx.contractTerms.get( - purch.download.contractTermsHash, - ); - if (contractTermsRecord) { - contractTermsRaw = contractTermsRecord.contractTermsRaw; - } - } - - backupPurchases.push({ - contract_terms_raw: contractTermsRaw, - auto_refund_deadline: purch.autoRefundDeadline, - merchant_pay_sig: purch.merchantPaySig, - pos_confirmation: purch.posConfirmation, - pay_info: backupPayInfo, - proposal_id: purch.proposalId, - refunds, - timestamp_accepted: purch.timestampAccept, - timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay, - nonce_priv: purch.noncePriv, - merchant_sig: purch.download?.contractTermsMerchantSig, - claim_token: purch.claimToken, - merchant_base_url: purch.merchantBaseUrl, - order_id: purch.orderId, - proposal_status: propStatus, - repurchase_proposal_id: purch.repurchaseProposalId, - download_session_id: purch.downloadSessionId, - timestamp_proposed: purch.timestamp, - shared: purch.shared, - }); - }); - - await tx.refreshGroups.iter().forEach((rg) => { - const oldCoins: BackupRefreshOldCoin[] = []; - - for (let i = 0; i < rg.oldCoinPubs.length; i++) { - let refreshSession: BackupRefreshSession | undefined; - const s = rg.refreshSessionPerCoin[i]; - if (s) { - refreshSession = { - new_denoms: s.newDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - session_secret_seed: s.sessionSecretSeed, - noreveal_index: s.norevealIndex, - }; - } - oldCoins.push({ - coin_pub: rg.oldCoinPubs[i], - estimated_output_amount: Amounts.stringify( - rg.estimatedOutputPerCoin[i], - ), - finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished, - input_amount: Amounts.stringify(rg.inputPerCoin[i]), - refresh_session: refreshSession, - }); - } - - backupRefreshGroups.push({ - reason: rg.reason as any, - refresh_group_id: rg.refreshGroupId, - timestamp_created: rg.timestampCreated, - timestamp_finish: rg.timestampFinished, - old_coins: oldCoins, - }); - }); - - const ts = TalerPreciseTimestamp.now(); - - if (!bs.lastBackupTimestamp) { - bs.lastBackupTimestamp = ts; - } - - const backupBlob: WalletBackupContentV1 = { - schema_id: "gnu-taler-wallet-backup-content", - schema_version: BACKUP_VERSION_MAJOR, - minor_version: BACKUP_VERSION_MINOR, - exchanges: backupExchanges, - exchange_details: backupExchangeDetails, - wallet_root_pub: bs.walletRootPub, - backup_providers: backupBackupProviders, - current_device_id: bs.deviceId, - purchases: backupPurchases, - recoup_groups: backupRecoupGroups, - refresh_groups: backupRefreshGroups, - tips: backupTips, - timestamp: bs.lastBackupTimestamp, - trusted_auditors: {}, - trusted_exchanges: {}, - intern_table: {}, - error_reports: [], - tombstones: [], - // FIXME! - withdrawal_groups: backupWithdrawalGroups, - }; - - // If the backup changed, we change our nonce and timestamp. - - let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob)))); - if (h !== bs.lastBackupPlainHash) { - logger.trace( - `plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`, - ); - bs.lastBackupTimestamp = ts; - backupBlob.timestamp = ts; - bs.lastBackupPlainHash = encodeCrock( - hash(stringToBytes(canonicalJson(backupBlob))), - ); - bs.lastBackupNonce = encodeCrock(getRandomBytes(32)); - logger.trace( - `setting timestamp to ${AbsoluteTime.toIsoString( - AbsoluteTime.fromPreciseTimestamp(ts), - )} and nonce to ${bs.lastBackupNonce}`, - ); - await tx.config.put({ - key: ConfigRecordKey.WalletBackupState, - value: bs, - }); - } else { - logger.trace("backup hash did not change"); - } - - return backupBlob; - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts deleted file mode 100644 index 836c65643..000000000 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ /dev/null @@ -1,874 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { - AgeRestriction, - AmountJson, - Amounts, - BackupCoin, - BackupCoinSourceType, - BackupDenomSel, - BackupPayInfo, - BackupProposalStatus, - BackupRefreshReason, - BackupRefundState, - BackupWgType, - codecForMerchantContractTerms, - CoinStatus, - DenomKeyType, - DenomSelectionState, - j2s, - Logger, - PayCoinSelection, - RefreshReason, - TalerProtocolTimestamp, - TalerPreciseTimestamp, - WalletBackupContentV1, - WireInfo, -} from "@gnu-taler/taler-util"; -import { - CoinRecord, - CoinSource, - CoinSourceType, - DenominationRecord, - DenominationVerificationStatus, - ProposalDownloadInfo, - PurchaseStatus, - PurchasePayInfo, - RefreshCoinStatus, - RefreshSessionRecord, - WalletContractData, - WalletStoresV1, - WgInfo, - WithdrawalGroupStatus, - WithdrawalRecordType, - RefreshOperationStatus, - RewardRecordStatus, -} from "../../db.js"; -import { InternalWalletState } from "../../internal-wallet-state.js"; -import { assertUnreachable } from "../../util/assertUnreachable.js"; -import { checkLogicInvariant } from "../../util/invariants.js"; -import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js"; -import { - constructTombstone, - makeCoinAvailable, - TombstoneTag, -} from "../common.js"; -import { getExchangeDetails } from "../exchanges.js"; -import { extractContractData } from "../pay-merchant.js"; -import { provideBackupState } from "./state.js"; - -const logger = new Logger("operations/backup/import.ts"); - -function checkBackupInvariant(b: boolean, m?: string): asserts b { - if (!b) { - if (m) { - throw Error(`BUG: backup invariant failed (${m})`); - } else { - throw Error("BUG: backup invariant failed"); - } - } -} - -/** - * Re-compute information about the coin selection for a payment. - */ -async function recoverPayCoinSelection( - tx: GetReadWriteAccess<{ - exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; - coins: typeof WalletStoresV1.coins; - denominations: typeof WalletStoresV1.denominations; - }>, - contractData: WalletContractData, - payInfo: BackupPayInfo, -): Promise<PayCoinSelection> { - const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub); - const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ); - - const coveredExchanges: Set<string> = new Set(); - - let totalWireFee: AmountJson = Amounts.zeroOfAmount(contractData.amount); - let totalDepositFees: AmountJson = Amounts.zeroOfAmount(contractData.amount); - - for (const coinPub of coinPubs) { - const coinRecord = await tx.coins.get(coinPub); - checkBackupInvariant(!!coinRecord); - const denom = await tx.denominations.get([ - coinRecord.exchangeBaseUrl, - coinRecord.denomPubHash, - ]); - checkBackupInvariant(!!denom); - totalDepositFees = Amounts.add( - totalDepositFees, - denom.fees.feeDeposit, - ).amount; - - if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) { - const exchangeDetails = await getExchangeDetails( - tx, - coinRecord.exchangeBaseUrl, - ); - checkBackupInvariant(!!exchangeDetails); - let wireFee: AmountJson | undefined; - const feesForType = exchangeDetails.wireInfo.feesForType; - checkBackupInvariant(!!feesForType); - for (const fee of feesForType[contractData.wireMethod] || []) { - if ( - fee.startStamp <= contractData.timestamp && - fee.endStamp >= contractData.timestamp - ) { - wireFee = Amounts.parseOrThrow(fee.wireFee); - break; - } - } - if (wireFee) { - totalWireFee = Amounts.add(totalWireFee, wireFee).amount; - } - coveredExchanges.add(coinRecord.exchangeBaseUrl); - } - } - - let customerWireFee: AmountJson; - - const amortizedWireFee = Amounts.divide( - totalWireFee, - contractData.wireFeeAmortization, - ); - if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) { - customerWireFee = amortizedWireFee; - } else { - customerWireFee = Amounts.zeroOfAmount(contractData.amount); - } - - const customerDepositFees = Amounts.sub( - totalDepositFees, - contractData.maxDepositFee, - ).amount; - - return { - coinPubs, - coinContributions: coinContributions.map((x) => Amounts.stringify(x)), - paymentAmount: Amounts.stringify(contractData.amount), - customerWireFees: Amounts.stringify(customerWireFee), - customerDepositFees: Amounts.stringify(customerDepositFees), - }; -} - -async function getDenomSelStateFromBackup( - tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>, - currency: string, - exchangeBaseUrl: string, - sel: BackupDenomSel, -): Promise<DenomSelectionState> { - const selectedDenoms: { - denomPubHash: string; - count: number; - }[] = []; - let totalCoinValue = Amounts.zeroOfCurrency(currency); - let totalWithdrawCost = Amounts.zeroOfCurrency(currency); - for (const s of sel) { - const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]); - checkBackupInvariant(!!d); - totalCoinValue = Amounts.add( - totalCoinValue, - DenominationRecord.getValue(d), - ).amount; - totalWithdrawCost = Amounts.add( - totalWithdrawCost, - DenominationRecord.getValue(d), - d.fees.feeWithdraw, - ).amount; - } - return { - selectedDenoms, - totalCoinValue: Amounts.stringify(totalCoinValue), - totalWithdrawCost: Amounts.stringify(totalWithdrawCost), - }; -} - -export interface CompletedCoin { - coinPub: string; - coinEvHash: string; -} - -/** - * Precomputed cryptographic material for a backup import. - * - * We separate this data from the backup blob as we want the backup - * blob to be small, and we can't compute it during the database transaction, - * as the async crypto worker communication would auto-close the database transaction. - */ -export interface BackupCryptoPrecomputedData { - rsaDenomPubToHash: Record<string, string>; - coinPrivToCompletedCoin: Record<string, CompletedCoin>; - proposalNoncePrivToPub: { [priv: string]: string }; - proposalIdToContractTermsHash: { [proposalId: string]: string }; - reservePrivToPub: Record<string, string>; -} - -export async function importCoin( - ws: InternalWalletState, - tx: GetReadWriteAccess<{ - coins: typeof WalletStoresV1.coins; - coinAvailability: typeof WalletStoresV1.coinAvailability; - denominations: typeof WalletStoresV1.denominations; - }>, - cryptoComp: BackupCryptoPrecomputedData, - args: { - backupCoin: BackupCoin; - exchangeBaseUrl: string; - denomPubHash: string; - }, -): Promise<void> { - const { backupCoin, exchangeBaseUrl, denomPubHash } = args; - const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv]; - checkLogicInvariant(!!compCoin); - const existingCoin = await tx.coins.get(compCoin.coinPub); - if (!existingCoin) { - let coinSource: CoinSource; - switch (backupCoin.coin_source.type) { - case BackupCoinSourceType.Refresh: - coinSource = { - type: CoinSourceType.Refresh, - oldCoinPub: backupCoin.coin_source.old_coin_pub, - refreshGroupId: backupCoin.coin_source.refresh_group_id, - }; - break; - case BackupCoinSourceType.Reward: - coinSource = { - type: CoinSourceType.Reward, - coinIndex: backupCoin.coin_source.coin_index, - walletRewardId: backupCoin.coin_source.wallet_tip_id, - }; - break; - case BackupCoinSourceType.Withdraw: - coinSource = { - type: CoinSourceType.Withdraw, - coinIndex: backupCoin.coin_source.coin_index, - reservePub: backupCoin.coin_source.reserve_pub, - withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id, - }; - break; - } - const coinRecord: CoinRecord = { - blindingKey: backupCoin.blinding_key, - coinEvHash: compCoin.coinEvHash, - coinPriv: backupCoin.coin_priv, - denomSig: backupCoin.denom_sig, - coinPub: compCoin.coinPub, - exchangeBaseUrl, - denomPubHash, - status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant, - coinSource, - // FIXME! - maxAge: AgeRestriction.AGE_UNRESTRICTED, - // FIXME! - ageCommitmentProof: undefined, - // FIXME! - spendAllocation: undefined, - }; - if (coinRecord.status === CoinStatus.Fresh) { - await makeCoinAvailable(ws, tx, coinRecord); - } else { - await tx.coins.put(coinRecord); - } - } -} - -export async function importBackup( - ws: InternalWalletState, - backupBlobArg: any, - cryptoComp: BackupCryptoPrecomputedData, -): Promise<void> { - await provideBackupState(ws); - - logger.info(`importing backup ${j2s(backupBlobArg)}`); - - return ws.db - .mktx((x) => [ - x.config, - x.exchangeDetails, - x.exchanges, - x.coins, - x.coinAvailability, - x.denominations, - x.purchases, - x.refreshGroups, - x.backupProviders, - x.rewards, - x.recoupGroups, - x.withdrawalGroups, - x.tombstones, - x.depositGroups, - ]) - .runReadWrite(async (tx) => { - // FIXME: validate schema! - const backupBlob = backupBlobArg as WalletBackupContentV1; - - // FIXME: validate version - - for (const tombstone of backupBlob.tombstones) { - await tx.tombstones.put({ - id: tombstone, - }); - } - - const tombstoneSet = new Set( - (await tx.tombstones.iter().toArray()).map((x) => x.id), - ); - - // FIXME: Validate that the "details pointer" is correct - - for (const backupExchange of backupBlob.exchanges) { - const existingExchange = await tx.exchanges.get( - backupExchange.base_url, - ); - if (existingExchange) { - continue; - } - // await tx.exchanges.put({ - // baseUrl: backupExchange.base_url, - // detailsPointer: { - // currency: backupExchange.currency, - // masterPublicKey: backupExchange.master_public_key, - // updateClock: backupExchange.update_clock, - // }, - // lastUpdate: undefined, - // nextUpdate: TalerPreciseTimestamp.now(), - // nextRefreshCheck: TalerPreciseTimestamp.now(), - // lastKeysEtag: undefined, - // lastWireEtag: undefined, - // }); - } - - for (const backupExchangeDetails of backupBlob.exchange_details) { - const existingExchangeDetails = - await tx.exchangeDetails.indexes.byPointer.get([ - backupExchangeDetails.base_url, - backupExchangeDetails.currency, - backupExchangeDetails.master_public_key, - ]); - - if (!existingExchangeDetails) { - const wireInfo: WireInfo = { - accounts: backupExchangeDetails.accounts.map((x) => ({ - master_sig: x.master_sig, - payto_uri: x.payto_uri, - })), - feesForType: {}, - }; - for (const fee of backupExchangeDetails.wire_fees) { - const w = (wireInfo.feesForType[fee.wire_type] ??= []); - w.push({ - closingFee: Amounts.stringify(fee.closing_fee), - endStamp: fee.end_stamp, - sig: fee.sig, - startStamp: fee.start_stamp, - wireFee: Amounts.stringify(fee.wire_fee), - }); - } - let tosAccepted = undefined; - if ( - backupExchangeDetails.tos_accepted_etag && - backupExchangeDetails.tos_accepted_timestamp - ) { - tosAccepted = { - etag: backupExchangeDetails.tos_accepted_etag, - timestamp: backupExchangeDetails.tos_accepted_timestamp, - }; - } - await tx.exchangeDetails.put({ - exchangeBaseUrl: backupExchangeDetails.base_url, - wireInfo, - currency: backupExchangeDetails.currency, - auditors: backupExchangeDetails.auditors.map((x) => ({ - auditor_pub: x.auditor_pub, - auditor_url: x.auditor_url, - denomination_keys: x.denomination_keys, - })), - masterPublicKey: backupExchangeDetails.master_public_key, - protocolVersionRange: backupExchangeDetails.protocol_version, - reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, - tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "", - tosAccepted, - globalFees: backupExchangeDetails.global_fees.map((x) => ({ - accountFee: Amounts.stringify(x.accountFee), - historyFee: Amounts.stringify(x.historyFee), - purseFee: Amounts.stringify(x.purseFee), - endDate: x.endDate, - historyTimeout: x.historyTimeout, - signature: x.signature, - purseLimit: x.purseLimit, - purseTimeout: x.purseTimeout, - startDate: x.startDate, - })), - }); - } - - for (const backupDenomination of backupExchangeDetails.denominations) { - if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - const denomPubHash = - cryptoComp.rsaDenomPubToHash[ - backupDenomination.denom_pub.rsa_public_key - ]; - checkLogicInvariant(!!denomPubHash); - const existingDenom = await tx.denominations.get([ - backupExchangeDetails.base_url, - denomPubHash, - ]); - if (!existingDenom) { - const value = Amounts.parseOrThrow(backupDenomination.value); - - await tx.denominations.put({ - denomPub: backupDenomination.denom_pub, - denomPubHash: denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - exchangeMasterPub: backupExchangeDetails.master_public_key, - fees: { - feeDeposit: Amounts.stringify(backupDenomination.fee_deposit), - feeRefresh: Amounts.stringify(backupDenomination.fee_refresh), - feeRefund: Amounts.stringify(backupDenomination.fee_refund), - feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw), - }, - isOffered: backupDenomination.is_offered, - isRevoked: backupDenomination.is_revoked, - masterSig: backupDenomination.master_sig, - stampExpireDeposit: backupDenomination.stamp_expire_deposit, - stampExpireLegal: backupDenomination.stamp_expire_legal, - stampExpireWithdraw: backupDenomination.stamp_expire_withdraw, - stampStart: backupDenomination.stamp_start, - verificationStatus: DenominationVerificationStatus.VerifiedGood, - currency: value.currency, - amountFrac: value.fraction, - amountVal: value.value, - listIssueDate: backupDenomination.list_issue_date, - }); - } - for (const backupCoin of backupDenomination.coins) { - await importCoin(ws, tx, cryptoComp, { - backupCoin, - denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - }); - } - } - } - - for (const backupWg of backupBlob.withdrawal_groups) { - const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv]; - checkLogicInvariant(!!reservePub); - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReserve, - reservePub, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingWg = await tx.withdrawalGroups.get( - backupWg.withdrawal_group_id, - ); - if (existingWg) { - continue; - } - let wgInfo: WgInfo; - switch (backupWg.info.type) { - case BackupWgType.BankIntegrated: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankIntegrated, - bankInfo: { - exchangePaytoUri: backupWg.info.exchange_payto_uri, - talerWithdrawUri: backupWg.info.taler_withdraw_uri, - confirmUrl: backupWg.info.confirm_url, - timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed, - timestampReserveInfoPosted: - backupWg.info.timestamp_reserve_info_posted, - }, - }; - break; - case BackupWgType.BankManual: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankManual, - }; - break; - case BackupWgType.PeerPullCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPullCredit, - contractTerms: backupWg.info.contract_terms, - contractPriv: backupWg.info.contract_priv, - }; - break; - case BackupWgType.PeerPushCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPushCredit, - contractTerms: backupWg.info.contract_terms, - }; - break; - case BackupWgType.Recoup: - wgInfo = { - withdrawalType: WithdrawalRecordType.Recoup, - }; - break; - default: - assertUnreachable(backupWg.info); - } - const instructedAmount = Amounts.parseOrThrow( - backupWg.instructed_amount, - ); - await tx.withdrawalGroups.put({ - withdrawalGroupId: backupWg.withdrawal_group_id, - exchangeBaseUrl: backupWg.exchange_base_url, - instructedAmount: Amounts.stringify(instructedAmount), - secretSeed: backupWg.secret_seed, - denomsSel: await getDenomSelStateFromBackup( - tx, - instructedAmount.currency, - backupWg.exchange_base_url, - backupWg.selected_denoms, - ), - denomSelUid: backupWg.selected_denoms_uid, - rawWithdrawalAmount: Amounts.stringify( - backupWg.raw_withdrawal_amount, - ), - effectiveWithdrawalAmount: Amounts.stringify( - backupWg.effective_withdrawal_amount, - ), - reservePriv: backupWg.reserve_priv, - reservePub, - status: backupWg.timestamp_finish - ? WithdrawalGroupStatus.Finished - : WithdrawalGroupStatus.PendingQueryingStatus, // FIXME! - timestampStart: backupWg.timestamp_created, - wgInfo, - restrictAge: backupWg.restrict_age, - senderWire: undefined, // FIXME! - timestampFinish: backupWg.timestamp_finish, - }); - } - - for (const backupPurchase of backupBlob.purchases) { - const ts = constructTombstone({ - tag: TombstoneTag.DeletePayment, - proposalId: backupPurchase.proposal_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingPurchase = await tx.purchases.get( - backupPurchase.proposal_id, - ); - let proposalStatus: PurchaseStatus; - switch (backupPurchase.proposal_status) { - case BackupProposalStatus.Paid: - proposalStatus = PurchaseStatus.Done; - break; - case BackupProposalStatus.Shared: - proposalStatus = PurchaseStatus.DialogShared; - break; - case BackupProposalStatus.Proposed: - proposalStatus = PurchaseStatus.DialogProposed; - break; - case BackupProposalStatus.PermanentlyFailed: - proposalStatus = PurchaseStatus.AbortedIncompletePayment; - break; - case BackupProposalStatus.Refused: - proposalStatus = PurchaseStatus.AbortedProposalRefused; - break; - case BackupProposalStatus.Repurchase: - proposalStatus = PurchaseStatus.RepurchaseDetected; - break; - default: { - const error: never = backupPurchase.proposal_status; - throw Error(`backup status ${error} is not handled`); - } - } - if (!existingPurchase) { - //const refunds: { [refundKey: string]: WalletRefundItem } = {}; - // for (const backupRefund of backupPurchase.refunds) { - // const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`; - // const coin = await tx.coins.get(backupRefund.coin_pub); - // checkBackupInvariant(!!coin); - // const denom = await tx.denominations.get([ - // coin.exchangeBaseUrl, - // coin.denomPubHash, - // ]); - // checkBackupInvariant(!!denom); - // const common = { - // coinPub: backupRefund.coin_pub, - // executionTime: backupRefund.execution_time, - // obtainedTime: backupRefund.obtained_time, - // refundAmount: Amounts.stringify(backupRefund.refund_amount), - // refundFee: Amounts.stringify(denom.fees.feeRefund), - // rtransactionId: backupRefund.rtransaction_id, - // totalRefreshCostBound: Amounts.stringify( - // 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; - // } - // } - const parsedContractTerms = codecForMerchantContractTerms().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.zeroOfCurrency(amount.currency); - } - const download: ProposalDownloadInfo = { - contractTermsHash, - contractTermsMerchantSig: backupPurchase.merchant_sig!, - currency: amount.currency, - fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url, - }; - - const contractData = extractContractData( - backupPurchase.contract_terms_raw, - contractTermsHash, - download.contractTermsMerchantSig, - ); - - let payInfo: PurchasePayInfo | undefined = undefined; - if (backupPurchase.pay_info) { - payInfo = { - payCoinSelection: await recoverPayCoinSelection( - tx, - contractData, - backupPurchase.pay_info, - ), - payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid, - totalPayCost: Amounts.stringify( - backupPurchase.pay_info.total_pay_cost, - ), - }; - } - - await tx.purchases.put({ - proposalId: backupPurchase.proposal_id, - noncePriv: backupPurchase.nonce_priv, - noncePub: - cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv], - autoRefundDeadline: TalerProtocolTimestamp.never(), - timestampAccept: backupPurchase.timestamp_accepted, - timestampFirstSuccessfulPay: - backupPurchase.timestamp_first_successful_pay, - timestampLastRefundStatus: undefined, - merchantPaySig: backupPurchase.merchant_pay_sig, - posConfirmation: backupPurchase.pos_confirmation, - lastSessionId: undefined, - download, - //refunds, - claimToken: backupPurchase.claim_token, - downloadSessionId: backupPurchase.download_session_id, - merchantBaseUrl: backupPurchase.merchant_base_url, - orderId: backupPurchase.order_id, - payInfo, - refundAmountAwaiting: undefined, - repurchaseProposalId: backupPurchase.repurchase_proposal_id, - purchaseStatus: proposalStatus, - timestamp: backupPurchase.timestamp_proposed, - shared: backupPurchase.shared, - }); - } - } - - for (const backupRefreshGroup of backupBlob.refresh_groups) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteRefreshGroup, - refreshGroupId: backupRefreshGroup.refresh_group_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingRg = await tx.refreshGroups.get( - backupRefreshGroup.refresh_group_id, - ); - if (!existingRg) { - let reason: RefreshReason; - switch (backupRefreshGroup.reason) { - case BackupRefreshReason.AbortPay: - reason = RefreshReason.AbortPay; - break; - case BackupRefreshReason.BackupRestored: - reason = RefreshReason.BackupRestored; - break; - case BackupRefreshReason.Manual: - reason = RefreshReason.Manual; - break; - case BackupRefreshReason.Pay: - reason = RefreshReason.PayMerchant; - break; - case BackupRefreshReason.Recoup: - reason = RefreshReason.Recoup; - break; - case BackupRefreshReason.Refund: - reason = RefreshReason.Refund; - break; - case BackupRefreshReason.Scheduled: - reason = RefreshReason.Scheduled; - break; - } - const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] = - []; - for (const oldCoin of backupRefreshGroup.old_coins) { - const c = await tx.coins.get(oldCoin.coin_pub); - checkBackupInvariant(!!c); - const d = await tx.denominations.get([ - c.exchangeBaseUrl, - c.denomPubHash, - ]); - checkBackupInvariant(!!d); - - if (oldCoin.refresh_session) { - const denomSel = await getDenomSelStateFromBackup( - tx, - d.currency, - c.exchangeBaseUrl, - oldCoin.refresh_session.new_denoms, - ); - refreshSessionPerCoin.push({ - sessionSecretSeed: oldCoin.refresh_session.session_secret_seed, - norevealIndex: oldCoin.refresh_session.noreveal_index, - newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({ - count: x.count, - denomPubHash: x.denom_pub_hash, - })), - amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue), - }); - } else { - refreshSessionPerCoin.push(undefined); - } - } - await tx.refreshGroups.put({ - timestampFinished: backupRefreshGroup.timestamp_finish, - timestampCreated: backupRefreshGroup.timestamp_created, - refreshGroupId: backupRefreshGroup.refresh_group_id, - currency: Amounts.currencyOf( - backupRefreshGroup.old_coins[0].input_amount, - ), - reason, - lastErrorPerCoin: {}, - oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub), - statusPerCoin: backupRefreshGroup.old_coins.map((x) => - x.finished - ? RefreshCoinStatus.Finished - : RefreshCoinStatus.Pending, - ), - operationStatus: backupRefreshGroup.timestamp_finish - ? RefreshOperationStatus.Finished - : RefreshOperationStatus.Pending, - inputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.input_amount, - ), - estimatedOutputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.estimated_output_amount, - ), - refreshSessionPerCoin, - }); - } - } - - for (const backupTip of backupBlob.tips) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReward, - walletTipId: backupTip.wallet_tip_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingTip = await tx.rewards.get(backupTip.wallet_tip_id); - if (!existingTip) { - const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); - const denomsSel = await getDenomSelStateFromBackup( - tx, - tipAmountRaw.currency, - backupTip.exchange_base_url, - backupTip.selected_denoms, - ); - await tx.rewards.put({ - acceptedTimestamp: backupTip.timestamp_accepted, - createdTimestamp: backupTip.timestamp_created, - denomsSel, - next_url: backupTip.next_url, - exchangeBaseUrl: backupTip.exchange_base_url, - merchantBaseUrl: backupTip.exchange_base_url, - merchantRewardId: backupTip.merchant_tip_id, - pickedUpTimestamp: backupTip.timestamp_finished, - secretSeed: backupTip.secret_seed, - rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), - rewardAmountRaw: Amounts.stringify(tipAmountRaw), - rewardExpiration: backupTip.timestamp_expiration, - walletRewardId: backupTip.wallet_tip_id, - denomSelUid: backupTip.selected_denoms_uid, - status: RewardRecordStatus.Done, // FIXME! - }); - } - } - - // We now process tombstones. - // The import code above should already prevent - // importing things that are tombstoned, - // but we do tombstone processing last just to be sure. - - for (const tombstone of tombstoneSet) { - const [type, ...rest] = tombstone.split(":"); - if (type === TombstoneTag.DeleteDepositGroup) { - await tx.depositGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeletePayment) { - await tx.purchases.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefreshGroup) { - await tx.refreshGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefund) { - // Nothing required, will just prevent display - // in the transactions list - } else if (type === TombstoneTag.DeleteReward) { - await tx.rewards.delete(rest[0]); - } else if (type === TombstoneTag.DeleteWithdrawalGroup) { - await tx.withdrawalGroups.delete(rest[0]); - } else { - logger.warn(`unable to process tombstone of type '${type}'`); - } - } - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index e35765165..a5e8dbd42 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -43,7 +43,6 @@ import { TalerErrorDetail, TalerPreciseTimestamp, URL, - WalletBackupContentV1, buildCodecForObject, buildCodecForUnion, bytesToString, @@ -99,9 +98,8 @@ import { TaskIdentifiers, } from "../common.js"; import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js"; -import { exportBackup } from "./export.js"; -import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; -import { getWalletBackupState, provideBackupState } from "./state.js"; +import { WalletStoresV1 } from "../../db.js"; +import { GetReadOnlyAccess } from "../../util/query.js"; const logger = new Logger("operations/backup.ts"); @@ -131,7 +129,7 @@ const magic = "TLRWBK01"; */ export async function encryptBackup( config: WalletBackupConfState, - blob: WalletBackupContentV1, + blob: any, ): Promise<Uint8Array> { const chunks: Uint8Array[] = []; chunks.push(stringToBytes(magic)); @@ -150,64 +148,6 @@ export async function encryptBackup( return concatArrays(chunks); } -/** - * 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: TalerCryptoInterface, - backupContent: WalletBackupContentV1, -): Promise<BackupCryptoPrecomputedData> { - const cryptoData: BackupCryptoPrecomputedData = { - coinPrivToCompletedCoin: {}, - rsaDenomPubToHash: {}, - proposalIdToContractTermsHash: {}, - proposalNoncePrivToPub: {}, - reservePrivToPub: {}, - }; - for (const backupExchangeDetails of backupContent.exchange_details) { - for (const backupDenom of backupExchangeDetails.denominations) { - if (backupDenom.denom_pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - 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.rsa_public_key), - ); - cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = { - coinEvHash: encodeCrock(hash(blindedCoin)), - coinPub, - }; - } - cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] = - encodeCrock(hashDenomPub(backupDenom.denom_pub)); - } - } - for (const backupWg of backupContent.withdrawal_groups) { - cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock( - eddsaGetPublic(decodeCrock(backupWg.reserve_priv)), - ); - } - for (const purch of backupContent.purchases) { - if (!purch.contract_terms_raw) continue; - const { h: contractTermsHash } = await cryptoApi.hashString({ - str: 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 deriveAccountKeyPair( bc: WalletBackupConfState, providerUrl: string, @@ -262,7 +202,9 @@ async function runBackupCycleForProvider( return TaskRunResult.finished(); } - const backupJson = await exportBackup(ws); + //const backupJson = await exportBackup(ws); + // FIXME: re-implement backup + const backupJson = {}; const backupConfig = await provideBackupState(ws); const encBackup = await encryptBackup(backupConfig, backupJson); const currentBackupHash = hash(encBackup); @@ -441,9 +383,9 @@ async function runBackupCycleForProvider( logger.info("conflicting backup found"); const backupEnc = new Uint8Array(await resp.bytes()); const backupConfig = await provideBackupState(ws); - const blob = await decryptBackup(backupConfig, backupEnc); - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob); - await importBackup(ws, blob, cryptoData); + // const blob = await decryptBackup(backupConfig, backupEnc); + // FIXME: Re-implement backup import with merging + // await importBackup(ws, blob, cryptoData); await ws.db .mktx((x) => [x.backupProviders, x.operationRetries]) .runReadWrite(async (tx) => { @@ -789,18 +731,6 @@ export interface BackupInfo { providers: ProviderInfo[]; } -export async function importBackupPlain( - ws: InternalWalletState, - blob: any, -): Promise<void> { - // FIXME: parse - const backup: WalletBackupContentV1 = blob; - - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, backup); - - await importBackup(ws, blob, cryptoData); -} - export enum ProviderPaymentType { Unpaid = "unpaid", Pending = "pending", @@ -1036,23 +966,10 @@ export async function loadBackupRecovery( } } -export async function exportBackupEncrypted( - ws: InternalWalletState, -): Promise<Uint8Array> { - await provideBackupState(ws); - const blob = await exportBackup(ws); - const bs = await ws.db - .mktx((x) => [x.config]) - .runReadOnly(async (tx) => { - return await getWalletBackupState(ws, tx); - }); - return encryptBackup(bs, blob); -} - export async function decryptBackup( backupConfig: WalletBackupConfState, data: Uint8Array, -): Promise<WalletBackupContentV1> { +): Promise<any> { const rMagic = bytesToString(data.slice(0, 8)); if (rMagic != magic) { throw Error("invalid backup file (magic tag mismatch)"); @@ -1068,12 +985,85 @@ export async function decryptBackup( return JSON.parse(bytesToString(gunzipSync(dataCompressed))); } -export async function importBackupEncrypted( +export async function provideBackupState( ws: InternalWalletState, - data: Uint8Array, +): Promise<WalletBackupConfState> { + const bs: ConfigRecord | undefined = await ws.db + .mktx((stores) => [stores.config]) + .runReadOnly(async (tx) => { + return await tx.config.get(ConfigRecordKey.WalletBackupState); + }); + if (bs) { + checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); + return bs.value; + } + // We need to generate the key outside of the transaction + // due to how IndexedDB works. + const k = await ws.cryptoApi.createEddsaKeypair({}); + const d = getRandomBytes(5); + // FIXME: device ID should be configured when wallet is initialized + // and be based on hostname + const deviceId = `wallet-core-${encodeCrock(d)}`; + return await ws.db + .mktx((x) => [x.config]) + .runReadWrite(async (tx) => { + let backupStateEntry: ConfigRecord | undefined = await tx.config.get( + ConfigRecordKey.WalletBackupState, + ); + if (!backupStateEntry) { + backupStateEntry = { + key: ConfigRecordKey.WalletBackupState, + value: { + deviceId, + walletRootPub: k.pub, + walletRootPriv: k.priv, + lastBackupPlainHash: undefined, + }, + }; + await tx.config.put(backupStateEntry); + } + checkDbInvariant( + backupStateEntry.key === ConfigRecordKey.WalletBackupState, + ); + return backupStateEntry.value; + }); +} + +export async function getWalletBackupState( + ws: InternalWalletState, + tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>, +): Promise<WalletBackupConfState> { + const bs = await tx.config.get(ConfigRecordKey.WalletBackupState); + checkDbInvariant(!!bs, "wallet backup state should be in DB"); + checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); + return bs.value; +} + +export async function setWalletDeviceId( + ws: InternalWalletState, + deviceId: string, ): Promise<void> { - const backupConfig = await provideBackupState(ws); - const blob = await decryptBackup(backupConfig, data); - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob); - await importBackup(ws, blob, cryptoData); + await provideBackupState(ws); + await ws.db + .mktx((x) => [x.config]) + .runReadWrite(async (tx) => { + let backupStateEntry: ConfigRecord | undefined = await tx.config.get( + ConfigRecordKey.WalletBackupState, + ); + if ( + !backupStateEntry || + backupStateEntry.key !== ConfigRecordKey.WalletBackupState + ) { + return; + } + backupStateEntry.value.deviceId = deviceId; + await tx.config.put(backupStateEntry); + }); +} + +export async function getWalletDeviceId( + ws: InternalWalletState, +): Promise<string> { + const bs = await provideBackupState(ws); + return bs.deviceId; } diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts index fa632f44c..d02ead783 100644 --- a/packages/taler-wallet-core/src/operations/backup/state.ts +++ b/packages/taler-wallet-core/src/operations/backup/state.ts @@ -14,96 +14,4 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; -import { - ConfigRecord, - ConfigRecordKey, - WalletBackupConfState, - WalletStoresV1, -} from "../../db.js"; -import { checkDbInvariant } from "../../util/invariants.js"; -import { GetReadOnlyAccess } from "../../util/query.js"; -import { InternalWalletState } from "../../internal-wallet-state.js"; -export async function provideBackupState( - ws: InternalWalletState, -): Promise<WalletBackupConfState> { - const bs: ConfigRecord | undefined = await ws.db - .mktx((stores) => [stores.config]) - .runReadOnly(async (tx) => { - return await tx.config.get(ConfigRecordKey.WalletBackupState); - }); - if (bs) { - checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); - return bs.value; - } - // We need to generate the key outside of the transaction - // due to how IndexedDB works. - const k = await ws.cryptoApi.createEddsaKeypair({}); - const d = getRandomBytes(5); - // FIXME: device ID should be configured when wallet is initialized - // and be based on hostname - const deviceId = `wallet-core-${encodeCrock(d)}`; - return await ws.db - .mktx((x) => [x.config]) - .runReadWrite(async (tx) => { - let backupStateEntry: ConfigRecord | undefined = await tx.config.get( - ConfigRecordKey.WalletBackupState, - ); - if (!backupStateEntry) { - backupStateEntry = { - key: ConfigRecordKey.WalletBackupState, - value: { - deviceId, - walletRootPub: k.pub, - walletRootPriv: k.priv, - lastBackupPlainHash: undefined, - }, - }; - await tx.config.put(backupStateEntry); - } - checkDbInvariant( - backupStateEntry.key === ConfigRecordKey.WalletBackupState, - ); - return backupStateEntry.value; - }); -} - -export async function getWalletBackupState( - ws: InternalWalletState, - tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>, -): Promise<WalletBackupConfState> { - const bs = await tx.config.get(ConfigRecordKey.WalletBackupState); - checkDbInvariant(!!bs, "wallet backup state should be in DB"); - checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState); - return bs.value; -} - -export async function setWalletDeviceId( - ws: InternalWalletState, - deviceId: string, -): Promise<void> { - await provideBackupState(ws); - await ws.db - .mktx((x) => [x.config]) - .runReadWrite(async (tx) => { - let backupStateEntry: ConfigRecord | undefined = await tx.config.get( - ConfigRecordKey.WalletBackupState, - ); - if ( - !backupStateEntry || - backupStateEntry.key !== ConfigRecordKey.WalletBackupState - ) { - return; - } - backupStateEntry.value.deviceId = deviceId; - await tx.config.put(backupStateEntry); - }); -} - -export async function getWalletDeviceId( - ws: InternalWalletState, -): Promise<string> { - const bs = await provideBackupState(ws); - return bs.deviceId; -} diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 06ccdf6f3..4d9d40c43 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -106,7 +106,6 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, - WalletBackupContentV1, WalletCoreVersion, WalletCurrencyInfo, WithdrawFakebankRequest, @@ -116,6 +115,10 @@ import { SharePaymentResult, GetCurrencyInfoRequest, GetCurrencyInfoResponse, + StoredBackupList, + CreateStoredBackupResponse, + RecoverStoredBackupRequest, + DeleteStoredBackupRequest, } from "@gnu-taler/taler-util"; import { AuditorTrustRecord, WalletContractData } from "./db.js"; import { @@ -195,7 +198,6 @@ export enum WalletApiOperation { GenerateDepositGroupTxId = "generateDepositGroupTxId", CreateDepositGroup = "createDepositGroup", SetWalletDeviceId = "setWalletDeviceId", - ExportBackupPlain = "exportBackupPlain", WithdrawFakebank = "withdrawFakebank", ImportDb = "importDb", ExportDb = "exportDb", @@ -214,6 +216,10 @@ export enum WalletApiOperation { TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", GetScopedCurrencyInfo = "getScopedCurrencyInfo", + ListStoredBackups = "listStoredBackups", + CreateStoredBackup = "createStoredBackup", + DeleteStoredBackup = "deleteStoredBackup", + RecoverStoredBackup = "recoverStoredBackup", } // group: Initialization @@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = { response: EmptyObject; }; -/** - * Export a backup JSON, mostly useful for testing. - */ -export type ExportBackupPlainOp = { - op: WalletApiOperation.ExportBackupPlain; +export type ListStoredBackupsOp = { + op: WalletApiOperation.ListStoredBackups; + request: EmptyObject; + response: StoredBackupList; +}; + +export type CreateStoredBackupsOp = { + op: WalletApiOperation.CreateStoredBackup; request: EmptyObject; - response: WalletBackupContentV1; + response: CreateStoredBackupResponse; +}; + +export type RecoverStoredBackupsOp = { + op: WalletApiOperation.RecoverStoredBackup; + request: RecoverStoredBackupRequest; + response: EmptyObject; +}; + +export type DeleteStoredBackupOp = { + op: WalletApiOperation.DeleteStoredBackup; + request: DeleteStoredBackupRequest; + response: EmptyObject; }; // group: Peer Payments @@ -1062,7 +1083,6 @@ export type WalletOperations = { [WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp; [WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp; [WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp; - [WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp; [WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp; [WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp; [WalletApiOperation.RunBackupCycle]: RunBackupCycleOp; @@ -1092,6 +1112,10 @@ export type WalletOperations = { [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; [WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp; + [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp; + [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp; + [WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp; + [WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 9f754ed69..283539a08 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -120,6 +120,7 @@ import { codecForSharePaymentRequest, GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, + CreateStoredBackupResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -139,6 +140,7 @@ import { clearDatabase, exportDb, importDb, + openStoredBackupsDatabase, openTalerDatabase, } from "./db.js"; import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js"; @@ -158,7 +160,6 @@ import { getUserAttentionsUnreadCount, markAttentionRequestAsRead, } from "./operations/attention.js"; -import { exportBackup } from "./operations/backup/export.js"; import { addBackupProvider, codecForAddBackupProviderRequest, @@ -166,13 +167,12 @@ import { codecForRunBackupCycle, getBackupInfo, getBackupRecovery, - importBackupPlain, loadBackupRecovery, processBackupForProvider, removeBackupProvider, runBackupCycle, + setWalletDeviceId, } from "./operations/backup/index.js"; -import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalanceDetail, getBalances } from "./operations/balance.js"; import { TaskIdentifiers, @@ -1025,6 +1025,17 @@ export async function getClientFromWalletState( return client; } +async function createStoredBackup( + ws: InternalWalletState, +): Promise<CreateStoredBackupResponse> { + const backup = await exportDb(ws.idb); + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const name = `backup-${new Date().getTime()}`; + backupsDb.mktxAll().runReadWrite(async (tx) => {}); + + throw Error("not implemented"); +} + /** * Implementation of the "wallet-core" API. */ @@ -1041,6 +1052,14 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( // FIXME: Can we make this more type-safe by using the request/response type // definitions we already have? switch (operation) { + case WalletApiOperation.CreateStoredBackup: + return createStoredBackup(ws); + case WalletApiOperation.DeleteStoredBackup: + return {}; + case WalletApiOperation.ListStoredBackups: + return {}; + case WalletApiOperation.RecoverStoredBackup: + return {}; case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; @@ -1382,9 +1401,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( const req = codecForAcceptTipRequest().decode(payload); return await acceptTip(ws, req.walletRewardId); } - case WalletApiOperation.ExportBackupPlain: { - return exportBackup(ws); - } case WalletApiOperation.AddBackupProvider: { const req = codecForAddBackupProviderRequest().decode(payload); return await addBackupProvider(ws, req); @@ -1535,9 +1551,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( await clearDatabase(ws.db.idbHandle()); return {}; case WalletApiOperation.Recycle: { - const backup = await exportBackup(ws); - await clearDatabase(ws.db.idbHandle()); - await importBackupPlain(ws, backup); + throw Error("not implemented"); return {}; } case WalletApiOperation.ExportDb: { |