aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-11-27 20:56:58 +0100
committerFlorian Dold <florian@dold.me>2021-11-27 20:57:07 +0100
commit5c4c25516df9d65d29dc7f3f38b5a2a1a8e9e374 (patch)
tree4665e79a6033ab949de211fd9de8de8ca681c2e0
parent403de8170ef538ef74505859b1c04e3542cad9fb (diff)
wallet: support both protocol versions
-rw-r--r--packages/taler-util/src/codec.ts23
-rw-r--r--packages/taler-util/src/libtool-version.ts7
-rw-r--r--packages/taler-util/src/logging.ts3
-rw-r--r--packages/taler-util/src/talerCrypto.ts27
-rw-r--r--packages/taler-util/src/talerTypes.ts111
-rw-r--r--packages/taler-util/src/walletTypes.ts2
-rw-r--r--packages/taler-wallet-cli/src/harness/harness.ts24
-rw-r--r--packages/taler-wallet-cli/src/index.ts7
-rw-r--r--packages/taler-wallet-core/src/common.ts5
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts2
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts245
-rw-r--r--packages/taler-wallet-core/src/db.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts30
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts74
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts62
-rw-r--r--packages/taler-wallet-core/src/operations/merchants.ts14
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts70
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts48
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts14
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts43
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts22
-rw-r--r--packages/taler-wallet-core/src/versions.ts6
-rw-r--r--packages/taler-wallet-core/src/wallet.ts4
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,