aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-08-23 11:29:45 +0200
committerFlorian Dold <florian@dold.me>2022-08-23 20:35:11 +0200
commitf3ff5a72257dda27cab555f8b8d921d45bfc3e4b (patch)
tree708494a3aa2346fead51931fe4bfc41cc0249659 /packages/taler-util
parent4ca38113abee2c0ca17b26aa55cf6a0ecafe49c9 (diff)
peer-to-peer pull payments MVP
p2p pull wip
Diffstat (limited to 'packages/taler-util')
-rw-r--r--packages/taler-util/src/talerCrypto.ts51
-rw-r--r--packages/taler-util/src/talerTypes.ts71
-rw-r--r--packages/taler-util/src/taleruri.ts66
-rw-r--r--packages/taler-util/src/walletTypes.ts81
4 files changed, 240 insertions, 29 deletions
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<Uint8Array, "ContractPrivateKey", 32> &
type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
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<OpaqueData> {
+ 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<DecryptForMergeResult> {
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<DecryptForDepositResult> {
+ 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<ExchangeAccount> =>
.property("master_sig", codecForString())
.build("codecForExchangeAccount");
-
const codecForWireFee = (): Codec<WireFee> =>
buildCodecForObject<WireFee>()
.property("sig", codecForString())
@@ -658,19 +657,18 @@ const codecForWireInfo = (): Codec<WireInfo> =>
const codecForDenominationInfo = (): Codec<DenominationInfo> =>
buildCodecForObject<DenominationInfo>()
- .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<AuditorDenomSig> =>
buildCodecForObject<AuditorDenomSig>()
.property("denom_pub_h", codecForString())
@@ -740,7 +736,6 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
.build("codecForExchangeAuditor");
-
const codecForExchangeTos = (): Codec<ExchangeTos> =>
buildCodecForObject<ExchangeTos>()
.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<CheckPeerPushPaymentRequest> =>
buildCodecForObject<CheckPeerPushPaymentRequest>()
.property("talerUri", codecForString())
.build("CheckPeerPushPaymentRequest");
+export const codecForCheckPeerPullPaymentRequest =
+ (): Codec<CheckPeerPullPaymentRequest> =>
+ buildCodecForObject<CheckPeerPullPaymentRequest>()
+ .property("talerUri", codecForString())
+ .build("CheckPeerPullPaymentRequest");
+
export interface AcceptPeerPushPaymentRequest {
/**
* Transparent identifier of the incoming peer push payment.
@@ -1476,3 +1487,41 @@ export const codecForAcceptPeerPushPaymentRequest =
buildCodecForObject<AcceptPeerPushPaymentRequest>()
.property("peerPushPaymentIncomingId", codecForString())
.build("AcceptPeerPushPaymentRequest");
+
+export interface AcceptPeerPullPaymentRequest {
+ /**
+ * Transparent identifier of the incoming peer pull payment.
+ */
+ peerPullPaymentIncomingId: string;
+}
+
+export const codecForAcceptPeerPullPaymentRequest =
+ (): Codec<AcceptPeerPullPaymentRequest> =>
+ buildCodecForObject<AcceptPeerPullPaymentRequest>()
+ .property("peerPullPaymentIncomingId", codecForString())
+ .build("AcceptPeerPllPaymentRequest");
+
+export interface InitiatePeerPullPaymentRequest {
+ /**
+ * FIXME: Make this optional?
+ */
+ exchangeBaseUrl: string;
+ amount: AmountString;
+ partialContractTerms: any;
+}
+
+export const codecForInitiatePeerPullPaymentRequest =
+ (): Codec<InitiatePeerPullPaymentRequest> =>
+ buildCodecForObject<InitiatePeerPullPaymentRequest>()
+ .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;
+}