From 5c4c25516df9d65d29dc7f3f38b5a2a1a8e9e374 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 27 Nov 2021 20:56:58 +0100 Subject: wallet: support both protocol versions --- packages/taler-wallet-core/src/common.ts | 5 +- .../src/crypto/workers/cryptoApi.ts | 2 + .../src/crypto/workers/cryptoImplementation.ts | 245 ++++++++++++++------- packages/taler-wallet-core/src/db.ts | 5 +- .../src/operations/backup/import.ts | 6 +- .../src/operations/backup/index.ts | 30 ++- .../taler-wallet-core/src/operations/deposits.ts | 74 +++++-- .../taler-wallet-core/src/operations/exchanges.ts | 62 ++++-- .../taler-wallet-core/src/operations/merchants.ts | 14 +- packages/taler-wallet-core/src/operations/pay.ts | 70 +++++- .../taler-wallet-core/src/operations/refresh.ts | 48 ++-- packages/taler-wallet-core/src/operations/tip.ts | 14 +- .../taler-wallet-core/src/operations/withdraw.ts | 43 +++- .../taler-wallet-core/src/util/coinSelection.ts | 22 +- packages/taler-wallet-core/src/versions.ts | 6 +- packages/taler-wallet-core/src/wallet.ts | 4 +- 16 files changed, 459 insertions(+), 191 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts index 81c43cf14..90c2afddb 100644 --- a/packages/taler-wallet-core/src/common.ts +++ b/packages/taler-wallet-core/src/common.ts @@ -51,9 +51,8 @@ export interface TrustInfo { isAudited: boolean; } -export interface MerchantInfo { - supportsMerchantProtocolV1: boolean; - supportsMerchantProtocolV2: boolean; +export interface MerchantInfo { + protocolVersionCurrent: number; } /** diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts index e6c0290f1..e88b64c3c 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts @@ -392,6 +392,7 @@ export class CryptoApi { } isValidWireAccount( + versionCurrent: number, paytoUri: string, sig: string, masterPub: string, @@ -399,6 +400,7 @@ export class CryptoApi { return this.doRpc( "isValidWireAccount", 4, + versionCurrent, paytoUri, sig, masterPub, diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts index 389b98b22..621105b63 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts @@ -154,56 +154,63 @@ export class CryptoImplementation { * reserve. */ createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult { - if (req.denomPub.cipher !== 1) { - throw Error("unsupported cipher"); + if ( + req.denomPub.cipher === DenomKeyType.Rsa || + req.denomPub.cipher === DenomKeyType.LegacyRsa + ) { + const reservePub = decodeCrock(req.reservePub); + const reservePriv = decodeCrock(req.reservePriv); + const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key); + const derivedPlanchet = setupWithdrawPlanchet( + decodeCrock(req.secretSeed), + req.coinIndex, + ); + const coinPubHash = hash(derivedPlanchet.coinPub); + const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa); + const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; + const denomPubHash = hashDenomPub(req.denomPub); + const evHash = hash(ev); + + const withdrawRequest = buildSigPS( + TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, + ) + .put(reservePub) + .put(amountToBuffer(amountWithFee)) + .put(denomPubHash) + .put(evHash) + .build(); + + const sig = eddsaSign(withdrawRequest, reservePriv); + + const planchet: PlanchetCreationResult = { + blindingKey: encodeCrock(derivedPlanchet.bks), + coinEv: encodeCrock(ev), + coinPriv: encodeCrock(derivedPlanchet.coinPriv), + coinPub: encodeCrock(derivedPlanchet.coinPub), + coinValue: req.value, + denomPub: { + cipher: req.denomPub.cipher, + rsa_public_key: encodeCrock(denomPubRsa), + }, + denomPubHash: encodeCrock(denomPubHash), + reservePub: encodeCrock(reservePub), + withdrawSig: encodeCrock(sig), + coinEvHash: encodeCrock(evHash), + }; + return planchet; + } else { + throw Error("unsupported cipher, unable to create planchet"); } - const reservePub = decodeCrock(req.reservePub); - const reservePriv = decodeCrock(req.reservePriv); - const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key); - const derivedPlanchet = setupWithdrawPlanchet( - decodeCrock(req.secretSeed), - req.coinIndex, - ); - const coinPubHash = hash(derivedPlanchet.coinPub); - const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa); - const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; - const denomPubHash = hashDenomPub(req.denomPub); - const evHash = hash(ev); - - const withdrawRequest = buildSigPS( - TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, - ) - .put(reservePub) - .put(amountToBuffer(amountWithFee)) - .put(denomPubHash) - .put(evHash) - .build(); - - const sig = eddsaSign(withdrawRequest, reservePriv); - - const planchet: PlanchetCreationResult = { - blindingKey: encodeCrock(derivedPlanchet.bks), - coinEv: encodeCrock(ev), - coinPriv: encodeCrock(derivedPlanchet.coinPriv), - coinPub: encodeCrock(derivedPlanchet.coinPub), - coinValue: req.value, - denomPub: { - cipher: 1, - rsa_public_key: encodeCrock(denomPubRsa), - }, - denomPubHash: encodeCrock(denomPubHash), - reservePub: encodeCrock(reservePub), - withdrawSig: encodeCrock(sig), - coinEvHash: encodeCrock(evHash), - }; - return planchet; } /** * Create a planchet used for tipping, including the private keys. */ createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet { - if (req.denomPub.cipher !== 1) { + if ( + req.denomPub.cipher !== DenomKeyType.Rsa && + req.denomPub.cipher !== DenomKeyType.LegacyRsa + ) { throw Error("unsupported cipher"); } const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); @@ -243,15 +250,29 @@ export class CryptoImplementation { const coinPriv = decodeCrock(coin.coinPriv); const coinSig = eddsaSign(p, coinPriv); - const paybackRequest: RecoupRequest = { - coin_blind_key_secret: coin.blindingKey, - coin_pub: coin.coinPub, - coin_sig: encodeCrock(coinSig), - denom_pub_hash: coin.denomPubHash, - denom_sig: coin.denomSig, - refreshed: coin.coinSource.type === CoinSourceType.Refresh, - }; - return paybackRequest; + if (coin.denomPub.cipher === DenomKeyType.LegacyRsa) { + logger.info("creating legacy recoup request"); + const paybackRequest: RecoupRequest = { + coin_blind_key_secret: coin.blindingKey, + coin_pub: coin.coinPub, + coin_sig: encodeCrock(coinSig), + denom_pub_hash: coin.denomPubHash, + denom_sig: coin.denomSig.rsa_signature, + refreshed: coin.coinSource.type === CoinSourceType.Refresh, + }; + return paybackRequest; + } else { + logger.info("creating v10 recoup request"); + const paybackRequest: RecoupRequest = { + coin_blind_key_secret: coin.blindingKey, + coin_pub: coin.coinPub, + coin_sig: encodeCrock(coinSig), + denom_pub_hash: coin.denomPubHash, + denom_sig: coin.denomSig, + refreshed: coin.coinSource.type === CoinSourceType.Refresh, + }; + return paybackRequest; + } } /** @@ -326,15 +347,31 @@ export class CryptoImplementation { } isValidWireAccount( + versionCurrent: number, paytoUri: string, sig: string, masterPub: string, ): boolean { - const paytoHash = hash(stringToBytes(paytoUri + "\0")); - const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) - .put(paytoHash) - .build(); - return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)); + if (versionCurrent === 10) { + const paytoHash = hash(stringToBytes(paytoUri + "\0")); + const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) + .put(paytoHash) + .build(); + return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)); + } else if (versionCurrent === 9) { + const h = kdf( + 64, + stringToBytes("exchange-wire-signature"), + stringToBytes(paytoUri + "\0"), + new Uint8Array(0), + ); + const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) + .put(h) + .build(); + return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)); + } else { + throw Error(`unsupported version (${versionCurrent})`); + } } isValidContractTermsSignature( @@ -393,31 +430,64 @@ export class CryptoImplementation { signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission { // FIXME: put extensions here if used const hExt = new Uint8Array(64); - const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) - .put(decodeCrock(depositInfo.contractTermsHash)) - .put(hExt) - .put(decodeCrock(depositInfo.wireInfoHash)) - .put(decodeCrock(depositInfo.denomPubHash)) - .put(timestampRoundedToBuffer(depositInfo.timestamp)) - .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) - .put(amountToBuffer(depositInfo.spendAmount)) - .put(amountToBuffer(depositInfo.feeDeposit)) - .put(decodeCrock(depositInfo.merchantPub)) - .build(); + let d: Uint8Array; + if (depositInfo.denomKeyType === DenomKeyType.Rsa) { + logger.warn("signing v10 deposit permission"); + d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) + .put(decodeCrock(depositInfo.contractTermsHash)) + .put(hExt) + .put(decodeCrock(depositInfo.wireInfoHash)) + .put(decodeCrock(depositInfo.denomPubHash)) + .put(timestampRoundedToBuffer(depositInfo.timestamp)) + .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) + .put(amountToBuffer(depositInfo.spendAmount)) + .put(amountToBuffer(depositInfo.feeDeposit)) + .put(decodeCrock(depositInfo.merchantPub)) + .build(); + } else if (depositInfo.denomKeyType === DenomKeyType.LegacyRsa) { + logger.warn("signing legacy deposit permission"); + d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) + .put(decodeCrock(depositInfo.contractTermsHash)) + .put(decodeCrock(depositInfo.wireInfoHash)) + .put(decodeCrock(depositInfo.denomPubHash)) + .put(timestampRoundedToBuffer(depositInfo.timestamp)) + .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) + .put(amountToBuffer(depositInfo.spendAmount)) + .put(amountToBuffer(depositInfo.feeDeposit)) + .put(decodeCrock(depositInfo.merchantPub)) + .put(decodeCrock(depositInfo.coinPub)) + .build(); + } else { + throw Error("unsupported exchange protocol version"); + } const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv)); - const s: CoinDepositPermission = { - coin_pub: depositInfo.coinPub, - coin_sig: encodeCrock(coinSig), - contribution: Amounts.stringify(depositInfo.spendAmount), - h_denom: depositInfo.denomPubHash, - exchange_url: depositInfo.exchangeBaseUrl, - ub_sig: { - cipher: DenomKeyType.Rsa, - rsa_signature: depositInfo.denomSig.rsa_signature, - }, - }; - return s; + if (depositInfo.denomKeyType === DenomKeyType.Rsa) { + const s: CoinDepositPermission = { + coin_pub: depositInfo.coinPub, + coin_sig: encodeCrock(coinSig), + contribution: Amounts.stringify(depositInfo.spendAmount), + h_denom: depositInfo.denomPubHash, + exchange_url: depositInfo.exchangeBaseUrl, + ub_sig: { + cipher: DenomKeyType.Rsa, + rsa_signature: depositInfo.denomSig.rsa_signature, + }, + }; + return s; + } else if (depositInfo.denomKeyType === DenomKeyType.LegacyRsa) { + const s: CoinDepositPermission = { + coin_pub: depositInfo.coinPub, + coin_sig: encodeCrock(coinSig), + contribution: Amounts.stringify(depositInfo.spendAmount), + h_denom: depositInfo.denomPubHash, + exchange_url: depositInfo.exchangeBaseUrl, + ub_sig: depositInfo.denomSig.rsa_signature, + }; + return s; + } else { + throw Error("unsupported merchant protocol version"); + } } async deriveRefreshSession( @@ -466,10 +536,12 @@ export class CryptoImplementation { for (const denomSel of newCoinDenoms) { for (let i = 0; i < denomSel.count; i++) { - if (denomSel.denomPub.cipher !== 1) { - throw Error("unsupported cipher"); + if (denomSel.denomPub.cipher === DenomKeyType.LegacyRsa) { + const r = decodeCrock(denomSel.denomPub.rsa_public_key); + sessionHc.update(r); + } else { + sessionHc.update(hashDenomPub(denomSel.denomPub)); } - sessionHc.update(hashDenomPub(denomSel.denomPub)); } } @@ -508,8 +580,11 @@ export class CryptoImplementation { blindingFactor = fresh.bks; } const pubHash = hash(coinPub); - if (denomSel.denomPub.cipher !== 1) { - throw Error("unsupported cipher"); + if ( + denomSel.denomPub.cipher !== DenomKeyType.Rsa && + denomSel.denomPub.cipher !== DenomKeyType.LegacyRsa + ) { + throw Error("unsupported cipher, can't create refresh session"); } const denomPub = decodeCrock(denomSel.denomPub.rsa_public_key); const ev = rsaBlind(pubHash, blindingFactor, denomPub); diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ff47cf30d..2d818f1db 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -25,7 +25,7 @@ import { import { AmountJson, AmountString, - Auditor, + ExchangeAuditor, CoinDepositPermission, ContractTerms, DenominationPubKey, @@ -427,7 +427,7 @@ export interface ExchangeDetailsRecord { /** * Auditors (partially) auditing the exchange. */ - auditors: Auditor[]; + auditors: ExchangeAuditor[]; /** * Last observed protocol version. @@ -1136,6 +1136,7 @@ export interface WalletContractData { timestamp: Timestamp; wireMethod: string; wireInfoHash: string; + wireInfoLegacyHash?: string; maxDepositFee: AmountJson; } diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 40fa4cdec..564d39797 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -27,6 +27,7 @@ import { BackupRefundState, RefreshReason, BackupRefreshReason, + DenomKeyType, } from "@gnu-taler/taler-util"; import { WalletContractData, @@ -331,7 +332,10 @@ export async function importBackup( } for (const backupDenomination of backupExchangeDetails.denominations) { - if (backupDenomination.denom_pub.cipher !== 1) { + if ( + backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa && + backupDenomination.denom_pub.cipher !== DenomKeyType.LegacyRsa + ) { throw Error("unsupported cipher"); } const denomPubHash = diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 9027625cd..e3950ef90 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -38,14 +38,15 @@ import { codecForString, codecOptional, ConfirmPayResultType, + DenomKeyType, durationFromSpec, getTimestampNow, hashDenomPub, HttpStatusCode, j2s, + LibtoolVersion, Logger, notEmpty, - NotificationType, PreparePayResultType, RecoveryLoadRequest, RecoveryMergeStrategy, @@ -167,7 +168,10 @@ async function computeBackupCryptoData( }; for (const backupExchangeDetails of backupContent.exchange_details) { for (const backupDenom of backupExchangeDetails.denominations) { - if (backupDenom.denom_pub.cipher !== 1) { + if ( + backupDenom.denom_pub.cipher !== DenomKeyType.Rsa && + backupDenom.denom_pub.cipher !== DenomKeyType.LegacyRsa + ) { throw Error("unsupported cipher"); } for (const backupCoin of backupDenom.coins) { @@ -184,9 +188,25 @@ async function computeBackupCryptoData( coinPub, }; } - cryptoData.rsaDenomPubToHash[ - backupDenom.denom_pub.rsa_public_key - ] = encodeCrock(hashDenomPub(backupDenom.denom_pub)); + if ( + LibtoolVersion.compare(backupExchangeDetails.protocol_version, "9") + ?.compatible + ) { + cryptoData.rsaDenomPubToHash[ + backupDenom.denom_pub.rsa_public_key + ] = encodeCrock( + hash(decodeCrock(backupDenom.denom_pub.rsa_public_key)), + ); + } else if ( + LibtoolVersion.compare(backupExchangeDetails.protocol_version, "10") + ?.compatible + ) { + cryptoData.rsaDenomPubToHash[ + backupDenom.denom_pub.rsa_public_key + ] = encodeCrock(hashDenomPub(backupDenom.denom_pub)); + } else { + throw Error("unsupported exchange protocol version"); + } } for (const backupReserve of backupExchangeDetails.reserves) { cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock( diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 8fe3702f5..f90172a45 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -26,6 +26,7 @@ import { CreateDepositGroupRequest, CreateDepositGroupResponse, decodeCrock, + DenomKeyType, durationFromSpec, getTimestampNow, Logger, @@ -59,6 +60,8 @@ import { getCandidatePayCoins, getEffectiveDepositAmount, getTotalPaymentCost, + hashWire, + hashWireLegacy, } from "./pay.js"; /** @@ -103,16 +106,6 @@ const codecForDepositSuccess = (): Codec => .property("transaction_base_url", codecOptional(codecForString())) .build("DepositSuccess"); -function hashWire(paytoUri: string, salt: string): string { - const r = kdf( - 64, - stringToBytes(paytoUri + "\0"), - decodeCrock(salt), - stringToBytes("merchant-wire-signature"), - ); - return encodeCrock(r); -} - async function resetDepositGroupRetry( ws: InternalWalletState, depositGroupId: string, @@ -211,21 +204,50 @@ async function processDepositGroupImpl( continue; } const perm = depositPermissions[i]; + let requestBody: any; + if ( + typeof perm.ub_sig === "string" || + perm.ub_sig.cipher === DenomKeyType.LegacyRsa + ) { + // Legacy request + logger.info("creating legacy deposit request"); + const wireHash = hashWireLegacy( + depositGroup.wire.payto_uri, + depositGroup.wire.salt, + ); + requestBody = { + contribution: Amounts.stringify(perm.contribution), + wire: depositGroup.wire, + h_wire: wireHash, + h_contract_terms: depositGroup.contractTermsHash, + ub_sig: perm.ub_sig, + timestamp: depositGroup.contractTermsRaw.timestamp, + wire_transfer_deadline: + depositGroup.contractTermsRaw.wire_transfer_deadline, + refund_deadline: depositGroup.contractTermsRaw.refund_deadline, + coin_sig: perm.coin_sig, + denom_pub_hash: perm.h_denom, + merchant_pub: depositGroup.merchantPub, + }; + } else { + logger.info("creating v10 deposit request"); + requestBody = { + contribution: Amounts.stringify(perm.contribution), + merchant_payto_uri: depositGroup.wire.payto_uri, + wire_salt: depositGroup.wire.salt, + h_contract_terms: depositGroup.contractTermsHash, + ub_sig: perm.ub_sig, + timestamp: depositGroup.contractTermsRaw.timestamp, + wire_transfer_deadline: + depositGroup.contractTermsRaw.wire_transfer_deadline, + refund_deadline: depositGroup.contractTermsRaw.refund_deadline, + coin_sig: perm.coin_sig, + denom_pub_hash: perm.h_denom, + merchant_pub: depositGroup.merchantPub, + }; + } const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url); - const httpResp = await ws.http.postJson(url.href, { - contribution: Amounts.stringify(perm.contribution), - merchant_payto_uri: depositGroup.wire.payto_uri, - wire_salt: depositGroup.wire.salt, - h_contract_terms: depositGroup.contractTermsHash, - ub_sig: perm.ub_sig, - timestamp: depositGroup.contractTermsRaw.timestamp, - wire_transfer_deadline: - depositGroup.contractTermsRaw.wire_transfer_deadline, - refund_deadline: depositGroup.contractTermsRaw.refund_deadline, - coin_sig: perm.coin_sig, - denom_pub_hash: perm.h_denom, - merchant_pub: depositGroup.merchantPub, - }); + const httpResp = await ws.http.postJson(url.href, requestBody); await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); await ws.db .mktx((x) => ({ depositGroups: x.depositGroups })) @@ -358,6 +380,7 @@ export async function createDepositGroup( const merchantPair = await ws.cryptoApi.createEddsaKeypair(); const wireSalt = encodeCrock(getRandomBytes(16)); const wireHash = hashWire(req.depositPaytoUri, wireSalt); + const wireHashLegacy = hashWireLegacy(req.depositPaytoUri, wireSalt); const contractTerms: ContractTerms = { auditors: [], exchanges: exchangeInfos, @@ -371,7 +394,10 @@ export async function createDepositGroup( nonce: noncePair.pub, wire_transfer_deadline: timestampRound, order_id: "", + // This is always the v2 wire hash, as we're the "merchant" and support v2. h_wire: wireHash, + // Required for older exchanges. + h_wire_legacy: wireHashLegacy, pay_deadline: timestampAddDuration( timestampRound, durationFromSpec({ hours: 1 }), diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index a10378a8c..16e37fd3e 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -19,11 +19,11 @@ */ import { Amounts, - Auditor, + ExchangeAuditor, canonicalizeBaseUrl, codecForExchangeKeysJson, codecForExchangeWireJson, - Denomination, + ExchangeDenomination, Duration, durationFromSpec, ExchangeSignKeyJson, @@ -40,6 +40,9 @@ import { Timestamp, hashDenomPub, LibtoolVersion, + codecForAny, + DenominationPubKey, + DenomKeyType, } from "@gnu-taler/taler-util"; import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util"; import { CryptoApi } from "../crypto/workers/cryptoApi.js"; @@ -77,11 +80,21 @@ function denominationRecordFromKeys( exchangeBaseUrl: string, exchangeMasterPub: string, listIssueDate: Timestamp, - denomIn: Denomination, + denomIn: ExchangeDenomination, ): DenominationRecord { - const denomPubHash = encodeCrock(hashDenomPub(denomIn.denom_pub)); + let denomPub: DenominationPubKey; + // We support exchange protocol v9 and v10. + if (typeof denomIn.denom_pub === "string") { + denomPub = { + cipher: DenomKeyType.LegacyRsa, + rsa_public_key: denomIn.denom_pub, + }; + } else { + denomPub = denomIn.denom_pub; + } + const denomPubHash = encodeCrock(hashDenomPub(denomPub)); const d: DenominationRecord = { - denomPub: denomIn.denom_pub, + denomPub, denomPubHash, exchangeBaseUrl, exchangeMasterPub, @@ -205,6 +218,7 @@ export async function acceptExchangeTermsOfService( } async function validateWireInfo( + versionCurrent: number, wireInfo: ExchangeWireJson, masterPublicKey: string, cryptoApi: CryptoApi, @@ -212,6 +226,7 @@ async function validateWireInfo( for (const a of wireInfo.accounts) { logger.trace("validating exchange acct"); const isValid = await cryptoApi.isValidWireAccount( + versionCurrent, a.payto_uri, a.master_sig, masterPublicKey, @@ -321,7 +336,7 @@ async function provideExchangeRecord( interface ExchangeKeysDownloadResult { masterPublicKey: string; currency: string; - auditors: Auditor[]; + auditors: ExchangeAuditor[]; currentDenominations: DenominationRecord[]; protocolVersion: string; signingKeys: ExchangeSignKeyJson[]; @@ -345,14 +360,14 @@ async function downloadKeysInfo( const resp = await http.get(keysUrl.href, { timeout, }); - const exchangeKeysJson = await readSuccessResponseJsonOrThrow( + const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow( resp, codecForExchangeKeysJson(), ); logger.info("received /keys response"); - if (exchangeKeysJson.denoms.length === 0) { + if (exchangeKeysJsonUnchecked.denoms.length === 0) { const opErr = makeErrorDetails( TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, "exchange doesn't offer any denominations", @@ -363,7 +378,7 @@ async function downloadKeysInfo( throw new OperationFailedError(opErr); } - const protocolVersion = exchangeKeysJson.version; + const protocolVersion = exchangeKeysJsonUnchecked.version; const versionRes = LibtoolVersion.compare( WALLET_EXCHANGE_PROTOCOL_VERSION, @@ -382,29 +397,29 @@ async function downloadKeysInfo( } const currency = Amounts.parseOrThrow( - exchangeKeysJson.denoms[0].value, + exchangeKeysJsonUnchecked.denoms[0].value, ).currency.toUpperCase(); return { - masterPublicKey: exchangeKeysJson.master_public_key, + masterPublicKey: exchangeKeysJsonUnchecked.master_public_key, currency, - auditors: exchangeKeysJson.auditors, - currentDenominations: exchangeKeysJson.denoms.map((d) => + auditors: exchangeKeysJsonUnchecked.auditors, + currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) => denominationRecordFromKeys( baseUrl, - exchangeKeysJson.master_public_key, - exchangeKeysJson.list_issue_date, + exchangeKeysJsonUnchecked.master_public_key, + exchangeKeysJsonUnchecked.list_issue_date, d, ), ), - protocolVersion: exchangeKeysJson.version, - signingKeys: exchangeKeysJson.signkeys, - reserveClosingDelay: exchangeKeysJson.reserve_closing_delay, + protocolVersion: exchangeKeysJsonUnchecked.version, + signingKeys: exchangeKeysJsonUnchecked.signkeys, + reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay, expiry: getExpiryTimestamp(resp, { minDuration: durationFromSpec({ hours: 1 }), }), - recoup: exchangeKeysJson.recoup ?? [], - listIssueDate: exchangeKeysJson.list_issue_date, + recoup: exchangeKeysJsonUnchecked.recoup ?? [], + listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, }; } @@ -466,7 +481,14 @@ async function updateExchangeFromUrlImpl( logger.info("validating exchange /wire info"); + const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion); + if (!version) { + // Should have been validated earlier. + throw Error("unexpected invalid version"); + } + const wireInfo = await validateWireInfo( + version.current, wireInfoDownload, keysInfo.masterPublicKey, ws.cryptoApi, diff --git a/packages/taler-wallet-core/src/operations/merchants.ts b/packages/taler-wallet-core/src/operations/merchants.ts index d12417c7c..fd628fa98 100644 --- a/packages/taler-wallet-core/src/operations/merchants.ts +++ b/packages/taler-wallet-core/src/operations/merchants.ts @@ -52,15 +52,13 @@ export async function getMerchantInfo( `merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`, ); + const parsedVersion = LibtoolVersion.parseVersion(configResp.version); + if (!parsedVersion) { + throw Error("invalid merchant version"); + } + const merchantInfo: MerchantInfo = { - supportsMerchantProtocolV1: !!LibtoolVersion.compare( - "1:0:0", - configResp.version, - )?.compatible, - supportsMerchantProtocolV2: !!LibtoolVersion.compare( - "2:0:0", - configResp.version, - )?.compatible, + protocolVersionCurrent: parsedVersion.current, }; ws.merchantInfoCache[canonBaseUrl] = merchantInfo; diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index acc592a72..73fc6537c 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -54,6 +54,10 @@ import { URL, getDurationRemaining, HttpStatusCode, + DenomKeyType, + kdf, + stringToBytes, + decodeCrock, } from "@gnu-taler/taler-util"; import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; import { @@ -108,6 +112,26 @@ import { */ const logger = new Logger("pay.ts"); +export function hashWire(paytoUri: string, salt: string): string { + const r = kdf( + 64, + stringToBytes(paytoUri + "\0"), + decodeCrock(salt), + stringToBytes("merchant-wire-signature"), + ); + return encodeCrock(r); +} + +export function hashWireLegacy(paytoUri: string, salt: string): string { + const r = kdf( + 64, + stringToBytes(paytoUri + "\0"), + stringToBytes(salt + "\0"), + stringToBytes("merchant-wire-signature"), + ); + return encodeCrock(r); +} + /** * Compute the total cost of a payment to the customer. * @@ -193,9 +217,9 @@ export async function getEffectiveDepositAmount( if (!exchangeDetails) { continue; } - // FIXME/NOTE: the line below _likely_ throws exception - // about "find method not found on undefined" when the wireType - // is not supported by the Exchange. + // FIXME/NOTE: the line below _likely_ throws exception + // about "find method not found on undefined" when the wireType + // is not supported by the Exchange. const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => { return timestampIsBetween( getTimestampNow(), @@ -669,6 +693,7 @@ export function extractContractData( timestamp: parsedContractTerms.timestamp, wireMethod: parsedContractTerms.wire_method, wireInfoHash: parsedContractTerms.h_wire, + wireInfoLegacyHash: parsedContractTerms.h_wire_legacy, maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee), merchant: parsedContractTerms.merchant, products: parsedContractTerms.products, @@ -882,7 +907,6 @@ async function startDownloadProposal( claimToken: string | undefined, noncePriv: string | undefined, ): Promise { - const oldProposal = await ws.db .mktx((x) => ({ proposals: x.proposals })) .runReadOnly(async (tx) => { @@ -891,20 +915,24 @@ async function startDownloadProposal( orderId, ]); }); - + /** * If we have already claimed this proposal with the same sessionId * nonce and claim token, reuse it. */ - if (oldProposal && - oldProposal.downloadSessionId === sessionId && - (!noncePriv || oldProposal.noncePriv === noncePriv) && - oldProposal.claimToken === claimToken) { + if ( + oldProposal && + oldProposal.downloadSessionId === sessionId && + (!noncePriv || oldProposal.noncePriv === noncePriv) && + oldProposal.claimToken === claimToken + ) { await processDownloadProposal(ws, oldProposal.proposalId); return oldProposal.proposalId; } - const { priv, pub } = await (noncePriv ? ws.cryptoApi.eddsaGetPublic(noncePriv) : ws.cryptoApi.createEddsaKeypair()); + const { priv, pub } = await (noncePriv + ? ws.cryptoApi.eddsaGetPublic(noncePriv) + : ws.cryptoApi.createEddsaKeypair()); const proposalId = encodeCrock(getRandomBytes(32)); const proposalRecord: ProposalRecord = { @@ -1169,6 +1197,11 @@ async function submitPay( logger.trace("paying with session ID", sessionId); + const merchantInfo = await ws.merchantOps.getMerchantInfo( + ws, + purchase.download.contractData.merchantBaseUrl, + ); + if (!purchase.merchantPaySig) { const payUrl = new URL( `orders/${purchase.download.contractData.orderId}/pay`, @@ -1568,11 +1601,21 @@ export async function generateDepositPermissions( for (let i = 0; i < payCoinSel.coinPubs.length; i++) { const { coin, denom } = coinWithDenom[i]; + let wireInfoHash: string; + if ( + coin.denomPub.cipher === DenomKeyType.LegacyRsa && + contractData.wireInfoLegacyHash + ) { + wireInfoHash = contractData.wireInfoLegacyHash; + } else { + wireInfoHash = contractData.wireInfoHash; + } const dp = await ws.cryptoApi.signDepositPermission({ coinPriv: coin.coinPriv, coinPub: coin.coinPub, contractTermsHash: contractData.contractTermsHash, denomPubHash: coin.denomPubHash, + denomKeyType: coin.denomPub.cipher, denomSig: coin.denomSig, exchangeBaseUrl: coin.exchangeBaseUrl, feeDeposit: denom.feeDeposit, @@ -1580,7 +1623,7 @@ export async function generateDepositPermissions( refundDeadline: contractData.refundDeadline, spendAmount: payCoinSel.coinContributions[i], timestamp: contractData.timestamp, - wireInfoHash: contractData.wireInfoHash, + wireInfoHash, }); depositPermissions.push(dp); } @@ -1613,6 +1656,11 @@ export async function confirmPay( throw Error("proposal is in invalid state"); } + const merchantInfo = await ws.merchantOps.getMerchantInfo( + ws, + d.contractData.merchantBaseUrl, + ); + const existingPurchase = await ws.db .mktx((x) => ({ purchases: x.purchases })) .runReadWrite(async (tx) => { diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index c1e672d63..51eac4a64 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -365,18 +365,29 @@ async function refreshMelt( `coins/${oldCoin.coinPub}/melt`, oldCoin.exchangeBaseUrl, ); - const meltReq = { - coin_pub: oldCoin.coinPub, - confirm_sig: derived.confirmSig, - denom_pub_hash: oldCoin.denomPubHash, - denom_sig: oldCoin.denomSig, - rc: derived.hash, - value_with_fee: Amounts.stringify(derived.meltValueWithFee), - }; - logger.trace(`melt request for coin:`, meltReq); + let meltReqBody: any; + if (oldCoin.denomPub.cipher === DenomKeyType.LegacyRsa) { + meltReqBody = { + coin_pub: oldCoin.coinPub, + confirm_sig: derived.confirmSig, + denom_pub_hash: oldCoin.denomPubHash, + denom_sig: oldCoin.denomSig.rsa_signature, + rc: derived.hash, + value_with_fee: Amounts.stringify(derived.meltValueWithFee), + }; + } else { + meltReqBody = { + coin_pub: oldCoin.coinPub, + confirm_sig: derived.confirmSig, + denom_pub_hash: oldCoin.denomPubHash, + denom_sig: oldCoin.denomSig, + rc: derived.hash, + value_with_fee: Amounts.stringify(derived.meltValueWithFee), + }; + } const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => { - return await ws.http.postJson(reqUrl.href, meltReq, { + return await ws.http.postJson(reqUrl.href, meltReqBody, { timeout: getRefreshRequestTimeout(refreshGroup), }); }); @@ -604,15 +615,26 @@ async function refreshReveal( continue; } const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex]; - if (denom.denomPub.cipher !== 1) { + if ( + denom.denomPub.cipher !== DenomKeyType.Rsa && + denom.denomPub.cipher !== DenomKeyType.LegacyRsa + ) { throw Error("cipher unsupported"); } const evSig = reveal.ev_sigs[newCoinIndex].ev_sig; - if (evSig.cipher !== DenomKeyType.Rsa) { + let rsaSig: string; + if (typeof evSig === "string") { + rsaSig = evSig; + } else if ( + evSig.cipher === DenomKeyType.Rsa || + evSig.cipher === DenomKeyType.LegacyRsa + ) { + rsaSig = evSig.blinded_rsa_signature; + } else { throw Error("unsupported cipher"); } const denomSigRsa = await ws.cryptoApi.rsaUnblind( - evSig.blinded_rsa_signature, + rsaSig, pc.blindingKey, denom.denomPub.rsa_public_key, ); diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 0253930ea..cf3502ecd 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -314,13 +314,13 @@ async function processTipImpl( let blindedSigs: BlindedDenominationSignature[] = []; - if (merchantInfo.supportsMerchantProtocolV2) { + if (merchantInfo.protocolVersionCurrent === 2) { const response = await readSuccessResponseJsonOrThrow( merchantResp, codecForMerchantTipResponseV2(), ); blindedSigs = response.blind_sigs.map((x) => x.blind_sig); - } else if (merchantInfo.supportsMerchantProtocolV1) { + } else if (merchantInfo.protocolVersionCurrent === 1) { const response = await readSuccessResponseJsonOrThrow( merchantResp, codecForMerchantTipResponseV1(), @@ -347,11 +347,17 @@ async function processTipImpl( const planchet = planchets[i]; checkLogicInvariant(!!planchet); - if (denom.denomPub.cipher !== DenomKeyType.Rsa) { + if ( + denom.denomPub.cipher !== DenomKeyType.Rsa && + denom.denomPub.cipher !== DenomKeyType.LegacyRsa + ) { throw Error("unsupported cipher"); } - if (blindedSig.cipher !== DenomKeyType.Rsa) { + if ( + blindedSig.cipher !== DenomKeyType.Rsa && + blindedSig.cipher !== DenomKeyType.LegacyRsa + ) { throw Error("unsupported cipher"); } diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 979bd0e53..8c9178f59 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -42,6 +42,7 @@ import { VersionMatchResult, DenomKeyType, LibtoolVersion, + UnblindedSignature, } from "@gnu-taler/taler-util"; import { CoinRecord, @@ -591,12 +592,28 @@ async function processPlanchetVerifyAndStoreCoin( const { planchet, exchangeBaseUrl } = d; const planchetDenomPub = planchet.denomPub; - if (planchetDenomPub.cipher !== DenomKeyType.Rsa) { - throw Error("cipher not supported"); + if ( + planchetDenomPub.cipher !== DenomKeyType.Rsa && + planchetDenomPub.cipher !== DenomKeyType.LegacyRsa + ) { + throw Error(`cipher (${planchetDenomPub.cipher}) not supported`); } - const evSig = resp.ev_sig; - if (evSig.cipher !== DenomKeyType.Rsa) { + let evSig = resp.ev_sig; + if (typeof resp.ev_sig === "string") { + evSig = { + cipher: DenomKeyType.LegacyRsa, + blinded_rsa_signature: resp.ev_sig, + }; + } else { + evSig = resp.ev_sig; + } + if ( + !( + evSig.cipher === DenomKeyType.Rsa || + evSig.cipher === DenomKeyType.LegacyRsa + ) + ) { throw Error("unsupported cipher"); } @@ -633,6 +650,19 @@ async function processPlanchetVerifyAndStoreCoin( return; } + let denomSig: UnblindedSignature; + if ( + planchet.denomPub.cipher === DenomKeyType.LegacyRsa || + planchet.denomPub.cipher === DenomKeyType.Rsa + ) { + denomSig = { + cipher: planchet.denomPub.cipher, + rsa_signature: denomSigRsa, + }; + } else { + throw Error("unsupported cipher"); + } + const coin: CoinRecord = { blindingKey: planchet.blindingKey, coinPriv: planchet.coinPriv, @@ -640,10 +670,7 @@ async function processPlanchetVerifyAndStoreCoin( currentAmount: planchet.coinValue, denomPub: planchet.denomPub, denomPubHash: planchet.denomPubHash, - denomSig: { - cipher: DenomKeyType.Rsa, - rsa_signature: denomSigRsa, - }, + denomSig, coinEvHash: planchet.coinEvHash, exchangeBaseUrl: exchangeBaseUrl, status: CoinStatus.Fresh, diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index ba26c98fe..bfc481eaf 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -23,7 +23,12 @@ /** * Imports. */ -import { AmountJson, Amounts, DenominationPubKey } from "@gnu-taler/taler-util"; +import { + AmountJson, + Amounts, + DenominationPubKey, + DenomKeyType, +} from "@gnu-taler/taler-util"; import { strcmp, Logger } from "@gnu-taler/taler-util"; const logger = new Logger("coinSelection.ts"); @@ -215,10 +220,21 @@ function denomPubCmp( } else if (p1.cipher > p2.cipher) { return +1; } - if (p1.cipher !== 1 || p2.cipher !== 1) { + if ( + p1.cipher === DenomKeyType.LegacyRsa && + p2.cipher === DenomKeyType.LegacyRsa + ) { + return strcmp(p1.rsa_public_key, p2.rsa_public_key); + } else if (p1.cipher === DenomKeyType.Rsa && p2.cipher === DenomKeyType.Rsa) { + if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) { + return -1; + } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) { + return 1; + } + return strcmp(p1.rsa_public_key, p2.rsa_public_key); + } else { throw Error("unsupported cipher"); } - return strcmp(p1.rsa_public_key, p2.rsa_public_key); } /** diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts index 7383355ba..9ef298d62 100644 --- a/packages/taler-wallet-core/src/versions.ts +++ b/packages/taler-wallet-core/src/versions.ts @@ -19,14 +19,14 @@ * * Uses libtool's current:revision:age versioning. */ -export const WALLET_EXCHANGE_PROTOCOL_VERSION = "10:0:0"; +export const WALLET_EXCHANGE_PROTOCOL_VERSION = "10:0:1"; /** * Protocol version spoken with the merchant. * * Uses libtool's current:revision:age versioning. */ -export const WALLET_MERCHANT_PROTOCOL_VERSION = "1:0:0"; +export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1"; /** * Protocol version spoken with the merchant. @@ -42,4 +42,4 @@ export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; * * This is only a temporary measure. */ -export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "3"; +export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "4"; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 576a44597..7233af3af 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -390,7 +390,7 @@ async function runTaskLoop( } catch (e) { if (e instanceof OperationFailedAndReportedError) { logger.warn("operation processed resulted in reported error"); - logger.warn(`reporred error was: ${j2s(e.operationError)}`); + logger.warn(`reported error was: ${j2s(e.operationError)}`); } else { logger.error("Uncaught exception", e); ws.notify({ @@ -985,6 +985,8 @@ export async function handleCoreApiRequest( e instanceof OperationFailedError || e instanceof OperationFailedAndReportedError ) { + logger.error("Caught operation failed error"); + logger.trace((e as any).stack); return { type: "error", operation, -- cgit v1.2.3