From f3ff5a72257dda27cab555f8b8d921d45bfc3e4b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 23 Aug 2022 11:29:45 +0200 Subject: peer-to-peer pull payments MVP p2p pull wip --- packages/taler-util/src/talerCrypto.ts | 51 +++++++++++++++++---- packages/taler-util/src/talerTypes.ts | 71 +++++++++++++++++++++++++++++ packages/taler-util/src/taleruri.ts | 66 +++++++++++++++++++++++++-- packages/taler-util/src/walletTypes.ts | 81 +++++++++++++++++++++++++++------- 4 files changed, 240 insertions(+), 29 deletions(-) (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index 38bb5ad0a..d7734707a 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -1214,6 +1214,9 @@ type ContractPrivateKey = FlavorP & type MergePrivateKey = FlavorP & MaterialEddsaPriv; +const mergeSalt = "p2p-merge-contract"; +const depositSalt = "p2p-deposit-contract"; + export function encryptContractForMerge( pursePub: PursePublicKey, contractPriv: ContractPrivateKey, @@ -1230,12 +1233,24 @@ export function encryptContractForMerge( contractTermsCompressed, ]); const key = keyExchangeEcdheEddsa(contractPriv, pursePub); - return encryptWithDerivedKey( - getRandomBytesF(24), - key, - data, - "p2p-merge-contract", - ); + return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt); +} + +export function encryptContractForDeposit( + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, + contractTerms: any, +): Promise { + const contractTermsCanon = canonicalJson(contractTerms) + "\0"; + const contractTermsBytes = stringToBytes(contractTermsCanon); + const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); + const data = typedArrayConcat([ + bufferForUint32(ContractFormatTag.PaymentRequest), + bufferForUint32(contractTermsBytes.length), + contractTermsCompressed, + ]); + const key = keyExchangeEcdheEddsa(contractPriv, pursePub); + return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt); } export interface DecryptForMergeResult { @@ -1243,13 +1258,17 @@ export interface DecryptForMergeResult { mergePriv: Uint8Array; } +export interface DecryptForDepositResult { + contractTerms: any; +} + export async function decryptContractForMerge( enc: OpaqueData, pursePub: PursePublicKey, contractPriv: ContractPrivateKey, ): Promise { const key = keyExchangeEcdheEddsa(contractPriv, pursePub); - const dec = await decryptWithDerivedKey(enc, key, "p2p-merge-contract"); + const dec = await decryptWithDerivedKey(enc, key, mergeSalt); const mergePriv = dec.slice(8, 8 + 32); const contractTermsCompressed = dec.slice(8 + 32); const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); @@ -1263,6 +1282,20 @@ export async function decryptContractForMerge( }; } -export function encryptContractForDeposit() { - throw Error("not implemented"); +export async function decryptContractForDeposit( + enc: OpaqueData, + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, +): Promise { + const key = keyExchangeEcdheEddsa(contractPriv, pursePub); + const dec = await decryptWithDerivedKey(enc, key, depositSalt); + const contractTermsCompressed = dec.slice(8); + const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); + // Slice of the '\0' at the end and decode to a string + const contractTermsString = bytesToString( + contractTermsBuf.slice(0, contractTermsBuf.length - 1), + ); + return { + contractTerms: JSON.parse(contractTermsString), + }; } diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index d4de8c37b..ee2dee93c 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -1874,3 +1874,74 @@ export interface PeerContractTerms { summary: string; purse_expiration: TalerProtocolTimestamp; } + +export interface EncryptedContract { + // Encrypted contract. + econtract: string; + + // Signature over the (encrypted) contract. + econtract_sig: string; + + // Ephemeral public key for the DH operation to decrypt the encrypted contract. + contract_pub: string; +} + +/** + * Payload for /reserves/{reserve_pub}/purse + * endpoint of the exchange. + */ +export interface ExchangeReservePurseRequest { + /** + * Minimum amount that must be credited to the reserve, that is + * the total value of the purse minus the deposit fees. + * If the deposit fees are lower, the contribution to the + * reserve can be higher! + */ + purse_value: AmountString; + + // Minimum age required for all coins deposited into the purse. + min_age: number; + + // Purse fee the reserve owner is willing to pay + // for the purse creation. Optional, if not present + // the purse is to be created from the purse quota + // of the reserve. + purse_fee: AmountString; + + // Optional encrypted contract, in case the buyer is + // proposing the contract and thus establishing the + // purse with the payment. + econtract?: EncryptedContract; + + // EdDSA public key used to approve merges of this purse. + merge_pub: EddsaPublicKeyString; + + // EdDSA signature of the purse private key affirming the merge + // over a TALER_PurseMergeSignaturePS. + // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. + merge_sig: EddsaSignatureString; + + // EdDSA signature of the account/reserve affirming the merge. + // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE + reserve_sig: EddsaSignatureString; + + // Purse public key. + purse_pub: EddsaPublicKeyString; + + // EdDSA signature of the purse over + // TALER_PurseRequestSignaturePS of + // purpose TALER_SIGNATURE_PURSE_REQUEST + // confirming that the + // above details hold for this purse. + purse_sig: EddsaSignatureString; + + // SHA-512 hash of the contact of the purse. + h_contract_terms: HashCodeString; + + // Client-side timestamp of when the merge request was made. + merge_timestamp: TalerProtocolTimestamp; + + // Indicative time by which the purse should expire + // if it has not been paid. + purse_expiration: TalerProtocolTimestamp; +} diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index e3bd120f0..e7d66d7d5 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -45,6 +45,11 @@ export interface PayPushUriResult { contractPriv: string; } +export interface PayPullUriResult { + exchangeBaseUrl: string; + contractPriv: string; +} + /** * Parse a taler[+http]://withdraw URI. * Return undefined if not passed a valid URI. @@ -84,10 +89,14 @@ export enum TalerUriType { TalerTip = "taler-tip", TalerRefund = "taler-refund", TalerNotifyReserve = "taler-notify-reserve", - TalerPayPush = "pay-push", + TalerPayPush = "taler-pay-push", + TalerPayPull = "taler-pay-pull", Unknown = "unknown", } +const talerActionPayPull = "pay-pull"; +const talerActionPayPush = "pay-push"; + /** * Classify a taler:// URI. */ @@ -117,12 +126,18 @@ export function classifyTalerUri(s: string): TalerUriType { if (sl.startsWith("taler+http://withdraw/")) { return TalerUriType.TalerWithdraw; } - if (sl.startsWith("taler://pay-push/")) { + if (sl.startsWith(`taler://${talerActionPayPush}/`)) { return TalerUriType.TalerPayPush; } - if (sl.startsWith("taler+http://pay-push/")) { + if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) { return TalerUriType.TalerPayPush; } + if (sl.startsWith(`taler://${talerActionPayPull}/`)) { + return TalerUriType.TalerPayPull; + } + if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) { + return TalerUriType.TalerPayPull; + } if (sl.startsWith("taler://notify-reserve/")) { return TalerUriType.TalerNotifyReserve; } @@ -189,7 +204,29 @@ export function parsePayUri(s: string): PayUriResult | undefined { } export function parsePayPushUri(s: string): PayPushUriResult | undefined { - const pi = parseProtoInfo(s, "pay-push"); + const pi = parseProtoInfo(s, talerActionPayPush); + if (!pi) { + return undefined; + } + const c = pi?.rest.split("?"); + const parts = c[0].split("/"); + if (parts.length < 2) { + return undefined; + } + const host = parts[0].toLowerCase(); + const contractPriv = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const p = [host, ...pathSegments].join("/"); + const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); + + return { + exchangeBaseUrl, + contractPriv, + }; +} + +export function parsePayPullUri(s: string): PayPullUriResult | undefined { + const pi = parseProtoInfo(s, talerActionPayPull); if (!pi) { return undefined; } @@ -283,3 +320,24 @@ export function constructPayPushUri(args: { } return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`; } + +export function constructPayPullUri(args: { + exchangeBaseUrl: string; + contractPriv: string; +}): string { + const url = new URL(args.exchangeBaseUrl); + let proto: string; + if (url.protocol === "https:") { + proto = "taler"; + } else if (url.protocol === "http:") { + proto = "taler+http"; + } else { + throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`); + } + if (!url.pathname.endsWith("/")) { + throw Error( + `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`, + ); + } + return `${proto}://pay-pull/${url.host}${url.pathname}${args.contractPriv}`; +} diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 7b482c60e..3a415b221 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -627,7 +627,7 @@ export interface ExchangeAccount { master_sig: string; } -export type WireFeeMap = { [wireMethod: string]: WireFee[] } +export type WireFeeMap = { [wireMethod: string]: WireFee[] }; export interface WireInfo { feesForType: WireFeeMap; accounts: ExchangeAccount[]; @@ -639,7 +639,6 @@ const codecForExchangeAccount = (): Codec => .property("master_sig", codecForString()) .build("codecForExchangeAccount"); - const codecForWireFee = (): Codec => buildCodecForObject() .property("sig", codecForString()) @@ -658,19 +657,18 @@ const codecForWireInfo = (): Codec => const codecForDenominationInfo = (): Codec => buildCodecForObject() - .property("denomPubHash", (codecForString())) - .property("value", (codecForAmountJson())) - .property("feeWithdraw", (codecForAmountJson())) - .property("feeDeposit", (codecForAmountJson())) - .property("feeRefresh", (codecForAmountJson())) - .property("feeRefund", (codecForAmountJson())) - .property("stampStart", (codecForTimestamp)) - .property("stampExpireWithdraw", (codecForTimestamp)) - .property("stampExpireLegal", (codecForTimestamp)) - .property("stampExpireDeposit", (codecForTimestamp)) + .property("denomPubHash", codecForString()) + .property("value", codecForAmountJson()) + .property("feeWithdraw", codecForAmountJson()) + .property("feeDeposit", codecForAmountJson()) + .property("feeRefresh", codecForAmountJson()) + .property("feeRefund", codecForAmountJson()) + .property("stampStart", codecForTimestamp) + .property("stampExpireWithdraw", codecForTimestamp) + .property("stampExpireLegal", codecForTimestamp) + .property("stampExpireDeposit", codecForTimestamp) .build("codecForDenominationInfo"); - export interface DenominationInfo { value: AmountJson; denomPubHash: string; @@ -713,7 +711,6 @@ export interface DenominationInfo { * Data after which coins of this denomination can't be deposited anymore. */ stampExpireDeposit: TalerProtocolTimestamp; - } export interface ExchangeListItem { @@ -726,7 +723,6 @@ export interface ExchangeListItem { denominations: DenominationInfo[]; } - const codecForAuditorDenomSig = (): Codec => buildCodecForObject() .property("denom_pub_h", codecForString()) @@ -740,7 +736,6 @@ const codecForExchangeAuditor = (): Codec => .property("denomination_keys", codecForList(codecForAuditorDenomSig())) .build("codecForExchangeAuditor"); - const codecForExchangeTos = (): Codec => buildCodecForObject() .property("acceptedVersion", codecOptional(codecForString())) @@ -1452,18 +1447,34 @@ export interface CheckPeerPushPaymentRequest { talerUri: string; } +export interface CheckPeerPullPaymentRequest { + talerUri: string; +} + export interface CheckPeerPushPaymentResponse { contractTerms: any; amount: AmountString; peerPushPaymentIncomingId: string; } +export interface CheckPeerPullPaymentResponse { + contractTerms: any; + amount: AmountString; + peerPullPaymentIncomingId: string; +} + export const codecForCheckPeerPushPaymentRequest = (): Codec => buildCodecForObject() .property("talerUri", codecForString()) .build("CheckPeerPushPaymentRequest"); +export const codecForCheckPeerPullPaymentRequest = + (): Codec => + buildCodecForObject() + .property("talerUri", codecForString()) + .build("CheckPeerPullPaymentRequest"); + export interface AcceptPeerPushPaymentRequest { /** * Transparent identifier of the incoming peer push payment. @@ -1476,3 +1487,41 @@ export const codecForAcceptPeerPushPaymentRequest = buildCodecForObject() .property("peerPushPaymentIncomingId", codecForString()) .build("AcceptPeerPushPaymentRequest"); + +export interface AcceptPeerPullPaymentRequest { + /** + * Transparent identifier of the incoming peer pull payment. + */ + peerPullPaymentIncomingId: string; +} + +export const codecForAcceptPeerPullPaymentRequest = + (): Codec => + buildCodecForObject() + .property("peerPullPaymentIncomingId", codecForString()) + .build("AcceptPeerPllPaymentRequest"); + +export interface InitiatePeerPullPaymentRequest { + /** + * FIXME: Make this optional? + */ + exchangeBaseUrl: string; + amount: AmountString; + partialContractTerms: any; +} + +export const codecForInitiatePeerPullPaymentRequest = + (): Codec => + buildCodecForObject() + .property("partialContractTerms", codecForAny()) + .property("amount", codecForAmountString()) + .property("exchangeBaseUrl", codecForAmountString()) + .build("InitiatePeerPullPaymentRequest"); + +export interface InitiatePeerPullPaymentResponse { + /** + * Taler URI for the other party to make the payment + * that was requested. + */ + talerUri: string; +} -- cgit v1.2.3