aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-07-12 17:41:14 +0200
committerFlorian Dold <florian@dold.me>2022-07-12 17:41:14 +0200
commitf11483b511ff1f839b9913c4832eee9109f67aeb (patch)
tree6f4e1c5891a24bbb7500cea3964d3826d2ef87e1
parentb214934b75418d0d01c9556577d9594f1db5a319 (diff)
wallet-core: implement accepting p2p push payments
-rw-r--r--packages/anastasis-core/src/crypto.ts6
-rw-r--r--packages/taler-util/package.json1
-rw-r--r--packages/taler-util/src/index.ts1
-rw-r--r--packages/taler-util/src/talerCrypto.test.ts28
-rw-r--r--packages/taler-util/src/talerCrypto.ts253
-rw-r--r--packages/taler-util/src/talerTypes.ts42
-rw-r--r--packages/taler-util/src/walletTypes.ts35
-rw-r--r--packages/taler-wallet-cli/src/index.ts9
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts26
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts132
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts83
-rw-r--r--packages/taler-wallet-core/src/db.ts40
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts52
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/peer-to-peer.ts270
-rw-r--r--packages/taler-wallet-core/src/util/contractTerms.test.ts122
-rw-r--r--packages/taler-wallet-core/src/util/contractTerms.ts230
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts11
-rw-r--r--packages/taler-wallet-core/src/wallet.ts17
-rw-r--r--pnpm-lock.yaml2
20 files changed, 898 insertions, 464 deletions
diff --git a/packages/anastasis-core/src/crypto.ts b/packages/anastasis-core/src/crypto.ts
index 815f84c11..5e45f995f 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -227,11 +227,11 @@ async function anastasisDecrypt(
const nonceBuf = ctBuf.slice(0, nonceSize);
const enc = ctBuf.slice(nonceSize);
const key = await deriveKey(keySeed, encodeCrock(nonceBuf), salt);
- const cipherText = secretbox_open(enc, nonceBuf, key);
- if (!cipherText) {
+ const clearText = secretbox_open(enc, nonceBuf, key);
+ if (!clearText) {
throw Error("could not decrypt");
}
- return encodeCrock(cipherText);
+ return encodeCrock(clearText);
}
export const asOpaque = (x: string): OpaqueData => x;
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 42ca8cb2a..af87742cd 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -38,6 +38,7 @@
},
"dependencies": {
"big-integer": "^1.6.51",
+ "fflate": "^0.7.3",
"jed": "^1.1.1",
"tslib": "^2.3.1"
},
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 199218d69..cf48ba803 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -32,3 +32,4 @@ export {
} from "./nacl-fast.js";
export { RequestThrottler } from "./RequestThrottler.js";
export * from "./CancellationToken.js";
+export * from "./contractTerms.js";
diff --git a/packages/taler-util/src/talerCrypto.test.ts b/packages/taler-util/src/talerCrypto.test.ts
index 5e8f37d80..b4a0106fa 100644
--- a/packages/taler-util/src/talerCrypto.test.ts
+++ b/packages/taler-util/src/talerCrypto.test.ts
@@ -374,7 +374,7 @@ test("taler age restriction crypto", async (t) => {
const priv1 = await Edx25519.keyCreate();
const pub1 = await Edx25519.getPublic(priv1);
- const seed = encodeCrock(getRandomBytes(32));
+ const seed = getRandomBytes(32);
const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
@@ -392,18 +392,18 @@ test("edx signing", async (t) => {
const sig = nacl.crypto_edx25519_sign_detached(
msg,
- decodeCrock(priv1),
- decodeCrock(pub1),
+ priv1,
+ pub1,
);
t.true(
- nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
+ nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1),
);
sig[0]++;
t.false(
- nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
+ nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1),
);
});
@@ -421,13 +421,19 @@ test("edx test vector", async (t) => {
};
{
- const pub1Prime = await Edx25519.getPublic(tv.priv1_edx);
- t.is(pub1Prime, tv.pub1_edx);
+ const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
+ t.is(pub1Prime, decodeCrock(tv.pub1_edx));
}
- const pub2Prime = await Edx25519.publicKeyDerive(tv.pub1_edx, tv.seed);
- t.is(pub2Prime, tv.pub2_edx);
+ const pub2Prime = await Edx25519.publicKeyDerive(
+ decodeCrock(tv.pub1_edx),
+ decodeCrock(tv.seed),
+ );
+ t.is(pub2Prime, decodeCrock(tv.pub2_edx));
- const priv2Prime = await Edx25519.privateKeyDerive(tv.priv1_edx, tv.seed);
- t.is(priv2Prime, tv.priv2_edx);
+ const priv2Prime = await Edx25519.privateKeyDerive(
+ decodeCrock(tv.priv1_edx),
+ decodeCrock(tv.seed),
+ );
+ t.is(priv2Prime, decodeCrock(tv.priv2_edx));
});
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts
index 188f5ec0a..5de767dda 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -25,7 +25,6 @@ import * as nacl from "./nacl-fast.js";
import { kdf, kdfKw } from "./kdf.js";
import bigint from "big-integer";
import {
- Base32String,
CoinEnvelope,
CoinPublicKeyString,
DenominationPubKey,
@@ -33,11 +32,29 @@ import {
HashCodeString,
} from "./talerTypes.js";
import { Logger } from "./logging.js";
+import { secretbox } from "./nacl-fast.js";
+import * as fflate from "fflate";
+import { canonicalJson } from "./helpers.js";
+
+export type Flavor<T, FlavorT extends string> = T & {
+ _flavor?: `taler.${FlavorT}`;
+};
+
+export type FlavorP<T, FlavorT extends string, S extends number> = T & {
+ _flavor?: `taler.${FlavorT}`;
+ _size?: S;
+};
export function getRandomBytes(n: number): Uint8Array {
return nacl.randomBytes(n);
}
+export function getRandomBytesF<T extends number, N extends string>(
+ n: T,
+): FlavorP<Uint8Array, N, T> {
+ return nacl.randomBytes(n);
+}
+
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
class EncodingError extends Error {
@@ -157,8 +174,8 @@ export function keyExchangeEddsaEcdhe(
}
export function keyExchangeEcdheEddsa(
- ecdhePriv: Uint8Array,
- eddsaPub: Uint8Array,
+ ecdhePriv: Uint8Array & MaterialEcdhePriv,
+ eddsaPub: Uint8Array & MaterialEddsaPub,
): Uint8Array {
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
@@ -679,7 +696,8 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
return nacl.hash(uint8ArrayBuf);
} else {
throw Error(
- `unsupported cipher (${(pub as DenominationPubKey).cipher
+ `unsupported cipher (${
+ (pub as DenominationPubKey).cipher
}), unable to hash`,
);
}
@@ -775,6 +793,9 @@ export enum TalerSignaturePurpose {
WALLET_AGE_ATTESTATION = 1207,
WALLET_PURSE_CREATE = 1210,
WALLET_PURSE_DEPOSIT = 1211,
+ WALLET_PURSE_MERGE = 1213,
+ WALLET_ACCOUNT_MERGE = 1214,
+ WALLET_PURSE_ECONTRACT = 1216,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400,
@@ -782,10 +803,26 @@ export enum TalerSignaturePurpose {
SYNC_BACKUP_UPLOAD = 1450,
}
+export const enum WalletAccountMergeFlags {
+ /**
+ * Not a legal mode!
+ */
+ None = 0,
+
+ /**
+ * We are merging a fully paid-up purse into a reserve.
+ */
+ MergeFullyPaidPurse = 1,
+
+ CreateFromPurseQuota = 2,
+
+ CreateWithPurseFee = 3,
+}
+
export class SignaturePurposeBuilder {
private chunks: Uint8Array[] = [];
- constructor(private purposeNum: number) { }
+ constructor(private purposeNum: number) {}
put(bytes: Uint8Array): SignaturePurposeBuilder {
this.chunks.push(Uint8Array.from(bytes));
@@ -815,19 +852,10 @@ export function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
return new SignaturePurposeBuilder(purposeNum);
}
-export type Flavor<T, FlavorT extends string> = T & {
- _flavor?: `taler.${FlavorT}`;
-};
-
-export type FlavorP<T, FlavorT extends string, S extends number> = T & {
- _flavor?: `taler.${FlavorT}`;
- _size?: S;
-};
-
-export type OpaqueData = Flavor<string, "OpaqueData">;
-export type Edx25519PublicKey = FlavorP<string, "Edx25519PublicKey", 32>;
-export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
-export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 64>;
+export type OpaqueData = Flavor<Uint8Array, any>;
+export type Edx25519PublicKey = FlavorP<Uint8Array, "Edx25519PublicKey", 32>;
+export type Edx25519PrivateKey = FlavorP<Uint8Array, "Edx25519PrivateKey", 64>;
+export type Edx25519Signature = FlavorP<Uint8Array, "Edx25519Signature", 64>;
/**
* Convert a big integer to a fixed-size, little-endian array.
@@ -859,19 +887,17 @@ export namespace Edx25519 {
export async function keyCreateFromSeed(
seed: OpaqueData,
): Promise<Edx25519PrivateKey> {
- return encodeCrock(
- nacl.crypto_edx25519_private_key_create_from_seed(decodeCrock(seed)),
- );
+ return nacl.crypto_edx25519_private_key_create_from_seed(seed);
}
export async function keyCreate(): Promise<Edx25519PrivateKey> {
- return encodeCrock(nacl.crypto_edx25519_private_key_create());
+ return nacl.crypto_edx25519_private_key_create();
}
export async function getPublic(
priv: Edx25519PrivateKey,
): Promise<Edx25519PublicKey> {
- return encodeCrock(nacl.crypto_edx25519_get_public(decodeCrock(priv)));
+ return nacl.crypto_edx25519_get_public(priv);
}
export function sign(
@@ -887,12 +913,12 @@ export namespace Edx25519 {
): Promise<OpaqueData> {
const res = kdfKw({
outputLength: 64,
- salt: decodeCrock(seed),
- ikm: decodeCrock(pub),
- info: stringToBytes("edx25519-derivation"),
+ salt: seed,
+ ikm: pub,
+ info: stringToBytes("edx2559-derivation"),
});
- return encodeCrock(res);
+ return res;
}
export async function privateKeyDerive(
@@ -900,21 +926,17 @@ export namespace Edx25519 {
seed: OpaqueData,
): Promise<Edx25519PrivateKey> {
const pub = await getPublic(priv);
- const privDec = decodeCrock(priv);
+ const privDec = priv;
const a = bigintFromNaclArr(privDec.subarray(0, 32));
const factorEnc = await deriveFactor(pub, seed);
- const factorModL = bigintFromNaclArr(decodeCrock(factorEnc)).mod(L);
+ const factorModL = bigintFromNaclArr(factorEnc).mod(L);
const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
const bPrime = nacl
- .hash(
- typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorEnc)]),
- )
+ .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc]))
.subarray(0, 32);
- const newPriv = encodeCrock(
- typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
- );
+ const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]);
return newPriv;
}
@@ -924,14 +946,9 @@ export namespace Edx25519 {
seed: OpaqueData,
): Promise<Edx25519PublicKey> {
const factorEnc = await deriveFactor(pub, seed);
- const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(
- decodeCrock(factorEnc),
- );
- const res = nacl.crypto_scalarmult_ed25519_noclamp(
- factorReduced,
- decodeCrock(pub),
- );
- return encodeCrock(res);
+ const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc);
+ const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub);
+ return res;
}
}
@@ -967,7 +984,7 @@ export namespace AgeRestriction {
export function hashCommitment(ac: AgeCommitment): HashCodeString {
const hc = new nacl.HashState();
for (const pub of ac.publicKeys) {
- hc.update(decodeCrock(pub));
+ hc.update(pub);
}
return encodeCrock(hc.finish().subarray(0, 32));
}
@@ -1091,16 +1108,12 @@ export namespace AgeRestriction {
const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
if (group === 0) {
// No attestation required.
- return encodeCrock(new Uint8Array(64));
+ return new Uint8Array(64);
}
const priv = commitmentProof.proof.privateKeys[group - 1];
const pub = commitmentProof.commitment.publicKeys[group - 1];
- const sig = nacl.crypto_edx25519_sign_detached(
- d,
- decodeCrock(priv),
- decodeCrock(pub),
- );
- return encodeCrock(sig);
+ const sig = nacl.crypto_edx25519_sign_detached(d, priv, pub);
+ return sig;
}
export function commitmentVerify(
@@ -1118,10 +1131,138 @@ export namespace AgeRestriction {
return true;
}
const pub = commitment.publicKeys[group - 1];
- return nacl.crypto_edx25519_sign_detached_verify(
- d,
- decodeCrock(sig),
- decodeCrock(pub),
- );
+ return nacl.crypto_edx25519_sign_detached_verify(d, decodeCrock(sig), pub);
}
}
+
+// FIXME: make it a branded type!
+type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
+
+async function deriveKey(
+ keySeed: OpaqueData,
+ nonce: EncryptionNonce,
+ salt: string,
+): Promise<Uint8Array> {
+ return kdfKw({
+ outputLength: 32,
+ salt: nonce,
+ ikm: keySeed,
+ info: stringToBytes(salt),
+ });
+}
+
+async function encryptWithDerivedKey(
+ nonce: EncryptionNonce,
+ keySeed: OpaqueData,
+ plaintext: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ const key = await deriveKey(keySeed, nonce, salt);
+ const cipherText = secretbox(plaintext, nonce, key);
+ return typedArrayConcat([nonce, cipherText]);
+}
+
+const nonceSize = 24;
+
+async function decryptWithDerivedKey(
+ ciphertext: OpaqueData,
+ keySeed: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ const ctBuf = ciphertext;
+ const nonceBuf = ctBuf.slice(0, nonceSize);
+ const enc = ctBuf.slice(nonceSize);
+ const key = await deriveKey(keySeed, nonceBuf, salt);
+ const clearText = nacl.secretbox_open(enc, nonceBuf, key);
+ if (!clearText) {
+ throw Error("could not decrypt");
+ }
+ return clearText;
+}
+
+enum ContractFormatTag {
+ PaymentOffer = 0,
+ PaymentRequest = 1,
+}
+
+type MaterialEddsaPub = {
+ _materialType?: "eddsa-pub";
+ _size?: 32;
+};
+
+type MaterialEddsaPriv = {
+ _materialType?: "ecdhe-priv";
+ _size?: 32;
+};
+
+type MaterialEcdhePub = {
+ _materialType?: "ecdhe-pub";
+ _size?: 32;
+};
+
+type MaterialEcdhePriv = {
+ _materialType?: "ecdhe-priv";
+ _size?: 32;
+};
+
+type PursePublicKey = FlavorP<Uint8Array, "PursePublicKey", 32> &
+ MaterialEddsaPub;
+
+type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
+ MaterialEcdhePriv;
+
+type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
+ MaterialEddsaPriv;
+
+export function encryptContractForMerge(
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+ mergePriv: MergePrivateKey,
+ contractTerms: any,
+): Promise<OpaqueData> {
+ const contractTermsCanon = canonicalJson(contractTerms) + "\0";
+ const contractTermsBytes = stringToBytes(contractTermsCanon);
+ const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
+ const data = typedArrayConcat([
+ bufferForUint32(ContractFormatTag.PaymentOffer),
+ bufferForUint32(contractTermsBytes.length),
+ mergePriv,
+ contractTermsCompressed,
+ ]);
+ const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(
+ getRandomBytesF(24),
+ key,
+ data,
+ "p2p-merge-contract",
+ );
+}
+
+export interface DecryptForMergeResult {
+ contractTerms: any;
+ mergePriv: Uint8Array;
+}
+
+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 mergePriv = dec.slice(8, 8 + 32);
+ const contractTermsCompressed = dec.slice(8 + 32);
+ 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 {
+ mergePriv: mergePriv,
+ contractTerms: JSON.parse(contractTermsString),
+ };
+}
+
+export function encryptContractForDeposit() {
+ throw Error("not implemented");
+}
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
index 7afa76e9e..d4de8c37b 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -1832,3 +1832,45 @@ export interface PurseDeposit {
*/
coin_pub: EddsaPublicKeyString;
}
+
+export interface ExchangePurseMergeRequest {
+ // payto://-URI of the account the purse is to be merged into.
+ // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
+ payto_uri: string;
+
+ // EdDSA signature of the account/reserve affirming the merge
+ // over a TALER_AccountMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // 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;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+}
+
+export interface ExchangeGetContractResponse {
+ purse_pub: string;
+ econtract_sig: string;
+ econtract: string;
+}
+
+export const codecForExchangeGetContractResponse =
+ (): Codec<ExchangeGetContractResponse> =>
+ buildCodecForObject<ExchangeGetContractResponse>()
+ .property("purse_pub", codecForString())
+ .property("econtract_sig", codecForString())
+ .property("econtract", codecForString())
+ .build("ExchangeGetContractResponse");
+
+/**
+ * Contract terms between two wallets (as opposed to a merchant and wallet).
+ */
+export interface PeerContractTerms {
+ amount: AmountString;
+ summary: string;
+ purse_expiration: TalerProtocolTimestamp;
+}
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 4b1911164..245b5654e 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -1263,15 +1263,50 @@ export interface PayCoinSelection {
export interface InitiatePeerPushPaymentRequest {
amount: AmountString;
+ partialContractTerms: any;
}
export interface InitiatePeerPushPaymentResponse {
+ exchangeBaseUrl: string;
pursePub: string;
mergePriv: string;
+ contractPriv: string;
}
export const codecForInitiatePeerPushPaymentRequest =
(): Codec<InitiatePeerPushPaymentRequest> =>
buildCodecForObject<InitiatePeerPushPaymentRequest>()
.property("amount", codecForAmountString())
+ .property("partialContractTerms", codecForAny())
.build("InitiatePeerPushPaymentRequest");
+
+export interface CheckPeerPushPaymentRequest {
+ exchangeBaseUrl: string;
+ pursePub: string;
+ contractPriv: string;
+}
+
+export interface CheckPeerPushPaymentResponse {
+ contractTerms: any;
+ amount: AmountString;
+}
+
+export const codecForCheckPeerPushPaymentRequest =
+ (): Codec<CheckPeerPushPaymentRequest> =>
+ buildCodecForObject<CheckPeerPushPaymentRequest>()
+ .property("pursePub", codecForString())
+ .property("contractPriv", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .build("CheckPeerPushPaymentRequest");
+
+export interface AcceptPeerPushPaymentRequest {
+ exchangeBaseUrl: string;
+ pursePub: string;
+}
+
+export const codecForAcceptPeerPushPaymentRequest =
+ (): Codec<AcceptPeerPushPaymentRequest> =>
+ buildCodecForObject<AcceptPeerPushPaymentRequest>()
+ .property("pursePub", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .build("AcceptPeerPushPaymentRequest");
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index ebcee2054..a1073dc31 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -1149,7 +1149,7 @@ testCli
tVerify.start();
const attestRes = AgeRestriction.commitmentVerify(
commitProof.commitment,
- attest,
+ encodeCrock(attest),
18,
);
tVerify.stop();
@@ -1157,9 +1157,12 @@ testCli
throw Error();
}
- const salt = encodeCrock(getRandomBytes(32));
+ const salt = getRandomBytes(32);
tDerive.start();
- const deriv = await AgeRestriction.commitmentDerive(commitProof, salt);
+ const deriv = await AgeRestriction.commitmentDerive(
+ commitProof,
+ salt,
+ );
tDerive.stop();
tCompare.start();
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts
index 4d27f45d7..5c716dc54 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts
@@ -44,10 +44,36 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
WalletApiOperation.InitiatePeerPushPayment,
{
amount: "TESTKUDOS:5",
+ partialContractTerms: {
+ summary: "Hello World",
+ },
},
);
console.log(resp);
+
+ const checkResp = await wallet.client.call(
+ WalletApiOperation.CheckPeerPushPayment,
+ {
+ contractPriv: resp.contractPriv,
+ exchangeBaseUrl: resp.exchangeBaseUrl,
+ pursePub: resp.pursePub,
+ },
+ );
+
+ console.log(checkResp);
+
+ const acceptResp = await wallet.client.call(
+ WalletApiOperation.AcceptPeerPushPayment,
+ {
+ exchangeBaseUrl: resp.exchangeBaseUrl,
+ pursePub: resp.pursePub,
+ },
+ );
+
+ console.log(acceptResp);
+
+ await wallet.runUntilDone();
}
runPeerToPeerTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 1d3641836..c177a51dd 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -33,10 +33,12 @@ import {
BlindedDenominationSignature,
bufferForUint32,
buildSigPS,
+ bytesToString,
CoinDepositPermission,
CoinEnvelope,
createHashContext,
decodeCrock,
+ decryptContractForMerge,
DenomKeyType,
DepositInfo,
ecdheGetPublic,
@@ -45,6 +47,7 @@ import {
eddsaSign,
eddsaVerify,
encodeCrock,
+ encryptContractForMerge,
ExchangeProtocolVersion,
getRandomBytes,
hash,
@@ -81,10 +84,17 @@ import { DenominationRecord, WireFee } from "../db.js";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
+ DecryptContractRequest,
+ DecryptContractResponse,
DerivedRefreshSession,
DerivedTipPlanchet,
DeriveRefreshSessionRequest,
DeriveTipRequest,
+ EncryptContractRequest,
+ EncryptContractResponse,
+ EncryptedContract,
+ SignPurseMergeRequest,
+ SignPurseMergeResponse,
SignTrackTransactionRequest,
} from "./cryptoTypes.js";
@@ -185,6 +195,16 @@ export interface TalerCryptoInterface {
signPurseDeposits(
req: SignPurseDepositsRequest,
): Promise<SignPurseDepositsResponse>;
+
+ encryptContractForMerge(
+ req: EncryptContractRequest,
+ ): Promise<EncryptContractResponse>;
+
+ decryptContractForMerge(
+ req: DecryptContractRequest,
+ ): Promise<DecryptContractResponse>;
+
+ signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;
}
/**
@@ -326,6 +346,21 @@ export const nullCrypto: TalerCryptoInterface = {
): 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.");
+ },
};
export type WithArg<X> = X extends (req: infer T) => infer R
@@ -502,6 +537,9 @@ export interface TransferPubResponse {
transferPriv: string;
}
+/**
+ * JS-native implementation of the Taler crypto worker operations.
+ */
export const nativeCryptoR: TalerCryptoInterfaceR = {
async eddsaSign(
tci: TalerCryptoInterfaceR,
@@ -960,9 +998,11 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
maybeAgeCommitmentHash = ach;
hAgeCommitment = decodeCrock(ach);
if (depositInfo.requiredMinimumAge != null) {
- minimumAgeSig = AgeRestriction.commitmentAttest(
- depositInfo.ageCommitmentProof,
- depositInfo.requiredMinimumAge,
+ minimumAgeSig = encodeCrock(
+ AgeRestriction.commitmentAttest(
+ depositInfo.ageCommitmentProof,
+ depositInfo.requiredMinimumAge,
+ ),
);
}
} else {
@@ -1094,7 +1134,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
if (req.meltCoinAgeCommitmentProof) {
newAc = await AgeRestriction.commitmentDerive(
req.meltCoinAgeCommitmentProof,
- transferSecretRes.h,
+ decodeCrock(transferSecretRes.h),
);
newAch = AgeRestriction.hashCommitment(newAc.commitment);
}
@@ -1280,6 +1320,9 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
for (const c of req.coins) {
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
.put(amountToBuffer(Amounts.parseOrThrow(c.contribution)))
+ .put(decodeCrock(c.denomPubHash))
+ // FIXME: use h_age_commitment here
+ .put(new Uint8Array(32))
.put(decodeCrock(req.pursePub))
.put(hExchangeBaseUrl)
.build();
@@ -1300,6 +1343,87 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
deposits,
};
},
+ async encryptContractForMerge(
+ tci: TalerCryptoInterfaceR,
+ req: EncryptContractRequest,
+ ): Promise<EncryptContractResponse> {
+ const contractKeyPair = await this.createEddsaKeypair(tci, {});
+ const enc = await encryptContractForMerge(
+ decodeCrock(req.pursePub),
+ decodeCrock(contractKeyPair.priv),
+ decodeCrock(req.mergePriv),
+ req.contractTerms,
+ );
+ const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
+ .put(hash(enc))
+ .put(decodeCrock(contractKeyPair.pub))
+ .build();
+ const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
+ return {
+ econtract: {
+ contract_pub: contractKeyPair.pub,
+ econtract: encodeCrock(enc),
+ econtract_sig: encodeCrock(sig),
+ },
+ contractPriv: contractKeyPair.priv,
+ };
+ },
+ 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 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,
+ };
+ },
};
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 52b96b1a5..6f4a5fa95 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -30,11 +30,16 @@
import {
AgeCommitmentProof,
AmountJson,
+ AmountString,
CoinEnvelope,
DenominationPubKey,
+ EddsaPublicKeyString,
+ EddsaSignatureString,
ExchangeProtocolVersion,
RefreshPlanchetInfo,
+ TalerProtocolTimestamp,
UnblindedSignature,
+ WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
export interface RefreshNewDenomInfo {
@@ -148,4 +153,80 @@ export interface CreateRecoupRefreshReqRequest {
denomPub: DenominationPubKey;
denomPubHash: string;
denomSig: UnblindedSignature;
-} \ No newline at end of file
+}
+
+export interface EncryptedContract {
+ /**
+ * Encrypted contract.
+ */
+ econtract: string;
+
+ /**
+ * Signature over the (encrypted) contract.
+ */
+ econtract_sig: EddsaSignatureString;
+
+ /**
+ * Ephemeral public key for the DH operation to decrypt the encrypted contract.
+ */
+ contract_pub: EddsaPublicKeyString;
+}
+
+export interface EncryptContractRequest {
+ contractTerms: any;
+
+ pursePub: string;
+ pursePriv: string;
+
+ mergePriv: string;
+}
+
+export interface EncryptContractResponse {
+ econtract: EncryptedContract;
+
+ contractPriv: string;
+}
+
+export interface DecryptContractRequest {
+ ciphertext: string;
+ pursePub: string;
+ contractPriv: string;
+}
+
+export interface DecryptContractResponse {
+ contractTerms: any;
+ mergePriv: string;
+}
+
+export interface SignPurseMergeRequest {
+ mergeTimestamp: TalerProtocolTimestamp;
+
+ pursePub: string;
+
+ reservePayto: string;
+
+ reservePriv: string;
+
+ mergePriv: string;
+
+ purseExpiration: TalerProtocolTimestamp;
+
+ purseAmount: AmountString;
+ purseFee: AmountString;
+
+ contractTermsHash: string;
+
+ /**
+ * Flags.
+ */
+ flags: WalletAccountMergeFlags;
+}
+
+export interface SignPurseMergeResponse {
+ /**
+ * Signature made by the purse's merge private key.
+ */
+ mergeSig: string;
+
+ accountSig: string;
+}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 8cf5170e5..e4f4ba255 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -42,6 +42,7 @@ import {
TalerProtocolDuration,
AgeCommitmentProof,
PayCoinSelection,
+ PeerContractTerms,
} from "@gnu-taler/taler-util";
import { RetryInfo } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
@@ -561,6 +562,12 @@ export interface ExchangeRecord {
* Retry status for fetching updated information about the exchange.
*/
retryInfo?: RetryInfo;
+
+ /**
+ * Public key of the reserve that we're currently using for
+ * receiving P2P payments.
+ */
+ currentMergeReservePub?: string;
}
/**
@@ -1675,7 +1682,6 @@ export interface BalancePerCurrencyRecord {
* Record for a push P2P payment that this wallet initiated.
*/
export interface PeerPushPaymentInitiationRecord {
-
/**
* What exchange are funds coming from?
*/
@@ -1704,18 +1710,40 @@ export interface PeerPushPaymentInitiationRecord {
*/
mergePriv: string;
+ contractPriv: string;
+
+ contractPub: string;
+
purseExpiration: TalerProtocolTimestamp;
/**
* Did we successfully create the purse with the exchange?
*/
purseCreated: boolean;
+
+ timestampCreated: TalerProtocolTimestamp;
}
/**
- * Record for a push P2P payment that this wallet accepted.
+ * Record for a push P2P payment that this wallet was offered.
+ *
+ * Primary key: (exchangeBaseUrl, pursePub)
*/
-export interface PeerPushPaymentAcceptanceRecord {}
+export interface PeerPushPaymentIncomingRecord {
+ exchangeBaseUrl: string;
+
+ pursePub: string;
+
+ mergePriv: string;
+
+ contractPriv: string;
+
+ timestampAccepted: TalerProtocolTimestamp;
+
+ contractTerms: PeerContractTerms;
+
+ // FIXME: add status etc.
+}
export const WalletStoresV1 = {
coins: describeStore(
@@ -1893,6 +1921,12 @@ export const WalletStoresV1 = {
}),
{},
),
+ peerPushPaymentIncoming: describeStore(
+ describeContents<PeerPushPaymentIncomingRecord>("peerPushPaymentIncoming", {
+ keyPath: ["exchangeBaseUrl", "pursePub"],
+ }),
+ {},
+ ),
};
export interface MetaConfigRecord {
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 3a9121502..e4eaf8913 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -16,22 +16,46 @@
import {
AmountJson,
- Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus,
- BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms,
- DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp,
- WalletBackupContentV1
+ Amounts,
+ BackupCoinSourceType,
+ BackupDenomSel,
+ BackupProposalStatus,
+ BackupPurchase,
+ BackupRefreshReason,
+ BackupRefundState,
+ codecForContractTerms,
+ DenomKeyType,
+ j2s,
+ Logger,
+ PayCoinSelection,
+ RefreshReason,
+ TalerProtocolTimestamp,
+ WalletBackupContentV1,
} from "@gnu-taler/taler-util";
import {
- AbortStatus, CoinSource,
+ AbortStatus,
+ CoinSource,
CoinSourceType,
- CoinStatus, DenominationVerificationStatus, DenomSelectionState, OperationStatus, ProposalDownload,
- ProposalStatus, RefreshCoinStatus, RefreshSessionRecord, RefundState, ReserveBankInfo,
- ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo
+ CoinStatus,
+ DenominationVerificationStatus,
+ DenomSelectionState,
+ OperationStatus,
+ ProposalDownload,
+ ProposalStatus,
+ RefreshCoinStatus,
+ RefreshSessionRecord,
+ RefundState,
+ ReserveBankInfo,
+ ReserveRecordStatus,
+ WalletContractData,
+ WalletRefundItem,
+ WalletStoresV1,
+ WireInfo,
} from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js";
import {
checkDbInvariant,
- checkLogicInvariant
+ checkLogicInvariant,
} from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
import { RetryInfo } from "../../util/retries.js";
@@ -313,14 +337,12 @@ export async function importBackup(
}
for (const backupDenomination of backupExchangeDetails.denominations) {
- if (
- backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa
- ) {
+ if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher");
}
const denomPubHash =
cryptoComp.rsaDenomPubToHash[
- backupDenomination.denom_pub.rsa_public_key
+ backupDenomination.denom_pub.rsa_public_key
];
checkLogicInvariant(!!denomPubHash);
const existingDenom = await tx.denominations.get([
@@ -535,7 +557,7 @@ export async function importBackup(
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
const contractTermsHash =
cryptoComp.proposalIdToContractTermsHash[
- backupProposal.proposal_id
+ backupProposal.proposal_id
];
let maxWireFee: AmountJson;
if (parsedContractTerms.max_wire_fee) {
@@ -679,7 +701,7 @@ export async function importBackup(
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
const contractTermsHash =
cryptoComp.proposalIdToContractTermsHash[
- backupPurchase.proposal_id
+ backupPurchase.proposal_id
];
let maxWireFee: AmountJson;
if (parsedContractTerms.max_wire_fee) {
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index b6bae7518..55b8f513d 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -35,6 +35,7 @@ import {
ConfirmPayResult,
ConfirmPayResultType,
ContractTerms,
+ ContractTermsUtil,
Duration,
durationMax,
durationMin,
@@ -87,7 +88,6 @@ import {
selectForcedPayCoins,
selectPayCoins,
} from "../util/coinSelection.js";
-import { ContractTermsUtil } from "../util/contractTerms.js";
import {
getHttpResponseErrorDetails,
readSuccessResponseJsonOrErrorCode,
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index e2ae1e66e..658cbe4f7 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -18,25 +18,47 @@
* Imports.
*/
import {
+ AbsoluteTime,
+ AcceptPeerPushPaymentRequest,
AmountJson,
Amounts,
- Logger,
- InitiatePeerPushPaymentResponse,
+ AmountString,
+ buildCodecForObject,
+ CheckPeerPushPaymentRequest,
+ CheckPeerPushPaymentResponse,
+ Codec,
+ codecForAmountString,
+ codecForAny,
+ codecForExchangeGetContractResponse,
+ ContractTermsUtil,
+ decodeCrock,
+ Duration,
+ eddsaGetPublic,
+ encodeCrock,
+ ExchangePurseMergeRequest,
InitiatePeerPushPaymentRequest,
- strcmp,
- CoinPublicKeyString,
+ InitiatePeerPushPaymentResponse,
j2s,
- getRandomBytes,
- Duration,
- durationAdd,
+ Logger,
+ strcmp,
TalerProtocolTimestamp,
- AbsoluteTime,
- encodeCrock,
- AmountString,
UnblindedSignature,
+ WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
-import { CoinStatus } from "../db.js";
+import { url } from "inspector";
+import {
+ CoinStatus,
+ OperationStatus,
+ ReserveRecord,
+ ReserveRecordStatus,
+} from "../db.js";
+import {
+ checkSuccessResponseOrThrow,
+ readSuccessResponseJsonOrThrow,
+ throwUnexpectedRequestError,
+} from "../util/http.js";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { checkDbInvariant } from "../util/invariants.js";
const logger = new Logger("operations/peer-to-peer.ts");
@@ -176,14 +198,22 @@ export async function initiatePeerToPeerPush(
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
- const hContractTerms = encodeCrock(getRandomBytes(64));
- const purseExpiration = AbsoluteTime.toTimestamp(
+
+ const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
Duration.fromSpec({ days: 2 }),
),
);
+ const contractTerms = {
+ ...req.partialContractTerms,
+ purse_expiration: purseExpiration,
+ amount: req.amount,
+ };
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
const purseSigResp = await ws.cryptoApi.signPurseCreation({
hContractTerms,
mergePub: mergePair.pub,
@@ -204,6 +234,13 @@ export async function initiatePeerToPeerPush(
coinSelRes.exchangeBaseUrl,
);
+ const econtractResp = await ws.cryptoApi.encryptContractForMerge({
+ contractTerms,
+ mergePriv: mergePair.priv,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ });
+
const httpResp = await ws.http.postJson(createPurseUrl.href, {
amount: Amounts.stringify(instructedAmount),
merge_pub: mergePair.pub,
@@ -212,11 +249,216 @@ export async function initiatePeerToPeerPush(
purse_expiration: purseExpiration,
deposits: depositSigsResp.deposits,
min_age: 0,
+ econtract: econtractResp.econtract,
});
const resp = await httpResp.json();
logger.info(`resp: ${j2s(resp)}`);
- throw Error("not yet implemented");
+ if (httpResp.status !== 200) {
+ throw Error("got error response from exchange");
+ }
+
+ return {
+ contractPriv: econtractResp.contractPriv,
+ mergePriv: mergePair.priv,
+ pursePub: pursePair.pub,
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ };
+}
+
+interface ExchangePurseStatus {
+ balance: AmountString;
+}
+
+export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
+ buildCodecForObject<ExchangePurseStatus>()
+ .property("balance", codecForAmountString())
+ .build("ExchangePurseStatus");
+
+export async function checkPeerPushPayment(
+ ws: InternalWalletState,
+ req: CheckPeerPushPaymentRequest,
+): Promise<CheckPeerPushPaymentResponse> {
+ const getPurseUrl = new URL(
+ `purses/${req.pursePub}/deposit`,
+ req.exchangeBaseUrl,
+ );
+
+ const contractPub = encodeCrock(
+ eddsaGetPublic(decodeCrock(req.contractPriv)),
+ );
+
+ const purseHttpResp = await ws.http.get(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const getContractUrl = new URL(
+ `contracts/${contractPub}`,
+ req.exchangeBaseUrl,
+ );
+
+ const contractHttpResp = await ws.http.get(getContractUrl.href);
+
+ const contractResp = await readSuccessResponseJsonOrThrow(
+ contractHttpResp,
+ codecForExchangeGetContractResponse(),
+ );
+
+ const dec = await ws.cryptoApi.decryptContractForMerge({
+ ciphertext: contractResp.econtract,
+ contractPriv: req.contractPriv,
+ pursePub: req.pursePub,
+ });
+
+ await ws.db
+ .mktx((x) => ({
+ peerPushPaymentIncoming: x.peerPushPaymentIncoming,
+ }))
+ .runReadWrite(async (tx) => {
+ await tx.peerPushPaymentIncoming.add({
+ contractPriv: req.contractPriv,
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ mergePriv: dec.mergePriv,
+ pursePub: req.pursePub,
+ timestampAccepted: TalerProtocolTimestamp.now(),
+ contractTerms: dec.contractTerms,
+ });
+ });
+
+ return {
+ amount: purseStatus.balance,
+ contractTerms: dec.contractTerms,
+ };
+}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler+http";
+ } else if (url.protocol === "https:") {
+ proto = "taler";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}
+
+export async function acceptPeerPushPayment(
+ ws: InternalWalletState,
+ req: AcceptPeerPushPaymentRequest,
+) {
+ const peerInc = await ws.db
+ .mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
+ .runReadOnly(async (tx) => {
+ return tx.peerPushPaymentIncoming.get([
+ req.exchangeBaseUrl,
+ req.pursePub,
+ ]);
+ });
+
+ if (!peerInc) {
+ throw Error("can't accept unknown incoming p2p push payment");
+ }
+
+ const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
+
+ // We have to create the key pair outside of the transaction,
+ // due to the async crypto API.
+ const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
+
+ const reserve: ReserveRecord | undefined = await ws.db
+ .mktx((x) => ({
+ exchanges: x.exchanges,
+ reserves: x.reserves,
+ }))
+ .runReadWrite(async (tx) => {
+ const ex = await tx.exchanges.get(req.exchangeBaseUrl);
+ checkDbInvariant(!!ex);
+ if (ex.currentMergeReservePub) {
+ return await tx.reserves.get(ex.currentMergeReservePub);
+ }
+ const rec: ReserveRecord = {
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ // FIXME: field will be removed in the future, folded into withdrawal/p2p record.
+ reserveStatus: ReserveRecordStatus.Dormant,
+ timestampCreated: TalerProtocolTimestamp.now(),
+ instructedAmount: Amounts.getZero(amount.currency),
+ currency: amount.currency,
+ reservePub: newReservePair.pub,
+ reservePriv: newReservePair.priv,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ // FIXME!
+ initialDenomSel: undefined as any,
+ // FIXME!
+ initialWithdrawalGroupId: "",
+ initialWithdrawalStarted: false,
+ lastError: undefined,
+ operationStatus: OperationStatus.Pending,
+ retryInfo: undefined,
+ bankInfo: undefined,
+ restrictAge: undefined,
+ senderWire: undefined,
+ };
+ await tx.reserves.put(rec);
+ return rec;
+ });
+
+ if (!reserve) {
+ throw Error("can't create reserve");
+ }
+
+ const mergeTimestamp = TalerProtocolTimestamp.now();
+
+ const reservePayto = talerPaytoFromExchangeReserve(
+ reserve.exchangeBaseUrl,
+ reserve.reservePub,
+ );
+
+ const sigRes = await ws.cryptoApi.signPurseMerge({
+ contractTermsHash: ContractTermsUtil.hashContractTerms(
+ peerInc.contractTerms,
+ ),
+ flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
+ mergePriv: peerInc.mergePriv,
+ mergeTimestamp: mergeTimestamp,
+ purseAmount: Amounts.stringify(amount),
+ purseExpiration: peerInc.contractTerms.purse_expiration,
+ purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
+ pursePub: peerInc.pursePub,
+ reservePayto,
+ reservePriv: reserve.reservePriv,
+ });
+
+ const mergePurseUrl = new URL(
+ `purses/${req.pursePub}/merge`,
+ req.exchangeBaseUrl,
+ );
+
+ const mergeReq: ExchangePurseMergeRequest = {
+ payto_uri: reservePayto,
+ merge_timestamp: mergeTimestamp,
+ merge_sig: sigRes.mergeSig,
+ reserve_sig: sigRes.accountSig,
+ };
+
+ const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
+
+ const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
+ logger.info(`merge result: ${j2s(res)}`);
}
diff --git a/packages/taler-wallet-core/src/util/contractTerms.test.ts b/packages/taler-wallet-core/src/util/contractTerms.test.ts
deleted file mode 100644
index 74cae4ca7..000000000
--- a/packages/taler-wallet-core/src/util/contractTerms.test.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- 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 <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import test from "ava";
-import { ContractTermsUtil } from "./contractTerms.js";
-
-test("contract terms canon hashing", (t) => {
- const cReq = {
- foo: 42,
- bar: "hello",
- $forgettable: {
- foo: true,
- },
- };
-
- const c1 = ContractTermsUtil.saltForgettable(cReq);
- const c2 = ContractTermsUtil.saltForgettable(cReq);
- t.assert(typeof cReq.$forgettable.foo === "boolean");
- t.assert(typeof c1.$forgettable.foo === "string");
- t.assert(c1.$forgettable.foo !== c2.$forgettable.foo);
-
- const h1 = ContractTermsUtil.hashContractTerms(c1);
-
- const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1)));
-
- t.assert(c3.foo === undefined);
- t.assert(c3.bar === cReq.bar);
-
- const h2 = ContractTermsUtil.hashContractTerms(c3);
-
- t.deepEqual(h1, h2);
-});
-
-test("contract terms canon hashing (nested)", (t) => {
- const cReq = {
- foo: 42,
- bar: {
- prop1: "hello, world",
- $forgettable: {
- prop1: true,
- },
- },
- $forgettable: {
- bar: true,
- },
- };
-
- const c1 = ContractTermsUtil.saltForgettable(cReq);
-
- t.is(typeof c1.$forgettable.bar, "string");
- t.is(typeof c1.bar.$forgettable.prop1, "string");
-
- const forgetPath = (x: any, s: string) =>
- ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s);
-
- // Forget bar first
- const c2 = forgetPath(c1, "bar");
-
- // Forget bar.prop1 first
- const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar");
-
- // Forget everything
- const c4 = ContractTermsUtil.scrub(c1);
-
- const h1 = ContractTermsUtil.hashContractTerms(c1);
- const h2 = ContractTermsUtil.hashContractTerms(c2);
- const h3 = ContractTermsUtil.hashContractTerms(c3);
- const h4 = ContractTermsUtil.hashContractTerms(c4);
-
- t.is(h1, h2);
- t.is(h1, h3);
- t.is(h1, h4);
-
- // Doesn't contain salt
- t.false(ContractTermsUtil.validateForgettable(cReq));
-
- t.true(ContractTermsUtil.validateForgettable(c1));
- t.true(ContractTermsUtil.validateForgettable(c2));
- t.true(ContractTermsUtil.validateForgettable(c3));
- t.true(ContractTermsUtil.validateForgettable(c4));
-});
-
-test("contract terms reference vector", (t) => {
- const j = {
- k1: 1,
- $forgettable: {
- k1: "SALT",
- },
- k2: {
- n1: true,
- $forgettable: {
- n1: "salt",
- },
- },
- k3: {
- n1: "string",
- },
- };
-
- const h = ContractTermsUtil.hashContractTerms(j);
-
- t.deepEqual(
- h,
- "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR",
- );
-});
diff --git a/packages/taler-wallet-core/src/util/contractTerms.ts b/packages/taler-wallet-core/src/util/contractTerms.ts
deleted file mode 100644
index c2f1ba075..000000000
--- a/packages/taler-wallet-core/src/util/contractTerms.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- 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 <http://www.gnu.org/licenses/>
- */
-
-import { canonicalJson, Logger } from "@gnu-taler/taler-util";
-import { kdf } from "@gnu-taler/taler-util";
-import {
- decodeCrock,
- encodeCrock,
- getRandomBytes,
- hash,
- stringToBytes,
-} from "@gnu-taler/taler-util";
-
-const logger = new Logger("contractTerms.ts");
-
-export namespace ContractTermsUtil {
- export type PathPredicate = (path: string[]) => boolean;
-
- /**
- * Scrub all forgettable members from an object.
- */
- export function scrub(anyJson: any): any {
- return forgetAllImpl(anyJson, [], () => true);
- }
-
- /**
- * Recursively forget all forgettable members of an object,
- * where the path matches a predicate.
- */
- export function forgetAll(anyJson: any, pred: PathPredicate): any {
- return forgetAllImpl(anyJson, [], pred);
- }
-
- function forgetAllImpl(
- anyJson: any,
- path: string[],
- pred: PathPredicate,
- ): any {
- const dup = JSON.parse(JSON.stringify(anyJson));
- if (Array.isArray(dup)) {
- for (let i = 0; i < dup.length; i++) {
- dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);
- }
- } else if (typeof dup === "object" && dup != null) {
- if (typeof dup.$forgettable === "object") {
- for (const x of Object.keys(dup.$forgettable)) {
- if (!pred([...path, x])) {
- continue;
- }
- if (!dup.$forgotten) {
- dup.$forgotten = {};
- }
- if (!dup.$forgotten[x]) {
- const membValCanon = stringToBytes(
- canonicalJson(scrub(dup[x])) + "\0",
- );
- const membSalt = stringToBytes(dup.$forgettable[x] + "\0");
- const h = kdf(64, membValCanon, membSalt, new Uint8Array([]));
- dup.$forgotten[x] = encodeCrock(h);
- }
- delete dup[x];
- delete dup.$forgettable[x];
- }
- if (Object.keys(dup.$forgettable).length === 0) {
- delete dup.$forgettable;
- }
- }
- for (const x of Object.keys(dup)) {
- if (x.startsWith("$")) {
- continue;
- }
- dup[x] = forgetAllImpl(dup[x], [...path, x], pred);
- }
- }
- return dup;
- }
-
- /**
- * Generate a salt for all members marked as forgettable,
- * but which don't have an actual salt yet.
- */
- export function saltForgettable(anyJson: any): any {
- const dup = JSON.parse(JSON.stringify(anyJson));
- if (Array.isArray(dup)) {
- for (let i = 0; i < dup.length; i++) {
- dup[i] = saltForgettable(dup[i]);
- }
- } else if (typeof dup === "object" && dup !== null) {
- if (typeof dup.$forgettable === "object") {
- for (const k of Object.keys(dup.$forgettable)) {
- if (dup.$forgettable[k] === true) {
- dup.$forgettable[k] = encodeCrock(getRandomBytes(32));
- }
- }
- }
- for (const x of Object.keys(dup)) {
- if (x.startsWith("$")) {
- continue;
- }
- dup[x] = saltForgettable(dup[x]);
- }
- }
- return dup;
- }
-
- const nameRegex = /^[0-9A-Za-z_]+$/;
-
- /**
- * Check that the given JSON object is well-formed with regards
- * to forgettable fields and other restrictions for forgettable JSON.
- */
- export function validateForgettable(anyJson: any): boolean {
- if (typeof anyJson === "string") {
- return true;
- }
- if (typeof anyJson === "number") {
- return (
- Number.isInteger(anyJson) &&
- anyJson >= Number.MIN_SAFE_INTEGER &&
- anyJson <= Number.MAX_SAFE_INTEGER
- );
- }
- if (typeof anyJson === "boolean") {
- return true;
- }
- if (anyJson === null) {
- return true;
- }
- if (Array.isArray(anyJson)) {
- return anyJson.every((x) => validateForgettable(x));
- }
- if (typeof anyJson === "object") {
- for (const k of Object.keys(anyJson)) {
- if (k.match(nameRegex)) {
- if (validateForgettable(anyJson[k])) {
- continue;
- } else {
- return false;
- }
- }
- if (k === "$forgettable") {
- const fga = anyJson.$forgettable;
- if (!fga || typeof fga !== "object") {
- return false;
- }
- for (const fk of Object.keys(fga)) {
- if (!fk.match(nameRegex)) {
- return false;
- }
- if (!(fk in anyJson)) {
- return false;
- }
- const fv = anyJson.$forgettable[fk];
- if (typeof fv !== "string") {
- return false;
- }
- }
- } else if (k === "$forgotten") {
- const fgo = anyJson.$forgotten;
- if (!fgo || typeof fgo !== "object") {
- return false;
- }
- for (const fk of Object.keys(fgo)) {
- if (!fk.match(nameRegex)) {
- return false;
- }
- // Check that the value has actually been forgotten.
- if (fk in anyJson) {
- return false;
- }
- const fv = anyJson.$forgotten[fk];
- if (typeof fv !== "string") {
- return false;
- }
- try {
- const decFv = decodeCrock(fv);
- if (decFv.length != 64) {
- return false;
- }
- } catch (e) {
- return false;
- }
- // Check that salt has been deleted after forgetting.
- if (anyJson.$forgettable?.[k] !== undefined) {
- return false;
- }
- }
- } else {
- return false;
- }
- }
- return true;
- }
- return false;
- }
-
- /**
- * Check that no forgettable information has been forgotten.
- *
- * Must only be called on an object already validated with validateForgettable.
- */
- export function validateNothingForgotten(contractTerms: any): boolean {
- throw Error("not implemented yet");
- }
-
- /**
- * Hash a contract terms object. Forgettable fields
- * are scrubbed and JSON canonicalization is applied
- * before hashing.
- */
- export function hashContractTerms(contractTerms: unknown): string {
- const cleaned = scrub(contractTerms);
- const canon = canonicalJson(cleaned) + "\0";
- const bytes = stringToBytes(canon);
- return encodeCrock(hash(bytes));
- }
-}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 5c0882ae0..cc9e98f8c 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -27,6 +27,7 @@ import {
AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest,
AcceptManualWithdrawalResult,
+ AcceptPeerPushPaymentRequest,
AcceptTipRequest,
AcceptWithdrawalResponse,
AddExchangeRequest,
@@ -34,6 +35,8 @@ import {
ApplyRefundResponse,
BackupRecovery,
BalancesResponse,
+ CheckPeerPushPaymentRequest,
+ CheckPeerPushPaymentResponse,
CoinDumpJson,
ConfirmPayRequest,
ConfirmPayResult,
@@ -286,6 +289,14 @@ export type WalletOperations = {
request: InitiatePeerPushPaymentRequest;
response: InitiatePeerPushPaymentResponse;
};
+ [WalletApiOperation.CheckPeerPushPayment]: {
+ request: CheckPeerPushPaymentRequest;
+ response: CheckPeerPushPaymentResponse;
+ };
+ [WalletApiOperation.AcceptPeerPushPayment]: {
+ request: AcceptPeerPushPaymentRequest;
+ response: {};
+ };
};
export type RequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index d072f9e96..b56e9402d 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -32,11 +32,13 @@ import {
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
codecForAcceptManualWithdrawalRequet,
+ codecForAcceptPeerPushPaymentRequest,
codecForAcceptTipRequest,
codecForAddExchangeRequest,
codecForAny,
codecForApplyRefundFromPurchaseIdRequest,
codecForApplyRefundRequest,
+ codecForCheckPeerPushPaymentRequest,
codecForConfirmPayRequest,
codecForCreateDepositGroupRequest,
codecForDeleteTransactionRequest,
@@ -144,7 +146,11 @@ import {
processDownloadProposal,
processPurchasePay,
} from "./operations/pay.js";
-import { initiatePeerToPeerPush } from "./operations/peer-to-peer.js";
+import {
+ acceptPeerPushPayment,
+ checkPeerPushPayment,
+ initiatePeerToPeerPush,
+} from "./operations/peer-to-peer.js";
import { getPendingOperations } from "./operations/pending.js";
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
import {
@@ -1055,6 +1061,15 @@ async function dispatchRequestInternal(
const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
return await initiatePeerToPeerPush(ws, req);
}
+ case "checkPeerPushPayment": {
+ const req = codecForCheckPeerPushPaymentRequest().decode(payload);
+ return await checkPeerPushPayment(ws, req);
+ }
+ case "acceptPeerPushPayment": {
+ const req = codecForAcceptPeerPushPaymentRequest().decode(payload);
+ await acceptPeerPushPayment(ws, req);
+ return {};
+ }
}
throw TalerError.fromDetail(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 190746c95..43bedddd2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -169,6 +169,7 @@ importers:
ava: ^4.0.1
big-integer: ^1.6.51
esbuild: ^0.14.21
+ fflate: ^0.7.3
jed: ^1.1.1
prettier: ^2.5.1
rimraf: ^3.0.2
@@ -176,6 +177,7 @@ importers:
typescript: ^4.5.5
dependencies:
big-integer: 1.6.51
+ fflate: 0.7.3
jed: 1.1.1
tslib: 2.3.1
devDependencies: