summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/crypto/cryptoImplementation.ts')
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts1752
1 files changed, 1752 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
new file mode 100644
index 000000000..0745d70c4
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -0,0 +1,1752 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of crypto-related high-level functions for the Taler wallet.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+
+import {
+ AgeCommitmentProof,
+ AgeRestriction,
+ AmountJson,
+ Amounts,
+ AmountString,
+ amountToBuffer,
+ BlindedDenominationSignature,
+ bufferForUint32,
+ bufferForUint64,
+ buildSigPS,
+ canonicalJson,
+ CoinDepositPermission,
+ CoinEnvelope,
+ createHashContext,
+ decodeCrock,
+ decryptContractForDeposit,
+ decryptContractForMerge,
+ DenomKeyType,
+ DepositInfo,
+ durationRoundedToBuffer,
+ ecdhGetPublic,
+ eddsaGetPublic,
+ EddsaPublicKeyString,
+ eddsaSign,
+ eddsaVerify,
+ encodeCrock,
+ encryptContractForDeposit,
+ encryptContractForMerge,
+ ExchangeProtocolVersion,
+ getRandomBytes,
+ GlobalFees,
+ hash,
+ HashCodeString,
+ hashCoinEv,
+ hashCoinEvInner,
+ hashCoinPub,
+ hashDenomPub,
+ hashTruncate32,
+ kdf,
+ kdfKw,
+ keyExchangeEcdhEddsa,
+ Logger,
+ MakeSyncSignatureRequest,
+ PlanchetCreationRequest,
+ PlanchetUnblindInfo,
+ PurseDeposit,
+ RecoupRefreshRequest,
+ RecoupRequest,
+ RefreshPlanchetInfo,
+ rsaBlind,
+ rsaUnblind,
+ rsaVerify,
+ setupTipPlanchet,
+ stringToBytes,
+ TalerProtocolTimestamp,
+ TalerSignaturePurpose,
+ timestampRoundedToBuffer,
+ UnblindedSignature,
+ WireFee,
+ WithdrawalPlanchet,
+} from "@gnu-taler/taler-util";
+// FIXME: Crypto should not use DB Types!
+import { DenominationRecord, timestampProtocolFromDb } from "../db.js";
+import {
+ CreateRecoupRefreshReqRequest,
+ CreateRecoupReqRequest,
+ DecryptContractForDepositRequest,
+ DecryptContractForDepositResponse,
+ DecryptContractRequest,
+ DecryptContractResponse,
+ DerivedRefreshSession,
+ DerivedTipPlanchet,
+ DeriveRefreshSessionRequest,
+ DeriveTipRequest,
+ EncryptContractForDepositRequest,
+ EncryptContractForDepositResponse,
+ EncryptContractRequest,
+ EncryptContractResponse,
+ SignCoinHistoryRequest,
+ SignCoinHistoryResponse,
+ SignDeletePurseRequest,
+ SignDeletePurseResponse,
+ SignPurseMergeRequest,
+ SignPurseMergeResponse,
+ SignRefundRequest,
+ SignRefundResponse,
+ SignReservePurseCreateRequest,
+ SignReservePurseCreateResponse,
+ SignTrackTransactionRequest,
+} from "./cryptoTypes.js";
+
+const logger = new Logger("cryptoImplementation.ts");
+
+/**
+ * Interface for (asynchronous) cryptographic operations that
+ * Taler uses.
+ */
+export interface TalerCryptoInterface {
+ /**
+ * Create a pre-coin of the given denomination to be withdrawn from then given
+ * reserve.
+ */
+ createPlanchet(req: PlanchetCreationRequest): Promise<WithdrawalPlanchet>;
+
+ eddsaSign(req: EddsaSignRequest): Promise<EddsaSignResponse>;
+
+ /**
+ * Create a planchet used for tipping, including the private keys.
+ */
+ createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet>;
+
+ signTrackTransaction(
+ req: SignTrackTransactionRequest,
+ ): Promise<EddsaSigningResult>;
+
+ createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest>;
+
+ createRecoupRefreshRequest(
+ req: CreateRecoupRefreshReqRequest,
+ ): Promise<RecoupRefreshRequest>;
+
+ isValidPaymentSignature(
+ req: PaymentSignatureValidationRequest,
+ ): Promise<ValidationResult>;
+
+ isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>;
+
+ isValidGlobalFees(
+ req: GlobalFeesValidationRequest,
+ ): Promise<ValidationResult>;
+
+ isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>;
+
+ isValidWireAccount(
+ req: WireAccountValidationRequest,
+ ): Promise<ValidationResult>;
+
+ isValidContractTermsSignature(
+ req: ContractTermsValidationRequest,
+ ): Promise<ValidationResult>;
+
+ createEddsaKeypair(req: {}): Promise<EddsaKeypair>;
+
+ eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;
+
+ unblindDenominationSignature(
+ req: UnblindDenominationSignatureRequest,
+ ): Promise<UnblindedSignature>;
+
+ rsaUnblind(req: RsaUnblindRequest): Promise<RsaUnblindResponse>;
+
+ rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>;
+
+ rsaBlind(req: RsaBlindRequest): Promise<RsaBlindResponse>;
+
+ signDepositPermission(
+ depositInfo: DepositInfo,
+ ): Promise<CoinDepositPermission>;
+
+ deriveRefreshSession(
+ req: DeriveRefreshSessionRequest,
+ ): Promise<DerivedRefreshSession>;
+
+ hashString(req: HashStringRequest): Promise<HashStringResult>;
+
+ signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>;
+
+ makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>;
+
+ setupRefreshPlanchet(
+ req: SetupRefreshPlanchetRequest,
+ ): Promise<FreshCoinEncoded>;
+
+ setupWithdrawalPlanchet(
+ req: SetupWithdrawalPlanchetRequest,
+ ): Promise<FreshCoinEncoded>;
+
+ keyExchangeEcdheEddsa(
+ req: KeyExchangeEcdheEddsaRequest,
+ ): Promise<KeyExchangeResult>;
+
+ ecdheGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;
+
+ setupRefreshTransferPub(
+ req: SetupRefreshTransferPubRequest,
+ ): Promise<TransferPubResponse>;
+
+ signPurseCreation(req: SignPurseCreationRequest): Promise<EddsaSigningResult>;
+
+ signPurseDeposits(
+ req: SignPurseDepositsRequest,
+ ): Promise<SignPurseDepositsResponse>;
+
+ encryptContractForMerge(
+ req: EncryptContractRequest,
+ ): Promise<EncryptContractResponse>;
+
+ decryptContractForMerge(
+ req: DecryptContractRequest,
+ ): Promise<DecryptContractResponse>;
+
+ encryptContractForDeposit(
+ req: EncryptContractForDepositRequest,
+ ): Promise<EncryptContractForDepositResponse>;
+
+ decryptContractForDeposit(
+ req: DecryptContractForDepositRequest,
+ ): Promise<DecryptContractForDepositResponse>;
+
+ signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;
+
+ signReservePurseCreate(
+ req: SignReservePurseCreateRequest,
+ ): Promise<SignReservePurseCreateResponse>;
+
+ signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;
+
+ signDeletePurse(
+ req: SignDeletePurseRequest,
+ ): Promise<SignDeletePurseResponse>;
+
+ signCoinHistoryRequest(
+ req: SignCoinHistoryRequest,
+ ): Promise<SignCoinHistoryResponse>;
+}
+
+/**
+ * Implementation of the Taler crypto interface where every function
+ * always throws. Only useful in practice as a way to iterate through
+ * all possible crypto functions.
+ *
+ * (This list can be easily auto-generated by your favorite IDE).
+ */
+export const nullCrypto: TalerCryptoInterface = {
+ createPlanchet: function (
+ req: PlanchetCreationRequest,
+ ): Promise<WithdrawalPlanchet> {
+ throw new Error("Function not implemented.");
+ },
+ eddsaSign: function (req: EddsaSignRequest): Promise<EddsaSignResponse> {
+ throw new Error("Function not implemented.");
+ },
+ createTipPlanchet: function (
+ req: DeriveTipRequest,
+ ): Promise<DerivedTipPlanchet> {
+ throw new Error("Function not implemented.");
+ },
+ signTrackTransaction: function (
+ req: SignTrackTransactionRequest,
+ ): Promise<EddsaSigningResult> {
+ throw new Error("Function not implemented.");
+ },
+ createRecoupRequest: function (
+ req: CreateRecoupReqRequest,
+ ): Promise<RecoupRequest> {
+ throw new Error("Function not implemented.");
+ },
+ createRecoupRefreshRequest: function (
+ req: CreateRecoupRefreshReqRequest,
+ ): Promise<RecoupRefreshRequest> {
+ throw new Error("Function not implemented.");
+ },
+ isValidPaymentSignature: function (
+ req: PaymentSignatureValidationRequest,
+ ): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ isValidWireFee: function (
+ req: WireFeeValidationRequest,
+ ): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ isValidDenom: function (
+ req: DenominationValidationRequest,
+ ): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ isValidWireAccount: function (
+ req: WireAccountValidationRequest,
+ ): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ isValidGlobalFees: function (
+ req: GlobalFeesValidationRequest,
+ ): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ isValidContractTermsSignature: function (
+ req: ContractTermsValidationRequest,
+ ): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ createEddsaKeypair: function (req: unknown): Promise<EddsaKeypair> {
+ throw new Error("Function not implemented.");
+ },
+ eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> {
+ throw new Error("Function not implemented.");
+ },
+ unblindDenominationSignature: function (
+ req: UnblindDenominationSignatureRequest,
+ ): Promise<UnblindedSignature> {
+ throw new Error("Function not implemented.");
+ },
+ rsaUnblind: function (req: RsaUnblindRequest): Promise<RsaUnblindResponse> {
+ throw new Error("Function not implemented.");
+ },
+ rsaVerify: function (req: RsaVerificationRequest): Promise<ValidationResult> {
+ throw new Error("Function not implemented.");
+ },
+ signDepositPermission: function (
+ depositInfo: DepositInfo,
+ ): Promise<CoinDepositPermission> {
+ throw new Error("Function not implemented.");
+ },
+ deriveRefreshSession: function (
+ req: DeriveRefreshSessionRequest,
+ ): Promise<DerivedRefreshSession> {
+ throw new Error("Function not implemented.");
+ },
+ hashString: function (req: HashStringRequest): Promise<HashStringResult> {
+ throw new Error("Function not implemented.");
+ },
+ signCoinLink: function (
+ req: SignCoinLinkRequest,
+ ): Promise<EddsaSigningResult> {
+ throw new Error("Function not implemented.");
+ },
+ makeSyncSignature: function (
+ req: MakeSyncSignatureRequest,
+ ): Promise<EddsaSigningResult> {
+ throw new Error("Function not implemented.");
+ },
+ setupRefreshPlanchet: function (
+ req: SetupRefreshPlanchetRequest,
+ ): Promise<FreshCoinEncoded> {
+ throw new Error("Function not implemented.");
+ },
+ rsaBlind: function (req: RsaBlindRequest): Promise<RsaBlindResponse> {
+ throw new Error("Function not implemented.");
+ },
+ keyExchangeEcdheEddsa: function (
+ req: KeyExchangeEcdheEddsaRequest,
+ ): Promise<KeyExchangeResult> {
+ throw new Error("Function not implemented.");
+ },
+ setupWithdrawalPlanchet: function (
+ req: SetupWithdrawalPlanchetRequest,
+ ): Promise<FreshCoinEncoded> {
+ throw new Error("Function not implemented.");
+ },
+ ecdheGetPublic: function (
+ req: EddsaGetPublicRequest,
+ ): Promise<EddsaGetPublicResponse> {
+ throw new Error("Function not implemented.");
+ },
+ setupRefreshTransferPub: function (
+ req: SetupRefreshTransferPubRequest,
+ ): Promise<TransferPubResponse> {
+ throw new Error("Function not implemented.");
+ },
+ signPurseCreation: function (
+ req: SignPurseCreationRequest,
+ ): Promise<EddsaSigningResult> {
+ throw new Error("Function not implemented.");
+ },
+ signPurseDeposits: function (
+ req: SignPurseDepositsRequest,
+ ): Promise<SignPurseDepositsResponse> {
+ throw new Error("Function not implemented.");
+ },
+ encryptContractForMerge: function (
+ req: EncryptContractRequest,
+ ): Promise<EncryptContractResponse> {
+ throw new Error("Function not implemented.");
+ },
+ decryptContractForMerge: function (
+ req: DecryptContractRequest,
+ ): Promise<DecryptContractResponse> {
+ throw new Error("Function not implemented.");
+ },
+ signPurseMerge: function (
+ req: SignPurseMergeRequest,
+ ): Promise<SignPurseMergeResponse> {
+ throw new Error("Function not implemented.");
+ },
+ encryptContractForDeposit: function (
+ req: EncryptContractForDepositRequest,
+ ): Promise<EncryptContractForDepositResponse> {
+ throw new Error("Function not implemented.");
+ },
+ decryptContractForDeposit: function (
+ req: DecryptContractForDepositRequest,
+ ): Promise<DecryptContractForDepositResponse> {
+ throw new Error("Function not implemented.");
+ },
+ signReservePurseCreate: function (
+ req: SignReservePurseCreateRequest,
+ ): 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
+ ? (tci: TalerCryptoInterfaceR, req: T) => R
+ : never;
+
+export type TalerCryptoInterfaceR = {
+ [x in keyof TalerCryptoInterface]: WithArg<TalerCryptoInterface[x]>;
+};
+
+export interface SignCoinLinkRequest {
+ oldCoinPriv: string;
+ newDenomHash: string;
+ oldCoinPub: string;
+ transferPub: string;
+ coinEv: CoinEnvelope;
+}
+
+export interface SetupRefreshPlanchetRequest {
+ transferSecret: string;
+ coinNumber: number;
+}
+
+export interface SetupWithdrawalPlanchetRequest {
+ secretSeed: string;
+ coinNumber: number;
+}
+
+export interface SignPurseCreationRequest {
+ pursePriv: string;
+ purseExpiration: TalerProtocolTimestamp;
+ purseAmount: AmountString;
+ hContractTerms: HashCodeString;
+ mergePub: EddsaPublicKeyString;
+ 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: SpendCoinDetails[];
+}
+
+export interface SignPurseDepositsResponse {
+ deposits: PurseDeposit[];
+}
+
+export interface RsaVerificationRequest {
+ hm: string;
+ sig: string;
+ pk: string;
+}
+
+export interface RsaBlindRequest {
+ hm: string;
+ bks: string;
+ pub: string;
+}
+
+export interface EddsaSigningResult {
+ sig: string;
+}
+
+export interface ValidationResult {
+ valid: boolean;
+}
+
+export interface HashStringRequest {
+ str: string;
+}
+
+export interface HashStringResult {
+ h: string;
+}
+
+export interface WireFeeValidationRequest {
+ type: string;
+ wf: WireFee;
+ masterPub: string;
+}
+
+export interface GlobalFeesValidationRequest {
+ gf: GlobalFees;
+ masterPub: string;
+}
+
+export interface DenominationValidationRequest {
+ denom: DenominationRecord;
+ masterPub: string;
+}
+
+export interface PaymentSignatureValidationRequest {
+ sig: string;
+ contractHash: string;
+ merchantPub: string;
+}
+
+export interface ContractTermsValidationRequest {
+ contractTermsHash: string;
+ sig: string;
+ merchantPub: string;
+}
+
+export interface WireAccountValidationRequest {
+ versionCurrent: ExchangeProtocolVersion;
+ paytoUri: string;
+ sig: string;
+ masterPub: string;
+ conversionUrl?: string;
+ debitRestrictions?: any[];
+ creditRestrictions?: any[];
+}
+
+export interface EddsaKeypair {
+ priv: string;
+ pub: string;
+}
+
+export interface EddsaGetPublicRequest {
+ priv: string;
+}
+
+export interface EddsaGetPublicResponse {
+ pub: string;
+}
+
+export interface EcdheGetPublicRequest {
+ priv: string;
+}
+
+export interface EcdheGetPublicResponse {
+ pub: string;
+}
+
+export interface UnblindDenominationSignatureRequest {
+ planchet: PlanchetUnblindInfo;
+ evSig: BlindedDenominationSignature;
+}
+
+export interface FreshCoinEncoded {
+ coinPub: string;
+ coinPriv: string;
+ bks: string;
+}
+
+export interface RsaUnblindRequest {
+ blindedSig: string;
+ bk: string;
+ pk: string;
+}
+
+export interface RsaBlindResponse {
+ blinded: string;
+}
+
+export interface RsaUnblindResponse {
+ sig: string;
+}
+
+export interface KeyExchangeEcdheEddsaRequest {
+ ecdhePriv: string;
+ eddsaPub: string;
+}
+
+export interface KeyExchangeResult {
+ h: string;
+}
+
+export interface SetupRefreshTransferPubRequest {
+ secretSeed: string;
+ transferPubIndex: number;
+}
+
+export interface TransferPubResponse {
+ transferPub: string;
+ transferPriv: string;
+}
+
+/**
+ * JS-native implementation of the Taler crypto worker operations.
+ */
+export const nativeCryptoR: TalerCryptoInterfaceR = {
+ async eddsaSign(
+ tci: TalerCryptoInterfaceR,
+ req: EddsaSignRequest,
+ ): Promise<EddsaSignResponse> {
+ return {
+ sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))),
+ };
+ },
+
+ async rsaBlind(
+ tci: TalerCryptoInterfaceR,
+ req: RsaBlindRequest,
+ ): Promise<RsaBlindResponse> {
+ const res = rsaBlind(
+ decodeCrock(req.hm),
+ decodeCrock(req.bks),
+ decodeCrock(req.pub),
+ );
+ return {
+ blinded: encodeCrock(res),
+ };
+ },
+
+ async setupRefreshPlanchet(
+ tci: TalerCryptoInterfaceR,
+ req: SetupRefreshPlanchetRequest,
+ ): Promise<FreshCoinEncoded> {
+ const transferSecret = decodeCrock(req.transferSecret);
+ const coinNumber = req.coinNumber;
+ // See TALER_transfer_secret_to_planchet_secret in C impl
+ const planchetMasterSecret = kdfKw({
+ ikm: transferSecret,
+ outputLength: 32,
+ salt: bufferForUint32(coinNumber),
+ info: stringToBytes("taler-coin-derivation"),
+ });
+
+ const coinPriv = kdfKw({
+ ikm: planchetMasterSecret,
+ outputLength: 32,
+ salt: stringToBytes("coin"),
+ });
+
+ const bks = kdfKw({
+ ikm: planchetMasterSecret,
+ outputLength: 32,
+ salt: stringToBytes("bks"),
+ });
+
+ const coinPrivEnc = encodeCrock(coinPriv);
+ const coinPubRes = await tci.eddsaGetPublic(tci, {
+ priv: coinPrivEnc,
+ });
+
+ return {
+ bks: encodeCrock(bks),
+ coinPriv: coinPrivEnc,
+ coinPub: coinPubRes.pub,
+ };
+ },
+
+ async setupWithdrawalPlanchet(
+ tci: TalerCryptoInterfaceR,
+ req: SetupWithdrawalPlanchetRequest,
+ ): Promise<FreshCoinEncoded> {
+ const info = stringToBytes("taler-withdrawal-coin-derivation");
+ const saltArrBuf = new ArrayBuffer(4);
+ const salt = new Uint8Array(saltArrBuf);
+ const saltDataView = new DataView(saltArrBuf);
+ saltDataView.setUint32(0, req.coinNumber);
+ 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);
+ const coinPubRes = await tci.eddsaGetPublic(tci, {
+ priv: coinPrivEnc,
+ });
+ return {
+ bks: encodeCrock(bks),
+ coinPriv: coinPrivEnc,
+ coinPub: coinPubRes.pub,
+ };
+ },
+
+ async createPlanchet(
+ tci: TalerCryptoInterfaceR,
+ req: PlanchetCreationRequest,
+ ): Promise<WithdrawalPlanchet> {
+ const denomPub = req.denomPub;
+ if (denomPub.cipher === DenomKeyType.Rsa) {
+ const reservePub = decodeCrock(req.reservePub);
+ const derivedPlanchet = await tci.setupWithdrawalPlanchet(tci, {
+ coinNumber: req.coinIndex,
+ secretSeed: req.secretSeed,
+ });
+
+ let maybeAcp: AgeCommitmentProof | undefined = undefined;
+ let maybeAgeCommitmentHash: string | undefined = undefined;
+ if (denomPub.age_mask) {
+ const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED;
+ logger.info(`creating age-restricted planchet (age ${age})`);
+ maybeAcp = await AgeRestriction.restrictionCommitSeeded(
+ denomPub.age_mask,
+ age,
+ stringToBytes(req.secretSeed),
+ );
+ maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
+ maybeAcp.commitment,
+ );
+ }
+
+ const coinPubHash = hashCoinPub(
+ derivedPlanchet.coinPub,
+ maybeAgeCommitmentHash,
+ );
+
+ const blindResp = await tci.rsaBlind(tci, {
+ bks: derivedPlanchet.bks,
+ hm: encodeCrock(coinPubHash),
+ pub: denomPub.rsa_public_key,
+ });
+ const coinEv: CoinEnvelope = {
+ cipher: DenomKeyType.Rsa,
+ rsa_blinded_planchet: blindResp.blinded,
+ };
+ const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
+ const denomPubHash = hashDenomPub(req.denomPub);
+ const evHash = hashCoinEv(coinEv, encodeCrock(denomPubHash));
+ const withdrawRequest = buildSigPS(
+ TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
+ )
+ .put(amountToBuffer(amountWithFee))
+ .put(denomPubHash)
+ .put(evHash)
+ .build();
+
+ const sigResult = await tci.eddsaSign(tci, {
+ msg: encodeCrock(withdrawRequest),
+ priv: req.reservePriv,
+ });
+
+ const planchet: WithdrawalPlanchet = {
+ blindingKey: derivedPlanchet.bks,
+ coinEv,
+ coinPriv: derivedPlanchet.coinPriv,
+ coinPub: derivedPlanchet.coinPub,
+ coinValue: req.value,
+ denomPub,
+ denomPubHash: encodeCrock(denomPubHash),
+ reservePub: encodeCrock(reservePub),
+ withdrawSig: sigResult.sig,
+ coinEvHash: encodeCrock(evHash),
+ ageCommitmentProof: maybeAcp,
+ };
+ return planchet;
+ } else {
+ throw Error("unsupported cipher, unable to create planchet");
+ }
+ },
+
+ async createTipPlanchet(
+ tci: TalerCryptoInterfaceR,
+ req: DeriveTipRequest,
+ ): Promise<DerivedTipPlanchet> {
+ if (req.denomPub.cipher !== DenomKeyType.Rsa) {
+ throw Error(`unsupported cipher (${req.denomPub.cipher})`);
+ }
+ const fc = await setupTipPlanchet(
+ decodeCrock(req.secretSeed),
+ req.denomPub,
+ req.planchetIndex,
+ );
+ const maybeAch = fc.ageCommitmentProof
+ ? AgeRestriction.hashCommitment(fc.ageCommitmentProof.commitment)
+ : undefined;
+ const denomPub = decodeCrock(req.denomPub.rsa_public_key);
+ const coinPubHash = hashCoinPub(encodeCrock(fc.coinPub), maybeAch);
+ const blindResp = await tci.rsaBlind(tci, {
+ bks: encodeCrock(fc.bks),
+ hm: encodeCrock(coinPubHash),
+ pub: encodeCrock(denomPub),
+ });
+ const coinEv = {
+ cipher: DenomKeyType.Rsa,
+ rsa_blinded_planchet: blindResp.blinded,
+ };
+ const tipPlanchet: DerivedTipPlanchet = {
+ blindingKey: encodeCrock(fc.bks),
+ coinEv,
+ coinEvHash: encodeCrock(
+ hashCoinEv(coinEv, encodeCrock(hashDenomPub(req.denomPub))),
+ ),
+ coinPriv: encodeCrock(fc.coinPriv),
+ coinPub: encodeCrock(fc.coinPub),
+ ageCommitmentProof: fc.ageCommitmentProof,
+ };
+ return tipPlanchet;
+ },
+
+ async signTrackTransaction(
+ tci: TalerCryptoInterfaceR,
+ req: SignTrackTransactionRequest,
+ ): Promise<EddsaSigningResult> {
+ const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION)
+ .put(decodeCrock(req.contractTermsHash))
+ .put(decodeCrock(req.wireHash))
+ .put(decodeCrock(req.coinPub))
+ .build();
+ return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) };
+ },
+
+ /**
+ * Create and sign a message to recoup a coin.
+ */
+ async createRecoupRequest(
+ tci: TalerCryptoInterfaceR,
+ req: CreateRecoupReqRequest,
+ ): Promise<RecoupRequest> {
+ const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
+ .put(decodeCrock(req.denomPubHash))
+ .put(decodeCrock(req.blindingKey))
+ .build();
+
+ const coinPriv = decodeCrock(req.coinPriv);
+ const coinSig = eddsaSign(p, coinPriv);
+ if (req.denomPub.cipher === DenomKeyType.Rsa) {
+ const paybackRequest: RecoupRequest = {
+ coin_blind_key_secret: req.blindingKey,
+ coin_sig: encodeCrock(coinSig),
+ denom_pub_hash: req.denomPubHash,
+ denom_sig: req.denomSig,
+ // FIXME!
+ ewv: {
+ cipher: "RSA",
+ },
+ };
+ return paybackRequest;
+ } else {
+ throw new Error();
+ }
+ },
+
+ /**
+ * Create and sign a message to recoup a coin.
+ */
+ async createRecoupRefreshRequest(
+ tci: TalerCryptoInterfaceR,
+ req: CreateRecoupRefreshReqRequest,
+ ): Promise<RecoupRefreshRequest> {
+ const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
+ .put(decodeCrock(req.denomPubHash))
+ .put(decodeCrock(req.blindingKey))
+ .build();
+
+ const coinPriv = decodeCrock(req.coinPriv);
+ const coinSig = eddsaSign(p, coinPriv);
+ if (req.denomPub.cipher === DenomKeyType.Rsa) {
+ const recoupRequest: RecoupRefreshRequest = {
+ coin_blind_key_secret: req.blindingKey,
+ coin_sig: encodeCrock(coinSig),
+ denom_pub_hash: req.denomPubHash,
+ denom_sig: req.denomSig,
+ // FIXME!
+ ewv: {
+ cipher: "RSA",
+ },
+ };
+ return recoupRequest;
+ } else {
+ throw new Error();
+ }
+ },
+
+ /**
+ * Check if a payment signature is valid.
+ */
+ async isValidPaymentSignature(
+ tci: TalerCryptoInterfaceR,
+ req: PaymentSignatureValidationRequest,
+ ): Promise<ValidationResult> {
+ const { contractHash, sig, merchantPub } = req;
+ const p = buildSigPS(TalerSignaturePurpose.MERCHANT_PAYMENT_OK)
+ .put(decodeCrock(contractHash))
+ .build();
+ const sigBytes = decodeCrock(sig);
+ const pubBytes = decodeCrock(merchantPub);
+ return { valid: eddsaVerify(p, sigBytes, pubBytes) };
+ },
+
+ /**
+ * Check if a wire fee is correctly signed.
+ */
+ async isValidWireFee(
+ tci: TalerCryptoInterfaceR,
+ req: WireFeeValidationRequest,
+ ): Promise<ValidationResult> {
+ const { type, wf, masterPub } = req;
+ const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES)
+ .put(hash(stringToBytes(type + "\0")))
+ .put(timestampRoundedToBuffer(wf.startStamp))
+ .put(timestampRoundedToBuffer(wf.endStamp))
+ .put(amountToBuffer(wf.wireFee))
+ .put(amountToBuffer(wf.closingFee))
+ .build();
+ const sig = decodeCrock(wf.sig);
+ const pub = decodeCrock(masterPub);
+ return { valid: eddsaVerify(p, sig, pub) };
+ },
+
+ /**
+ * Check if a global fee is correctly signed.
+ */
+ async isValidGlobalFees(
+ tci: TalerCryptoInterfaceR,
+ req: GlobalFeesValidationRequest,
+ ): Promise<ValidationResult> {
+ const { gf, masterPub } = req;
+ const p = buildSigPS(TalerSignaturePurpose.GLOBAL_FEES)
+ .put(timestampRoundedToBuffer(gf.start_date))
+ .put(timestampRoundedToBuffer(gf.end_date))
+ .put(durationRoundedToBuffer(gf.purse_timeout))
+ .put(durationRoundedToBuffer(gf.history_expiration))
+ .put(amountToBuffer(Amounts.parseOrThrow(gf.history_fee)))
+ .put(amountToBuffer(Amounts.parseOrThrow(gf.account_fee)))
+ .put(amountToBuffer(Amounts.parseOrThrow(gf.purse_fee)))
+ .put(bufferForUint32(gf.purse_account_limit))
+ .build();
+ const sig = decodeCrock(gf.master_sig);
+ const pub = decodeCrock(masterPub);
+ return { valid: eddsaVerify(p, sig, pub) };
+ },
+
+ /**
+ * Check if the signature of a denomination is valid.
+ */
+ async isValidDenom(
+ tci: TalerCryptoInterfaceR,
+ req: DenominationValidationRequest,
+ ): Promise<ValidationResult> {
+ const { masterPub, denom } = req;
+ const value: AmountJson = Amounts.parseOrThrow(denom.value);
+ const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
+ .put(decodeCrock(masterPub))
+ .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))
+ .put(amountToBuffer(denom.fees.feeRefresh))
+ .put(amountToBuffer(denom.fees.feeRefund))
+ .put(decodeCrock(denom.denomPubHash))
+ .build();
+ const sig = decodeCrock(denom.masterSig);
+ const pub = decodeCrock(masterPub);
+ const res = eddsaVerify(p, sig, pub);
+ return { valid: res };
+ },
+
+ async isValidWireAccount(
+ tci: TalerCryptoInterfaceR,
+ req: WireAccountValidationRequest,
+ ): Promise<ValidationResult> {
+ const { sig, masterPub, paytoUri } = req;
+ const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0"));
+ 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)) };
+ },
+
+ async isValidContractTermsSignature(
+ tci: TalerCryptoInterfaceR,
+ req: ContractTermsValidationRequest,
+ ): Promise<ValidationResult> {
+ const cthDec = decodeCrock(req.contractTermsHash);
+ const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT)
+ .put(cthDec)
+ .build();
+ return {
+ valid: eddsaVerify(p, decodeCrock(req.sig), decodeCrock(req.merchantPub)),
+ };
+ },
+
+ /**
+ * Create a new EdDSA key pair.
+ */
+ async createEddsaKeypair(tci: TalerCryptoInterfaceR): Promise<EddsaKeypair> {
+ const eddsaPriv = encodeCrock(getRandomBytes(32));
+ const eddsaPubRes = await tci.eddsaGetPublic(tci, {
+ priv: eddsaPriv,
+ });
+ return {
+ priv: eddsaPriv,
+ pub: eddsaPubRes.pub,
+ };
+ },
+
+ async eddsaGetPublic(
+ tci: TalerCryptoInterfaceR,
+ req: EddsaGetPublicRequest,
+ ): Promise<EddsaKeypair> {
+ return {
+ priv: req.priv,
+ pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))),
+ };
+ },
+
+ async unblindDenominationSignature(
+ tci: TalerCryptoInterfaceR,
+ req: UnblindDenominationSignatureRequest,
+ ): Promise<UnblindedSignature> {
+ if (req.evSig.cipher === DenomKeyType.Rsa) {
+ if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) {
+ throw new Error(
+ "planchet cipher does not match blind signature cipher",
+ );
+ }
+ const denomSig = rsaUnblind(
+ decodeCrock(req.evSig.blinded_rsa_signature),
+ decodeCrock(req.planchet.denomPub.rsa_public_key),
+ decodeCrock(req.planchet.blindingKey),
+ );
+ return {
+ cipher: DenomKeyType.Rsa,
+ rsa_signature: encodeCrock(denomSig),
+ };
+ } else {
+ throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`);
+ }
+ },
+
+ /**
+ * Unblind a blindly signed value.
+ */
+ async rsaUnblind(
+ tci: TalerCryptoInterfaceR,
+ req: RsaUnblindRequest,
+ ): Promise<RsaUnblindResponse> {
+ const denomSig = rsaUnblind(
+ decodeCrock(req.blindedSig),
+ decodeCrock(req.pk),
+ decodeCrock(req.bk),
+ );
+ return { sig: encodeCrock(denomSig) };
+ },
+
+ /**
+ * Unblind a blindly signed value.
+ */
+ async rsaVerify(
+ tci: TalerCryptoInterfaceR,
+ req: RsaVerificationRequest,
+ ): Promise<ValidationResult> {
+ return {
+ valid: rsaVerify(
+ hash(decodeCrock(req.hm)),
+ decodeCrock(req.sig),
+ decodeCrock(req.pk),
+ ),
+ };
+ },
+
+ /**
+ * Generate updated coins (to store in the database)
+ * and deposit permissions for each given coin.
+ */
+ async signDepositPermission(
+ tci: TalerCryptoInterfaceR,
+ depositInfo: DepositInfo,
+ ): Promise<CoinDepositPermission> {
+ // FIXME: put extensions here if used
+ const hExt = new Uint8Array(64);
+ let hAgeCommitment: Uint8Array;
+ let minimumAgeSig: string | undefined = undefined;
+ if (depositInfo.ageCommitmentProof) {
+ const ach = AgeRestriction.hashCommitment(
+ depositInfo.ageCommitmentProof.commitment,
+ );
+ hAgeCommitment = decodeCrock(ach);
+ if (depositInfo.requiredMinimumAge) {
+ minimumAgeSig = encodeCrock(
+ AgeRestriction.commitmentAttest(
+ depositInfo.ageCommitmentProof,
+ depositInfo.requiredMinimumAge,
+ ),
+ );
+ }
+ } else {
+ // 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)
+ .put(decodeCrock(depositInfo.contractTermsHash))
+ .put(hAgeCommitment)
+ .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))
+ .put(walletDataHash)
+ .build();
+ } else {
+ throw Error("unsupported exchange protocol version");
+ }
+ const coinSigRes = await this.eddsaSign(tci, {
+ msg: encodeCrock(d),
+ priv: depositInfo.coinPriv,
+ });
+
+ if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
+ const s: CoinDepositPermission = {
+ coin_pub: depositInfo.coinPub,
+ coin_sig: coinSigRes.sig,
+ contribution: Amounts.stringify(depositInfo.spendAmount),
+ h_denom: depositInfo.denomPubHash,
+ exchange_url: depositInfo.exchangeBaseUrl,
+ ub_sig: {
+ cipher: DenomKeyType.Rsa,
+ rsa_signature: depositInfo.denomSig.rsa_signature,
+ },
+ };
+
+ if (depositInfo.requiredMinimumAge) {
+ // These are only required by the merchant
+ s.minimum_age_sig = minimumAgeSig;
+ s.age_commitment =
+ depositInfo.ageCommitmentProof?.commitment.publicKeys;
+ } else if (depositInfo.ageCommitmentProof) {
+ s.h_age_commitment = encodeCrock(hAgeCommitment);
+ }
+
+ return s;
+ } else {
+ throw Error(
+ `unsupported denomination cipher (${depositInfo.denomKeyType})`,
+ );
+ }
+ },
+
+ async deriveRefreshSession(
+ tci: TalerCryptoInterfaceR,
+ req: DeriveRefreshSessionRequest,
+ ): Promise<DerivedRefreshSession> {
+ const {
+ newCoinDenoms,
+ feeRefresh: meltFee,
+ kappa,
+ meltCoinDenomPubHash,
+ meltCoinPriv,
+ meltCoinPub,
+ sessionSecretSeed: refreshSessionSecretSeed,
+ } = req;
+
+ const currency = Amounts.currencyOf(newCoinDenoms[0].value);
+ let valueWithFee = Amounts.zeroOfCurrency(currency);
+
+ for (const ncd of newCoinDenoms) {
+ const t = Amounts.add(ncd.value, ncd.feeWithdraw).amount;
+ valueWithFee = Amounts.add(
+ valueWithFee,
+ Amounts.mult(t, ncd.count).amount,
+ ).amount;
+ }
+
+ // melt fee
+ valueWithFee = Amounts.add(valueWithFee, meltFee).amount;
+
+ const sessionHc = createHashContext();
+
+ const transferPubs: string[] = [];
+ const transferPrivs: string[] = [];
+
+ const planchetsForGammas: RefreshPlanchetInfo[][] = [];
+
+ for (let i = 0; i < kappa; i++) {
+ const transferKeyPair = await tci.setupRefreshTransferPub(tci, {
+ secretSeed: refreshSessionSecretSeed,
+ transferPubIndex: i,
+ });
+ sessionHc.update(decodeCrock(transferKeyPair.transferPub));
+ transferPrivs.push(transferKeyPair.transferPriv);
+ transferPubs.push(transferKeyPair.transferPub);
+ }
+
+ for (const denomSel of newCoinDenoms) {
+ for (let i = 0; i < denomSel.count; i++) {
+ if (denomSel.denomPub.cipher === DenomKeyType.Rsa) {
+ const denomPubHash = hashDenomPub(denomSel.denomPub);
+ sessionHc.update(denomPubHash);
+ } else {
+ throw new Error();
+ }
+ }
+ }
+
+ sessionHc.update(decodeCrock(meltCoinPub));
+ sessionHc.update(amountToBuffer(valueWithFee));
+
+ for (let i = 0; i < kappa; i++) {
+ const planchets: RefreshPlanchetInfo[] = [];
+ for (let j = 0; j < newCoinDenoms.length; j++) {
+ const denomSel = newCoinDenoms[j];
+ for (let k = 0; k < denomSel.count; k++) {
+ const coinIndex = planchets.length;
+ const transferSecretRes = await tci.keyExchangeEcdheEddsa(tci, {
+ ecdhePriv: transferPrivs[i],
+ eddsaPub: meltCoinPub,
+ });
+ let coinPub: Uint8Array;
+ let coinPriv: Uint8Array;
+ let blindingFactor: Uint8Array;
+ let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, {
+ coinNumber: coinIndex,
+ transferSecret: transferSecretRes.h,
+ });
+ let newAc: AgeCommitmentProof | undefined = undefined;
+ let newAch: HashCodeString | undefined = undefined;
+ if (req.meltCoinAgeCommitmentProof) {
+ newAc = await AgeRestriction.commitmentDerive(
+ req.meltCoinAgeCommitmentProof,
+ decodeCrock(transferSecretRes.h),
+ );
+ newAch = AgeRestriction.hashCommitment(newAc.commitment);
+ }
+ coinPriv = decodeCrock(fresh.coinPriv);
+ coinPub = decodeCrock(fresh.coinPub);
+ blindingFactor = decodeCrock(fresh.bks);
+ const coinPubHash = hashCoinPub(fresh.coinPub, newAch);
+ if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
+ throw Error("unsupported cipher, can't create refresh session");
+ }
+ const blindResult = await tci.rsaBlind(tci, {
+ bks: encodeCrock(blindingFactor),
+ hm: encodeCrock(coinPubHash),
+ pub: denomSel.denomPub.rsa_public_key,
+ });
+ const coinEv: CoinEnvelope = {
+ cipher: DenomKeyType.Rsa,
+ rsa_blinded_planchet: blindResult.blinded,
+ };
+ const coinEvHash = hashCoinEv(
+ coinEv,
+ encodeCrock(hashDenomPub(denomSel.denomPub)),
+ );
+ const planchet: RefreshPlanchetInfo = {
+ blindingKey: encodeCrock(blindingFactor),
+ coinEv,
+ coinPriv: encodeCrock(coinPriv),
+ coinPub: encodeCrock(coinPub),
+ coinEvHash: encodeCrock(coinEvHash),
+ maxAge: req.meltCoinMaxAge,
+ ageCommitmentProof: newAc,
+ };
+ planchets.push(planchet);
+ hashCoinEvInner(coinEv, sessionHc);
+ }
+ }
+ planchetsForGammas.push(planchets);
+ }
+
+ const sessionHash = sessionHc.finish();
+ let confirmData: Uint8Array;
+ let hAgeCommitment: Uint8Array;
+ if (req.meltCoinAgeCommitmentProof) {
+ hAgeCommitment = decodeCrock(
+ AgeRestriction.hashCommitment(
+ req.meltCoinAgeCommitmentProof.commitment,
+ ),
+ );
+ } else {
+ hAgeCommitment = new Uint8Array(32);
+ }
+ confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
+ .put(sessionHash)
+ .put(decodeCrock(meltCoinDenomPubHash))
+ .put(hAgeCommitment)
+ .put(amountToBuffer(valueWithFee))
+ .put(amountToBuffer(meltFee))
+ .build();
+
+ const confirmSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(confirmData),
+ priv: meltCoinPriv,
+ });
+
+ const refreshSession: DerivedRefreshSession = {
+ confirmSig: confirmSigResp.sig,
+ hash: encodeCrock(sessionHash),
+ meltCoinPub: meltCoinPub,
+ planchetsForGammas: planchetsForGammas,
+ transferPrivs,
+ transferPubs,
+ meltValueWithFee: valueWithFee,
+ };
+
+ return refreshSession;
+ },
+
+ /**
+ * Hash a string including the zero terminator.
+ */
+ async hashString(
+ tci: TalerCryptoInterfaceR,
+ req: HashStringRequest,
+ ): Promise<HashStringResult> {
+ const b = stringToBytes(req.str + "\0");
+ return { h: encodeCrock(hash(b)) };
+ },
+
+ async signCoinLink(
+ tci: TalerCryptoInterfaceR,
+ req: SignCoinLinkRequest,
+ ): Promise<EddsaSigningResult> {
+ const coinEvHash = hashCoinEv(req.coinEv, req.newDenomHash);
+ // FIXME: fill in
+ const hAgeCommitment = new Uint8Array(32);
+ const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK)
+ .put(decodeCrock(req.newDenomHash))
+ .put(decodeCrock(req.transferPub))
+ .put(hAgeCommitment)
+ .put(coinEvHash)
+ .build();
+ return tci.eddsaSign(tci, {
+ msg: encodeCrock(coinLink),
+ priv: req.oldCoinPriv,
+ });
+ },
+
+ async makeSyncSignature(
+ tci: TalerCryptoInterfaceR,
+ req: MakeSyncSignatureRequest,
+ ): Promise<EddsaSigningResult> {
+ const hNew = decodeCrock(req.newHash);
+ let hOld: Uint8Array;
+ if (req.oldHash) {
+ hOld = decodeCrock(req.oldHash);
+ } else {
+ hOld = new Uint8Array(64);
+ }
+ const sigBlob = buildSigPS(TalerSignaturePurpose.SYNC_BACKUP_UPLOAD)
+ .put(hOld)
+ .put(hNew)
+ .build();
+ const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
+ return { sig: encodeCrock(uploadSig) };
+ },
+ async keyExchangeEcdheEddsa(
+ tci: TalerCryptoInterfaceR,
+ req: KeyExchangeEcdheEddsaRequest,
+ ): Promise<KeyExchangeResult> {
+ return {
+ h: encodeCrock(
+ keyExchangeEcdhEddsa(
+ decodeCrock(req.ecdhePriv),
+ decodeCrock(req.eddsaPub),
+ ),
+ ),
+ };
+ },
+ async ecdheGetPublic(
+ tci: TalerCryptoInterfaceR,
+ req: EcdheGetPublicRequest,
+ ): Promise<EcdheGetPublicResponse> {
+ return {
+ pub: encodeCrock(ecdhGetPublic(decodeCrock(req.priv))),
+ };
+ },
+ async setupRefreshTransferPub(
+ tci: TalerCryptoInterfaceR,
+ req: SetupRefreshTransferPubRequest,
+ ): Promise<TransferPubResponse> {
+ const info = stringToBytes("taler-transfer-pub-derivation");
+ const saltArrBuf = new ArrayBuffer(4);
+ const salt = new Uint8Array(saltArrBuf);
+ const saltDataView = new DataView(saltArrBuf);
+ saltDataView.setUint32(0, req.transferPubIndex);
+ const out = kdf(32, decodeCrock(req.secretSeed), salt, info);
+ const transferPriv = encodeCrock(out);
+ return {
+ transferPriv,
+ transferPub: (await tci.ecdheGetPublic(tci, { priv: transferPriv })).pub,
+ };
+ },
+ async signPurseCreation(
+ tci: TalerCryptoInterfaceR,
+ req: SignPurseCreationRequest,
+ ): Promise<EddsaSigningResult> {
+ const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
+ .put(timestampRoundedToBuffer(req.purseExpiration))
+ .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
+ .put(decodeCrock(req.hContractTerms))
+ .put(decodeCrock(req.mergePub))
+ .put(bufferForUint32(req.minAge))
+ .build();
+ return await tci.eddsaSign(tci, {
+ msg: encodeCrock(sigBlob),
+ priv: req.pursePriv,
+ });
+ },
+ async signPurseDeposits(
+ tci: TalerCryptoInterfaceR,
+ req: SignPurseDepositsRequest,
+ ): Promise<SignPurseDepositsResponse> {
+ const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0"));
+ const deposits: PurseDeposit[] = [];
+ for (const c of req.coins) {
+ let maybeAch: Uint8Array;
+ if (c.ageCommitmentProof) {
+ maybeAch = decodeCrock(
+ AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment),
+ );
+ } else {
+ maybeAch = new Uint8Array(32);
+ }
+ const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
+ .put(amountToBuffer(Amounts.parseOrThrow(c.contribution)))
+ .put(decodeCrock(c.denomPubHash))
+ .put(maybeAch)
+ .put(decodeCrock(req.pursePub))
+ .put(hExchangeBaseUrl)
+ .build();
+ const sigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(sigBlob),
+ priv: c.coinPriv,
+ });
+ deposits.push({
+ amount: c.contribution,
+ coin_pub: c.coinPub,
+ coin_sig: sigResp.sig,
+ denom_pub_hash: c.denomPubHash,
+ ub_sig: c.denomSig,
+ age_commitment: c.ageCommitmentProof
+ ? c.ageCommitmentProof.commitment.publicKeys
+ : undefined,
+ });
+ }
+ return {
+ deposits,
+ };
+ },
+ async encryptContractForMerge(
+ tci: TalerCryptoInterfaceR,
+ req: EncryptContractRequest,
+ ): Promise<EncryptContractResponse> {
+ const enc = await encryptContractForMerge(
+ decodeCrock(req.pursePub),
+ decodeCrock(req.contractPriv),
+ decodeCrock(req.mergePriv),
+ req.contractTerms,
+ decodeCrock(req.nonce),
+ );
+ const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
+ .put(hash(enc))
+ .put(decodeCrock(req.contractPub))
+ .build();
+ const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
+ return {
+ econtract: {
+ contract_pub: req.contractPub,
+ econtract: encodeCrock(enc),
+ econtract_sig: encodeCrock(sig),
+ },
+ };
+ },
+ async decryptContractForMerge(
+ tci: TalerCryptoInterfaceR,
+ req: DecryptContractRequest,
+ ): Promise<DecryptContractResponse> {
+ const res = await decryptContractForMerge(
+ decodeCrock(req.ciphertext),
+ decodeCrock(req.pursePub),
+ decodeCrock(req.contractPriv),
+ );
+ return {
+ contractTerms: res.contractTerms,
+ mergePriv: encodeCrock(res.mergePriv),
+ };
+ },
+ async encryptContractForDeposit(
+ tci: TalerCryptoInterfaceR,
+ req: EncryptContractForDepositRequest,
+ ): Promise<EncryptContractForDepositResponse> {
+ const enc = await encryptContractForDeposit(
+ decodeCrock(req.pursePub),
+ decodeCrock(req.contractPriv),
+ req.contractTerms,
+ decodeCrock(req.nonce),
+ );
+ const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
+ .put(hash(enc))
+ .put(decodeCrock(req.contractPub))
+ .build();
+ const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
+ return {
+ econtract: {
+ contract_pub: req.contractPub,
+ econtract: encodeCrock(enc),
+ econtract_sig: encodeCrock(sig),
+ },
+ };
+ },
+ async decryptContractForDeposit(
+ tci: TalerCryptoInterfaceR,
+ req: DecryptContractForDepositRequest,
+ ): Promise<DecryptContractForDepositResponse> {
+ const res = await decryptContractForDeposit(
+ decodeCrock(req.ciphertext),
+ decodeCrock(req.pursePub),
+ decodeCrock(req.contractPriv),
+ );
+ return {
+ contractTerms: res.contractTerms,
+ };
+ },
+ async signPurseMerge(
+ tci: TalerCryptoInterfaceR,
+ req: SignPurseMergeRequest,
+ ): Promise<SignPurseMergeResponse> {
+ const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE)
+ .put(timestampRoundedToBuffer(req.mergeTimestamp))
+ .put(decodeCrock(req.pursePub))
+ .put(hashTruncate32(stringToBytes(req.reservePayto + "\0")))
+ .build();
+ const mergeSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(mergeSigBlob),
+ priv: req.mergePriv,
+ });
+
+ const reserveSigBlob = buildSigPS(
+ TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
+ )
+ .put(timestampRoundedToBuffer(req.purseExpiration))
+ .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
+ .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee)))
+ .put(decodeCrock(req.contractTermsHash))
+ .put(decodeCrock(req.pursePub))
+ .put(timestampRoundedToBuffer(req.mergeTimestamp))
+ // FIXME: put in min_age
+ .put(bufferForUint32(0))
+ .put(bufferForUint32(req.flags))
+ .build();
+
+ logger.info(
+ `signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`,
+ );
+
+ const reserveSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(reserveSigBlob),
+ priv: req.reservePriv,
+ });
+
+ return {
+ mergeSig: mergeSigResp.sig,
+ accountSig: reserveSigResp.sig,
+ };
+ },
+ async signReservePurseCreate(
+ tci: TalerCryptoInterfaceR,
+ req: SignReservePurseCreateRequest,
+ ): Promise<SignReservePurseCreateResponse> {
+ const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE)
+ .put(timestampRoundedToBuffer(req.mergeTimestamp))
+ .put(decodeCrock(req.pursePub))
+ .put(hashTruncate32(stringToBytes(req.reservePayto + "\0")))
+ .build();
+ const mergeSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(mergeSigBlob),
+ priv: req.mergePriv,
+ });
+
+ logger.info(`payto URI: ${req.reservePayto}`);
+ logger.info(`signing WALLET_PURSE_MERGE over ${encodeCrock(mergeSigBlob)}`);
+
+ const reserveSigBlob = buildSigPS(
+ TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
+ )
+ .put(timestampRoundedToBuffer(req.purseExpiration))
+ .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
+ .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee)))
+ .put(decodeCrock(req.contractTermsHash))
+ .put(decodeCrock(req.pursePub))
+ .put(timestampRoundedToBuffer(req.mergeTimestamp))
+ // FIXME: put in min_age
+ .put(bufferForUint32(0))
+ .put(bufferForUint32(req.flags))
+ .build();
+
+ logger.info(
+ `signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`,
+ );
+
+ const reserveSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(reserveSigBlob),
+ priv: req.reservePriv,
+ });
+
+ const mergePub = encodeCrock(eddsaGetPublic(decodeCrock(req.mergePriv)));
+
+ const purseSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
+ .put(timestampRoundedToBuffer(req.purseExpiration))
+ .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
+ .put(decodeCrock(req.contractTermsHash))
+ .put(decodeCrock(mergePub))
+ // FIXME: add age!
+ .put(bufferForUint32(0))
+ .build();
+
+ const purseSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(purseSigBlob),
+ priv: req.pursePriv,
+ });
+
+ return {
+ mergeSig: mergeSigResp.sig,
+ accountSig: reserveSigResp.sig,
+ 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,
+ };
+ },
+};
+
+export interface EddsaSignRequest {
+ msg: string;
+ priv: string;
+}
+
+export interface EddsaSignResponse {
+ sig: string;
+}
+
+export const nativeCrypto: TalerCryptoInterface = Object.fromEntries(
+ Object.keys(nativeCryptoR).map((name) => {
+ return [
+ name,
+ (req: any) =>
+ nativeCryptoR[name as keyof TalerCryptoInterfaceR](nativeCryptoR, req),
+ ];
+ }),
+) as any;