From f11483b511ff1f839b9913c4832eee9109f67aeb Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 Jul 2022 17:41:14 +0200 Subject: wallet-core: implement accepting p2p push payments --- packages/taler-util/src/index.ts | 1 + packages/taler-util/src/talerCrypto.test.ts | 28 +-- packages/taler-util/src/talerCrypto.ts | 253 ++++++++++++++++++++++------ packages/taler-util/src/talerTypes.ts | 42 +++++ packages/taler-util/src/walletTypes.ts | 35 ++++ 5 files changed, 292 insertions(+), 67 deletions(-) (limited to 'packages/taler-util/src') 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 & { + _flavor?: `taler.${FlavorT}`; +}; + +export type FlavorP = T & { + _flavor?: `taler.${FlavorT}`; + _size?: S; +}; export function getRandomBytes(n: number): Uint8Array { return nacl.randomBytes(n); } +export function getRandomBytesF( + n: T, +): FlavorP { + 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 & { - _flavor?: `taler.${FlavorT}`; -}; - -export type FlavorP = T & { - _flavor?: `taler.${FlavorT}`; - _size?: S; -}; - -export type OpaqueData = Flavor; -export type Edx25519PublicKey = FlavorP; -export type Edx25519PrivateKey = FlavorP; -export type Edx25519Signature = FlavorP; +export type OpaqueData = Flavor; +export type Edx25519PublicKey = FlavorP; +export type Edx25519PrivateKey = FlavorP; +export type Edx25519Signature = FlavorP; /** * 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 { - 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 { - return encodeCrock(nacl.crypto_edx25519_private_key_create()); + return nacl.crypto_edx25519_private_key_create(); } export async function getPublic( priv: Edx25519PrivateKey, ): Promise { - 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 { 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 { 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 { 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; + +async function deriveKey( + keySeed: OpaqueData, + nonce: EncryptionNonce, + salt: string, +): Promise { + return kdfKw({ + outputLength: 32, + salt: nonce, + ikm: keySeed, + info: stringToBytes(salt), + }); +} + +async function encryptWithDerivedKey( + nonce: EncryptionNonce, + keySeed: OpaqueData, + plaintext: OpaqueData, + salt: string, +): Promise { + 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 { + 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 & + MaterialEddsaPub; + +type ContractPrivateKey = FlavorP & + MaterialEcdhePriv; + +type MergePrivateKey = FlavorP & + MaterialEddsaPriv; + +export function encryptContractForMerge( + pursePub: PursePublicKey, + contractPriv: ContractPrivateKey, + mergePriv: MergePrivateKey, + contractTerms: any, +): Promise { + 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 { + 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 => + buildCodecForObject() + .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 => buildCodecForObject() .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 => + buildCodecForObject() + .property("pursePub", codecForString()) + .property("contractPriv", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .build("CheckPeerPushPaymentRequest"); + +export interface AcceptPeerPushPaymentRequest { + exchangeBaseUrl: string; + pursePub: string; +} + +export const codecForAcceptPeerPushPaymentRequest = + (): Codec => + buildCodecForObject() + .property("pursePub", codecForString()) + .property("exchangeBaseUrl", codecForString()) + .build("AcceptPeerPushPaymentRequest"); -- cgit v1.2.3