diff options
Diffstat (limited to 'packages/taler-util/src/taler-crypto.ts')
-rw-r--r-- | packages/taler-util/src/taler-crypto.ts | 338 |
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); +} |