diff options
author | Florian Dold <florian@dold.me> | 2021-11-27 20:56:58 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-11-27 20:57:07 +0100 |
commit | 5c4c25516df9d65d29dc7f3f38b5a2a1a8e9e374 (patch) | |
tree | 4665e79a6033ab949de211fd9de8de8ca681c2e0 | |
parent | 403de8170ef538ef74505859b1c04e3542cad9fb (diff) |
wallet: support both protocol versions
24 files changed, 623 insertions, 231 deletions
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts index 8605ff335..2ea64a249 100644 --- a/packages/taler-util/src/codec.ts +++ b/packages/taler-util/src/codec.ts @@ -417,3 +417,26 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> { }, }; } + +export type CodecType<T> = T extends Codec<infer X> ? X : any; + +export function codecForEither<T extends Array<Codec<unknown>>>( + ...alts: [...T] +): Codec<CodecType<T[number]>> { + return { + decode(x: any, c?: Context): any { + for (const alt of alts) { + try { + return alt.decode(x, c); + } catch (e) { + continue; + } + } + throw new DecodingError( + `No alternative matched at at ${renderContext(c)}`, + ); + }, + }; +} + +const x = codecForEither(codecForString(), codecForNumber()); diff --git a/packages/taler-util/src/libtool-version.ts b/packages/taler-util/src/libtool-version.ts index 17d2bbbdc..ed11a4e95 100644 --- a/packages/taler-util/src/libtool-version.ts +++ b/packages/taler-util/src/libtool-version.ts @@ -27,14 +27,15 @@ export interface VersionMatchResult { * Is the first version compatible with the second? */ compatible: boolean; + /** - * Is the first version older (-1), newser (+1) or + * Is the first version older (-1), newer (+1) or * identical (0)? */ currentCmp: number; } -interface Version { +export interface Version { current: number; revision: number; age: number; @@ -64,7 +65,7 @@ export namespace LibtoolVersion { return { compatible, currentCmp }; } - function parseVersion(v: string): Version | undefined { + export function parseVersion(v: string): Version | undefined { const [currentStr, revisionStr, ageStr, ...rest] = v.split(":"); if (rest.length !== 0) { return undefined; diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts index 8b9de1ab0..117664d8c 100644 --- a/packages/taler-util/src/logging.ts +++ b/packages/taler-util/src/logging.ts @@ -55,7 +55,7 @@ export function setGlobalLogLevelFromString(logLevelStr: string) { break; default: if (isNode) { - process.stderr.write(`Invalid log level, defaulting to WARNING`); + process.stderr.write(`Invalid log level, defaulting to WARNING\n`); } else { console.warn(`Invalid log level, defaulting to WARNING`); } @@ -143,6 +143,7 @@ export class Logger { case LogLevel.Info: case LogLevel.Warn: case LogLevel.Error: + return true; case LogLevel.None: return false; } diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index c20ce72a6..d96c23236 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -349,18 +349,25 @@ export function hash(d: Uint8Array): Uint8Array { return nacl.hash(d); } +/** + * Hash a denomination public key according to the + * algorithm of exchange protocol v10. + */ export function hashDenomPub(pub: DenominationPubKey): Uint8Array { - if (pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); + if (pub.cipher === DenomKeyType.Rsa) { + const pubBuf = decodeCrock(pub.rsa_public_key); + const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); + const uint8ArrayBuf = new Uint8Array(hashInputBuf); + const dv = new DataView(hashInputBuf); + dv.setUint32(0, pub.age_mask ?? 0); + dv.setUint32(4, pub.cipher); + uint8ArrayBuf.set(pubBuf, 8); + return nacl.hash(uint8ArrayBuf); + } else if (pub.cipher === DenomKeyType.LegacyRsa) { + return hash(decodeCrock(pub.rsa_public_key)); + } else { + throw Error(`unsupported cipher (${pub.cipher}), unable to hash`); } - const pubBuf = decodeCrock(pub.rsa_public_key); - const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); - const uint8ArrayBuf = new Uint8Array(hashInputBuf); - const dv = new DataView(hashInputBuf); - dv.setUint32(0, pub.age_mask ?? 0); - dv.setUint32(4, pub.cipher); - uint8ArrayBuf.set(pubBuf, 8); - return nacl.hash(uint8ArrayBuf); } export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array { diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index bd9c67d7e..15dc88ca5 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -38,6 +38,7 @@ import { codecForConstNumber, buildCodecForUnion, codecForConstString, + codecForEither, } from "./codec.js"; import { Timestamp, @@ -50,7 +51,7 @@ import { codecForAmountString } from "./amounts.js"; /** * Denomination as found in the /keys response from the exchange. */ -export class Denomination { +export class ExchangeDenomination { /** * Value of one coin of the denomination. */ @@ -58,8 +59,11 @@ export class Denomination { /** * Public signing key of the denomination. + * + * The "string" alternative is for the old exchange protocol (v9) that + * only supports RSA keys. */ - denom_pub: DenominationPubKey; + denom_pub: DenominationPubKey | string; /** * Fee for withdrawing. @@ -128,7 +132,7 @@ export class AuditorDenomSig { /** * Auditor information as given by the exchange in /keys. */ -export class Auditor { +export class ExchangeAuditor { /** * Auditor's public key. */ @@ -157,8 +161,10 @@ export interface RecoupRequest { /** * Signature over the coin public key by the denomination. + * + * The string variant is for the legacy exchange protocol. */ - denom_sig: UnblindedSignature; + denom_sig: UnblindedSignature | string; /** * Coin public key of the coin we want to refund. @@ -198,11 +204,20 @@ export interface RecoupConfirmation { old_coin_pub?: string; } -export interface UnblindedSignature { +export type UnblindedSignature = + | RsaUnblindedSignature + | LegacyRsaUnblindedSignature; + +export interface RsaUnblindedSignature { cipher: DenomKeyType.Rsa; rsa_signature: string; } +export interface LegacyRsaUnblindedSignature { + cipher: DenomKeyType.LegacyRsa; + rsa_signature: string; +} + /** * Deposit permission for a single coin. */ @@ -211,18 +226,25 @@ export interface CoinDepositPermission { * Signature by the coin. */ coin_sig: string; + /** * Public key of the coin being spend. */ coin_pub: string; + /** * Signature made by the denomination public key. + * + * The string variant is for legacy protocol support. */ - ub_sig: UnblindedSignature; + + ub_sig: UnblindedSignature | string; + /** * The denomination public key associated with this coin. */ h_denom: string; + /** * The amount that is subtracted from this coin with this payment. */ @@ -359,6 +381,11 @@ export interface ContractTerms { h_wire: string; /** + * Legacy wire hash, used for deposit operations with an older exchange. + */ + h_wire_legacy?: string; + + /** * Hash of the merchant's wire details. */ auto_refund?: Duration; @@ -662,7 +689,7 @@ export class ExchangeKeysJson { /** * List of offered denominations. */ - denoms: Denomination[]; + denoms: ExchangeDenomination[]; /** * The exchange's master public key. @@ -672,7 +699,7 @@ export class ExchangeKeysJson { /** * The list of auditors (partially) auditing the exchange. */ - auditors: Auditor[]; + auditors: ExchangeAuditor[]; /** * Timestamp when this response was issued. @@ -802,6 +829,7 @@ export class TipPickupGetResponse { export enum DenomKeyType { Rsa = 1, ClauseSchnorr = 2, + LegacyRsa = 3, } export interface RsaBlindedDenominationSignature { @@ -809,18 +837,25 @@ export interface RsaBlindedDenominationSignature { blinded_rsa_signature: string; } +export interface LegacyRsaBlindedDenominationSignature { + cipher: DenomKeyType.LegacyRsa; + blinded_rsa_signature: string; +} + export interface CSBlindedDenominationSignature { cipher: DenomKeyType.ClauseSchnorr; } export type BlindedDenominationSignature = | RsaBlindedDenominationSignature - | CSBlindedDenominationSignature; + | CSBlindedDenominationSignature + | LegacyRsaBlindedDenominationSignature; export const codecForBlindedDenominationSignature = () => buildCodecForUnion<BlindedDenominationSignature>() .discriminateOn("cipher") .alternative(1, codecForRsaBlindedDenominationSignature()) + .alternative(3, codecForLegacyRsaBlindedDenominationSignature()) .build("BlindedDenominationSignature"); export const codecForRsaBlindedDenominationSignature = () => @@ -829,8 +864,17 @@ export const codecForRsaBlindedDenominationSignature = () => .property("blinded_rsa_signature", codecForString()) .build("RsaBlindedDenominationSignature"); +export const codecForLegacyRsaBlindedDenominationSignature = () => + buildCodecForObject<LegacyRsaBlindedDenominationSignature>() + .property("cipher", codecForConstNumber(1)) + .property("blinded_rsa_signature", codecForString()) + .build("LegacyRsaBlindedDenominationSignature"); + export class WithdrawResponse { - ev_sig: BlindedDenominationSignature; + /** + * The string variant is for legacy protocol support. + */ + ev_sig: BlindedDenominationSignature | string; } /** @@ -925,7 +969,10 @@ export interface ExchangeMeltResponse { } export interface ExchangeRevealItem { - ev_sig: BlindedDenominationSignature; + /** + * The string variant is for the legacy v9 protocol. + */ + ev_sig: BlindedDenominationSignature | string; } export interface ExchangeRevealResponse { @@ -1044,7 +1091,15 @@ export interface BankWithdrawalOperationPostResponse { transfer_done: boolean; } -export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey; +export type DenominationPubKey = + | RsaDenominationPubKey + | CsDenominationPubKey + | LegacyRsaDenominationPubKey; + +export interface LegacyRsaDenominationPubKey { + cipher: DenomKeyType.LegacyRsa; + rsa_public_key: string; +} export interface RsaDenominationPubKey { cipher: DenomKeyType.Rsa; @@ -1061,6 +1116,7 @@ export const codecForDenominationPubKey = () => buildCodecForUnion<DenominationPubKey>() .discriminateOn("cipher") .alternative(1, codecForRsaDenominationPubKey()) + .alternative(3, codecForLegacyRsaDenominationPubKey()) .build("DenominationPubKey"); export const codecForRsaDenominationPubKey = () => @@ -1069,6 +1125,12 @@ export const codecForRsaDenominationPubKey = () => .property("rsa_public_key", codecForString()) .build("DenominationPubKey"); +export const codecForLegacyRsaDenominationPubKey = () => + buildCodecForObject<LegacyRsaDenominationPubKey>() + .property("cipher", codecForConstNumber(3)) + .property("rsa_public_key", codecForString()) + .build("LegacyRsaDenominationPubKey"); + export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> => buildCodecForObject<BankWithdrawalOperationPostResponse>() .property("transfer_done", codecForBoolean()) @@ -1080,10 +1142,13 @@ export type EddsaSignatureString = string; export type EddsaPublicKeyString = string; export type CoinPublicKeyString = string; -export const codecForDenomination = (): Codec<Denomination> => - buildCodecForObject<Denomination>() +export const codecForDenomination = (): Codec<ExchangeDenomination> => + buildCodecForObject<ExchangeDenomination>() .property("value", codecForString()) - .property("denom_pub", codecForDenominationPubKey()) + .property( + "denom_pub", + codecForEither(codecForDenominationPubKey(), codecForString()), + ) .property("fee_withdraw", codecForString()) .property("fee_deposit", codecForString()) .property("fee_refresh", codecForString()) @@ -1101,8 +1166,8 @@ export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> => .property("auditor_sig", codecForString()) .build("AuditorDenomSig"); -export const codecForAuditor = (): Codec<Auditor> => - buildCodecForObject<Auditor>() +export const codecForAuditor = (): Codec<ExchangeAuditor> => + buildCodecForObject<ExchangeAuditor>() .property("auditor_pub", codecForString()) .property("auditor_url", codecForString()) .property("denomination_keys", codecForList(codecForAuditorDenomSig())) @@ -1261,7 +1326,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> => .property("signkeys", codecForList(codecForExchangeSigningKey())) .property("version", codecForString()) .property("reserve_closing_delay", codecForDuration) - .build("KeysJson"); + .build("ExchangeKeysJson"); export const codecForWireFeesJson = (): Codec<WireFeesJson> => buildCodecForObject<WireFeesJson>() @@ -1327,7 +1392,10 @@ export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> => export const codecForWithdrawResponse = (): Codec<WithdrawResponse> => buildCodecForObject<WithdrawResponse>() - .property("ev_sig", codecForBlindedDenominationSignature()) + .property( + "ev_sig", + codecForEither(codecForBlindedDenominationSignature(), codecForString()), + ) .build("WithdrawResponse"); export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> => @@ -1345,7 +1413,10 @@ export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> => export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> => buildCodecForObject<ExchangeRevealItem>() - .property("ev_sig", codecForBlindedDenominationSignature()) + .property( + "ev_sig", + codecForEither(codecForBlindedDenominationSignature(), codecForString()), + ) .build("ExchangeRevealItem"); export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> => diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index f00e2907f..ced30e4db 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -49,6 +49,7 @@ import { codecForContractTerms, ContractTerms, DenominationPubKey, + DenomKeyType, UnblindedSignature, } from "./talerTypes.js"; import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js"; @@ -515,6 +516,7 @@ export interface DepositInfo { merchantPub: string; feeDeposit: AmountJson; wireInfoHash: string; + denomKeyType: DenomKeyType; denomPubHash: string; denomSig: UnblindedSignature; } diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts index 9a33d572a..37a192db6 100644 --- a/packages/taler-wallet-cli/src/harness/harness.ts +++ b/packages/taler-wallet-cli/src/harness/harness.ts @@ -1173,12 +1173,24 @@ export class ExchangeService implements ExchangeServiceInterface { } async runAggregatorOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-aggregator-once`, - "taler-exchange-aggregator", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); + try { + await runCommand( + this.globalState, + `exchange-${this.name}-aggregator-once`, + "taler-exchange-aggregator", + [...this.timetravelArgArr, "-c", this.configFilename, "-t", "-y"], + ); + } catch (e) { + console.log( + "running aggregator with KYC off didn't work, might be old version, running again", + ); + await runCommand( + this.globalState, + `exchange-${this.name}-aggregator-once`, + "taler-exchange-aggregator", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } } async runTransferOnce() { diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 71431b5eb..b57e73a1c 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -1018,6 +1018,13 @@ const testCli = walletCli.subcommand("testingArgs", "testing", { help: "Subcommands for testing.", }); +testCli.subcommand("logtest", "logtest").action(async (args) => { + logger.trace("This is a trace message."); + logger.info("This is an info message."); + logger.warn("This is an warning message."); + logger.error("This is an error message."); +}); + testCli .subcommand("listIntegrationtests", "list-integrationtests") .action(async (args) => { 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<boolean>( "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<DepositSuccess> => .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<string> { - 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, |