-
Notifications
You must be signed in to change notification settings - Fork 222
Expand file tree
/
Copy pathnip44.test.ts
More file actions
275 lines (245 loc) · 10.3 KB
/
nip44.test.ts
File metadata and controls
275 lines (245 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
import { test, expect } from 'bun:test'
import { v2 } from './nip44.js'
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'
import { default as vec } from './nip44.vectors.json' with { type: 'json' }
import { schnorr } from '@noble/curves/secp256k1.js'
const v2vec = vec.v2
test('get_conversation_key', () => {
for (const v of v2vec.valid.get_conversation_key) {
const key = v2.utils.getConversationKey(hexToBytes(v.sec1), v.pub2)
expect(bytesToHex(key)).toEqual(v.conversation_key)
}
})
test('encrypt_decrypt', () => {
for (const v of v2vec.valid.encrypt_decrypt) {
const pub2 = bytesToHex(schnorr.getPublicKey(hexToBytes(v.sec2)))
const key = v2.utils.getConversationKey(hexToBytes(v.sec1), pub2)
expect(bytesToHex(key)).toEqual(v.conversation_key)
const ciphertext = v2.encrypt(v.plaintext, key, hexToBytes(v.nonce))
expect(ciphertext).toEqual(v.payload)
const decrypted = v2.decrypt(ciphertext, key)
expect(decrypted).toEqual(v.plaintext)
}
})
test('calc_padded_len', () => {
for (const [len, shouldBePaddedTo] of v2vec.valid.calc_padded_len) {
const actual = v2.utils.calcPaddedLen(len)
expect(actual).toEqual(shouldBePaddedTo)
}
})
test('decrypt', async () => {
for (const v of v2vec.invalid.decrypt) {
expect(() => v2.decrypt(v.payload, hexToBytes(v.conversation_key))).toThrow(new RegExp(v.note))
}
})
test('get_conversation_key', async () => {
for (const v of v2vec.invalid.get_conversation_key) {
expect(() => v2.utils.getConversationKey(hexToBytes(v.sec1), v.pub2)).toThrow(
/(Point is not on curve|Cannot find square root|invalid field element)/,
)
}
})
// Extended prefix (big payload) tests
test('pad/unpad boundary: 65535 bytes uses 2-byte u16 prefix', () => {
const plaintext = 'a'.repeat(65535)
const padded = v2.utils.pad(plaintext)
// First 2 bytes should be 0xff 0xff (65535 as u16 BE)
expect(padded[0]).toEqual(0xff)
expect(padded[1]).toEqual(0xff)
const unpadded = v2.utils.unpad(padded)
expect(unpadded).toEqual(plaintext)
})
test('pad/unpad boundary: 65536 bytes uses 6-byte extended prefix', () => {
const plaintext = 'a'.repeat(65536)
const padded = v2.utils.pad(plaintext)
// First 2 bytes should be 0x00 0x00 (sentinel)
expect(padded[0]).toEqual(0x00)
expect(padded[1]).toEqual(0x00)
// Next 4 bytes should be 0x00 0x01 0x00 0x00 (65536 as u32 BE)
expect(padded[2]).toEqual(0x00)
expect(padded[3]).toEqual(0x01)
expect(padded[4]).toEqual(0x00)
expect(padded[5]).toEqual(0x00)
const unpadded = v2.utils.unpad(padded)
expect(unpadded).toEqual(plaintext)
})
test('pad/unpad boundary: 65537 bytes uses 6-byte extended prefix', () => {
const plaintext = 'a'.repeat(65537)
const padded = v2.utils.pad(plaintext)
// First 2 bytes should be sentinel
expect(padded[0]).toEqual(0x00)
expect(padded[1]).toEqual(0x00)
// Next 4 bytes should be 0x00 0x01 0x00 0x01 (65537 as u32 BE)
expect(padded[2]).toEqual(0x00)
expect(padded[3]).toEqual(0x01)
expect(padded[4]).toEqual(0x00)
expect(padded[5]).toEqual(0x01)
const unpadded = v2.utils.unpad(padded)
expect(unpadded).toEqual(plaintext)
})
test('encrypt/decrypt round-trip with big payload (65536 bytes)', () => {
const plaintext = 'x'.repeat(65536)
const sec1 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
const sec2 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
const pub2 = bytesToHex(schnorr.getPublicKey(sec2))
const conversationKey = v2.utils.getConversationKey(sec1, pub2)
const encrypted = v2.encrypt(plaintext, conversationKey)
const decrypted = v2.decrypt(encrypted, conversationKey)
expect(decrypted).toEqual(plaintext)
})
test('encrypt/decrypt round-trip with 100000 byte payload', () => {
const plaintext = 'z'.repeat(100000)
const sec1 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
const sec2 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
const pub2 = bytesToHex(schnorr.getPublicKey(sec2))
const conversationKey = v2.utils.getConversationKey(sec1, pub2)
const encrypted = v2.encrypt(plaintext, conversationKey)
const decrypted = v2.decrypt(encrypted, conversationKey)
expect(decrypted).toEqual(plaintext)
})
// Canonicality: reject non-canonical extended prefix for small lengths
test('unpad rejects non-canonical extended prefix for length=1', () => {
const unpaddedLen = 1
const calcPaddedLen = v2.utils.calcPaddedLen(unpaddedLen) // 32
const buf = new Uint8Array(6 + calcPaddedLen) // 6-byte prefix + 32 bytes padded
buf[0] = 0x00
buf[1] = 0x00 // sentinel
buf[2] = 0x00
buf[3] = 0x00
buf[4] = 0x00
buf[5] = 0x01 // u32 BE = 1
buf[6] = 0x61 // 'a'
expect(() => v2.utils.unpad(buf)).toThrow(/invalid padding/)
})
test('unpad rejects non-canonical extended prefix for length=1000', () => {
const unpaddedLen = 1000
const calcPaddedLen = v2.utils.calcPaddedLen(unpaddedLen) // 1024
const buf = new Uint8Array(6 + calcPaddedLen)
buf[0] = 0x00
buf[1] = 0x00 // sentinel
buf[2] = 0x00
buf[3] = 0x00
buf[4] = 0x03
buf[5] = 0xe8 // u32 BE = 1000
for (let i = 0; i < unpaddedLen; i++) buf[6 + i] = 0x61 // 'a'
expect(() => v2.utils.unpad(buf)).toThrow(/invalid padding/)
})
test('unpad rejects non-canonical extended prefix for length=65535', () => {
const unpaddedLen = 65535
const calcPaddedLen = v2.utils.calcPaddedLen(unpaddedLen) // 65536
const buf = new Uint8Array(6 + calcPaddedLen)
buf[0] = 0x00
buf[1] = 0x00 // sentinel
buf[2] = 0x00
buf[3] = 0x00
buf[4] = 0xff
buf[5] = 0xff // u32 BE = 65535
for (let i = 0; i < unpaddedLen; i++) buf[6 + i] = 0x61
expect(() => v2.utils.unpad(buf)).toThrow(/invalid padding/)
})
// Malformed extended prefix: buffer too short for the 6-byte header
test('unpad rejects truncated extended prefix (buffer shorter than 6 bytes)', () => {
const buf = new Uint8Array([0x00, 0x00, 0x00, 0x01])
expect(() => v2.utils.unpad(buf)).toThrow()
})
// calcPaddedLen must not overflow for large values (regression: 1 << 31 is negative in JS)
test('calcPaddedLen handles values above 2^30 correctly', () => {
const len = 2 ** 30 + 1 // 1073741825
const padded = v2.utils.calcPaddedLen(len)
expect(padded).toBeGreaterThanOrEqual(len)
// chunk = 2^31 / 8 = 268435456
expect(padded % 268435456).toEqual(0)
})
test('calcPaddedLen handles 2^31 correctly', () => {
const len = 2 ** 31 // 2147483648
const padded = v2.utils.calcPaddedLen(len)
expect(padded).toBeGreaterThanOrEqual(len)
// chunk = 2^32 / 8 = 536870912
expect(padded % 536870912).toEqual(0)
})
test('calcPaddedLen handles max plaintext size (2^32 - 1)', () => {
const len = 0xffffffff // 4294967295
const padded = v2.utils.calcPaddedLen(len)
expect(padded).toBeGreaterThanOrEqual(len)
// Must be exactly 2^32 = 4294967296
expect(padded).toEqual(4294967296)
})
// Multi-byte UTF-8 near the 65536 boundary: byte length != char length
test('pad/unpad with multi-byte UTF-8 near 65536 byte boundary', () => {
// U+00E9 (é) is 2 bytes in UTF-8. Use 32768 of them = 65536 bytes but 32768 chars
const plaintext = '\u00e9'.repeat(32768)
const encoded = new TextEncoder().encode(plaintext)
expect(encoded.length).toEqual(65536) // byte length triggers extended prefix
expect(plaintext.length).toEqual(32768) // char length is much smaller
const padded = v2.utils.pad(plaintext)
// Should use extended prefix since byte length is 65536
expect(padded[0]).toEqual(0x00)
expect(padded[1]).toEqual(0x00)
const unpadded = v2.utils.unpad(padded)
expect(unpadded).toEqual(plaintext)
})
test('encrypt/decrypt with multi-byte UTF-8 at 65536 bytes', () => {
const plaintext = '\u00e9'.repeat(32768) // 65536 bytes
const sec1 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
const sec2 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
const pub2 = bytesToHex(schnorr.getPublicKey(sec2))
const conversationKey = v2.utils.getConversationKey(sec1, pub2)
const encrypted = v2.encrypt(plaintext, conversationKey)
const decrypted = v2.decrypt(encrypted, conversationKey)
expect(decrypted).toEqual(plaintext)
})
// Spec test vectors: SHA-256 checksums for boundary payloads
test('spec test vectors: SHA-256 of payload at 65535/65536/65537', async () => {
const convKey = hexToBytes('c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d')
const nonce = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
async function sha256hex(data: Uint8Array): Promise<string> {
const hash = await crypto.subtle.digest('SHA-256', data)
return bytesToHex(new Uint8Array(hash))
}
const vectors = [
{
len: 65535,
prefix: 'u16',
padded_len: 65536,
plaintext_sha256: '6e1bebca6a8229364a162a72ef064826c4cd7457bf54f190ef782bd9deff3e42',
payload_sha256: '6d8c2810d1e870fbaa1f0a0937126cca837a15f9260e27060c331d70a3c0bc84',
},
{
len: 65536,
prefix: 'extended',
padded_len: 65536,
plaintext_sha256: 'bf718b6f653bebc184e1479f1935b8da974d701b893afcf49e701f3e2f9f9c5a',
payload_sha256: 'b7b4edb36ba92e267d322d56d9aebc22e7fa96ff52e3c12adc07f07a43cbc616',
},
{
len: 65537,
prefix: 'extended',
padded_len: 81920,
plaintext_sha256: '008ffc88d3c96a9f307524eb361e47c5222a887fc45fa0c1fb8d429c5c23b430',
payload_sha256: 'eeb7c7c5373894ea2c1547cfd3ccb15d5a0b2d619da852e5c79df792dcc9e435',
},
]
for (const vec of vectors) {
const plaintext = 'a'.repeat(vec.len)
const ptBytes = new TextEncoder().encode(plaintext)
// Verify plaintext SHA-256
expect(await sha256hex(ptBytes)).toEqual(vec.plaintext_sha256)
// Verify padded length
expect(v2.utils.calcPaddedLen(vec.len)).toEqual(vec.padded_len)
// Verify prefix type
const padded = v2.utils.pad(plaintext)
if (vec.prefix === 'u16') {
expect(padded[0]).not.toEqual(0)
} else {
expect(padded[0]).toEqual(0)
expect(padded[1]).toEqual(0)
}
// Encrypt and verify payload SHA-256
const payload = v2.encrypt(plaintext, convKey, nonce)
const payloadBytes = new TextEncoder().encode(payload)
expect(await sha256hex(payloadBytes)).toEqual(vec.payload_sha256)
// Verify round-trip decrypt
const decrypted = v2.decrypt(payload, convKey)
expect(decrypted).toEqual(plaintext)
}
})