summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/taler-crypto.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/taler-crypto.ts')
-rw-r--r--packages/taler-util/src/taler-crypto.ts338
1 files changed, 289 insertions, 49 deletions
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index 113e4194b..e587773e2 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -22,8 +22,9 @@
* Imports.
*/
import * as nacl from "./nacl-fast.js";
-import { kdf } from "./kdf.js";
+import { hmacSha256, hmacSha512 } from "./kdf.js";
import bigint from "big-integer";
+import * as argon2 from "./argon2.js";
import {
CoinEnvelope,
CoinPublicKeyString,
@@ -35,6 +36,8 @@ import { Logger } from "./logging.js";
import { secretbox } from "./nacl-fast.js";
import * as fflate from "fflate";
import { canonicalJson } from "./helpers.js";
+import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
+import { AmountLike, Amounts } from "./amounts.js";
export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `taler.${FlavorT}`;
@@ -55,7 +58,56 @@ export function getRandomBytesF<T extends number, N extends string>(
return nacl.randomBytes(n);
}
-const useNative = true;
+export const useNative = true;
+
+/**
+ * Interface of the native Taler runtime library.
+ */
+interface NativeTartLib {
+ decodeUtf8(buf: Uint8Array): string;
+ decodeUtf8(str: string): Uint8Array;
+ randomBytes(n: number): Uint8Array;
+ encodeCrock(buf: Uint8Array | ArrayBuffer): string;
+ decodeCrock(str: string): Uint8Array;
+ hash(buf: Uint8Array): Uint8Array;
+ hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+ ): Uint8Array;
+ eddsaGetPublic(buf: Uint8Array): Uint8Array;
+ ecdheGetPublic(buf: Uint8Array): Uint8Array;
+ eddsaSign(msg: Uint8Array, priv: Uint8Array): Uint8Array;
+ eddsaVerify(msg: Uint8Array, sig: Uint8Array, pub: Uint8Array): boolean;
+ kdf(
+ outLen: number,
+ ikm: Uint8Array,
+ salt?: Uint8Array,
+ info?: Uint8Array,
+ ): Uint8Array;
+ keyExchangeEcdhEddsa(ecdhPriv: Uint8Array, eddsaPub: Uint8Array): Uint8Array;
+ keyExchangeEddsaEcdh(eddsaPriv: Uint8Array, ecdhPub: Uint8Array): Uint8Array;
+ rsaBlind(hmsg: Uint8Array, bks: Uint8Array, rsaPub: Uint8Array): Uint8Array;
+ rsaUnblind(
+ blindSig: Uint8Array,
+ rsaPub: Uint8Array,
+ bks: Uint8Array,
+ ): Uint8Array;
+ rsaVerify(hmsg: Uint8Array, rsaSig: Uint8Array, rsaPub: Uint8Array): boolean;
+ hashStateInit(): any;
+ hashStateUpdate(st: any, data: Uint8Array): any;
+ hashStateFinish(st: any): Uint8Array;
+}
+
+// @ts-ignore
+let tart: NativeTartLib | undefined;
+
+if (useNative) {
+ // @ts-ignore
+ tart = globalThis._tart;
+}
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -71,7 +123,7 @@ function getValue(chr: string): number {
switch (chr) {
case "O":
case "o":
- a = "0;";
+ a = "0";
break;
case "i":
case "I":
@@ -101,9 +153,8 @@ function getValue(chr: string): number {
}
export function encodeCrock(data: ArrayBuffer): string {
- if (useNative && "_encodeCrock" in globalThis) {
- // @ts-ignore
- return globalThis._encodeCrock(data);
+ if (tart) {
+ return tart.encodeCrock(data);
}
const dataBytes = new Uint8Array(data);
let sb = "";
@@ -129,6 +180,44 @@ export function encodeCrock(data: ArrayBuffer): string {
return sb;
}
+export function kdf(
+ outputLength: number,
+ ikm: Uint8Array,
+ salt?: Uint8Array,
+ info?: Uint8Array,
+): Uint8Array {
+ if (tart) {
+ return tart.kdf(outputLength, ikm, salt, info);
+ }
+ salt = salt ?? new Uint8Array(64);
+ // extract
+ const prk = hmacSha512(salt, ikm);
+
+ info = info ?? new Uint8Array(0);
+
+ // expand
+ const N = Math.ceil(outputLength / 32);
+ const output = new Uint8Array(N * 32);
+ for (let i = 0; i < N; i++) {
+ let buf;
+ if (i == 0) {
+ buf = new Uint8Array(info.byteLength + 1);
+ buf.set(info, 0);
+ } else {
+ buf = new Uint8Array(info.byteLength + 1 + 32);
+ for (let j = 0; j < 32; j++) {
+ buf[j] = output[(i - 1) * 32 + j];
+ }
+ buf.set(info, 32);
+ }
+ buf[buf.length - 1] = i + 1;
+ const chunk = hmacSha256(prk, buf);
+ output.set(chunk, i * 32);
+ }
+
+ return output.slice(0, outputLength);
+}
+
/**
* HMAC-SHA512-SHA256 (see RFC 5869).
*/
@@ -142,9 +231,8 @@ export function kdfKw(args: {
}
export function decodeCrock(encoded: string): Uint8Array {
- if (useNative && "_decodeCrock" in globalThis) {
- // @ts-ignore
- return globalThis._decodeCrock(encoded);
+ if (tart) {
+ return tart.decodeCrock(encoded);
}
const size = encoded.length;
let bitpos = 0;
@@ -173,38 +261,71 @@ export function decodeCrock(encoded: string): Uint8Array {
return out;
}
+export async function hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ if (tart) {
+ return tart.hashArgon2id(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+ }
+ return await argon2.hashArgon2id(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+}
+
export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
- if (useNative && "_eddsaGetPublic" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaGetPublic(eddsaPriv);
+ if (tart) {
+ return tart.eddsaGetPublic(eddsaPriv);
}
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return pair.publicKey;
}
-export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+export function ecdhGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+ if (tart) {
+ return tart.ecdheGetPublic(ecdhePriv);
+ }
return nacl.scalarMult_base(ecdhePriv);
}
-export function keyExchangeEddsaEcdhe(
+export function keyExchangeEddsaEcdh(
eddsaPriv: Uint8Array,
- ecdhePub: Uint8Array,
+ ecdhPub: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.keyExchangeEddsaEcdh(eddsaPriv, ecdhPub);
+ }
const ph = hash(eddsaPriv);
const a = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
a[i] = ph[i];
}
- const x = nacl.scalarMult(a, ecdhePub);
+ const x = nacl.scalarMult(a, ecdhPub);
return hash(x);
}
-export function keyExchangeEcdheEddsa(
- ecdhePriv: Uint8Array & MaterialEcdhePriv,
+export function keyExchangeEcdhEddsa(
+ ecdhPriv: Uint8Array & MaterialEcdhePriv,
eddsaPub: Uint8Array & MaterialEddsaPub,
): Uint8Array {
+ if (tart) {
+ return tart.keyExchangeEcdhEddsa(ecdhPriv, eddsaPub);
+ }
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
- const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
+ const x = nacl.scalarMult(ecdhPriv, curve25519Pub);
return hash(x);
}
@@ -271,7 +392,7 @@ function csKdfMod(
// Newer versions of node have TextEncoder and TextDecoder as a global,
// just like modern browsers.
// In older versions of node or environments that do not have these
-// globals, they must be polyfilled (by adding them to globa/globalThis)
+// globals, they must be polyfilled (by adding them to global/globalThis)
// before stringToBytes or bytesToString is called the first time.
let encoder: any;
@@ -365,6 +486,9 @@ export function rsaBlind(
bks: Uint8Array,
rsaPubEnc: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.rsaBlind(hm, bks, rsaPubEnc);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const data = rsaFullDomainHash(hm, rsaPub);
const r = rsaBlindingKeyDerive(rsaPub, bks);
@@ -378,6 +502,9 @@ export function rsaUnblind(
rsaPubEnc: Uint8Array,
bks: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.rsaUnblind(sig, rsaPubEnc, bks);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const blinded_s = loadBigInt(sig);
const r = rsaBlindingKeyDerive(rsaPub, bks);
@@ -391,6 +518,9 @@ export function rsaVerify(
rsaSig: Uint8Array,
rsaPubEnc: Uint8Array,
): boolean {
+ if (tart) {
+ return tart.rsaVerify(hm, rsaSig, rsaPubEnc);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const d = rsaFullDomainHash(hm, rsaPub);
const sig = loadBigInt(rsaSig);
@@ -563,7 +693,7 @@ export async function csBlind(
* Unblind operation to unblind the signature
* @param bseed seed to derive secrets
* @param rPub public R received from /csr
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @param b returned from exchange to select c
* @param csSig blinded signature
* @returns unblinded signature
@@ -591,7 +721,7 @@ export async function csUnblind(
* Verification algorithm for CS signatures
* @param hm message signed
* @param csSig unblinded signature
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @returns true if valid, false if invalid
*/
export async function csVerify(
@@ -629,14 +759,13 @@ export function createEddsaKeyPair(): EddsaKeyPair {
export function createEcdheKeyPair(): EcdheKeyPair {
const ecdhePriv = nacl.randomBytes(32);
- const ecdhePub = ecdheGetPublic(ecdhePriv);
+ const ecdhePub = ecdhGetPublic(ecdhePriv);
return { ecdhePriv, ecdhePub };
}
export function hash(d: Uint8Array): Uint8Array {
- if (useNative && "_hash" in globalThis) {
- // @ts-ignore
- return globalThis._hash(d);
+ if (tart) {
+ return tart.hash(d);
}
return nacl.hash(d);
}
@@ -664,7 +793,7 @@ const logger = new Logger("talerCrypto.ts");
export function hashCoinEvInner(
coinEv: CoinEnvelope,
- hashState: nacl.HashState,
+ hashState: TalerHashState,
): void {
const hashInputBuf = new ArrayBuffer(4);
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
@@ -723,9 +852,8 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
}
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
- if (useNative && "_eddsaSign" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaSign(msg, eddsaPriv);
+ if (tart) {
+ return tart.eddsaSign(msg, eddsaPriv);
}
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return nacl.sign_detached(msg, pair.secretKey);
@@ -736,14 +864,26 @@ export function eddsaVerify(
sig: Uint8Array,
eddsaPub: Uint8Array,
): boolean {
- if (useNative && "_eddsaVerify" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaVerify(msg, sig, eddsaPub);
+ if (tart) {
+ return tart.eddsaVerify(msg, sig, eddsaPub);
}
return nacl.sign_detached_verify(msg, sig, eddsaPub);
}
-export function createHashContext(): nacl.HashState {
+export interface TalerHashState {
+ update(data: Uint8Array): void;
+ finish(): Uint8Array;
+}
+
+export function createHashContext(): TalerHashState {
+ if (tart) {
+ const t = tart;
+ const st = tart.hashStateInit();
+ return {
+ finish: () => t.hashStateFinish(st),
+ update: (d) => t.hashStateUpdate(st, d),
+ };
+ }
return new nacl.HashState();
}
@@ -763,6 +903,21 @@ export function bufferForUint32(n: number): Uint8Array {
return buf;
}
+/**
+ * This makes the assumption that the uint64 fits a float,
+ * which should be true for all Taler protocol messages.
+ */
+export function bufferForUint64(n: number): Uint8Array {
+ const arrBuf = new ArrayBuffer(8);
+ const buf = new Uint8Array(arrBuf);
+ const dv = new DataView(arrBuf);
+ if (n < 0 || !Number.isInteger(n)) {
+ throw Error("non-negative integer expected");
+ }
+ dv.setBigUint64(0, BigInt(n));
+ return buf;
+}
+
export function bufferForUint8(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(1);
const buf = new Uint8Array(arrBuf);
@@ -828,6 +983,7 @@ export enum TalerSignaturePurpose {
TEST = 4242,
MERCHANT_PAYMENT_OK = 1104,
MERCHANT_CONTRACT = 1101,
+ MERCHANT_REFUND = 1102,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
@@ -837,14 +993,19 @@ export enum TalerSignaturePurpose {
WALLET_PURSE_MERGE = 1213,
WALLET_ACCOUNT_MERGE = 1214,
WALLET_PURSE_ECONTRACT = 1216,
+ WALLET_PURSE_DELETE = 1220,
+ WALLET_COIN_HISTORY = 1209,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+ TALER_SIGNATURE_AML_DECISION = 1350,
+ TALER_SIGNATURE_AML_QUERY = 1351,
+ TALER_SIGNATURE_MASTER_AML_KEY = 1017,
ANASTASIS_POLICY_UPLOAD = 1400,
ANASTASIS_POLICY_DOWNLOAD = 1401,
SYNC_BACKUP_UPLOAD = 1450,
}
-export const enum WalletAccountMergeFlags {
+export enum WalletAccountMergeFlags {
/**
* Not a legal mode!
*/
@@ -1120,6 +1281,10 @@ export namespace AgeRestriction {
};
}
+ const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
+ "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG",
+ );
+
export async function restrictionCommitSeeded(
ageMask: number,
age: number,
@@ -1132,19 +1297,32 @@ export namespace AgeRestriction {
const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = [];
- for (let i = 0; i < numPubs; i++) {
+ for (let i = 0; i < numPrivs; i++) {
const privSeed = await kdfKw({
outputLength: 32,
ikm: seed,
- info: stringToBytes("age-restriction-commit"),
+ info: stringToBytes("age-commitment"),
salt: bufferForUint32(i),
});
+
const priv = await Edx25519.keyCreateFromSeed(privSeed);
const pub = await Edx25519.getPublic(priv);
pubs.push(pub);
- if (i < numPrivs) {
- privs.push(priv);
- }
+ privs.push(priv);
+ }
+
+ for (let i = numPrivs; i < numPubs; i++) {
+ const deriveSeed = await kdfKw({
+ outputLength: 32,
+ ikm: seed,
+ info: stringToBytes("age-factor"),
+ salt: bufferForUint32(i),
+ });
+ const pub = await Edx25519.publicKeyDerive(
+ PublishedAgeRestrictionBaseKey,
+ deriveSeed,
+ );
+ pubs.push(pub);
}
return {
@@ -1257,7 +1435,7 @@ export namespace AgeRestriction {
}
// FIXME: make it a branded type!
-type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
+export type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
async function deriveKey(
keySeed: OpaqueData,
@@ -1272,7 +1450,7 @@ async function deriveKey(
});
}
-async function encryptWithDerivedKey(
+export async function encryptWithDerivedKey(
nonce: EncryptionNonce,
keySeed: OpaqueData,
plaintext: OpaqueData,
@@ -1285,7 +1463,7 @@ async function encryptWithDerivedKey(
const nonceSize = 24;
-async function decryptWithDerivedKey(
+export async function decryptWithDerivedKey(
ciphertext: OpaqueData,
keySeed: OpaqueData,
salt: string,
@@ -1343,6 +1521,7 @@ export function encryptContractForMerge(
contractPriv: ContractPrivateKey,
mergePriv: MergePrivateKey,
contractTerms: any,
+ nonce: EncryptionNonce,
): Promise<OpaqueData> {
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
const contractTermsBytes = stringToBytes(contractTermsCanon);
@@ -1353,14 +1532,15 @@ export function encryptContractForMerge(
mergePriv,
contractTermsCompressed,
]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, mergeSalt);
}
export function encryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
contractTerms: any,
+ nonce: EncryptionNonce,
): Promise<OpaqueData> {
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
const contractTermsBytes = stringToBytes(contractTermsCanon);
@@ -1370,8 +1550,8 @@ export function encryptContractForDeposit(
bufferForUint32(contractTermsBytes.length),
contractTermsCompressed,
]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, depositSalt);
}
export interface DecryptForMergeResult {
@@ -1388,7 +1568,7 @@ export async function decryptContractForMerge(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
): Promise<DecryptForMergeResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
const dec = await decryptWithDerivedKey(enc, key, mergeSalt);
const mergePriv = dec.slice(8, 8 + 32);
const contractTermsCompressed = dec.slice(8 + 32);
@@ -1408,7 +1588,7 @@ export async function decryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
): Promise<DecryptForDepositResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
const dec = await decryptWithDerivedKey(enc, key, depositSalt);
const contractTermsCompressed = dec.slice(8);
const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
@@ -1420,3 +1600,63 @@ export async function decryptContractForDeposit(
contractTerms: JSON.parse(contractTermsString),
};
}
+
+export function amountToBuffer(amount: AmountLike): Uint8Array {
+ const amountJ = Amounts.jsonifyAmount(amount);
+ const buffer = new ArrayBuffer(8 + 4 + 12);
+ const dvbuf = new DataView(buffer);
+ const u8buf = new Uint8Array(buffer);
+ const curr = stringToBytes(amountJ.currency);
+ if (typeof dvbuf.setBigUint64 !== "undefined") {
+ dvbuf.setBigUint64(0, BigInt(amountJ.value));
+ } else {
+ const arr = bigint(amountJ.value).toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ dvbuf.setUint8(offset++, arr[i]);
+ }
+ }
+ dvbuf.setUint32(8, amountJ.fraction);
+ u8buf.set(curr, 8 + 4);
+
+ return u8buf;
+}
+
+export function timestampRoundedToBuffer(
+ ts: TalerProtocolTimestamp,
+): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ // The buffer we sign over represents the timestamp in microseconds.
+ if (typeof v.setBigUint64 !== "undefined") {
+ const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
+ v.setBigUint64(0, s);
+ } else {
+ const s =
+ ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
+ const arr = s.toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ v.setUint8(offset++, arr[i]);
+ }
+ }
+ return new Uint8Array(b);
+}
+
+export function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ // The buffer we sign over represents the timestamp in microseconds.
+ if (typeof v.setBigUint64 !== "undefined") {
+ const s = BigInt(ts.d_us);
+ v.setBigUint64(0, s);
+ } else {
+ const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us);
+ const arr = s.toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ v.setUint8(offset++, arr[i]);
+ }
+ }
+ return new Uint8Array(b);
+}