aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util/src
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 /packages/taler-util/src
parentb214934b75418d0d01c9556577d9594f1db5a319 (diff)
downloadwallet-core-f11483b511ff1f839b9913c4832eee9109f67aeb.tar.xz
wallet-core: implement accepting p2p push payments
Diffstat (limited to 'packages/taler-util/src')
-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
5 files changed, 292 insertions, 67 deletions
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");