From 50a29632c704850f49188cdd1078fbbc4d6dc4f7 Mon Sep 17 00:00:00 2001 From: ABHAY PANDEY Date: Fri, 22 May 2026 15:14:46 +0530 Subject: [PATCH] test: optimize NIP-44 unit tests Signed-off-by: ABHAY PANDEY --- .changeset/vast-signs-melt.md | 2 + test/unit/utils/nip44.spec.ts | 116 ++++++++++++---------------------- 2 files changed, 41 insertions(+), 77 deletions(-) create mode 100644 .changeset/vast-signs-melt.md diff --git a/.changeset/vast-signs-melt.md b/.changeset/vast-signs-melt.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/vast-signs-melt.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/test/unit/utils/nip44.spec.ts b/test/unit/utils/nip44.spec.ts index 29ec4a50..a7d91a13 100644 --- a/test/unit/utils/nip44.spec.ts +++ b/test/unit/utils/nip44.spec.ts @@ -18,55 +18,56 @@ function pubkeyFromPrivkey(secHex: string): string { const SEC1 = '0000000000000000000000000000000000000000000000000000000000000001' const SEC2 = '0000000000000000000000000000000000000000000000000000000000000002' +const SEC3 = '0000000000000000000000000000000000000000000000000000000000000003' const KNOWN_CONVERSATION_KEY = 'c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d' const KNOWN_NONCE = '0000000000000000000000000000000000000000000000000000000000000001' const KNOWN_PLAINTEXT = 'a' const KNOWN_PAYLOAD = 'AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb' +let PUB1: string +let PUB2: string +let PUB3: string +let CONVERSATION_KEY: Buffer +let RECIPIENT_CONVERSATION_KEY: Buffer +let DIFFERENT_CONVERSATION_KEY: Buffer + // --------------------------------------------------------------------------- describe('NIP-44', () => { + before(() => { + PUB1 = pubkeyFromPrivkey(SEC1) + PUB2 = pubkeyFromPrivkey(SEC2) + PUB3 = pubkeyFromPrivkey(SEC3) + CONVERSATION_KEY = getConversationKey(SEC1, PUB2) + RECIPIENT_CONVERSATION_KEY = getConversationKey(SEC2, PUB1) + DIFFERENT_CONVERSATION_KEY = getConversationKey(SEC1, PUB3) + }) + describe('getConversationKey', () => { it('derives the correct conversation key from sec1 and pub2', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const key = getConversationKey(SEC1, pub2) - expect(key.toString('hex')).to.equal(KNOWN_CONVERSATION_KEY) + expect(CONVERSATION_KEY.toString('hex')).to.equal(KNOWN_CONVERSATION_KEY) }) it('is symmetric: conv(a, B) == conv(b, A)', () => { - const pub1 = pubkeyFromPrivkey(SEC1) - const pub2 = pubkeyFromPrivkey(SEC2) - const keyAB = getConversationKey(SEC1, pub2) - const keyBA = getConversationKey(SEC2, pub1) - expect(keyAB.toString('hex')).to.equal(keyBA.toString('hex')) + expect(CONVERSATION_KEY.toString('hex')).to.equal(RECIPIENT_CONVERSATION_KEY.toString('hex')) }) it('produces different keys for different key pairs', () => { - const sec3 = '0000000000000000000000000000000000000000000000000000000000000003' - const pub2 = pubkeyFromPrivkey(SEC2) - const pub3 = pubkeyFromPrivkey(sec3) - const key12 = getConversationKey(SEC1, pub2) - const key13 = getConversationKey(SEC1, pub3) - expect(key12.toString('hex')).to.not.equal(key13.toString('hex')) + expect(CONVERSATION_KEY.toString('hex')).to.not.equal(DIFFERENT_CONVERSATION_KEY.toString('hex')) }) }) describe('nip44Encrypt', () => { it('produces the canonical payload from the NIP-44 spec test vector', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) const nonce = Buffer.from(KNOWN_NONCE, 'hex') - const payload = nip44Encrypt(KNOWN_PLAINTEXT, conversationKey, nonce) + const payload = nip44Encrypt(KNOWN_PLAINTEXT, CONVERSATION_KEY, nonce) expect(payload).to.equal(KNOWN_PAYLOAD) }) it('produces a valid base64 string starting with version byte 0x02', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - const payload = nip44Encrypt('hello', conversationKey) + const payload = nip44Encrypt('hello', CONVERSATION_KEY) const decoded = Buffer.from(payload, 'base64') expect(decoded[0]).to.equal(2) // version byte @@ -74,99 +75,64 @@ describe('NIP-44', () => { }) it('produces different ciphertexts for the same plaintext (random nonce)', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - const payload1 = nip44Encrypt('same message', conversationKey) - const payload2 = nip44Encrypt('same message', conversationKey) + const payload1 = nip44Encrypt('same message', CONVERSATION_KEY) + const payload2 = nip44Encrypt('same message', CONVERSATION_KEY) expect(payload1).to.not.equal(payload2) }) it('throws for empty plaintext', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - expect(() => nip44Encrypt('', conversationKey)).to.throw('invalid plaintext length') + expect(() => nip44Encrypt('', CONVERSATION_KEY)).to.throw('invalid plaintext length') }) it('throws for plaintext exceeding 65535 bytes', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - expect(() => nip44Encrypt('x'.repeat(65536), conversationKey)).to.throw('invalid plaintext length') + expect(() => nip44Encrypt('x'.repeat(65536), CONVERSATION_KEY)).to.throw('invalid plaintext length') }) }) describe('nip44Decrypt', () => { it('decrypts the canonical NIP-44 spec test vector', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - const plaintext = nip44Decrypt(KNOWN_PAYLOAD, conversationKey) + const plaintext = nip44Decrypt(KNOWN_PAYLOAD, CONVERSATION_KEY) expect(plaintext).to.equal(KNOWN_PLAINTEXT) }) it('round-trips any plaintext through encrypt then decrypt', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) const original = 'Hola, que tal? 🌍' - const payload = nip44Encrypt(original, conversationKey) - const recovered = nip44Decrypt(payload, conversationKey) + const payload = nip44Encrypt(original, CONVERSATION_KEY) + const recovered = nip44Decrypt(payload, CONVERSATION_KEY) expect(recovered).to.equal(original) }) it('works with the symmetric key (recipient decrypts sender message)', () => { - const pub1 = pubkeyFromPrivkey(SEC1) - const pub2 = pubkeyFromPrivkey(SEC2) - - const senderKey = getConversationKey(SEC1, pub2) - const recipientKey = getConversationKey(SEC2, pub1) - - const payload = nip44Encrypt('secret message', senderKey) - const plaintext = nip44Decrypt(payload, recipientKey) + const payload = nip44Encrypt('secret message', CONVERSATION_KEY) + const plaintext = nip44Decrypt(payload, RECIPIENT_CONVERSATION_KEY) expect(plaintext).to.equal('secret message') }) it('throws when MAC is tampered', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - const payload = nip44Encrypt('tamper me', conversationKey) + const payload = nip44Encrypt('tamper me', CONVERSATION_KEY) // Flip the last character of the base64 payload to corrupt the MAC const tampered = payload.slice(0, -4) + 'AAAA' - expect(() => nip44Decrypt(tampered, conversationKey)).to.throw() + expect(() => nip44Decrypt(tampered, CONVERSATION_KEY)).to.throw() }) it('throws for payload starting with # (unsupported future version)', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - expect(() => nip44Decrypt('#not-base64', conversationKey)).to.throw('unknown version') + expect(() => nip44Decrypt('#not-base64', CONVERSATION_KEY)).to.throw('unknown version') }) it('throws for payload that is too short', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - - expect(() => nip44Decrypt('dG9vc2hvcnQ=', conversationKey)).to.throw('invalid payload size') + expect(() => nip44Decrypt('dG9vc2hvcnQ=', CONVERSATION_KEY)).to.throw('invalid payload size') }) it('throws for wrong conversation key', () => { - const sec3 = '0000000000000000000000000000000000000000000000000000000000000003' - const pub2 = pubkeyFromPrivkey(SEC2) - const pub3 = pubkeyFromPrivkey(sec3) - - const senderKey = getConversationKey(SEC1, pub2) - const wrongKey = getConversationKey(SEC1, pub3) - - const payload = nip44Encrypt('private', senderKey) + const payload = nip44Encrypt('private', CONVERSATION_KEY) - expect(() => nip44Decrypt(payload, wrongKey)).to.throw() + expect(() => nip44Decrypt(payload, DIFFERENT_CONVERSATION_KEY)).to.throw() }) }) @@ -176,9 +142,7 @@ describe('NIP-44', () => { }) it('returns undefined for a freshly encrypted payload', () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) - const payload = nip44Encrypt('hello', conversationKey) + const payload = nip44Encrypt('hello', CONVERSATION_KEY) expect(validateNip44Payload(payload)).to.be.undefined }) @@ -228,11 +192,9 @@ describe('NIP-44', () => { for (const [unpaddedLen, expectedPaddedLen] of cases) { it(`pads ${unpaddedLen} bytes to ${expectedPaddedLen} bytes`, () => { - const pub2 = pubkeyFromPrivkey(SEC2) - const conversationKey = getConversationKey(SEC1, pub2) const plaintext = 'a'.repeat(unpaddedLen) - const payload = nip44Encrypt(plaintext, conversationKey) + const payload = nip44Encrypt(plaintext, CONVERSATION_KEY) const decoded = Buffer.from(payload, 'base64') // Layout: 1 (version) + 32 (nonce) + paddedLen + 2 (length prefix) + 32 (mac)