From b214934b75418d0d01c9556577d9594f1db5a319 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 21 Jun 2022 12:40:12 +0200 Subject: wallet-core: P2P push payments (still incomplete) --- .../src/crypto/cryptoImplementation.ts | 120 +++++++++-- .../taler-wallet-core/src/crypto/cryptoTypes.ts | 2 +- packages/taler-wallet-core/src/db.ts | 70 +++++-- .../src/operations/peer-to-peer.ts | 222 +++++++++++++++++++++ packages/taler-wallet-core/src/wallet-api-types.ts | 9 + packages/taler-wallet-core/src/wallet.ts | 6 + 6 files changed, 400 insertions(+), 29 deletions(-) create mode 100644 packages/taler-wallet-core/src/operations/peer-to-peer.ts (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 7c6b00bcc..1d3641836 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -24,33 +24,44 @@ * Imports. */ -// FIXME: Crypto should not use DB Types! import { + AgeCommitmentProof, + AgeRestriction, AmountJson, Amounts, + AmountString, + BlindedDenominationSignature, + bufferForUint32, buildSigPS, CoinDepositPermission, CoinEnvelope, - createEddsaKeyPair, createHashContext, decodeCrock, DenomKeyType, DepositInfo, + ecdheGetPublic, eddsaGetPublic, + EddsaPublicKeyString, eddsaSign, eddsaVerify, encodeCrock, ExchangeProtocolVersion, + getRandomBytes, hash, + HashCodeString, hashCoinEv, hashCoinEvInner, + hashCoinPub, hashDenomPub, hashTruncate32, + kdf, + kdfKw, keyExchangeEcdheEddsa, Logger, MakeSyncSignatureRequest, PlanchetCreationRequest, - WithdrawalPlanchet, + PlanchetUnblindInfo, + PurseDeposit, RecoupRefreshRequest, RecoupRequest, RefreshPlanchetInfo, @@ -59,23 +70,14 @@ import { rsaVerify, setupTipPlanchet, stringToBytes, + TalerProtocolTimestamp, TalerSignaturePurpose, - BlindedDenominationSignature, UnblindedSignature, - PlanchetUnblindInfo, - TalerProtocolTimestamp, - kdfKw, - bufferForUint32, - kdf, - ecdheGetPublic, - getRandomBytes, - AgeCommitmentProof, - AgeRestriction, - hashCoinPub, - HashCodeString, + WithdrawalPlanchet, } from "@gnu-taler/taler-util"; import bigint from "big-integer"; -import { DenominationRecord, TipCoinSource, WireFee } from "../db.js"; +// FIXME: Crypto should not use DB Types! +import { DenominationRecord, WireFee } from "../db.js"; import { CreateRecoupRefreshReqRequest, CreateRecoupReqRequest, @@ -177,6 +179,12 @@ export interface TalerCryptoInterface { setupRefreshTransferPub( req: SetupRefreshTransferPubRequest, ): Promise; + + signPurseCreation(req: SignPurseCreationRequest): Promise; + + signPurseDeposits( + req: SignPurseDepositsRequest, + ): Promise; } /** @@ -308,6 +316,16 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise { throw new Error("Function not implemented."); }, + signPurseCreation: function ( + req: SignPurseCreationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + signPurseDeposits: function ( + req: SignPurseDepositsRequest, + ): Promise { + throw new Error("Function not implemented."); + }, }; export type WithArg = X extends (req: infer T) => infer R @@ -336,6 +354,31 @@ export interface SetupWithdrawalPlanchetRequest { coinNumber: number; } +export interface SignPurseCreationRequest { + pursePriv: string; + purseExpiration: TalerProtocolTimestamp; + purseAmount: AmountString; + hContractTerms: HashCodeString; + mergePub: EddsaPublicKeyString; + minAge: number; +} + +export interface SignPurseDepositsRequest { + pursePub: string; + exchangeBaseUrl: string; + coins: { + coinPub: string; + coinPriv: string; + contribution: AmountString; + denomPubHash: string; + denomSig: UnblindedSignature; + }[]; +} + +export interface SignPurseDepositsResponse { + deposits: PurseDeposit[]; +} + export interface RsaVerificationRequest { hm: string; sig: string; @@ -1212,6 +1255,51 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { transferPub: (await tci.ecdheGetPublic(tci, { priv: transferPriv })).pub, }; }, + async signPurseCreation( + tci: TalerCryptoInterfaceR, + req: SignPurseCreationRequest, + ): Promise { + 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 { + const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0")); + const deposits: PurseDeposit[] = []; + for (const c of req.coins) { + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT) + .put(amountToBuffer(Amounts.parseOrThrow(c.contribution))) + .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, + h_age_commitment: undefined, + }); + } + return { + deposits, + }; + }, }; function amountToBuffer(amount: AmountJson): Uint8Array { diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index fe5dbcec6..52b96b1a5 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -148,4 +148,4 @@ export interface CreateRecoupRefreshReqRequest { denomPub: DenominationPubKey; denomPubHash: string; denomSig: UnblindedSignature; -} +} \ No newline at end of file diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index eefa43113..8cf5170e5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1309,9 +1309,9 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState"; */ export type ConfigRecord = | { - key: typeof WALLET_BACKUP_STATE_KEY; - value: WalletBackupConfState; - } + key: typeof WALLET_BACKUP_STATE_KEY; + value: WalletBackupConfState; + } | { key: "currencyDefaultsApplied"; value: boolean }; export interface WalletBackupConfState { @@ -1497,17 +1497,17 @@ export enum BackupProviderStateTag { export type BackupProviderState = | { - tag: BackupProviderStateTag.Provisional; - } + tag: BackupProviderStateTag.Provisional; + } | { - tag: BackupProviderStateTag.Ready; - nextBackupTimestamp: TalerProtocolTimestamp; - } + tag: BackupProviderStateTag.Ready; + nextBackupTimestamp: TalerProtocolTimestamp; + } | { - tag: BackupProviderStateTag.Retrying; - retryInfo: RetryInfo; - lastError?: TalerErrorDetail; - }; + tag: BackupProviderStateTag.Retrying; + retryInfo: RetryInfo; + lastError?: TalerErrorDetail; + }; export interface BackupProviderTerms { supportedProtocolVersion: string; @@ -1671,6 +1671,52 @@ export interface BalancePerCurrencyRecord { pendingOutgoing: AmountString; } +/** + * Record for a push P2P payment that this wallet initiated. + */ +export interface PeerPushPaymentInitiationRecord { + + /** + * What exchange are funds coming from? + */ + exchangeBaseUrl: string; + + amount: AmountString; + + /** + * Purse public key. Used as the primary key to look + * up this record. + */ + pursePub: string; + + /** + * Purse private key. + */ + pursePriv: string; + + /** + * Public key of the merge capability of the purse. + */ + mergePub: string; + + /** + * Private key of the merge capability of the purse. + */ + mergePriv: string; + + purseExpiration: TalerProtocolTimestamp; + + /** + * Did we successfully create the purse with the exchange? + */ + purseCreated: boolean; +} + +/** + * Record for a push P2P payment that this wallet accepted. + */ +export interface PeerPushPaymentAcceptanceRecord {} + export const WalletStoresV1 = { coins: describeStore( describeContents("coins", { diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts new file mode 100644 index 000000000..e2ae1e66e --- /dev/null +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -0,0 +1,222 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU 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. + + GNU 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 + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + AmountJson, + Amounts, + Logger, + InitiatePeerPushPaymentResponse, + InitiatePeerPushPaymentRequest, + strcmp, + CoinPublicKeyString, + j2s, + getRandomBytes, + Duration, + durationAdd, + TalerProtocolTimestamp, + AbsoluteTime, + encodeCrock, + AmountString, + UnblindedSignature, +} from "@gnu-taler/taler-util"; +import { CoinStatus } from "../db.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; + +const logger = new Logger("operations/peer-to-peer.ts"); + +export interface PeerCoinSelection { + exchangeBaseUrl: string; + + /** + * Info of Coins that were selected. + */ + coins: { + coinPub: string; + coinPriv: string; + contribution: AmountString; + denomPubHash: string; + denomSig: UnblindedSignature; + }[]; + + /** + * How much of the deposit fees is the customer paying? + */ + depositFees: AmountJson; +} + +interface CoinInfo { + /** + * Public key of the coin. + */ + coinPub: string; + + coinPriv: string; + + /** + * Deposit fee for the coin. + */ + feeDeposit: AmountJson; + + value: AmountJson; + + denomPubHash: string; + + denomSig: UnblindedSignature; +} + +export async function initiatePeerToPeerPush( + ws: InternalWalletState, + req: InitiatePeerPushPaymentRequest, +): Promise { + const instructedAmount = Amounts.parseOrThrow(req.amount); + const coinSelRes: PeerCoinSelection | undefined = await ws.db + .mktx((x) => ({ + exchanges: x.exchanges, + coins: x.coins, + denominations: x.denominations, + })) + .runReadOnly(async (tx) => { + const exchanges = await tx.exchanges.iter().toArray(); + for (const exch of exchanges) { + if (exch.detailsPointer?.currency !== instructedAmount.currency) { + continue; + } + const coins = ( + await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl) + ).filter((x) => x.status === CoinStatus.Fresh); + const coinInfos: CoinInfo[] = []; + for (const coin of coins) { + const denom = await ws.getDenomInfo( + ws, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + if (!denom) { + throw Error("denom not found"); + } + coinInfos.push({ + coinPub: coin.coinPub, + feeDeposit: denom.feeDeposit, + value: denom.value, + denomPubHash: denom.denomPubHash, + coinPriv: coin.coinPriv, + denomSig: coin.denomSig, + }); + } + if (coinInfos.length === 0) { + continue; + } + coinInfos.sort( + (o1, o2) => + -Amounts.cmp(o1.value, o2.value) || + strcmp(o1.denomPubHash, o2.denomPubHash), + ); + let amountAcc = Amounts.getZero(instructedAmount.currency); + let depositFeesAcc = Amounts.getZero(instructedAmount.currency); + const resCoins: { + coinPub: string; + coinPriv: string; + contribution: AmountString; + denomPubHash: string; + denomSig: UnblindedSignature; + }[] = []; + for (const coin of coinInfos) { + if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { + const res: PeerCoinSelection = { + exchangeBaseUrl: exch.baseUrl, + coins: resCoins, + depositFees: depositFeesAcc, + }; + return res; + } + const gap = Amounts.add( + coin.feeDeposit, + Amounts.sub(instructedAmount, amountAcc).amount, + ).amount; + const contrib = Amounts.min(gap, coin.value); + amountAcc = Amounts.add( + amountAcc, + Amounts.sub(contrib, coin.feeDeposit).amount, + ).amount; + depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount; + resCoins.push({ + coinPriv: coin.coinPriv, + coinPub: coin.coinPub, + contribution: Amounts.stringify(contrib), + denomPubHash: coin.denomPubHash, + denomSig: coin.denomSig, + }); + } + continue; + } + return undefined; + }); + logger.info(`selected p2p coins: ${j2s(coinSelRes)}`); + + if (!coinSelRes) { + throw Error("insufficient balance"); + } + + const pursePair = await ws.cryptoApi.createEddsaKeypair({}); + const mergePair = await ws.cryptoApi.createEddsaKeypair({}); + const hContractTerms = encodeCrock(getRandomBytes(64)); + const purseExpiration = AbsoluteTime.toTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + + const purseSigResp = await ws.cryptoApi.signPurseCreation({ + hContractTerms, + mergePub: mergePair.pub, + minAge: 0, + purseAmount: Amounts.stringify(instructedAmount), + purseExpiration, + pursePriv: pursePair.priv, + }); + + const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ + exchangeBaseUrl: coinSelRes.exchangeBaseUrl, + pursePub: pursePair.pub, + coins: coinSelRes.coins, + }); + + const createPurseUrl = new URL( + `purses/${pursePair.pub}/create`, + coinSelRes.exchangeBaseUrl, + ); + + const httpResp = await ws.http.postJson(createPurseUrl.href, { + amount: Amounts.stringify(instructedAmount), + merge_pub: mergePair.pub, + purse_sig: purseSigResp.sig, + h_contract_terms: hContractTerms, + purse_expiration: purseExpiration, + deposits: depositSigsResp.deposits, + min_age: 0, + }); + + const resp = await httpResp.json(); + + logger.info(`resp: ${j2s(resp)}`); + + throw Error("not yet implemented"); +} diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 9acfbf103..5c0882ae0 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -46,6 +46,8 @@ import { GetExchangeTosResult, GetWithdrawalDetailsForAmountRequest, GetWithdrawalDetailsForUriRequest, + InitiatePeerPushPaymentRequest, + InitiatePeerPushPaymentResponse, IntegrationTestArgs, ManualWithdrawalDetails, PreparePayRequest, @@ -118,6 +120,9 @@ export enum WalletApiOperation { ExportBackupPlain = "exportBackupPlain", WithdrawFakebank = "withdrawFakebank", ExportDb = "exportDb", + InitiatePeerPushPayment = "initiatePeerPushPayment", + CheckPeerPushPayment = "checkPeerPushPayment", + AcceptPeerPushPayment = "acceptPeerPushPayment", } export type WalletOperations = { @@ -277,6 +282,10 @@ export type WalletOperations = { request: {}; response: any; }; + [WalletApiOperation.InitiatePeerPushPayment]: { + request: InitiatePeerPushPaymentRequest; + response: InitiatePeerPushPaymentResponse; + }; }; export type RequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index c7b94138e..d072f9e96 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -47,6 +47,7 @@ import { codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, codecForImportDbRequest, + codecForInitiatePeerPushPaymentRequest, codecForIntegrationTestArgs, codecForListKnownBankAccounts, codecForPrepareDepositRequest, @@ -143,6 +144,7 @@ import { processDownloadProposal, processPurchasePay, } from "./operations/pay.js"; +import { initiatePeerToPeerPush } from "./operations/peer-to-peer.js"; import { getPendingOperations } from "./operations/pending.js"; import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js"; import { @@ -1049,6 +1051,10 @@ async function dispatchRequestInternal( await importDb(ws.db.idbHandle(), req.dump); return []; } + case "initiatePeerPushPayment": { + const req = codecForInitiatePeerPushPaymentRequest().decode(payload); + return await initiatePeerToPeerPush(ws, req); + } } throw TalerError.fromDetail( TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, -- cgit v1.2.3