aboutsummaryrefslogtreecommitdiffstats
path: root/js
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-09 17:36:21 -0700
committerbndw <ben@bdw.to>2026-03-09 17:36:21 -0700
commiteca962d7c26bbea57801576935b98f3540e43da6 (patch)
tree19653f192107b5b74a8113af570d4b9c979a7e0d /js
parent9886a5c9054f3308482fdcca0fa545c8befbcf5b (diff)
fix: harden DM crypto — HKDF key derivation, AEAD associated data, ModInverse nil check
- Derive symmetric key via HKDF-SHA256 instead of using raw X25519 shared secret - Bind sender + recipient pubkeys as ChaCha20-Poly1305 associated data to prevent key-confusion attacks - Guard against ModInverse panic on degenerate public keys (y=1) - Wrap DecryptDM error instead of swallowing it - Update JS client to match Go implementation - Document encryption details in PROTOCOL.md
Diffstat (limited to 'js')
-rw-r--r--js/axon.js30
1 files changed, 28 insertions, 2 deletions
diff --git a/js/axon.js b/js/axon.js
index 9f50340..761910c 100644
--- a/js/axon.js
+++ b/js/axon.js
@@ -8,6 +8,7 @@
8 8
9import { ed25519, x25519, edwardsToMontgomeryPriv, edwardsToMontgomeryPub } from '@noble/curves/ed25519'; 9import { ed25519, x25519, edwardsToMontgomeryPriv, edwardsToMontgomeryPub } from '@noble/curves/ed25519';
10import { sha256 } from '@noble/hashes/sha256'; 10import { sha256 } from '@noble/hashes/sha256';
11import { hkdf } from '@noble/hashes/hkdf';
11import { chacha20poly1305 } from '@noble/ciphers/chacha'; 12import { chacha20poly1305 } from '@noble/ciphers/chacha';
12import { encode, decode } from '@msgpack/msgpack'; 13import { encode, decode } from '@msgpack/msgpack';
13 14
@@ -287,6 +288,25 @@ function dhSharedSecret(localSeed, remoteEd25519Pub) {
287} 288}
288 289
289/** 290/**
291 * dmKey derives a 32-byte symmetric key from the raw DH shared secret
292 * via HKDF-SHA256, matching Go's dmKey().
293 */
294function dmKey(shared) {
295 return hkdf(sha256, shared, undefined, 'axon-dm-v1', 32);
296}
297
298/**
299 * dmAD builds associated data by concatenating sender + recipient Ed25519
300 * public keys, matching Go's dmAD().
301 */
302function dmAD(senderPub, recipientPub) {
303 const ad = new Uint8Array(senderPub.length + recipientPub.length);
304 ad.set(senderPub);
305 ad.set(recipientPub, senderPub.length);
306 return ad;
307}
308
309/**
290 * encryptDM(senderSeed, recipientPubkey, plaintext) → Uint8Array 310 * encryptDM(senderSeed, recipientPubkey, plaintext) → Uint8Array
291 * 311 *
292 * Returns nonce(12) || ciphertext+tag, matching Go's EncryptDM. 312 * Returns nonce(12) || ciphertext+tag, matching Go's EncryptDM.
@@ -296,8 +316,11 @@ export function encryptDM(senderSeed, recipientPubkey, plaintext) {
296 const enc = new TextEncoder(); 316 const enc = new TextEncoder();
297 const pt = typeof plaintext === 'string' ? enc.encode(plaintext) : plaintext; 317 const pt = typeof plaintext === 'string' ? enc.encode(plaintext) : plaintext;
298 const shared = dhSharedSecret(senderSeed, recipientPubkey); 318 const shared = dhSharedSecret(senderSeed, recipientPubkey);
319 const key = dmKey(shared);
299 const nonce = crypto.getRandomValues(new Uint8Array(12)); 320 const nonce = crypto.getRandomValues(new Uint8Array(12));
300 const ct = chacha20poly1305(shared, nonce).encrypt(pt); 321 const senderPub = ed25519.getPublicKey(senderSeed);
322 const ad = dmAD(senderPub, recipientPubkey);
323 const ct = chacha20poly1305(key, nonce, ad).encrypt(pt);
301 const out = new Uint8Array(12 + ct.length); 324 const out = new Uint8Array(12 + ct.length);
302 out.set(nonce); 325 out.set(nonce);
303 out.set(ct, 12); 326 out.set(ct, 12);
@@ -312,9 +335,12 @@ export function encryptDM(senderSeed, recipientPubkey, plaintext) {
312export function decryptDM(recipientSeed, senderPubkey, blob) { 335export function decryptDM(recipientSeed, senderPubkey, blob) {
313 if (blob.length < 12) throw new Error('axon: dm blob too short'); 336 if (blob.length < 12) throw new Error('axon: dm blob too short');
314 const shared = dhSharedSecret(recipientSeed, senderPubkey); 337 const shared = dhSharedSecret(recipientSeed, senderPubkey);
338 const key = dmKey(shared);
315 const nonce = blob.slice(0, 12); 339 const nonce = blob.slice(0, 12);
316 const ct = blob.slice(12); 340 const ct = blob.slice(12);
317 return chacha20poly1305(shared, nonce).decrypt(ct); 341 const recipientPub = ed25519.getPublicKey(recipientSeed);
342 const ad = dmAD(senderPubkey, recipientPub);
343 return chacha20poly1305(key, nonce, ad).decrypt(ct);
318} 344}
319 345
320// --------------------------------------------------------------------------- 346// ---------------------------------------------------------------------------