summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/backupTypes.ts21
-rw-r--r--packages/taler-util/src/payto.ts8
-rw-r--r--packages/taler-util/src/talerCrypto.ts1
-rw-r--r--packages/taler-util/src/talerTypes.ts139
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts62
-rw-r--r--packages/taler-wallet-core/src/db.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts36
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,