taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 28d808ba98fcb37fa5f041420219e894836aa559
parent 272af1340c7fa49e6bcbd76bcca00a4b5f94f39a
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Tue,  4 Nov 2025 21:21:27 +0100

hpke cleanups and bug fixes. Test against test vectors in RFC

Diffstat:
Mpackages/taler-util/src/taler-crypto.test.ts | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-util/src/taler-crypto.ts | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
2 files changed, 159 insertions(+), 41 deletions(-)

diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts @@ -42,6 +42,16 @@ import { chacha20poly1305_ietf_encrypt, chacha20poly1305_ietf_decrypt, chacha20_ietf, + hpkeSealOneshot, + hpkeKemEncapsNorand, + hpkeSenderSetup, + hpkeKeySchedule, + HpkeRole, + HpkeMode, + hpkeSenderSetupNorand, + hpkeSealOneshotNorand, + hpkeComputeNonce, + hpkeOpenOneshot, } from "./taler-crypto.js"; import { sha512 } from "./kdf.js"; import * as nacl from "./nacl-fast.js"; @@ -542,7 +552,7 @@ test("chacha20poly1305 test vector", async (t) => { t.deepEqual(block0.slice(0, 32), polyKey); // encrypt const ciphertext = chacha20poly1305_ietf_encrypt(plaintextBytes, aad, nonce, key); - + // decrypt const plaintext = chacha20poly1305_ietf_decrypt(ciphertext, aad, nonce, key); t.false(plaintext === undefined); @@ -550,3 +560,61 @@ test("chacha20poly1305 test vector", async (t) => { const plaintextStr = decoder.decode(plaintext); t.deepEqual(plaintextStr, 'Ladies and Gentlemen of the class of \'99: If I could offer you only one tip for the future, sunscreen would be it.'); }); + + +// TV are from RFC9180. Not all intermediate steps tested. +test("rfc9180 HPKE DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305 test vector", async (t) => { + const info = new Uint8Array([ + 0x4f, 0x64, 0x65, 0x20, + 0x6f, 0x6e, 0x20, 0x61, + 0x20, 0x47, 0x72, 0x65, + 0x63, 0x69, 0x61, 0x6e, + 0x20, 0x55, 0x72, 0x6e, + ]); + const pkEm = new Uint8Array([ + 0x1a, 0xfa, 0x08, 0xd3, 0xde, 0xc0, 0x47, 0xa6, + 0x43, 0x88, 0x51, 0x63, 0xf1, 0x18, 0x04, 0x76, + 0xfa, 0x7d, 0xdb, 0x54, 0xc6, 0xa8, 0x02, 0x9e, + 0xa3, 0x3f, 0x95, 0x79, 0x6b, 0xf2, 0xac, 0x4a, + ]); + const pkRm = new Uint8Array([ + 0x43, 0x10, 0xee, 0x97, 0xd8, 0x8c, 0xc1, 0xf0, 0x88, 0xa5, 0x57, 0x6c, 0x77, 0xab, 0x0c, 0xf5, 0xc3, 0xac, 0x79, 0x7f, 0x3d, 0x95, 0x13, 0x9c, 0x6c, 0x84, 0xb5, 0x42, 0x9c, 0x59, 0x66, 0x2a, + ]); + const skRm = new Uint8Array([ + 0x80, 0x57, 0x99, 0x1e, 0xef, 0x8f, 0x1f, 0x1a, 0xf1, 0x8f, 0x4a, 0x94, 0x91, 0xd1, 0x6a, 0x1c, 0xe3, 0x33, 0xf6, 0x95, 0xd4, 0xdb, 0x8e, 0x38, 0xda, 0x75, 0x97, 0x5c, 0x44, 0x78, 0xe0, 0xfb + ]); + const skEm = new Uint8Array([ + 0xf4, 0xec, 0x9b, 0x33, 0xb7, 0x92, 0xc3, 0x72, 0xc1, 0xd2, 0xc2, 0x06, 0x35, 0x07, 0xb6, 0x84, 0xef, 0x92, 0x5b, 0x8c, 0x75, 0xa4, 0x2d, 0xbc, 0xbf, 0x57, 0xd6, 0x3c, 0xcd, 0x38, 0x16, 0x00, + ]); + const enc = pkEm; + const key = new Uint8Array([ + 0xad, 0x27, 0x44, 0xde, 0x8e, 0x17, 0xf4, 0xeb, 0xba, 0x57, 0x5b, 0x3f, 0x5f, 0x5a, 0x8f, 0xa1, 0xf6, 0x9c, 0x2a, 0x07, 0xf6, 0xe7, 0x50, 0x0b, 0xc6, 0x0c, 0xa6, 0xe3, 0xe3, 0xec, 0x1c, 0x91, + ]); + const pt = new Uint8Array([ + 0x42, 0x65, 0x61, 0x75, 0x74, 0x79, 0x20, 0x69, 0x73, 0x20, 0x74, 0x72, 0x75, 0x74, 0x68, 0x2c, 0x20, 0x74, 0x72, 0x75, 0x74, 0x68, 0x20, 0x62, 0x65, 0x61, 0x75, 0x74, 0x79, + ]); + const aad = new Uint8Array([ + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x2d, 0x30, + ]); + const ct = new Uint8Array([ + 0x1c, 0x52, 0x50, 0xd8, 0x03, 0x4e, 0xc2, 0xb7, 0x84, 0xba, 0x2c, 0xfd, 0x69, 0xdb, 0xdb, 0x8a, 0xf4, 0x06, 0xcf, 0xe3, 0xff, 0x93, 0x8e, 0x13, 0x1f, 0x0d, 0xef, 0x8c, 0x8b, 0x60, 0xb4, 0xdb, 0x21, 0x99, 0x3c, 0x62, 0xce, 0x81, 0x88, 0x3d, 0x2d, 0xd1, 0xb5, 0x1a, 0x28, + ]); + const nonce = new Uint8Array([ + 0x5c, 0x4d, 0x98, 0x15, 0x06, 0x61, 0xb8, 0x48, 0x85, 0x3b, 0x54, 0x7f, + ]); + const ss = new Uint8Array([ + 0x0b, 0xbe, 0x78, 0x49, 0x04, 0x12, 0xb4, 0xbb, 0xea, 0x48, 0x12, 0x66, 0x6f, 0x79, 0x16, 0x93, 0x2b, 0x82, 0x8b, 0xba, 0x79, 0x94, 0x24, 0x24, 0xab, 0xb6, 0x52, 0x44, 0x93, 0x0d, 0x69, 0xa7, + ]); + const [enc_cand, ss_cand] = hpkeKemEncapsNorand(pkRm, skEm); + t.deepEqual(enc_cand, enc); + const [enc_cand_setup, ctx_cand] = hpkeSenderSetupNorand(pkRm, skEm, info); + t.deepEqual(ctx_cand.key, key); + t.deepEqual(enc_cand_setup, enc); + t.deepEqual(ss_cand, ss); + const nonce_cand = hpkeComputeNonce(ctx_cand); + t.deepEqual(nonce_cand, nonce); + const ct_cand = hpkeSealOneshotNorand(pkRm, skEm, info, aad, pt); + t.deepEqual(ct_cand.slice(enc.length), ct); + const m_cand = hpkeOpenOneshot(skRm, info, aad, ct_cand); + t.deepEqual(m_cand, pt); +}); diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts @@ -1758,10 +1758,45 @@ export function toHexString(byteArray: Uint8Array) { ); } +// RFC 9180 Hybrid Public-Key Encryption +// Currently, no agility implemented, we only support +// DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305 + +// A X25519 public key export type HpkePublicKey = Uint8Array +// A X25519 secret key +export type HpkeSecretKey = Uint8Array; + +// An X25519 public key export type HpkeEncapsulation = Uint8Array +// This makes sure that sender confusion is +// avoided. +// In a non-oneshot API (currently not implemented) +// this is a requried input. +export enum HpkeRole { + Sender, + Receiver, +} + +// We do support pre-shared keys in this API. +export enum HpkeMode { + Base = 0x00, + PSK = 0x01, +} + +// Currently only used internally. +// Necessary when we support anything +// else than the oneshot APIs +export interface HpkeContext { + key: Uint8Array, + nonce: Uint8Array, + seq: number, + role: HpkeRole, +} + + export function hkdf_extract_sha256( ikm: Uint8Array, salt?: Uint8Array, @@ -1804,7 +1839,7 @@ export function hkdf_expand_sha256( } -export function labeledExpand( +export function hpkeLabeledExpand( ctx: string, prk: Uint8Array, label: string, @@ -1820,7 +1855,7 @@ export function labeledExpand( return hkdf_expand_sha256(outLength, prk, labeledInfo); } -export function labeledExtract( +export function hpkeLabeledExtract( ctx: string, label: Uint8Array, ikm: Uint8Array, @@ -1854,11 +1889,11 @@ export function hpkeKemEncapsNorand( const kem_context = new Uint8Array([...enc, ...pkR]); const dh = ecdh_x25519 (skE, pkR); const suiteId = new Uint8Array([0x4B,0x45,0x4D,0x00,0x20]); - const prk = labeledExtract("HPKE-v1", + const prk = hpkeLabeledExtract("HPKE-v1", stringToBytes("eae_prk"), dh, suiteId); - const ss = labeledExpand("HPKE-v1", + const ss = hpkeLabeledExpand("HPKE-v1", prk, "shared_secret", kem_context, @@ -1874,11 +1909,11 @@ export function hpkeKemDecaps( const dh = ecdh_x25519(skR, enc); const kem_context = new Uint8Array([...enc, ...pkR]); const suiteId = new Uint8Array([0x4B,0x45,0x4D,0x00,0x20]); - const prk = labeledExtract("HPKE-v1", + const prk = hpkeLabeledExtract("HPKE-v1", stringToBytes("eae_prk"), dh, suiteId); - const ss = labeledExpand("HPKE-v1", + const ss = hpkeLabeledExpand("HPKE-v1", prk, "shared_secret", kem_context, @@ -1888,23 +1923,6 @@ export function hpkeKemDecaps( } -enum HpkeRole { - Sender, - Receiver, -} - -enum HpkeMode { - Base = 0x00, - PSK = 0x01, -} - -export interface HpkeContext { - key: Uint8Array, - nonce: Uint8Array, - seq: number, - role: HpkeRole, -} - export function hpkeKeySchedule( role: HpkeRole, mode: HpkeMode, @@ -1938,27 +1956,27 @@ export function hpkeKeySchedule( } } const suiteIdBytes = new Uint8Array(suiteId); - const pskIdHash = labeledExtract("HPKE-v1", + const pskIdHash = hpkeLabeledExtract("HPKE-v1", stringToBytes("psk_id_hash"), new Uint8Array([]), suiteIdBytes); - const infoHash = labeledExtract("HPKE-v1", + const infoHash = hpkeLabeledExtract("HPKE-v1", stringToBytes("info_hash"), info, suiteIdBytes); const keyScheduleCtx = new Uint8Array([mode, ...pskIdHash, ...infoHash]); - const secret = labeledExtract("HPKE-v1", + const secret = hpkeLabeledExtract("HPKE-v1", stringToBytes("secret"), psk || new Uint8Array([]), suiteIdBytes, sharedSecret); - const ctxKey = labeledExpand("HPKE-v1", + const ctxKey = hpkeLabeledExpand("HPKE-v1", secret, "key", keyScheduleCtx, suiteIdBytes, 32); // key 32 bytes / 256 bit - const ctxNonce = labeledExpand("HPKE-v1", + const ctxNonce = hpkeLabeledExpand("HPKE-v1", secret, "base_nonce", keyScheduleCtx, @@ -1970,11 +1988,11 @@ export function hpkeKeySchedule( } -export function hpkeSenderSetup( +export function hpkeSenderSetupNorand( pkR: HpkePublicKey, + skE: HpkeSecretKey, info: Uint8Array) : [HpkeEncapsulation, HpkeContext] { - const keypair = createEcdheKeyPair(); - const [enc, sharedSecret] = hpkeKemEncapsNorand(pkR, keypair.ecdhePriv); + const [enc, sharedSecret] = hpkeKemEncapsNorand(pkR, skE); const ctx = hpkeKeySchedule(HpkeRole.Sender, HpkeMode.Base, sharedSecret, @@ -1982,18 +2000,51 @@ export function hpkeSenderSetup( return [enc, ctx]; } -function hpkeComputeNonce( + +export function hpkeSenderSetup( + pkR: HpkePublicKey, + info: Uint8Array) : [HpkeEncapsulation, HpkeContext] { + const keypair = createEcdheKeyPair(); + return hpkeSenderSetupNorand(pkR, keypair.ecdhePriv, info); +} + +export function hpkeComputeNonce( ctx: HpkeContext) : Uint8Array { const nonce = new ArrayBuffer(12); + const seqNboBuf = new ArrayBuffer(8); const v = new DataView(nonce); + const vSeq = new DataView(seqNboBuf); + const seqNbo = new Uint8Array(seqNboBuf); + vSeq.setBigUint64(0, BigInt(ctx.seq)); const offset = 12 - 8; // nonce length - sequence counter of 64 bits - for (let i = 0; i < offset; i++) { - v.setUint8(i, ctx.nonce[i]); + let i = 0; + let j = 0; + for (i = 0; i < 12; i++) { + if (i < offset) { + v.setUint8(i, ctx.nonce[i]); + } else { + v.setUint8(i, ctx.nonce[i] ^ seqNbo[j++]); + } } - v.setBigUint64(offset, BigInt(ctx.seq)); return new Uint8Array(nonce); } +export function hpkeSealOneshotNorand( + pkR: HpkePublicKey, + skE: HpkeSecretKey, + info: Uint8Array, + aad: Uint8Array, + plaintext: Uint8Array): Uint8Array { + const [enc, ctx] = hpkeSenderSetupNorand(pkR, skE, info); + const nonce = hpkeComputeNonce(ctx); + const ct = chacha20poly1305_ietf_encrypt(plaintext, + aad, + nonce, + ctx.key); + ctx.seq++; + return new Uint8Array([...enc, ...ct]); +} + export function hpkeSealOneshot( pkR: HpkePublicKey, info: Uint8Array, @@ -2009,7 +2060,6 @@ export function hpkeSealOneshot( return new Uint8Array([...enc, ...ct]); } -export type HpkeSecretKey = Uint8Array; export function hpkeReceiverSetup( enc: Uint8Array, @@ -2032,7 +2082,7 @@ export function hpkeOpenOneshot( skR, info); const nonce = hpkeComputeNonce(ctx); - return chacha20poly1305_ietf_decrypt(ciphertext, + return chacha20poly1305_ietf_decrypt(ciphertext.slice(32), aad, nonce, ctx.key); @@ -2048,9 +2098,7 @@ export function hpkeCreateSecretKey() : HpkeSecretKey { return keypair.ecdhePriv as HpkeSecretKey; } -export interface ChaCha20Context { - input: Uint32Array, -} +// RFC 8439 ChaCha20-Poly1305 (IETF variants) function chacha20_toUint32(data: Uint8Array | number[], index: number): number { return data[index++] ^ (data[index++] << 8) ^ (data[index++] << 16) ^ (data[index] << 24) @@ -2161,6 +2209,8 @@ export function chacha20_ietf_xor( return out; } + + export function chacha20_ietf( clen: number, key: Uint8Array,