aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util/src/talerCrypto.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/talerCrypto.ts')
-rw-r--r--packages/taler-util/src/talerCrypto.ts253
1 files changed, 197 insertions, 56 deletions
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");
+}