wallet-core: implement accepting p2p push payments
This commit is contained in:
parent
b214934b75
commit
f11483b511
@ -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;
|
||||
|
@ -38,6 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.51",
|
||||
"fflate": "^0.7.3",
|
||||
"jed": "^1.1.1",
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
|
@ -32,3 +32,4 @@ export {
|
||||
} from "./nacl-fast.js";
|
||||
export { RequestThrottler } from "./RequestThrottler.js";
|
||||
export * from "./CancellationToken.js";
|
||||
export * from "./contractTerms.js";
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -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"];
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -18,25 +18,47 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
AbsoluteTime,
|
||||
AcceptPeerPushPaymentRequest,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
Logger,
|
||||
InitiatePeerPushPaymentResponse,
|
||||
InitiatePeerPushPaymentRequest,
|
||||
strcmp,
|
||||
CoinPublicKeyString,
|
||||
j2s,
|
||||
getRandomBytes,
|
||||
Duration,
|
||||
durationAdd,
|
||||
TalerProtocolTimestamp,
|
||||
AbsoluteTime,
|
||||
encodeCrock,
|
||||
AmountString,
|
||||
buildCodecForObject,
|
||||
CheckPeerPushPaymentRequest,
|
||||
CheckPeerPushPaymentResponse,
|
||||
Codec,
|
||||
codecForAmountString,
|
||||
codecForAny,
|
||||
codecForExchangeGetContractResponse,
|
||||
ContractTermsUtil,
|
||||
decodeCrock,
|
||||
Duration,
|
||||
eddsaGetPublic,
|
||||
encodeCrock,
|
||||
ExchangePurseMergeRequest,
|
||||
InitiatePeerPushPaymentRequest,
|
||||
InitiatePeerPushPaymentResponse,
|
||||
j2s,
|
||||
Logger,
|
||||
strcmp,
|
||||
TalerProtocolTimestamp,
|
||||
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)}`);
|
||||
}
|
||||
|
@ -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",
|
||||
);
|
||||
});
|
@ -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));
|
||||
}
|
||||
}
|
@ -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<
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user