diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/taler-util/src/backupTypes.ts | 21 | ||||
| -rw-r--r-- | packages/taler-util/src/payto.ts | 8 | ||||
| -rw-r--r-- | packages/taler-util/src/talerCrypto.ts | 1 | ||||
| -rw-r--r-- | packages/taler-util/src/talerTypes.ts | 139 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 62 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/db.ts | 5 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/backup/export.ts | 1 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/backup/import.ts | 1 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/operations/exchanges.ts | 36 | 
9 files changed, 233 insertions, 41 deletions
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts index 620f476ad..8222bdeab 100644 --- a/packages/taler-util/src/backupTypes.ts +++ b/packages/taler-util/src/backupTypes.ts @@ -909,7 +909,7 @@ export interface BackupPurchase {    /**     * Signature on the contract terms. -   *  +   *     * FIXME: Better name needed.     */    merchant_sig?: string; @@ -1087,6 +1087,23 @@ export interface BackupExchangeWireFee {  }  /** + * Global fee as stored in the wallet's database. + * + */ +export interface BackupExchangeGlobalFees { +  start_date: TalerProtocolTimestamp; +  end_date: TalerProtocolTimestamp; +  kyc_fee: BackupAmountString; +  history_fee: BackupAmountString; +  account_fee: BackupAmountString; +  purse_fee: BackupAmountString; +  history_expiration: TalerProtocolDuration; +  account_kyc_timeout: TalerProtocolDuration; +  purse_account_limit: number; +  purse_timeout: TalerProtocolDuration; +  master_sig: string; +} +/**   * Structure of one exchange signing key in the /keys response.   */  export class BackupExchangeSignKey { @@ -1206,6 +1223,8 @@ export interface BackupExchangeDetails {    wire_fees: BackupExchangeWireFee[]; +  global_fees: BackupExchangeGlobalFees[]; +    /**     * Bank accounts offered by the exchange;     */ diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index c5a58022d..b474e533c 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -63,9 +63,9 @@ export function addPaytoQueryParams(  ): string {    const [acct, search] = s.slice(paytoPfx.length).split("?");    const searchParams = new URLSearchParams(search || ""); -  const keys = Object.keys(params) +  const keys = Object.keys(params);    if (keys.length === 0) { -    return paytoPfx + acct +    return paytoPfx + acct;    }    for (const k of keys) {      searchParams.set(k, params[k]); @@ -83,9 +83,7 @@ export function stringifyPaytoUri(p: PaytoUri): string {    const url = `${paytoPfx}${p.targetType}/${p.targetPath}`;    const paramList = !p.params ? [] : Object.entries(p.params);    if (paramList.length > 0) { -    const search = paramList -      .map(([key, value]) => `${key}=${value}`) -      .join("&"); +    const search = paramList.map(([key, value]) => `${key}=${value}`).join("&");      return `${url}?${search}`;    }    return url; diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index 28fdab8e3..84842a69f 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -793,6 +793,7 @@ export enum TalerSignaturePurpose {    MERCHANT_TRACK_TRANSACTION = 1103,    WALLET_RESERVE_WITHDRAW = 1200,    WALLET_COIN_DEPOSIT = 1201, +  GLOBAL_FEES = 1022,    MASTER_DENOMINATION_KEY_VALIDITY = 1025,    MASTER_WIRE_FEES = 1028,    MASTER_WIRE_DETAILS = 1030, diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index 471c7e927..1cb4e2bde 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -43,6 +43,7 @@ import {  import { strcmp } from "./helpers.js";  import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./talerCrypto.js";  import { +  codecForAbsoluteTime,    codecForDuration,    codecForTimestamp,    TalerProtocolDuration, @@ -757,8 +758,64 @@ export class ExchangeKeysJson {    version: string;    reserve_closing_delay: TalerProtocolDuration; + +  global_fees: GlobalFees[];  } +export interface GlobalFees { +  // What date (inclusive) does these fees go into effect? +  start_date: TalerProtocolTimestamp; + +  // What date (exclusive) does this fees stop going into effect? +  end_date: TalerProtocolTimestamp; + +  // KYC fee, charged when a user wants to create an account. +  // The first year of the account_annual_fee after the KYC is +  // always included. +  kyc_fee: AmountString; + +  // Account history fee, charged when a user wants to +  // obtain a reserve/account history. +  history_fee: AmountString; + +  // Annual fee charged for having an open account at the +  // exchange.  Charged to the account.  If the account +  // balance is insufficient to cover this fee, the account +  // is automatically deleted/closed. (Note that the exchange +  // will keep the account history around for longer for +  // regulatory reasons.) +  account_fee: AmountString; + +  // Purse fee, charged only if a purse is abandoned +  // and was not covered by the account limit. +  purse_fee: AmountString; + +  // How long will the exchange preserve the account history? +  // After an account was deleted/closed, the exchange will +  // retain the account history for legal reasons until this time. +  history_expiration: TalerProtocolDuration; + +  // How long does the exchange promise to keep funds +  // an account for which the KYC has never happened +  // after a purse was merged into an account? Basically, +  // after this time funds in an account without KYC are +  // forfeit. +  account_kyc_timeout: TalerProtocolDuration; + +  // Non-negative number of concurrent purses that any +  // account holder is allowed to create without having +  // to pay the purse_fee. +  purse_account_limit: number; + +  // How long does an exchange keep a purse around after a purse +  // has expired (or been successfully merged)?  A 'GET' request +  // for a purse will succeed until the purse expiration time +  // plus this value. +  purse_timeout: TalerProtocolDuration; + +  // Signature of TALER_GlobalFeesPS. +  master_sig: string; +}  /**   * Wire fees as announced by the exchange.   */ @@ -1188,13 +1245,6 @@ export namespace DenominationPubKey {    }  } -export const codecForDenominationPubKey = () => -  buildCodecForUnion<DenominationPubKey>() -    .discriminateOn("cipher") -    .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey()) -    .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) -    .build("DenominationPubKey"); -  export const codecForRsaDenominationPubKey = () =>    buildCodecForObject<RsaDenominationPubKey>()      .property("cipher", codecForConstString(DenomKeyType.Rsa)) @@ -1209,6 +1259,13 @@ export const codecForCsDenominationPubKey = () =>      .property("age_mask", codecForNumber())      .build("CsDenominationPubKey"); +export const codecForDenominationPubKey = () => +  buildCodecForUnion<DenominationPubKey>() +    .discriminateOn("cipher") +    .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey()) +    .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) +    .build("DenominationPubKey"); +  export const codecForBankWithdrawalOperationPostResponse =    (): Codec<BankWithdrawalOperationPostResponse> =>      buildCodecForObject<BankWithdrawalOperationPostResponse>() @@ -1385,6 +1442,21 @@ export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>      .property("stamp_expire", codecForTimestamp)      .build("ExchangeSignKeyJson"); +export const codecForGlobalFees = (): Codec<GlobalFees> => +  buildCodecForObject<GlobalFees>() +    .property("start_date", codecForTimestamp) +    .property("end_date", codecForTimestamp) +    .property("kyc_fee", codecForAmountString()) +    .property("history_fee", codecForAmountString()) +    .property("account_fee", codecForAmountString()) +    .property("purse_fee", codecForAmountString()) +    .property("history_expiration", codecForDuration) +    .property("account_kyc_timeout", codecForDuration) +    .property("purse_account_limit", codecForNumber()) +    .property("purse_timeout", codecForDuration) +    .property("master_sig", codecForString()) +    .build("GlobalFees"); +  export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>    buildCodecForObject<ExchangeKeysJson>()      .property("denoms", codecForList(codecForDenomination())) @@ -1395,6 +1467,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>      .property("signkeys", codecForList(codecForExchangeSigningKey()))      .property("version", codecForString())      .property("reserve_closing_delay", codecForDuration) +    .property("global_fees", codecForList(codecForGlobalFees()))      .build("ExchangeKeysJson");  export const codecForWireFeesJson = (): Codec<WireFeesJson> => @@ -1583,6 +1656,32 @@ export interface AbortResponse {    refunds: MerchantAbortPayRefundStatus[];  } +export const codecForMerchantAbortPayRefundSuccessStatus = +  (): Codec<MerchantAbortPayRefundSuccessStatus> => +    buildCodecForObject<MerchantAbortPayRefundSuccessStatus>() +      .property("exchange_pub", codecForString()) +      .property("exchange_sig", codecForString()) +      .property("exchange_status", codecForConstNumber(200)) +      .property("type", codecForConstString("success")) +      .build("MerchantAbortPayRefundSuccessStatus"); + +export const codecForMerchantAbortPayRefundFailureStatus = +  (): Codec<MerchantAbortPayRefundFailureStatus> => +    buildCodecForObject<MerchantAbortPayRefundFailureStatus>() +      .property("exchange_code", codecForNumber()) +      .property("exchange_reply", codecForAny()) +      .property("exchange_status", codecForNumber()) +      .property("type", codecForConstString("failure")) +      .build("MerchantAbortPayRefundFailureStatus"); + +export const codecForMerchantAbortPayRefundStatus = +  (): Codec<MerchantAbortPayRefundStatus> => +    buildCodecForUnion<MerchantAbortPayRefundStatus>() +      .discriminateOn("type") +      .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) +      .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) +      .build("MerchantAbortPayRefundStatus"); +  export const codecForAbortResponse = (): Codec<AbortResponse> =>    buildCodecForObject<AbortResponse>()      .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) @@ -1629,32 +1728,6 @@ export interface MerchantAbortPayRefundSuccessStatus {    exchange_pub: string;  } -export const codecForMerchantAbortPayRefundSuccessStatus = -  (): Codec<MerchantAbortPayRefundSuccessStatus> => -    buildCodecForObject<MerchantAbortPayRefundSuccessStatus>() -      .property("exchange_pub", codecForString()) -      .property("exchange_sig", codecForString()) -      .property("exchange_status", codecForConstNumber(200)) -      .property("type", codecForConstString("success")) -      .build("MerchantAbortPayRefundSuccessStatus"); - -export const codecForMerchantAbortPayRefundFailureStatus = -  (): Codec<MerchantAbortPayRefundFailureStatus> => -    buildCodecForObject<MerchantAbortPayRefundFailureStatus>() -      .property("exchange_code", codecForNumber()) -      .property("exchange_reply", codecForAny()) -      .property("exchange_status", codecForNumber()) -      .property("type", codecForConstString("failure")) -      .build("MerchantAbortPayRefundFailureStatus"); - -export const codecForMerchantAbortPayRefundStatus = -  (): Codec<MerchantAbortPayRefundStatus> => -    buildCodecForUnion<MerchantAbortPayRefundStatus>() -      .discriminateOn("type") -      .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) -      .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) -      .build("MerchantAbortPayRefundStatus"); -  export interface TalerConfigResponse {    name: string;    version: string; diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index bfc48d961..98bb6c9cb 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -51,6 +51,7 @@ import {    encryptContractForMerge,    ExchangeProtocolVersion,    getRandomBytes, +  GlobalFees,    hash,    HashCodeString,    hashCoinEv, @@ -74,6 +75,7 @@ import {    rsaVerify,    setupTipPlanchet,    stringToBytes, +  TalerProtocolDuration,    TalerProtocolTimestamp,    TalerSignaturePurpose,    UnblindedSignature, @@ -142,6 +144,10 @@ export interface TalerCryptoInterface {    isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>; +  isValidGlobalFees( +    req: GlobalFeesValidationRequest, +  ): Promise<ValidationResult>; +    isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>;    isValidWireAccount( @@ -152,7 +158,7 @@ export interface TalerCryptoInterface {      req: ContractTermsValidationRequest,    ): Promise<ValidationResult>; -  createEddsaKeypair(req: {}): Promise<EddsaKeypair>; +  createEddsaKeypair(req: unknown): Promise<EddsaKeypair>;    eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>; @@ -283,12 +289,17 @@ export const nullCrypto: TalerCryptoInterface = {    ): Promise<ValidationResult> {      throw new Error("Function not implemented.");    }, +  isValidGlobalFees: function ( +    req: GlobalFeesValidationRequest, +  ): Promise<ValidationResult> { +    throw new Error("Function not implemented."); +  },    isValidContractTermsSignature: function (      req: ContractTermsValidationRequest,    ): Promise<ValidationResult> {      throw new Error("Function not implemented.");    }, -  createEddsaKeypair: function (req: {}): Promise<EddsaKeypair> { +  createEddsaKeypair: function (req: unknown): Promise<EddsaKeypair> {      throw new Error("Function not implemented.");    },    eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> { @@ -484,6 +495,11 @@ export interface WireFeeValidationRequest {    masterPub: string;  } +export interface GlobalFeesValidationRequest { +  gf: GlobalFees; +  masterPub: string; +} +  export interface DenominationValidationRequest {    denom: DenominationRecord;    masterPub: string; @@ -888,6 +904,30 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {    },    /** +   * Check if a global fee is correctly signed. +   */ +  async isValidGlobalFees( +    tci: TalerCryptoInterfaceR, +    req: GlobalFeesValidationRequest, +  ): Promise<ValidationResult> { +    const { gf, masterPub } = req; +    const p = buildSigPS(TalerSignaturePurpose.GLOBAL_FEES) +      .put(timestampRoundedToBuffer(gf.start_date)) +      .put(timestampRoundedToBuffer(gf.end_date)) +      .put(durationRoundedToBuffer(gf.purse_timeout)) +      .put(durationRoundedToBuffer(gf.account_kyc_timeout)) +      .put(durationRoundedToBuffer(gf.history_expiration)) +      .put(amountToBuffer(Amounts.parseOrThrow(gf.history_fee))) +      .put(amountToBuffer(Amounts.parseOrThrow(gf.kyc_fee))) +      .put(amountToBuffer(Amounts.parseOrThrow(gf.account_fee))) +      .put(amountToBuffer(Amounts.parseOrThrow(gf.purse_fee))) +      .put(bufferForUint32(gf.purse_account_limit)) +      .build(); +    const sig = decodeCrock(gf.master_sig); +    const pub = decodeCrock(masterPub); +    return { valid: eddsaVerify(p, sig, pub) }; +  }, +  /**     * Check if the signature of a denomination is valid.     */    async isValidDenom( @@ -1630,6 +1670,24 @@ function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {    return new Uint8Array(b);  } +function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array { +  const b = new ArrayBuffer(8); +  const v = new DataView(b); +  // The buffer we sign over represents the timestamp in microseconds. +  if (typeof v.setBigUint64 !== "undefined") { +    const s = BigInt(ts.d_us); +    v.setBigUint64(0, s); +  } else { +    const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us); +    const arr = s.toArray(2 ** 8).value; +    let offset = 8 - arr.length; +    for (let i = 0; i < arr.length; i++) { +      v.setUint8(offset++, arr[i]); +    } +  } +  return new Uint8Array(b); +} +  export interface EddsaSignRequest {    msg: string;    priv: string; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ec11f4d47..e266275c1 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -45,6 +45,7 @@ import {    Location,    WireInfo,    DenominationInfo, +  GlobalFees,  } from "@gnu-taler/taler-util";  import { RetryInfo, RetryTags } from "./util/retries.js";  import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; @@ -425,6 +426,10 @@ export interface ExchangeDetailsRecord {    reserveClosingDelay: TalerProtocolDuration;    /** +   * Fees for exchange services +   */ +  globalFees: GlobalFees[]; +  /**     * Signing keys we got from the exchange, can also contain     * older signing keys that are not returned by /keys anymore.     * diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 2e2a1c4b4..f611a2380 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -345,6 +345,7 @@ export async function exportBackup(              stamp_expire: x.stamp_expire,              stamp_start: x.stamp_start,            })), +          global_fees: ex.globalFees,            tos_accepted_etag: ex.termsOfServiceAcceptedEtag,            tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,            denominations: diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 3ee3680fe..ee8cb6f6c 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -405,6 +405,7 @@ export async function importBackup(              masterPublicKey: backupExchangeDetails.master_public_key,              protocolVersion: backupExchangeDetails.protocol_version,              reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, +            globalFees: backupExchangeDetails.global_fees,              signingKeys: backupExchangeDetails.signing_keys.map((x) => ({                key: x.key,                master_sig: x.master_sig, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 9a6c72577..a26c14fcc 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import {    ExchangeDenomination,    ExchangeSignKeyJson,    ExchangeWireJson, +  GlobalFees,    hashDenomPub,    j2s,    LibtoolVersion, @@ -269,6 +270,32 @@ async function validateWireInfo(    };  } +async function validateGlobalFees( +  ws: InternalWalletState, +  fees: GlobalFees[], +  masterPub: string, +): Promise<GlobalFees[]> { +  for (const gf of fees) { +    logger.trace("validating exchange global fees"); +    let isValid = false; +    if (ws.insecureTrustExchange) { +      isValid = true; +    } else { +      const { valid: v } = await ws.cryptoApi.isValidGlobalFees({ +        masterPub, +        gf, +      }); +      isValid = v; +    } + +    if (!isValid) { +      throw Error("exchange global fees signature invalid: " + gf.master_sig); +    } +  } + +  return fees; +} +  export interface ExchangeInfo {    wire: ExchangeWireJson;    keys: ExchangeKeysDownloadResult; @@ -359,6 +386,7 @@ interface ExchangeKeysDownloadResult {    expiry: TalerProtocolTimestamp;    recoup: Recoup[];    listIssueDate: TalerProtocolTimestamp; +  globalFees: GlobalFees[];  }  /** @@ -432,6 +460,7 @@ async function downloadExchangeKeysInfo(      ),      recoup: exchangeKeysJsonUnchecked.recoup ?? [],      listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, +    globalFees: exchangeKeysJsonUnchecked.global_fees,    };  } @@ -552,6 +581,12 @@ export async function updateExchangeFromUrlHandler(      keysInfo.masterPublicKey,    ); +  const globalFees = await validateGlobalFees( +    ws, +    keysInfo.globalFees, +    keysInfo.masterPublicKey, +  ); +    logger.info("finished validating exchange /wire info");    const tosDownload = await downloadTosFromAcceptedFormat( @@ -594,6 +629,7 @@ export async function updateExchangeFromUrlHandler(          protocolVersion: keysInfo.protocolVersion,          signingKeys: keysInfo.signingKeys,          reserveClosingDelay: keysInfo.reserveClosingDelay, +        globalFees,          exchangeBaseUrl: r.baseUrl,          wireInfo,          termsOfServiceText: tosDownload.tosText,  | 
