diff options
Diffstat (limited to 'packages')
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, | 
