wallet: support both protocol versions
This commit is contained in:
parent
403de8170e
commit
5c4c25516d
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -349,10 +349,12 @@ 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);
|
||||
@ -361,6 +363,11 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
|
||||
|
@ -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.
|
||||
*/
|
||||
@ -358,6 +380,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.
|
||||
*/
|
||||
@ -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> =>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1173,6 +1173,17 @@ export class ExchangeService implements ExchangeServiceInterface {
|
||||
}
|
||||
|
||||
async runAggregatorOnce() {
|
||||
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`,
|
||||
@ -1180,6 +1191,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
||||
[...this.timetravelArgArr, "-c", this.configFilename, "-t"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async runTransferOnce() {
|
||||
await runCommand(
|
||||
|
@ -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) => {
|
||||
|
@ -52,8 +52,7 @@ export interface TrustInfo {
|
||||
}
|
||||
|
||||
export interface MerchantInfo {
|
||||
supportsMerchantProtocolV1: boolean;
|
||||
supportsMerchantProtocolV2: boolean;
|
||||
protocolVersionCurrent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -154,9 +154,10 @@ 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);
|
||||
@ -188,7 +189,7 @@ export class CryptoImplementation {
|
||||
coinPub: encodeCrock(derivedPlanchet.coinPub),
|
||||
coinValue: req.value,
|
||||
denomPub: {
|
||||
cipher: 1,
|
||||
cipher: req.denomPub.cipher,
|
||||
rsa_public_key: encodeCrock(denomPubRsa),
|
||||
},
|
||||
denomPubHash: encodeCrock(denomPubHash),
|
||||
@ -197,13 +198,19 @@ export class CryptoImplementation {
|
||||
coinEvHash: encodeCrock(evHash),
|
||||
};
|
||||
return planchet;
|
||||
} else {
|
||||
throw Error("unsupported cipher, unable to create 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,6 +250,19 @@ export class CryptoImplementation {
|
||||
|
||||
const coinPriv = decodeCrock(coin.coinPriv);
|
||||
const coinSig = eddsaSign(p, coinPriv);
|
||||
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,
|
||||
@ -253,6 +273,7 @@ export class CryptoImplementation {
|
||||
};
|
||||
return paybackRequest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a payment signature is valid.
|
||||
@ -326,15 +347,31 @@ export class CryptoImplementation {
|
||||
}
|
||||
|
||||
isValidWireAccount(
|
||||
versionCurrent: number,
|
||||
paytoUri: string,
|
||||
sig: string,
|
||||
masterPub: string,
|
||||
): boolean {
|
||||
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,7 +430,10 @@ 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)
|
||||
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))
|
||||
@ -404,8 +444,25 @@ export class CryptoImplementation {
|
||||
.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));
|
||||
|
||||
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
|
||||
const s: CoinDepositPermission = {
|
||||
coin_pub: depositInfo.coinPub,
|
||||
coin_sig: encodeCrock(coinSig),
|
||||
@ -418,6 +475,19 @@ export class CryptoImplementation {
|
||||
},
|
||||
};
|
||||
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,12 +536,14 @@ 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(decodeCrock(meltCoinPub));
|
||||
sessionHc.update(amountToBuffer(valueWithFee));
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
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(
|
||||
|
@ -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,8 +204,34 @@ async function processDepositGroupImpl(
|
||||
continue;
|
||||
}
|
||||
const perm = depositPermissions[i];
|
||||
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
|
||||
const httpResp = await ws.http.postJson(url.href, {
|
||||
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,
|
||||
@ -225,7 +244,10 @@ async function processDepositGroupImpl(
|
||||
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, 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 }),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*
|
||||
@ -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) => {
|
||||
@ -896,15 +920,19 @@ async function startDownloadProposal(
|
||||
* If we have already claimed this proposal with the same sessionId
|
||||
* nonce and claim token, reuse it.
|
||||
*/
|
||||
if (oldProposal &&
|
||||
if (
|
||||
oldProposal &&
|
||||
oldProposal.downloadSessionId === sessionId &&
|
||||
(!noncePriv || oldProposal.noncePriv === noncePriv) &&
|
||||
oldProposal.claimToken === claimToken) {
|
||||
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) => {
|
||||
|
@ -365,7 +365,18 @@ async function refreshMelt(
|
||||
`coins/${oldCoin.coinPub}/melt`,
|
||||
oldCoin.exchangeBaseUrl,
|
||||
);
|
||||
const 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,
|
||||
@ -373,10 +384,10 @@ async function refreshMelt(
|
||||
rc: derived.hash,
|
||||
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
||||
};
|
||||
logger.trace(`melt request for coin:`, meltReq);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
throw Error("unsupported cipher");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user