@pryv/encryption 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, expect */
6
+
7
+ const aesText = require('../src/methods/aes-text-base64');
8
+ const { EventsCipher, Keyring } = require('../src');
9
+ const { base64ToBytes, bytesToBase64 } = require('../src/lib/base64');
10
+ const fixture = require('./fixtures/aes-text-base64.json');
11
+
12
+ describe('[ENCL] aes-text-base64 (legacy, decrypt-only)', function () {
13
+ describe('[ENCLV] known-answer vectors', function () {
14
+ fixture.vectors.forEach(function (vec, i) {
15
+ it(`[ENCLV${i}] vector ${i} decrypts to the exact material object`, async function () {
16
+ const material = await aesText.decrypt({ payload: vec.payload }, vec.passphrase);
17
+ expect(material).to.deep.equal(vec.plaintextObject);
18
+ });
19
+ });
20
+ });
21
+
22
+ describe('[ENCLN] negative cases (must reject)', function () {
23
+ const vec = fixture.vectors[0];
24
+
25
+ it('[ENCLNA] wrong passphrase throws', async function () {
26
+ let threw = false;
27
+ try {
28
+ await aesText.decrypt({ payload: vec.payload }, 'not the passphrase');
29
+ } catch (e) { threw = true; }
30
+ expect(threw).to.equal(true);
31
+ });
32
+
33
+ it('[ENCLNB] missing Salted__ prefix throws', async function () {
34
+ // Re-base64 the payload bytes with the 8-byte "Salted__" magic stripped.
35
+ const bytes = base64ToBytes(vec.payload);
36
+ const noMagic = bytes.slice(8);
37
+ let threw = false;
38
+ try {
39
+ await aesText.decrypt({ payload: bytesToBase64(noMagic) }, vec.passphrase);
40
+ } catch (e) { threw = true; }
41
+ expect(threw).to.equal(true);
42
+ });
43
+
44
+ it('[ENCLNC] truncated payload throws', async function () {
45
+ const truncated = vec.payload.slice(0, 24);
46
+ let threw = false;
47
+ try {
48
+ await aesText.decrypt({ payload: truncated }, vec.passphrase);
49
+ } catch (e) { threw = true; }
50
+ expect(threw).to.equal(true);
51
+ });
52
+
53
+ it('[ENCLND] missing payload throws', async function () {
54
+ let threw = false;
55
+ try {
56
+ await aesText.decrypt({}, vec.passphrase);
57
+ } catch (e) { threw = true; }
58
+ expect(threw).to.equal(true);
59
+ });
60
+ });
61
+
62
+ describe('[ENCLC] via EventsCipher', function () {
63
+ const vec = fixture.vectors[0];
64
+ const KEY_REF = 'legacy-passphrase';
65
+
66
+ it('[ENCLCA] decrypts an encrypted/aes-text-base64 event with the passphrase in the keyring', async function () {
67
+ const keyring = new Keyring({ [KEY_REF]: vec.passphrase });
68
+ const cipher = new EventsCipher(keyring);
69
+ const event = {
70
+ id: 'legacy-1',
71
+ streamIds: ['journal'],
72
+ type: 'encrypted/aes-text-base64',
73
+ content: { payload: vec.payload, keyRef: KEY_REF }
74
+ };
75
+ const dec = await cipher.decryptEvent(event);
76
+ expect(dec.type).to.equal(vec.plaintextObject.type);
77
+ expect(dec.content).to.deep.equal(vec.plaintextObject.content);
78
+ expect(dec.decryptedFrom).to.equal(event);
79
+ });
80
+
81
+ it('[ENCLCB] encryptEvent throws — the method is decrypt-only', async function () {
82
+ const keyring = new Keyring({ [KEY_REF]: vec.passphrase });
83
+ const cipher = new EventsCipher(keyring);
84
+ await expect(cipher.encryptEvent(
85
+ { type: 'note/txt', content: 'x' },
86
+ { method: 'aes-text-base64', keyRef: KEY_REF }
87
+ )).to.be.rejected;
88
+ });
89
+
90
+ it('[ENCLCC] encryptEventContent throws — the method is decrypt-only', async function () {
91
+ const keyring = new Keyring({ [KEY_REF]: vec.passphrase });
92
+ const cipher = new EventsCipher(keyring);
93
+ await expect(cipher.encryptEventContent(
94
+ { type: 'note/txt', content: 'x' },
95
+ { method: 'aes-text-base64', keyRef: KEY_REF }
96
+ )).to.be.rejected;
97
+ });
98
+ });
99
+
100
+ describe('[ENCLM] module shape', function () {
101
+ it('[ENCLMA] exposes decrypt but no encrypt (decrypt-only)', function () {
102
+ expect(aesText.decrypt).to.be.a('function');
103
+ expect(aesText.encrypt).to.equal(undefined);
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,244 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, expect */
6
+
7
+ const aes = require('../src/methods/aes-256-gcm');
8
+ const ecies = require('../src/methods/ecies-aes-256-gcm');
9
+ const { EventsCipher, Keyring } = require('../src');
10
+ const { base64ToBytes } = require('../src/lib/base64');
11
+ const aesVector = require('./fixtures/aes-256-gcm.json');
12
+ const eciesFixture = require('./fixtures/ecies-aes-256-gcm.json');
13
+
14
+ /**
15
+ * Fill a Uint8Array of the requested length with random bytes, chunked to stay
16
+ * within the 65 536-byte-per-call quota of `crypto.getRandomValues`.
17
+ */
18
+ function randomBytes (n) {
19
+ const bytes = new Uint8Array(n);
20
+ for (let offset = 0; offset < n; offset += 65536) {
21
+ globalThis.crypto.getRandomValues(bytes.subarray(offset, Math.min(offset + 65536, n)));
22
+ }
23
+ return bytes;
24
+ }
25
+
26
+ /** Deep-equal comparison of two byte arrays (as plain arrays for a clear diff). */
27
+ function sameBytes (a, b) {
28
+ expect(Array.from(a)).to.deep.equal(Array.from(b));
29
+ }
30
+
31
+ async function expectThrows (promiseFactory) {
32
+ let threw = false;
33
+ try { await promiseFactory(); } catch (e) { threw = true; }
34
+ expect(threw).to.equal(true);
35
+ }
36
+
37
+ const AES_KEY = aesVector.keyBase64;
38
+ const BIG = 100 * 1024; // ~100 KB
39
+
40
+ describe('[ENCF] encrypted attachments (raw-byte primitives)', function () {
41
+ this.timeout(20000);
42
+
43
+ // A pair holding BOTH sides of the fixed ecies recipient so a single keyRef
44
+ // resolves for encrypt (public) and decrypt (private).
45
+ const eciesPair = { publicKey: eciesFixture.recipient.publicKey, privateKey: eciesFixture.recipient.privateKey };
46
+
47
+ describe('[ENCFB] aes-256-gcm byte primitives', function () {
48
+ it('[ENCFBA] round-trips random bytes', async function () {
49
+ const bytes = randomBytes(1234);
50
+ const enc = await aes.encryptBytes(bytes, AES_KEY);
51
+ expect(enc).to.be.an.instanceOf(Uint8Array);
52
+ const back = await aes.decryptBytes(enc, AES_KEY);
53
+ sameBytes(back, bytes);
54
+ });
55
+
56
+ it('[ENCFBB] round-trips empty bytes', async function () {
57
+ const bytes = new Uint8Array(0);
58
+ const enc = await aes.encryptBytes(bytes, AES_KEY);
59
+ const back = await aes.decryptBytes(enc, AES_KEY);
60
+ sameBytes(back, bytes);
61
+ });
62
+
63
+ it('[ENCFBC] round-trips ~100 KB', async function () {
64
+ const bytes = randomBytes(BIG);
65
+ const enc = await aes.encryptBytes(bytes, AES_KEY);
66
+ const back = await aes.decryptBytes(enc, AES_KEY);
67
+ sameBytes(back, bytes);
68
+ });
69
+
70
+ it('[ENCFBD] does not mutate the input bytes', async function () {
71
+ const bytes = randomBytes(64);
72
+ const copy = bytes.slice();
73
+ await aes.encryptBytes(bytes, AES_KEY);
74
+ sameBytes(bytes, copy);
75
+ });
76
+
77
+ it('[ENCFBE] tampered ciphertext rejects', async function () {
78
+ const enc = await aes.encryptBytes(randomBytes(64), AES_KEY);
79
+ enc[enc.length - 1] ^= 0xff;
80
+ await expectThrows(() => aes.decryptBytes(enc, AES_KEY));
81
+ });
82
+
83
+ it('[ENCFBF] truncated bytes reject', async function () {
84
+ const enc = await aes.encryptBytes(randomBytes(64), AES_KEY);
85
+ await expectThrows(() => aes.decryptBytes(enc.slice(0, 8), AES_KEY));
86
+ });
87
+
88
+ it('[ENCFBG] wrong key rejects', async function () {
89
+ const enc = await aes.encryptBytes(randomBytes(64), AES_KEY);
90
+ const wrong = new Uint8Array(32).fill(9);
91
+ await expectThrows(() => aes.decryptBytes(enc, wrong));
92
+ });
93
+ });
94
+
95
+ describe('[ENCFE] ecies-aes-256-gcm byte primitives', function () {
96
+ const pub = eciesFixture.recipient.publicKey;
97
+ const priv = eciesFixture.recipient.privateKey;
98
+
99
+ it('[ENCFEA] round-trips random bytes', async function () {
100
+ const bytes = randomBytes(2048);
101
+ const enc = await ecies.encryptBytes(bytes, pub);
102
+ expect(enc).to.be.an.instanceOf(Uint8Array);
103
+ const back = await ecies.decryptBytes(enc, priv);
104
+ sameBytes(back, bytes);
105
+ });
106
+
107
+ it('[ENCFEB] round-trips empty bytes', async function () {
108
+ const bytes = new Uint8Array(0);
109
+ const enc = await ecies.encryptBytes(bytes, pub);
110
+ const back = await ecies.decryptBytes(enc, priv);
111
+ sameBytes(back, bytes);
112
+ });
113
+
114
+ it('[ENCFEC] round-trips ~100 KB', async function () {
115
+ const bytes = randomBytes(BIG);
116
+ const enc = await ecies.encryptBytes(bytes, pub);
117
+ const back = await ecies.decryptBytes(enc, priv);
118
+ sameBytes(back, bytes);
119
+ });
120
+
121
+ it('[ENCFED] tampered / truncated / wrong-key reject', async function () {
122
+ const enc = await ecies.encryptBytes(randomBytes(64), pub);
123
+ const tampered = enc.slice();
124
+ tampered[tampered.length - 1] ^= 0xff;
125
+ await expectThrows(() => ecies.decryptBytes(tampered, priv));
126
+ await expectThrows(() => ecies.decryptBytes(enc.slice(0, 10), priv));
127
+ const other = await ecies.generateKeyPair();
128
+ await expectThrows(() => ecies.decryptBytes(enc, other.privateKey));
129
+ });
130
+ });
131
+
132
+ describe('[ENCFC] cross-consistency: payload === base64(encryptBytes)', function () {
133
+ it('[ENCFCA] aes: decryptBytes(base64ToBytes(payload)) yields the UTF-8 JSON bytes', async function () {
134
+ const material = { type: 'note/txt', content: 'cross check' };
135
+ const content = await aes.encrypt(material, AES_KEY);
136
+ const plainBytes = await aes.decryptBytes(base64ToBytes(content.payload), AES_KEY);
137
+ const expected = new TextEncoder().encode(JSON.stringify(material));
138
+ sameBytes(plainBytes, expected);
139
+ });
140
+
141
+ it('[ENCFCB] ecies: decryptBytes(base64ToBytes(payload)) yields the UTF-8 JSON bytes', async function () {
142
+ const material = { type: 'note/txt', content: 'cross check' };
143
+ const content = await ecies.encrypt(material, eciesFixture.recipient.publicKey);
144
+ const plainBytes = await ecies.decryptBytes(base64ToBytes(content.payload), eciesFixture.recipient.privateKey);
145
+ const expected = new TextEncoder().encode(JSON.stringify(material));
146
+ sameBytes(plainBytes, expected);
147
+ });
148
+ });
149
+
150
+ describe('[ENCFH] EventsCipher attachment helpers', function () {
151
+ const AES_REF = 'att-key';
152
+ const ECIES_REF = 'att-recipient';
153
+ let aesCipher;
154
+ let eciesCipher;
155
+
156
+ before(function () {
157
+ aesCipher = new EventsCipher(new Keyring({ [AES_REF]: AES_KEY }));
158
+ eciesCipher = new EventsCipher(new Keyring({ [ECIES_REF]: eciesPair }));
159
+ });
160
+
161
+ it('[ENCFHA] aes: encryptAttachmentData -> decryptAttachmentData round-trips', async function () {
162
+ const bytes = randomBytes(4096);
163
+ const enc = await aesCipher.encryptAttachmentData(bytes, { method: 'aes-256-gcm', keyRef: AES_REF });
164
+ expect(enc).to.be.an.instanceOf(Uint8Array);
165
+ const event = await aesCipher.encryptEvent({ type: 'note/txt', content: 'x' }, { method: 'aes-256-gcm', keyRef: AES_REF });
166
+ const back = await aesCipher.decryptAttachmentData(event, enc);
167
+ sameBytes(back, bytes);
168
+ });
169
+
170
+ it('[ENCFHB] ecies: encryptAttachmentData -> decryptAttachmentData round-trips', async function () {
171
+ const bytes = randomBytes(4096);
172
+ const enc = await eciesCipher.encryptAttachmentData(bytes, { method: 'ecies-aes-256-gcm', keyRef: ECIES_REF });
173
+ const event = await eciesCipher.encryptEvent({ type: 'note/txt', content: 'x' }, { method: 'ecies-aes-256-gcm', keyRef: ECIES_REF });
174
+ const back = await eciesCipher.decryptAttachmentData(event, enc);
175
+ sameBytes(back, bytes);
176
+ });
177
+
178
+ it('[ENCFHC] decryptAttachmentData accepts an already-decrypted event (unwrap path)', async function () {
179
+ const bytes = randomBytes(512);
180
+ const enc = await aesCipher.encryptAttachmentData(bytes, { method: 'aes-256-gcm', keyRef: AES_REF });
181
+ const event = await aesCipher.encryptEvent({ type: 'note/txt', content: 'x' }, { method: 'aes-256-gcm', keyRef: AES_REF });
182
+ const decryptedEvent = await aesCipher.decryptEvent(event);
183
+ expect(decryptedEvent.decryptedFrom).to.equal(event); // sanity: it is the unwrapped form
184
+ const back = await aesCipher.decryptAttachmentData(decryptedEvent, enc);
185
+ sameBytes(back, bytes);
186
+ });
187
+
188
+ it('[ENCFHD] does not mutate the input bytes', async function () {
189
+ const bytes = randomBytes(64);
190
+ const copy = bytes.slice();
191
+ await aesCipher.encryptAttachmentData(bytes, { method: 'aes-256-gcm', keyRef: AES_REF });
192
+ sameBytes(bytes, copy);
193
+ });
194
+
195
+ it('[ENCFHE] legacy method has no byte support -> throws', async function () {
196
+ const ring = new Keyring({ legacy: 'passphrase' });
197
+ const cipher = new EventsCipher(ring);
198
+ await expectThrows(() => cipher.encryptAttachmentData(randomBytes(16), { method: 'aes-text-base64', keyRef: 'legacy' }));
199
+ const legacyEvent = { type: 'encrypted/aes-text-base64', content: { payload: 'x', keyRef: 'legacy' } };
200
+ await expectThrows(() => cipher.decryptAttachmentData(legacyEvent, randomBytes(16)));
201
+ });
202
+
203
+ it('[ENCFHF] unknown method throws', async function () {
204
+ await expectThrows(() => aesCipher.encryptAttachmentData(randomBytes(16), { method: 'nope', keyRef: AES_REF }));
205
+ const badEvent = { type: 'encrypted/nope', content: { payload: 'x' } };
206
+ await expectThrows(() => aesCipher.decryptAttachmentData(badEvent, randomBytes(16)));
207
+ });
208
+
209
+ it('[ENCFHG] missing key throws', async function () {
210
+ const cipher = new EventsCipher(new Keyring());
211
+ await expectThrows(() => cipher.encryptAttachmentData(randomBytes(16), { method: 'aes-256-gcm', keyRef: 'absent' }));
212
+ const event = { type: 'encrypted/aes-256-gcm', content: { payload: 'x', keyRef: 'absent' } };
213
+ await expectThrows(() => cipher.decryptAttachmentData(event, randomBytes(16)));
214
+ });
215
+
216
+ it('[ENCFHH] non-encrypted event throws on decrypt', async function () {
217
+ const plainEvent = { type: 'note/txt', content: 'plain' };
218
+ await expectThrows(() => aesCipher.decryptAttachmentData(plainEvent, randomBytes(16)));
219
+ });
220
+
221
+ it('[ENCFHI] a custom method may provide byte primitives', async function () {
222
+ // Identity "cipher" — enough to exercise the registration + validation path.
223
+ const custom = {
224
+ encrypt: async (m) => ({ payload: 'noop' }),
225
+ decrypt: async () => ({ type: 'note/txt', content: 'noop' }),
226
+ encryptBytes: async (bytes) => bytes,
227
+ decryptBytes: async (bytes) => bytes
228
+ };
229
+ const ring = new Keyring({ k: 'anything' });
230
+ const cipher = new EventsCipher(ring).registerMethod('passthrough', custom);
231
+ const bytes = randomBytes(32);
232
+ const enc = await cipher.encryptAttachmentData(bytes, { method: 'passthrough', keyRef: 'k' });
233
+ const event = { type: 'encrypted/passthrough', content: { keyRef: 'k' } };
234
+ const back = await cipher.decryptAttachmentData(event, enc);
235
+ sameBytes(back, bytes);
236
+ });
237
+
238
+ it('[ENCFHJ] registerMethod rejects a non-function encryptBytes/decryptBytes', function () {
239
+ const cipher = new EventsCipher(new Keyring());
240
+ expect(() => cipher.registerMethod('bad', { decrypt: async () => {}, encryptBytes: 123 })).to.throw();
241
+ expect(() => cipher.registerMethod('bad', { decrypt: async () => {}, decryptBytes: 'x' })).to.throw();
242
+ });
243
+ });
244
+ });
@@ -0,0 +1,203 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, expect */
6
+
7
+ const ecies = require('../src/methods/ecies-aes-256-gcm');
8
+ const { EventsCipher, Keyring } = require('../src');
9
+ const { base64ToBytes, bytesToBase64 } = require('../src/lib/base64');
10
+ const fixture = require('./fixtures/ecies-aes-256-gcm.json');
11
+
12
+ const subtle = globalThis.crypto.subtle;
13
+
14
+ // Various encodings of the fixed recipient key, prepared once.
15
+ let rawPubBytes; // Uint8Array, 65-byte SEC1 uncompressed public key
16
+ let rawPubBase64;
17
+ let pkcs8Bytes; // Uint8Array, PKCS#8 private key
18
+ let pkcs8Base64;
19
+ let pubCryptoKey; // ECDH public CryptoKey
20
+ let privCryptoKey; // ECDH private CryptoKey
21
+
22
+ before(async function () {
23
+ pubCryptoKey = await subtle.importKey('jwk', fixture.recipient.publicKey, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
24
+ privCryptoKey = await subtle.importKey('jwk', fixture.recipient.privateKey, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
25
+ rawPubBytes = new Uint8Array(await subtle.exportKey('raw', pubCryptoKey));
26
+ rawPubBase64 = bytesToBase64(rawPubBytes);
27
+ pkcs8Bytes = new Uint8Array(await subtle.exportKey('pkcs8', privCryptoKey));
28
+ pkcs8Base64 = bytesToBase64(pkcs8Bytes);
29
+ });
30
+
31
+ describe('[ENCP] ecies-aes-256-gcm (asymmetric)', function () {
32
+ describe('[ENCPV] known-answer decrypt vector', function () {
33
+ it('[ENCPVA] decrypts the committed vector byte-exactly (JWK private key)', async function () {
34
+ const material = await ecies.decrypt({ payload: fixture.payload }, fixture.recipient.privateKey);
35
+ expect(material).to.deep.equal(fixture.plaintextObject);
36
+ });
37
+ });
38
+
39
+ describe('[ENCPG] generateKeyPair + full cycle', function () {
40
+ it('[ENCPGA] mint -> encrypt(public) -> decrypt(private) round-trips', async function () {
41
+ const pair = await ecies.generateKeyPair();
42
+ expect(pair.publicKey).to.have.property('kty', 'EC');
43
+ expect(pair.privateKey).to.have.property('d');
44
+ const material = { type: 'note/txt', content: 'minted round trip' };
45
+ const content = await ecies.encrypt(material, pair.publicKey);
46
+ expect(content).to.have.property('payload').that.is.a('string');
47
+ const back = await ecies.decrypt(content, pair.privateKey);
48
+ expect(back).to.deep.equal(material);
49
+ });
50
+
51
+ it('[ENCPGB] two encryptions of the same material differ (random ephemeral + IV)', async function () {
52
+ const material = { type: 'note/txt', content: 'nonce check' };
53
+ const a = await ecies.encrypt(material, fixture.recipient.publicKey);
54
+ const b = await ecies.encrypt(material, fixture.recipient.publicKey);
55
+ expect(a.payload).to.not.equal(b.payload);
56
+ });
57
+ });
58
+
59
+ describe('[ENCPR] round-trips across key-material shapes', function () {
60
+ const material = { type: 'mass/kg', content: { value: 7 } };
61
+
62
+ it('[ENCPRA] JWK public -> JWK private', async function () {
63
+ const c = await ecies.encrypt(material, fixture.recipient.publicKey);
64
+ const back = await ecies.decrypt(c, fixture.recipient.privateKey);
65
+ expect(back).to.deep.equal(material);
66
+ });
67
+
68
+ it('[ENCPRB] CryptoKey public -> CryptoKey private', async function () {
69
+ const c = await ecies.encrypt(material, pubCryptoKey);
70
+ const back = await ecies.decrypt(c, privCryptoKey);
71
+ expect(back).to.deep.equal(material);
72
+ });
73
+
74
+ it('[ENCPRC] raw 65-byte public (Uint8Array) -> PKCS#8 private (Uint8Array)', async function () {
75
+ const c = await ecies.encrypt(material, rawPubBytes);
76
+ const back = await ecies.decrypt(c, pkcs8Bytes);
77
+ expect(back).to.deep.equal(material);
78
+ });
79
+
80
+ it('[ENCPRD] raw public (base64) -> PKCS#8 private (base64)', async function () {
81
+ const c = await ecies.encrypt(material, rawPubBase64);
82
+ const back = await ecies.decrypt(c, pkcs8Base64);
83
+ expect(back).to.deep.equal(material);
84
+ });
85
+
86
+ it('[ENCPRE] pair object -> pair object (each side picked by usage)', async function () {
87
+ const pair = { publicKey: fixture.recipient.publicKey, privateKey: fixture.recipient.privateKey };
88
+ const c = await ecies.encrypt(material, pair);
89
+ const back = await ecies.decrypt(c, pair);
90
+ expect(back).to.deep.equal(material);
91
+ });
92
+
93
+ it('[ENCPRF] pair object of CryptoKeys', async function () {
94
+ const pair = { publicKey: pubCryptoKey, privateKey: privCryptoKey };
95
+ const c = await ecies.encrypt(material, pair);
96
+ const back = await ecies.decrypt(c, pair);
97
+ expect(back).to.deep.equal(material);
98
+ });
99
+ });
100
+
101
+ describe('[ENCPK] normalizeKey', function () {
102
+ it('[ENCPKA] usage=encrypt accepts JWK public, CryptoKey, raw bytes, base64, pair', async function () {
103
+ for (const m of [fixture.recipient.publicKey, pubCryptoKey, rawPubBytes, rawPubBase64,
104
+ { publicKey: fixture.recipient.publicKey }]) {
105
+ const k = await ecies.normalizeKey(m, 'encrypt');
106
+ expect(k).to.be.an.instanceOf(CryptoKey);
107
+ expect(k.type).to.equal('public');
108
+ }
109
+ });
110
+
111
+ it('[ENCPKB] usage=decrypt accepts JWK private, CryptoKey, PKCS#8 bytes, base64, pair', async function () {
112
+ for (const m of [fixture.recipient.privateKey, privCryptoKey, pkcs8Bytes, pkcs8Base64,
113
+ { privateKey: fixture.recipient.privateKey }]) {
114
+ const k = await ecies.normalizeKey(m, 'decrypt');
115
+ expect(k).to.be.an.instanceOf(CryptoKey);
116
+ expect(k.type).to.equal('private');
117
+ }
118
+ });
119
+
120
+ it('[ENCPKC] throws on null/unsupported material', async function () {
121
+ let threw = false;
122
+ try { await ecies.normalizeKey(null, 'encrypt'); } catch (e) { threw = true; }
123
+ expect(threw).to.equal(true);
124
+ });
125
+
126
+ it('[ENCPKD] throws on unknown usage', async function () {
127
+ let threw = false;
128
+ try { await ecies.normalizeKey(fixture.recipient.publicKey, 'sign'); } catch (e) { threw = true; }
129
+ expect(threw).to.equal(true);
130
+ });
131
+ });
132
+
133
+ describe('[ENCPN] negative cases (must reject)', function () {
134
+ it('[ENCPNA] wrong private key fails authentication', async function () {
135
+ const other = await ecies.generateKeyPair();
136
+ let threw = false;
137
+ try {
138
+ await ecies.decrypt({ payload: fixture.payload }, other.privateKey);
139
+ } catch (e) { threw = true; }
140
+ expect(threw).to.equal(true);
141
+ });
142
+
143
+ it('[ENCPNB] tampered tag fails', async function () {
144
+ const bytes = base64ToBytes(fixture.payload);
145
+ bytes[bytes.length - 1] ^= 0xff;
146
+ let threw = false;
147
+ try {
148
+ await ecies.decrypt({ payload: bytesToBase64(bytes) }, fixture.recipient.privateKey);
149
+ } catch (e) { threw = true; }
150
+ expect(threw).to.equal(true);
151
+ });
152
+
153
+ it('[ENCPNC] truncated payload fails', async function () {
154
+ const truncated = fixture.payload.slice(0, 40);
155
+ let threw = false;
156
+ try {
157
+ await ecies.decrypt({ payload: truncated }, fixture.recipient.privateKey);
158
+ } catch (e) { threw = true; }
159
+ expect(threw).to.equal(true);
160
+ });
161
+
162
+ it('[ENCPND] missing payload throws', async function () {
163
+ let threw = false;
164
+ try { await ecies.decrypt({}, fixture.recipient.privateKey); } catch (e) { threw = true; }
165
+ expect(threw).to.equal(true);
166
+ });
167
+ });
168
+
169
+ describe('[ENCPC] via EventsCipher (sender/recipient split)', function () {
170
+ it('[ENCPCA] encrypt with public key in one keyring -> decrypt with private key in a DIFFERENT keyring', async function () {
171
+ const KEY_REF = 'recipient-alice';
172
+ const senderRing = new Keyring({ [KEY_REF]: fixture.recipient.publicKey });
173
+ const senderCipher = new EventsCipher(senderRing);
174
+ const recipientRing = new Keyring({ [KEY_REF]: fixture.recipient.privateKey });
175
+ const recipientCipher = new EventsCipher(recipientRing);
176
+
177
+ const plain = { streamIds: ['shared'], type: 'note/txt', content: 'for alice only' };
178
+ const enc = await senderCipher.encryptEvent(plain, { method: 'ecies-aes-256-gcm', keyRef: KEY_REF });
179
+ expect(enc.type).to.equal('encrypted/ecies-aes-256-gcm');
180
+ expect(enc.content).to.have.property('keyRef', KEY_REF);
181
+
182
+ const dec = await recipientCipher.decryptEvent(enc);
183
+ expect(dec.type).to.equal('note/txt');
184
+ expect(dec.content).to.equal('for alice only');
185
+ expect(dec.decryptedFrom).to.equal(enc);
186
+ });
187
+
188
+ it('[ENCPCB] decrypt failure (only public key available) falls back to the same ref', async function () {
189
+ const KEY_REF = 'recipient-alice';
190
+ const senderRing = new Keyring({ [KEY_REF]: fixture.recipient.publicKey });
191
+ const senderCipher = new EventsCipher(senderRing);
192
+ const plain = { streamIds: ['shared'], type: 'note/txt', content: 'no private key here' };
193
+ const enc = await senderCipher.encryptEvent(plain, { method: 'ecies-aes-256-gcm', keyRef: KEY_REF });
194
+
195
+ // The sender's keyring only holds the PUBLIC key, so decrypt cannot succeed.
196
+ const errors = [];
197
+ const cipher = new EventsCipher(senderRing, { onDecryptError: () => errors.push(1) });
198
+ const out = await cipher.decryptEvent(enc);
199
+ expect(out).to.equal(enc);
200
+ expect(errors).to.have.length(1);
201
+ });
202
+ });
203
+ });