From ae8af3f27c0ed1746c49a7608fe05af24ae8a18b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 23 Nov 2021 23:51:12 +0100 Subject: [PATCH] wallet: tipping protocol change / merchant version info --- packages/taler-util/src/backupTypes.ts | 2 + .../taler-util/src/libtool-version.test.ts | 2 +- packages/taler-util/src/libtool-version.ts | 82 ++++++------ packages/taler-util/src/talerCrypto.ts | 4 +- packages/taler-util/src/talerTypes.ts | 63 +++++++-- .../taler-wallet-cli/src/harness/harness.ts | 122 +++++++++--------- packages/taler-wallet-core/src/common.ts | 18 +++ packages/taler-wallet-core/src/db.ts | 6 + .../src/operations/backup/export.ts | 1 + .../src/operations/backup/import.ts | 1 + .../src/operations/exchanges.ts | 8 +- .../src/operations/merchants.ts | 68 ++++++++++ .../taler-wallet-core/src/operations/tip.ts | 46 +++++-- .../src/operations/withdraw.ts | 6 +- packages/taler-wallet-core/src/wallet.ts | 9 ++ 15 files changed, 307 insertions(+), 131 deletions(-) create mode 100644 packages/taler-wallet-core/src/operations/merchants.ts diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts index ecdd6fdf8..8663850c9 100644 --- a/packages/taler-util/src/backupTypes.ts +++ b/packages/taler-util/src/backupTypes.ts @@ -1102,6 +1102,8 @@ export interface BackupExchange { currency: string; + protocol_version_range: string; + /** * Time when the pointer to the exchange details * was last updated. diff --git a/packages/taler-util/src/libtool-version.test.ts b/packages/taler-util/src/libtool-version.test.ts index d35642518..c1683f0df 100644 --- a/packages/taler-util/src/libtool-version.test.ts +++ b/packages/taler-util/src/libtool-version.test.ts @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see */ -import * as LibtoolVersion from "./libtool-version.js"; +import { LibtoolVersion } from "./libtool-version.js"; import test from "ava"; diff --git a/packages/taler-util/src/libtool-version.ts b/packages/taler-util/src/libtool-version.ts index 5e9d0b74e..17d2bbbdc 100644 --- a/packages/taler-util/src/libtool-version.ts +++ b/packages/taler-util/src/libtool-version.ts @@ -40,49 +40,51 @@ interface Version { age: number; } -/** - * Compare two libtool-style version strings. - */ -export function compare( - me: string, - other: string, -): VersionMatchResult | undefined { - const meVer = parseVersion(me); - const otherVer = parseVersion(other); +export namespace LibtoolVersion { + /** + * Compare two libtool-style version strings. + */ + export function compare( + me: string, + other: string, + ): VersionMatchResult | undefined { + const meVer = parseVersion(me); + const otherVer = parseVersion(other); - if (!(meVer && otherVer)) { - return undefined; + if (!(meVer && otherVer)) { + return undefined; + } + + const compatible = + meVer.current - meVer.age <= otherVer.current && + meVer.current >= otherVer.current - otherVer.age; + + const currentCmp = Math.sign(meVer.current - otherVer.current); + + return { compatible, currentCmp }; } - const compatible = - meVer.current - meVer.age <= otherVer.current && - meVer.current >= otherVer.current - otherVer.age; + function parseVersion(v: string): Version | undefined { + const [currentStr, revisionStr, ageStr, ...rest] = v.split(":"); + if (rest.length !== 0) { + return undefined; + } + const current = Number.parseInt(currentStr); + const revision = Number.parseInt(revisionStr); + const age = Number.parseInt(ageStr); - const currentCmp = Math.sign(meVer.current - otherVer.current); + if (Number.isNaN(current)) { + return undefined; + } - return { compatible, currentCmp }; -} - -function parseVersion(v: string): Version | undefined { - const [currentStr, revisionStr, ageStr, ...rest] = v.split(":"); - if (rest.length !== 0) { - return undefined; - } - const current = Number.parseInt(currentStr); - const revision = Number.parseInt(revisionStr); - const age = Number.parseInt(ageStr); - - if (Number.isNaN(current)) { - return undefined; - } - - if (Number.isNaN(revision)) { - return undefined; - } - - if (Number.isNaN(age)) { - return undefined; - } - - return { current, revision, age }; + if (Number.isNaN(revision)) { + return undefined; + } + + if (Number.isNaN(age)) { + return undefined; + } + + return { current, revision, age }; + } } diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index b107786cd..c20ce72a6 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -24,7 +24,7 @@ import * as nacl from "./nacl-fast.js"; import { kdf } from "./kdf.js"; import bigint from "big-integer"; -import { DenominationPubKey } from "./talerTypes.js"; +import { DenominationPubKey, DenomKeyType } from "./talerTypes.js"; export function getRandomBytes(n: number): Uint8Array { return nacl.randomBytes(n); @@ -350,7 +350,7 @@ export function hash(d: Uint8Array): Uint8Array { } export function hashDenomPub(pub: DenominationPubKey): Uint8Array { - if (pub.cipher !== 1) { + if (pub.cipher !== DenomKeyType.Rsa) { throw Error("unsupported cipher"); } const pubBuf = decodeCrock(pub.rsa_public_key); diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index 04d700483..bd9c67d7e 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -598,9 +598,9 @@ export interface TipPickupRequest { /** * Reserve signature, defined as separate class to facilitate - * schema validation with "@Checkable". + * schema validation. */ -export interface BlindSigWrapper { +export interface MerchantBlindSigWrapperV1 { /** * Reserve signature. */ @@ -611,11 +611,26 @@ export interface BlindSigWrapper { * Response of the merchant * to the TipPickupRequest. */ -export interface TipResponse { +export interface MerchantTipResponseV1 { /** * The order of the signatures matches the planchets list. */ - blind_sigs: BlindSigWrapper[]; + blind_sigs: MerchantBlindSigWrapperV1[]; +} + +export interface MerchantBlindSigWrapperV2 { + blind_sig: BlindedDenominationSignature; +} + +/** + * Response of the merchant + * to the TipPickupRequest. + */ +export interface MerchantTipResponseV2 { + /** + * The order of the signatures matches the planchets list. + */ + blind_sigs: MerchantBlindSigWrapperV2[]; } /** @@ -1032,13 +1047,14 @@ export interface BankWithdrawalOperationPostResponse { export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey; export interface RsaDenominationPubKey { - cipher: 1; + cipher: DenomKeyType.Rsa; rsa_public_key: string; age_mask?: number; } export interface CsDenominationPubKey { - cipher: 2; + cipher: DenomKeyType.ClauseSchnorr; + // FIXME: finish definition } export const codecForDenominationPubKey = () => @@ -1201,15 +1217,25 @@ export const codecForMerchantRefundResponse = (): Codec .property("refunds", codecForList(codecForMerchantRefundPermission())) .build("MerchantRefundResponse"); -export const codecForBlindSigWrapper = (): Codec => - buildCodecForObject() +export const codecForMerchantBlindSigWrapperV1 = (): Codec => + buildCodecForObject() .property("blind_sig", codecForString()) .build("BlindSigWrapper"); -export const codecForTipResponse = (): Codec => - buildCodecForObject() - .property("blind_sigs", codecForList(codecForBlindSigWrapper())) - .build("TipResponse"); +export const codecForMerchantTipResponseV1 = (): Codec => + buildCodecForObject() + .property("blind_sigs", codecForList(codecForMerchantBlindSigWrapperV1())) + .build("MerchantTipResponseV1"); + +export const codecForBlindSigWrapperV2 = (): Codec => + buildCodecForObject() + .property("blind_sig", codecForBlindedDenominationSignature()) + .build("MerchantBlindSigWrapperV2"); + +export const codecForMerchantTipResponseV2 = (): Codec => + buildCodecForObject() + .property("blind_sigs", codecForList(codecForBlindSigWrapperV2())) + .build("MerchantTipResponseV2"); export const codecForRecoup = (): Codec => buildCodecForObject() @@ -1510,3 +1536,16 @@ export const codecForKeysManagementResponse = (): Codec => .property("denom_secmod_public_key", codecForAny()) .property("signkey_secmod_public_key", codecForAny()) .build("FutureKeysResponse"); + +export interface MerchantConfigResponse { + currency: string; + name: string; + version: string; +} + +export const codecForMerchantConfigResponse = (): Codec => + buildCodecForObject() + .property("currency", codecForString()) + .property("name", codecForString()) + .property("version", codecForString()) + .build("MerchantConfigResponse"); diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts index 4944e3471..9a33d572a 100644 --- a/packages/taler-wallet-cli/src/harness/harness.ts +++ b/packages/taler-wallet-cli/src/harness/harness.ts @@ -66,7 +66,7 @@ import { encodeCrock, getRandomBytes, hash, - stringToBytes + stringToBytes, } from "@gnu-taler/taler-util"; import { CoinConfig } from "./denomStructures.js"; import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; @@ -445,7 +445,7 @@ export async function pingProc( } while (true) { try { - console.log(`pinging ${serviceName}`); + console.log(`pinging ${serviceName} at ${url}`); const resp = await axios.get(url); console.log(`service ${serviceName} available`); return; @@ -556,7 +556,6 @@ export namespace BankApi { debitAccountPayto: string; }, ) { - let maybeBaseUrl = bank.baseUrl; if (process.env.WALLET_HARNESS_WITH_EUFIN) { maybeBaseUrl = (bank as EufinBankService).baseUrlDemobank; @@ -618,7 +617,6 @@ export namespace BankApi { } } - class BankServiceBase { proc: ProcessWrapper | undefined; @@ -641,7 +639,6 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { gc: GlobalTestState, bc: BankConfig, ): Promise { - return new EufinBankService(gc, bc, "foo"); } @@ -650,16 +647,14 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { } get nexusPort() { return this.bankConfig.httpPort + 1000; - } get nexusDbConn(): string { - return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`; + return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`; } get sandboxDbConn(): string { - return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`; - + return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`; } get nexusBaseUrl(): string { @@ -673,9 +668,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { get baseUrlAccessApi(): string { let url = new URL("access-api/", this.baseUrlDemobank); - return url.href; + return url.href; } - + get baseUrlNetloc(): string { return `http://localhost:${this.bankConfig.httpPort}/`; } @@ -686,7 +681,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { async setSuggestedExchange( e: ExchangeServiceInterface, - exchangePayto: string + exchangePayto: string, ) { await sh( this.globalTestState, @@ -712,11 +707,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { */ await this.start(); await this.pingUntilAvailable(); - await LibeufinSandboxApi.createDemobankAccount( - accountName, - password, - { baseUrl: this.baseUrlAccessApi } - ); + await LibeufinSandboxApi.createDemobankAccount(accountName, password, { + baseUrl: this.baseUrlAccessApi, + }); let bankAccountLabel = accountName; await LibeufinSandboxApi.createDemobankEbicsSubscriber( { @@ -725,47 +718,49 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { partnerID: "exchangeEbicsPartner", }, bankAccountLabel, - { baseUrl: this.baseUrlDemobank } + { baseUrl: this.baseUrlDemobank }, ); - + await LibeufinNexusApi.createUser( { baseUrl: this.nexusBaseUrl }, { username: accountName, - password: password - } + password: password, + }, ); await LibeufinNexusApi.createEbicsBankConnection( { baseUrl: this.nexusBaseUrl }, { name: "ebics-connection", // connection name. - ebicsURL: (new URL("ebicsweb", this.baseUrlNetloc)).href, + ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href, hostID: "talertestEbicsHost", userID: "exchangeEbicsUser", partnerID: "exchangeEbicsPartner", - } + }, ); await LibeufinNexusApi.connectBankConnection( - { baseUrl: this.nexusBaseUrl }, "ebics-connection" + { baseUrl: this.nexusBaseUrl }, + "ebics-connection", ); await LibeufinNexusApi.fetchAccounts( - { baseUrl: this.nexusBaseUrl }, "ebics-connection" + { baseUrl: this.nexusBaseUrl }, + "ebics-connection", ); await LibeufinNexusApi.importConnectionAccount( { baseUrl: this.nexusBaseUrl }, "ebics-connection", // connection name accountName, // offered account label - `${accountName}-nexus-label` // bank account label at Nexus + `${accountName}-nexus-label`, // bank account label at Nexus ); await LibeufinNexusApi.createTwgFacade( { baseUrl: this.nexusBaseUrl }, { name: "exchange-facade", - connectionName: "ebics-connection", + connectionName: "ebics-connection", accountName: `${accountName}-nexus-label`, - currency: "EUR", - reserveTransferLevel: "report" - } + currency: "EUR", + reserveTransferLevel: "report", + }, ); await LibeufinNexusApi.postPermission( { baseUrl: this.nexusBaseUrl }, @@ -778,7 +773,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { resourceId: "exchange-facade", // facade name permissionName: "facade.talerWireGateway.transfer", }, - } + }, ); await LibeufinNexusApi.postPermission( { baseUrl: this.nexusBaseUrl }, @@ -791,7 +786,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { resourceId: "exchange-facade", // facade name permissionName: "facade.talerWireGateway.history", }, - } + }, ); // Set fetch task. await LibeufinNexusApi.postTask( @@ -804,8 +799,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { params: { level: "all", rangeType: "all", + }, }, - }); + ); await LibeufinNexusApi.postTask( { baseUrl: this.nexusBaseUrl }, `${accountName}-nexus-label`, @@ -814,14 +810,16 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { cronspec: "* * *", type: "submit", params: {}, - } + }, ); - let facadesResp = await LibeufinNexusApi.getAllFacades({ baseUrl: this.nexusBaseUrl }); + let facadesResp = await LibeufinNexusApi.getAllFacades({ + baseUrl: this.nexusBaseUrl, + }); let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo( "admin", "secret", { baseUrl: this.baseUrlAccessApi }, - accountName // bank account label. + accountName, // bank account label. ); return { accountName: accountName, @@ -840,7 +838,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { * them if they weren't launched earlier. */ - // Only go ahead if BOTH aren't running. + // Only go ahead if BOTH aren't running. if (this.sandboxProc || this.nexusProc) { console.log("Nexus or Sandbox already running, not taking any action."); return; @@ -864,7 +862,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", }, - ); + ); await runCommand( this.globalTestState, "libeufin-nexus-superuser", @@ -889,7 +887,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { await this.pingUntilAvailable(); LibeufinSandboxApi.createEbicsHost( { baseUrl: this.baseUrlNetloc }, - "talertestEbicsHost" + "talertestEbicsHost", ); } @@ -897,12 +895,12 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface { await pingProc( this.sandboxProc, `http://localhost:${this.bankConfig.httpPort}`, - "libeufin-sandbox" + "libeufin-sandbox", ); await pingProc( this.nexusProc, `${this.nexusBaseUrl}/config`, - "libeufin-nexus" + "libeufin-nexus", ); } } @@ -999,7 +997,6 @@ class PybankService extends BankServiceBase implements BankServiceInterface { } } - /** * Return a euFin or a pyBank implementation of * the exported BankService class. This allows @@ -1007,19 +1004,18 @@ class PybankService extends BankServiceBase implements BankServiceInterface { * on a particular env variable. */ function getBankServiceImpl(): { - prototype: typeof PybankService.prototype, - create: typeof PybankService.create + prototype: typeof PybankService.prototype; + create: typeof PybankService.create; } { - - if (process.env.WALLET_HARNESS_WITH_EUFIN) + if (process.env.WALLET_HARNESS_WITH_EUFIN) return { prototype: EufinBankService.prototype, - create: EufinBankService.create - } + create: EufinBankService.create, + }; return { prototype: PybankService.prototype, - create: PybankService.create - } + create: PybankService.create, + }; } export type BankService = PybankService; @@ -2088,10 +2084,8 @@ export class WalletCli { } export function getRandomIban(salt: string | null = null): string { - function getBban(salt: string | null): string { - if (!salt) - return Math.random().toString().substring(2, 6); + if (!salt) return Math.random().toString().substring(2, 6); let hashed = hash(stringToBytes(salt)); let ret = ""; for (let i = 0; i < hashed.length; i++) { @@ -2101,19 +2095,21 @@ export function getRandomIban(salt: string | null = null): string { } let cc_no_check = "131400"; // == DE00 - let bban = getBban(salt) - let check_digits = (98 - (Number.parseInt(`${bban}${cc_no_check}`) % 97)).toString(); + let bban = getBban(salt); + let check_digits = ( + 98 - + (Number.parseInt(`${bban}${cc_no_check}`) % 97) + ).toString(); if (check_digits.length == 1) { check_digits = `0${check_digits}`; } - return `DE${check_digits}${bban}`; + return `DE${check_digits}${bban}`; } // Only used in one tipping test. export function getWireMethod(): string { - if (process.env.WALLET_HARNESS_WITH_EUFIN) - return "iban" - return "x-taler-bank" + if (process.env.WALLET_HARNESS_WITH_EUFIN) return "iban"; + return "x-taler-bank"; } /** @@ -2122,10 +2118,12 @@ export function getWireMethod(): string { */ export function getPayto(label: string): string { if (process.env.WALLET_HARNESS_WITH_EUFIN) - return `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}` - return `payto://x-taler-bank/${label}` + return `payto://iban/SANDBOXX/${getRandomIban( + label, + )}?receiver-name=${label}`; + return `payto://x-taler-bank/${label}`; } function waitMs(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts index dd8542def..81c43cf14 100644 --- a/packages/taler-wallet-core/src/common.ts +++ b/packages/taler-wallet-core/src/common.ts @@ -51,6 +51,21 @@ export interface TrustInfo { isAudited: boolean; } +export interface MerchantInfo { + supportsMerchantProtocolV1: boolean; + supportsMerchantProtocolV2: boolean; +} + +/** + * Interface for merchant-related operations. + */ +export interface MerchantOperations { + getMerchantInfo( + ws: InternalWalletState, + merchantBaseUrl: string, + ): Promise; +} + /** * Interface for exchange-related operations. */ @@ -131,8 +146,11 @@ export interface InternalWalletState { initCalled: boolean; + merchantInfoCache: Record; + exchangeOps: ExchangeOperations; recoupOps: RecoupOperations; + merchantOps: MerchantOperations; db: DbAccess; http: HttpRequestLibrary; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 483cb16c2..ff47cf30d 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -484,8 +484,14 @@ export interface WireInfo { export interface ExchangeDetailsPointer { masterPublicKey: string; + currency: string; + /** + * Last observed protocol version range offered by the exchange. + */ + protocolVersionRange: string; + /** * Timestamp when the (masterPublicKey, currency) pointer * has been updated. diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index a66bc2e84..75724dca7 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -273,6 +273,7 @@ export async function exportBackup( currency: dp.currency, master_public_key: dp.masterPublicKey, update_clock: dp.updateClock, + protocol_version_range: dp.protocolVersionRange, }); }); diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index e8e1de0b9..40fa4cdec 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -267,6 +267,7 @@ export async function importBackup( currency: backupExchange.currency, masterPublicKey: backupExchange.master_public_key, updateClock: backupExchange.update_clock, + protocolVersionRange: backupExchange.protocol_version_range, }, permanent: true, retryInfo: initRetryInfo(), diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index c170c5469..638af813a 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -23,7 +23,6 @@ import { canonicalizeBaseUrl, codecForExchangeKeysJson, codecForExchangeWireJson, - compare, Denomination, Duration, durationFromSpec, @@ -40,6 +39,7 @@ import { TalerErrorDetails, Timestamp, hashDenomPub, + LibtoolVersion, } from "@gnu-taler/taler-util"; import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util"; import { CryptoApi } from "../crypto/workers/cryptoApi.js"; @@ -365,7 +365,10 @@ async function downloadKeysInfo( const protocolVersion = exchangeKeysJson.version; - const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion); + const versionRes = LibtoolVersion.compare( + WALLET_EXCHANGE_PROTOCOL_VERSION, + protocolVersion, + ); if (versionRes?.compatible != true) { const opErr = makeErrorDetails( TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, @@ -548,6 +551,7 @@ async function updateExchangeFromUrlImpl( masterPublicKey: details.masterPublicKey, // FIXME: only change if pointer really changed updateClock: getTimestampNow(), + protocolVersionRange: keysInfo.protocolVersion, }; await tx.exchanges.put(r); await tx.exchangeDetails.put(details); diff --git a/packages/taler-wallet-core/src/operations/merchants.ts b/packages/taler-wallet-core/src/operations/merchants.ts new file mode 100644 index 000000000..d12417c7c --- /dev/null +++ b/packages/taler-wallet-core/src/operations/merchants.ts @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A.. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + canonicalizeBaseUrl, + Logger, + URL, + codecForMerchantConfigResponse, + LibtoolVersion, +} from "@gnu-taler/taler-util"; +import { InternalWalletState, MerchantInfo } from "../common.js"; +import { readSuccessResponseJsonOrThrow } from "../index.js"; + +const logger = new Logger("taler-wallet-core:merchants.ts"); + +export async function getMerchantInfo( + ws: InternalWalletState, + merchantBaseUrl: string, +): Promise { + const canonBaseUrl = canonicalizeBaseUrl(merchantBaseUrl); + + const existingInfo = ws.merchantInfoCache[canonBaseUrl]; + if (existingInfo) { + return existingInfo; + } + + const configUrl = new URL("config", canonBaseUrl); + const resp = await ws.http.get(configUrl.href); + + const configResp = await readSuccessResponseJsonOrThrow( + resp, + codecForMerchantConfigResponse(), + ); + + logger.info( + `merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`, + ); + + const merchantInfo: MerchantInfo = { + supportsMerchantProtocolV1: !!LibtoolVersion.compare( + "1:0:0", + configResp.version, + )?.compatible, + supportsMerchantProtocolV2: !!LibtoolVersion.compare( + "2:0:0", + configResp.version, + )?.compatible, + }; + + ws.merchantInfoCache[canonBaseUrl] = merchantInfo; + return merchantInfo; +} diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 07ce00d2e..0253930ea 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -27,10 +27,12 @@ import { NotificationType, TipPlanchetDetail, TalerErrorCode, - codecForTipResponse, + codecForMerchantTipResponseV1, Logger, URL, DenomKeyType, + BlindedDenominationSignature, + codecForMerchantTipResponseV2, } from "@gnu-taler/taler-util"; import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js"; import { @@ -304,31 +306,57 @@ async function processTipImpl( return; } - const response = await readSuccessResponseJsonOrThrow( - merchantResp, - codecForTipResponse(), + // FIXME: Do this earlier? + const merchantInfo = await ws.merchantOps.getMerchantInfo( + ws, + tipRecord.merchantBaseUrl, ); - if (response.blind_sigs.length !== planchets.length) { + let blindedSigs: BlindedDenominationSignature[] = []; + + if (merchantInfo.supportsMerchantProtocolV2) { + const response = await readSuccessResponseJsonOrThrow( + merchantResp, + codecForMerchantTipResponseV2(), + ); + blindedSigs = response.blind_sigs.map((x) => x.blind_sig); + } else if (merchantInfo.supportsMerchantProtocolV1) { + const response = await readSuccessResponseJsonOrThrow( + merchantResp, + codecForMerchantTipResponseV1(), + ); + blindedSigs = response.blind_sigs.map((x) => ({ + cipher: DenomKeyType.Rsa, + blinded_rsa_signature: x.blind_sig, + })); + } else { + throw Error("unsupported merchant protocol version"); + } + + if (blindedSigs.length !== planchets.length) { throw Error("number of tip responses does not match requested planchets"); } const newCoinRecords: CoinRecord[] = []; - for (let i = 0; i < response.blind_sigs.length; i++) { - const blindedSig = response.blind_sigs[i].blind_sig; + for (let i = 0; i < blindedSigs.length; i++) { + const blindedSig = blindedSigs[i]; const denom = denomForPlanchet[i]; checkLogicInvariant(!!denom); const planchet = planchets[i]; checkLogicInvariant(!!planchet); - if (denom.denomPub.cipher !== 1) { + if (denom.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error("unsupported cipher"); + } + + if (blindedSig.cipher !== DenomKeyType.Rsa) { throw Error("unsupported cipher"); } const denomSigRsa = await ws.cryptoApi.rsaUnblind( - blindedSig, + blindedSig.blinded_rsa_signature, planchet.blindingKey, denom.denomPub.rsa_public_key, ); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 57bd49d23..a5a8653c6 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -24,7 +24,6 @@ import { codecForTalerConfigResponse, codecForWithdrawOperationStatusResponse, codecForWithdrawResponse, - compare, durationFromSpec, ExchangeListItem, getDurationRemaining, @@ -42,6 +41,7 @@ import { WithdrawUriInfoResponse, VersionMatchResult, DenomKeyType, + LibtoolVersion, } from "@gnu-taler/taler-util"; import { CoinRecord, @@ -285,7 +285,7 @@ export async function getBankWithdrawalInfo( codecForTalerConfigResponse(), ); - const versionRes = compare( + const versionRes = LibtoolVersion.compare( WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, config.version, ); @@ -985,7 +985,7 @@ export async function getExchangeWithdrawalInfo( let versionMatch; if (exchangeDetails.protocolVersion) { - versionMatch = compare( + versionMatch = LibtoolVersion.compare( WALLET_EXCHANGE_PROTOCOL_VERSION, exchangeDetails.protocolVersion, ); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index cd2dd7f1e..44591a268 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -99,6 +99,8 @@ import { import { ExchangeOperations, InternalWalletState, + MerchantInfo, + MerchantOperations, NotificationListener, RecoupOperations, } from "./common.js"; @@ -180,6 +182,7 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "./util/http.js"; +import { getMerchantInfo } from "./operations/merchants.js"; const builtinAuditors: AuditorTrustRecord[] = [ { @@ -1069,6 +1072,8 @@ class InternalWalletStateImpl implements InternalWalletState { memoProcessDeposit: AsyncOpMemoMap = new AsyncOpMemoMap(); cryptoApi: CryptoApi; + merchantInfoCache: Record = {}; + timerGroup: TimerGroup = new TimerGroup(); latch = new AsyncCondition(); stopped = false; @@ -1088,6 +1093,10 @@ class InternalWalletStateImpl implements InternalWalletState { processRecoupGroup: processRecoupGroup, }; + merchantOps: MerchantOperations = { + getMerchantInfo: getMerchantInfo, + }; + /** * Promises that are waiting for a particular resource. */