Skip to content

Commit 11562f1

Browse files
committed
test: NIP-44 length-bound and key-isolation edges
nip44-edges.test.js (8): plaintext bounds (1..65535) — empty and oversized are rejected, the single-byte minimum and exact 65535-byte maximum round-trip; key isolation — a payload encrypted under convKey(alice,bob) fails the MAC under convKey(alice,eve) rather than silently decrypting; malformed/too-short and non-base64 payloads are rejected. Complements (does not duplicate) the spec vectors and basic round-trip already in nip44.test.js. Co-Authored-By: jjohare <github@thedreamlab.uk>
1 parent 56fc537 commit 11562f1

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

test/nip44-edges.test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* NIP-44 (v2) edge cases and security boundaries NOT already covered by
3+
* nip44.test.js (which owns the spec vectors and the basic round-trip).
4+
*
5+
* Focus here:
6+
* - plaintext length bounds (1..65535): empty is rejected, max is allowed
7+
* - key isolation: a payload does not decrypt under the wrong conversation key
8+
* - malformed-payload rejection (too short to contain version+nonce+mac)
9+
*/
10+
11+
import { describe, it } from 'node:test';
12+
import assert from 'node:assert/strict';
13+
import {
14+
getConversationKey, encrypt, decrypt, hexToBytes
15+
} from '../src/nip44.js';
16+
import { generateKeypair } from '../src/crypto.js';
17+
18+
describe('NIP-44 plaintext length bounds', () => {
19+
it('rejects empty plaintext (NIP-44 forbids 0-length)', async () => {
20+
const a = await generateKeypair();
21+
const b = await generateKeypair();
22+
const ck = getConversationKey(a.privateKey, b.publicKey);
23+
assert.throws(() => encrypt('', ck), /Invalid plaintext length/);
24+
});
25+
26+
it('encrypts/decrypts a single-byte plaintext (min boundary)', async () => {
27+
const a = await generateKeypair();
28+
const b = await generateKeypair();
29+
const ck = getConversationKey(a.privateKey, b.publicKey);
30+
assert.equal(decrypt(encrypt('x', ck), ck), 'x');
31+
});
32+
33+
it('encrypts/decrypts the maximum-length plaintext (65535 bytes)', async () => {
34+
const a = await generateKeypair();
35+
const b = await generateKeypair();
36+
const ck = getConversationKey(a.privateKey, b.publicKey);
37+
const max = 'a'.repeat(65535);
38+
assert.equal(decrypt(encrypt(max, ck), ck), max);
39+
});
40+
41+
it('rejects plaintext above the maximum length', async () => {
42+
const a = await generateKeypair();
43+
const b = await generateKeypair();
44+
const ck = getConversationKey(a.privateKey, b.publicKey);
45+
assert.throws(() => encrypt('a'.repeat(65536), ck), /Invalid plaintext length/);
46+
});
47+
});
48+
49+
describe('NIP-44 key isolation', () => {
50+
it('a third party with a different conversation key cannot decrypt', async () => {
51+
const alice = await generateKeypair();
52+
const bob = await generateKeypair();
53+
const eve = await generateKeypair();
54+
55+
const ckAB = getConversationKey(alice.privateKey, bob.publicKey);
56+
const payload = encrypt('for bob only', ckAB);
57+
58+
// Eve derives a conversation key with alice — different shared secret.
59+
const ckAE = getConversationKey(alice.privateKey, eve.publicKey);
60+
assert.throws(() => decrypt(payload, ckAE), /Invalid MAC/,
61+
'wrong conversation key must fail the MAC check, not silently decrypt');
62+
});
63+
64+
it('the same conversation key from both peers decrypts (sanity, no key reuse leak)', async () => {
65+
const alice = await generateKeypair();
66+
const bob = await generateKeypair();
67+
const ckA = getConversationKey(alice.privateKey, bob.publicKey);
68+
const ckB = getConversationKey(bob.privateKey, alice.publicKey);
69+
const payload = encrypt('round', ckA);
70+
assert.equal(decrypt(payload, ckB), 'round');
71+
});
72+
});
73+
74+
describe('NIP-44 malformed payload rejection', () => {
75+
it('rejects a payload too short to hold version+nonce+ciphertext+mac', () => {
76+
const ck = hexToBytes('11'.repeat(32));
77+
// base64 of a single version byte 0x02 — far below the minimum frame size.
78+
const tooShort = btoa(String.fromCharCode(2));
79+
assert.throws(() => decrypt(tooShort, ck));
80+
});
81+
82+
it('rejects non-base64 garbage', () => {
83+
const ck = hexToBytes('22'.repeat(32));
84+
assert.throws(() => decrypt('!!!not base64!!!', ck));
85+
});
86+
});

0 commit comments

Comments
 (0)