diff options
Diffstat (limited to 'packages/taler-wallet-core/src/crypto/cryptoImplementation.ts')
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 258 |
1 files changed, 152 insertions, 106 deletions
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index d239270c8..0745d70c4 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -28,12 +28,14 @@ import { AgeCommitmentProof, AgeRestriction, AmountJson, - AmountLike, Amounts, AmountString, + amountToBuffer, BlindedDenominationSignature, bufferForUint32, + bufferForUint64, buildSigPS, + canonicalJson, CoinDepositPermission, CoinEnvelope, createHashContext, @@ -42,7 +44,8 @@ import { decryptContractForMerge, DenomKeyType, DepositInfo, - ecdheGetPublic, + durationRoundedToBuffer, + ecdhGetPublic, eddsaGetPublic, EddsaPublicKeyString, eddsaSign, @@ -62,7 +65,7 @@ import { hashTruncate32, kdf, kdfKw, - keyExchangeEcdheEddsa, + keyExchangeEcdhEddsa, Logger, MakeSyncSignatureRequest, PlanchetCreationRequest, @@ -76,16 +79,15 @@ import { rsaVerify, setupTipPlanchet, stringToBytes, - TalerProtocolDuration, TalerProtocolTimestamp, TalerSignaturePurpose, + timestampRoundedToBuffer, UnblindedSignature, WireFee, WithdrawalPlanchet, } from "@gnu-taler/taler-util"; -import bigint from "big-integer"; // FIXME: Crypto should not use DB Types! -import { DenominationRecord } from "../db.js"; +import { DenominationRecord, timestampProtocolFromDb } from "../db.js"; import { CreateRecoupRefreshReqRequest, CreateRecoupReqRequest, @@ -101,9 +103,14 @@ import { EncryptContractForDepositResponse, EncryptContractRequest, EncryptContractResponse, - EncryptedContract, + SignCoinHistoryRequest, + SignCoinHistoryResponse, + SignDeletePurseRequest, + SignDeletePurseResponse, SignPurseMergeRequest, SignPurseMergeResponse, + SignRefundRequest, + SignRefundResponse, SignReservePurseCreateRequest, SignReservePurseCreateResponse, SignTrackTransactionRequest, @@ -159,7 +166,7 @@ export interface TalerCryptoInterface { req: ContractTermsValidationRequest, ): Promise<ValidationResult>; - createEddsaKeypair(req: unknown): Promise<EddsaKeypair>; + createEddsaKeypair(req: {}): Promise<EddsaKeypair>; eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>; @@ -232,6 +239,16 @@ export interface TalerCryptoInterface { signReservePurseCreate( req: SignReservePurseCreateRequest, ): Promise<SignReservePurseCreateResponse>; + + signRefund(req: SignRefundRequest): Promise<SignRefundResponse>; + + signDeletePurse( + req: SignDeletePurseRequest, + ): Promise<SignDeletePurseResponse>; + + signCoinHistoryRequest( + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse>; } /** @@ -408,6 +425,19 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise<SignReservePurseCreateResponse> { throw new Error("Function not implemented."); }, + signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> { + throw new Error("Function not implemented."); + }, + signDeletePurse: function ( + req: SignDeletePurseRequest, + ): Promise<SignDeletePurseResponse> { + throw new Error("Function not implemented."); + }, + signCoinHistoryRequest: function ( + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse> { + throw new Error("Function not implemented."); + }, }; export type WithArg<X> = X extends (req: infer T) => infer R @@ -445,17 +475,19 @@ export interface SignPurseCreationRequest { minAge: number; } +export interface SpendCoinDetails { + coinPub: string; + coinPriv: string; + contribution: AmountString; + denomPubHash: string; + denomSig: UnblindedSignature; + ageCommitmentProof: AgeCommitmentProof | undefined; +} + export interface SignPurseDepositsRequest { pursePub: string; exchangeBaseUrl: string; - coins: { - coinPub: string; - coinPriv: string; - contribution: AmountString; - denomPubHash: string; - denomSig: UnblindedSignature; - ageCommitmentProof: AgeCommitmentProof | undefined; - }[]; + coins: SpendCoinDetails[]; } export interface SignPurseDepositsResponse { @@ -523,6 +555,9 @@ export interface WireAccountValidationRequest { paytoUri: string; sig: string; masterPub: string; + conversionUrl?: string; + debitRestrictions?: any[]; + creditRestrictions?: any[]; } export interface EddsaKeypair { @@ -664,7 +699,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const salt = new Uint8Array(saltArrBuf); const saltDataView = new DataView(saltArrBuf); saltDataView.setUint32(0, req.coinNumber); - const out = kdf(64, decodeCrock(req.secretSeed), salt, info); + const secretSeedDec = decodeCrock(req.secretSeed); + const out = kdf(64, secretSeedDec, salt, info); const coinPriv = out.slice(0, 32); const bks = out.slice(32, 64); const coinPrivEnc = encodeCrock(coinPriv); @@ -695,9 +731,10 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { if (denomPub.age_mask) { const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED; logger.info(`creating age-restricted planchet (age ${age})`); - maybeAcp = await AgeRestriction.restrictionCommit( + maybeAcp = await AgeRestriction.restrictionCommitSeeded( denomPub.age_mask, age, + stringToBytes(req.secretSeed), ); maybeAgeCommitmentHash = AgeRestriction.hashCommitment( maybeAcp.commitment, @@ -799,7 +836,6 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION) .put(decodeCrock(req.contractTermsHash)) .put(decodeCrock(req.wireHash)) - .put(decodeCrock(req.merchantPub)) .put(decodeCrock(req.coinPub)) .build(); return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) }; @@ -925,6 +961,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const pub = decodeCrock(masterPub); return { valid: eddsaVerify(p, sig, pub) }; }, + /** * Check if the signature of a denomination is valid. */ @@ -933,17 +970,25 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { req: DenominationValidationRequest, ): Promise<ValidationResult> { const { masterPub, denom } = req; - const value: AmountJson = { - currency: denom.currency, - fraction: denom.amountFrac, - value: denom.amountVal, - }; + const value: AmountJson = Amounts.parseOrThrow(denom.value); const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) .put(decodeCrock(masterPub)) - .put(timestampRoundedToBuffer(denom.stampStart)) - .put(timestampRoundedToBuffer(denom.stampExpireWithdraw)) - .put(timestampRoundedToBuffer(denom.stampExpireDeposit)) - .put(timestampRoundedToBuffer(denom.stampExpireLegal)) + .put(timestampRoundedToBuffer(timestampProtocolFromDb(denom.stampStart))) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireWithdraw), + ), + ) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireDeposit), + ), + ) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireLegal), + ), + ) .put(amountToBuffer(value)) .put(amountToBuffer(denom.fees.feeWithdraw)) .put(amountToBuffer(denom.fees.feeDeposit)) @@ -963,9 +1008,20 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { ): Promise<ValidationResult> { const { sig, masterPub, paytoUri } = req; const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0")); - const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) - .put(paytoHash) - .build(); + const pb = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS); + pb.put(paytoHash); + if (req.versionCurrent >= 15) { + let conversionUrlHash; + if (!req.conversionUrl) { + conversionUrlHash = new Uint8Array(64); + } else { + conversionUrlHash = hash(stringToBytes(req.conversionUrl + "\0")); + } + pb.put(conversionUrlHash); + pb.put(hash(stringToBytes(canonicalJson(req.debitRestrictions) + "\0"))); + pb.put(hash(stringToBytes(canonicalJson(req.creditRestrictions) + "\0"))); + } + const p = pb.build(); return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) }; }, @@ -1078,7 +1134,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { depositInfo.ageCommitmentProof.commitment, ); hAgeCommitment = decodeCrock(ach); - if (depositInfo.requiredMinimumAge != null) { + if (depositInfo.requiredMinimumAge) { minimumAgeSig = encodeCrock( AgeRestriction.commitmentAttest( depositInfo.ageCommitmentProof, @@ -1090,6 +1146,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { // All zeros. hAgeCommitment = new Uint8Array(32); } + // FIXME: Actually allow passing user data here! + const walletDataHash = new Uint8Array(64); let d: Uint8Array; if (depositInfo.denomKeyType === DenomKeyType.Rsa) { d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) @@ -1103,6 +1161,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .put(amountToBuffer(depositInfo.spendAmount)) .put(amountToBuffer(depositInfo.feeDeposit)) .put(decodeCrock(depositInfo.merchantPub)) + .put(walletDataHash) .build(); } else { throw Error("unsupported exchange protocol version"); @@ -1125,7 +1184,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { }, }; - if (depositInfo.requiredMinimumAge != null) { + if (depositInfo.requiredMinimumAge) { // These are only required by the merchant s.minimum_age_sig = minimumAgeSig; s.age_commitment = @@ -1355,7 +1414,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { ): Promise<KeyExchangeResult> { return { h: encodeCrock( - keyExchangeEcdheEddsa( + keyExchangeEcdhEddsa( decodeCrock(req.ecdhePriv), decodeCrock(req.eddsaPub), ), @@ -1367,7 +1426,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { req: EcdheGetPublicRequest, ): Promise<EcdheGetPublicResponse> { return { - pub: encodeCrock(ecdheGetPublic(decodeCrock(req.priv))), + pub: encodeCrock(ecdhGetPublic(decodeCrock(req.priv))), }; }, async setupRefreshTransferPub( @@ -1409,15 +1468,12 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0")); const deposits: PurseDeposit[] = []; for (const c of req.coins) { - let haveAch: boolean; let maybeAch: Uint8Array; if (c.ageCommitmentProof) { - haveAch = true; maybeAch = decodeCrock( AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment), ); } else { - haveAch = false; maybeAch = new Uint8Array(32); } const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT) @@ -1450,25 +1506,24 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { tci: TalerCryptoInterfaceR, req: EncryptContractRequest, ): Promise<EncryptContractResponse> { - const contractKeyPair = await this.createEddsaKeypair(tci, {}); const enc = await encryptContractForMerge( decodeCrock(req.pursePub), - decodeCrock(contractKeyPair.priv), + decodeCrock(req.contractPriv), decodeCrock(req.mergePriv), req.contractTerms, + decodeCrock(req.nonce), ); const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT) .put(hash(enc)) - .put(decodeCrock(contractKeyPair.pub)) + .put(decodeCrock(req.contractPub)) .build(); const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv)); return { econtract: { - contract_pub: contractKeyPair.pub, + contract_pub: req.contractPub, econtract: encodeCrock(enc), econtract_sig: encodeCrock(sig), }, - contractPriv: contractKeyPair.priv, }; }, async decryptContractForMerge( @@ -1489,24 +1544,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { tci: TalerCryptoInterfaceR, req: EncryptContractForDepositRequest, ): Promise<EncryptContractForDepositResponse> { - const contractKeyPair = await this.createEddsaKeypair(tci, {}); const enc = await encryptContractForDeposit( decodeCrock(req.pursePub), - decodeCrock(contractKeyPair.priv), + decodeCrock(req.contractPriv), req.contractTerms, + decodeCrock(req.nonce), ); const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT) .put(hash(enc)) - .put(decodeCrock(contractKeyPair.pub)) + .put(decodeCrock(req.contractPub)) .build(); const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv)); return { econtract: { - contract_pub: contractKeyPair.pub, + contract_pub: req.contractPub, econtract: encodeCrock(enc), econtract_sig: encodeCrock(sig), }, - contractPriv: contractKeyPair.priv, }; }, async decryptContractForDeposit( @@ -1626,66 +1680,58 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { purseSig: purseSigResp.sig, }; }, + async signRefund( + tci: TalerCryptoInterfaceR, + req: SignRefundRequest, + ): Promise<SignRefundResponse> { + const refundSigBlob = buildSigPS(TalerSignaturePurpose.MERCHANT_REFUND) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.coinPub)) + .put(bufferForUint64(req.rtransactionId)) + .put(amountToBuffer(req.refundAmount)) + .build(); + const refundSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(refundSigBlob), + priv: req.merchantPriv, + }); + return { + sig: refundSigResp.sig, + }; + }, + async signDeletePurse( + tci: TalerCryptoInterfaceR, + req: SignDeletePurseRequest, + ): Promise<SignDeletePurseResponse> { + const deleteSigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_PURSE_DELETE, + ).build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(deleteSigBlob), + priv: req.pursePriv, + }); + return { + sig: sigResp.sig, + }; + }, + async signCoinHistoryRequest( + tci: TalerCryptoInterfaceR, + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse> { + const coinHistorySigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_COIN_HISTORY, + ) + .put(bufferForUint64(req.startOffset)) + .build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(coinHistorySigBlob), + priv: req.coinPriv, + }); + return { + sig: sigResp.sig, + }; + }, }; -function amountToBuffer(amount: AmountLike): Uint8Array { - const amountJ = Amounts.jsonifyAmount(amount); - const buffer = new ArrayBuffer(8 + 4 + 12); - const dvbuf = new DataView(buffer); - const u8buf = new Uint8Array(buffer); - const curr = stringToBytes(amountJ.currency); - if (typeof dvbuf.setBigUint64 !== "undefined") { - dvbuf.setBigUint64(0, BigInt(amountJ.value)); - } else { - const arr = bigint(amountJ.value).toArray(2 ** 8).value; - let offset = 8 - arr.length; - for (let i = 0; i < arr.length; i++) { - dvbuf.setUint8(offset++, arr[i]); - } - } - dvbuf.setUint32(8, amountJ.fraction); - u8buf.set(curr, 8 + 4); - - return u8buf; -} - -function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { - const b = new ArrayBuffer(8); - const v = new DataView(b); - // The buffer we sign over represents the timestamp in microseconds. - if (typeof v.setBigUint64 !== "undefined") { - const s = BigInt(ts.t_s) * BigInt(1000 * 1000); - v.setBigUint64(0, s); - } else { - const s = - ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000); - const arr = s.toArray(2 ** 8).value; - let offset = 8 - arr.length; - for (let i = 0; i < arr.length; i++) { - v.setUint8(offset++, arr[i]); - } - } - return new Uint8Array(b); -} - -function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array { - const b = new ArrayBuffer(8); - const v = new DataView(b); - // The buffer we sign over represents the timestamp in microseconds. - if (typeof v.setBigUint64 !== "undefined") { - const s = BigInt(ts.d_us); - v.setBigUint64(0, s); - } else { - const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us); - const arr = s.toArray(2 ** 8).value; - let offset = 8 - arr.length; - for (let i = 0; i < arr.length; i++) { - v.setUint8(offset++, arr[i]); - } - } - return new Uint8Array(b); -} - export interface EddsaSignRequest { msg: string; priv: string; |