@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,166 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, after, expect */
6
+
7
+ const { createId: cuid } = require('@paralleldrive/cuid2');
8
+ const { EventsCipher, Keyring } = require('../src');
9
+ const vector = require('./fixtures/aes-256-gcm.json');
10
+
11
+ // Mirror the sibling components: `testData`/`pryv` are provided as globals by the
12
+ // Node mocha loader and by the browser bundle; fall back to a require in Node.
13
+ const testData = global.testData || require('../../../test/test-data');
14
+ const pryv = global.pryv || require('pryv');
15
+
16
+ const METHOD = 'aes-256-gcm';
17
+ const KEY_REF = 'integration-key';
18
+ const STREAM_ID = 'data';
19
+
20
+ describe('[ENCI] EventsCipher integration (live service)', function () {
21
+ this.timeout(30000);
22
+
23
+ let conn;
24
+ let cipher;
25
+ const createdIds = [];
26
+
27
+ before(async function () {
28
+ this.timeout(30000);
29
+ await testData.prepare();
30
+ conn = new pryv.Connection(testData.apiEndpointWithToken);
31
+ const keyring = new Keyring({ [KEY_REF]: vector.keyBase64 });
32
+ cipher = new EventsCipher(keyring);
33
+ });
34
+
35
+ after(async function () {
36
+ // Best-effort cleanup: trash + delete each created event (delete twice).
37
+ for (const id of createdIds) {
38
+ try {
39
+ await conn.api([
40
+ { method: 'events.delete', params: { id } },
41
+ { method: 'events.delete', params: { id } }
42
+ ]);
43
+ } catch (e) {
44
+ // ignore cleanup failures — the run is over
45
+ }
46
+ }
47
+ });
48
+
49
+ it('[ENCIA] creates, reads (batch + streamed), updates and re-reads an encrypted event', async function () {
50
+ // Unique per-run marker so parallel / repeated runs never collide.
51
+ const marker = cuid();
52
+ const originalContent = 'secret ' + marker;
53
+ const updatedContent = 'updated ' + marker;
54
+
55
+ // --- (b) create an encrypted event -------------------------------------
56
+ const plain = { streamIds: [STREAM_ID], type: 'note/txt', content: originalContent };
57
+ const encrypted = await cipher.encryptEvent(plain, { method: METHOD, keyRef: KEY_REF });
58
+ expect(encrypted.type).to.equal('encrypted/' + METHOD);
59
+
60
+ const createRes = await conn.api([{ method: 'events.create', params: encrypted }]);
61
+ // Surface the exact server behaviour for the encrypted/* type.
62
+ expect(
63
+ createRes[0].event,
64
+ 'events.create rejected the encrypted/* event: ' + JSON.stringify(createRes[0].error)
65
+ ).to.exist;
66
+ const created = createRes[0].event;
67
+ createdIds.push(created.id);
68
+
69
+ // The server stores the encrypted envelope verbatim: type + ciphertext content.
70
+ expect(created.type).to.equal('encrypted/' + METHOD);
71
+ expect(created.content).to.have.property('payload').that.is.a('string');
72
+ expect(created.content).to.have.property('keyRef', KEY_REF);
73
+
74
+ // --- (c1) retrieve via events.get + decryptEvents -----------------------
75
+ const getRes = await conn.api([{ method: 'events.get', params: { streams: [STREAM_ID], limit: 20 } }]);
76
+ expect(getRes[0].events, 'events.get failed: ' + JSON.stringify(getRes[0].error)).to.be.an('array');
77
+ const fetched = getRes[0].events.find((e) => e.id === created.id);
78
+ expect(fetched, 'created event not returned by events.get').to.exist;
79
+ expect(fetched.type).to.equal('encrypted/' + METHOD);
80
+
81
+ const decrypted = await cipher.decryptEvents([fetched]);
82
+ expect(decrypted[0].type).to.equal('note/txt');
83
+ expect(decrypted[0].content).to.equal(originalContent);
84
+ expect(decrypted[0].decryptedFrom).to.equal(fetched);
85
+
86
+ // --- (c2) retrieve via getEventsStreamed + wrapForEachEvent -------------
87
+ const streamed = [];
88
+ await conn.getEventsStreamed(
89
+ { streams: [STREAM_ID], limit: 100 },
90
+ cipher.wrapForEachEvent((event) => { streamed.push(event); })
91
+ );
92
+ const streamedOne = streamed.find((e) => e.decryptedFrom != null && e.decryptedFrom.id === created.id);
93
+ expect(streamedOne, 'created event not seen in getEventsStreamed').to.exist;
94
+ expect(streamedOne.type).to.equal('note/txt');
95
+ expect(streamedOne.content).to.equal(originalContent);
96
+
97
+ // --- (d) update with NEW plaintext, re-fetch and decrypt ---------------
98
+ const reEncrypted = await cipher.encryptEventContent(
99
+ { type: 'note/txt', content: updatedContent },
100
+ { method: METHOD, keyRef: KEY_REF }
101
+ );
102
+ expect(reEncrypted.type).to.equal('encrypted/' + METHOD);
103
+
104
+ const updateRes = await conn.api([{ method: 'events.update', params: { id: created.id, update: reEncrypted } }]);
105
+ expect(
106
+ updateRes[0].event,
107
+ 'events.update rejected the re-encrypted content: ' + JSON.stringify(updateRes[0].error)
108
+ ).to.exist;
109
+
110
+ const reGet = await conn.api([{ method: 'events.get', params: { streams: [STREAM_ID], limit: 20 } }]);
111
+ const reFetched = reGet[0].events.find((e) => e.id === created.id);
112
+ expect(reFetched, 'updated event not returned by events.get').to.exist;
113
+ expect(reFetched.type).to.equal('encrypted/' + METHOD);
114
+
115
+ const reDecrypted = await cipher.decryptEvent(reFetched);
116
+ expect(reDecrypted.type).to.equal('note/txt');
117
+ expect(reDecrypted.content).to.equal(updatedContent);
118
+ });
119
+
120
+ it('[ENCIB] round-trips an ENCRYPTED attachment through the live service', async function () {
121
+ const marker = cuid();
122
+
123
+ // Random binary payload — encrypted client-side into the method's raw byte
124
+ // layout (no Base64), then uploaded as the file body.
125
+ const original = new Uint8Array(3000);
126
+ for (let i = 0; i < original.length; i += 65536) {
127
+ globalThis.crypto.getRandomValues(original.subarray(i, Math.min(i + 65536, original.length)));
128
+ }
129
+ const encryptedBytes = await cipher.encryptAttachmentData(original, { method: METHOD, keyRef: KEY_REF });
130
+
131
+ // The event itself is encrypted with the same method/key.
132
+ const plain = { streamIds: [STREAM_ID], type: 'note/txt', content: 'attach ' + marker };
133
+ const encrypted = await cipher.encryptEvent(plain, { method: METHOD, keyRef: KEY_REF });
134
+
135
+ // Isomorphic upload: a Blob of the encrypted bytes via createEventWithFileFromBuffer
136
+ // (accepts a Blob in both Node >= 20 and the browser).
137
+ const blob = new Blob([encryptedBytes], { type: 'application/octet-stream' });
138
+ const createBody = await conn.createEventWithFileFromBuffer(encrypted, blob, marker + '.bin');
139
+ expect(createBody.event, 'create-with-file failed: ' + JSON.stringify(createBody.error)).to.exist;
140
+ const created = createBody.event;
141
+ createdIds.push(created.id);
142
+
143
+ // The server stores the encrypted envelope + the (already-encrypted) file verbatim.
144
+ expect(created.type).to.equal('encrypted/' + METHOD);
145
+ expect(created.attachments, 'no attachments on created event').to.be.an('array').with.length(1);
146
+ const att = created.attachments[0];
147
+ expect(att.id).to.be.a('string');
148
+ expect(att.readToken).to.be.a('string');
149
+ // The core sees only ciphertext, so the stored size is the encrypted length
150
+ // (aes-256-gcm: iv[12] + plaintext + tag[16]).
151
+ expect(att.size).to.equal(original.length + 12 + 16);
152
+
153
+ // Download the raw bytes: GET {endpoint}events/{eventId}/{fileId}?readToken=...
154
+ const url = conn.endpoint + 'events/' + created.id + '/' + att.id + '?readToken=' + att.readToken;
155
+ const dlRes = await fetch(url);
156
+ expect(dlRes.status, 'attachment download HTTP status').to.equal(200);
157
+ const downloaded = new Uint8Array(await dlRes.arrayBuffer());
158
+
159
+ // Bytes served back must equal the ciphertext we uploaded...
160
+ expect(Array.from(downloaded)).to.deep.equal(Array.from(encryptedBytes));
161
+
162
+ // ...and decrypting them (via the created event) yields the original bytes exactly.
163
+ const decrypted = await cipher.decryptAttachmentData(created, downloaded);
164
+ expect(Array.from(decrypted)).to.deep.equal(Array.from(original));
165
+ });
166
+ });
@@ -0,0 +1,91 @@
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 { Keyring } = require('../src');
8
+
9
+ describe('[ENCK] Keyring', function () {
10
+ describe('[ENCKV] key/value store', function () {
11
+ it('[ENCKVA] returns a key stored under its keyRef', async function () {
12
+ const material = new Uint8Array(32).fill(7);
13
+ const keyring = new Keyring({ 'journal-2026': material });
14
+ const found = await keyring.getKeyFor('aes-256-gcm', 'journal-2026', undefined);
15
+ expect(found).to.equal(material);
16
+ });
17
+
18
+ it('[ENCKVB] returns null when the keyRef is unknown and no resolvers set', async function () {
19
+ const keyring = new Keyring({ 'journal-2026': new Uint8Array(32) });
20
+ const found = await keyring.getKeyFor('aes-256-gcm', 'nope', undefined);
21
+ expect(found).to.equal(null);
22
+ });
23
+
24
+ it('[ENCKVC] can be constructed empty', async function () {
25
+ const keyring = new Keyring();
26
+ const found = await keyring.getKeyFor('aes-256-gcm', 'anything', undefined);
27
+ expect(found).to.equal(null);
28
+ });
29
+
30
+ it('[ENCKVD] set() adds a key to the store', async function () {
31
+ const keyring = new Keyring();
32
+ const material = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
33
+ keyring.set('later', material);
34
+ const found = await keyring.getKeyFor('aes-256-gcm', 'later', undefined);
35
+ expect(found).to.equal(material);
36
+ });
37
+ });
38
+
39
+ describe('[ENCKR] resolver chain', function () {
40
+ it('[ENCKRA] falls back to resolvers when the store misses', async function () {
41
+ const keyring = new Keyring();
42
+ const material = new Uint8Array(32).fill(3);
43
+ keyring.use(async (method, keyRef, hint) => (keyRef === 'remote' ? material : null));
44
+ const found = await keyring.getKeyFor('aes-256-gcm', 'remote', undefined);
45
+ expect(found).to.equal(material);
46
+ });
47
+
48
+ it('[ENCKRB] tries resolvers in registration order, first non-null wins', async function () {
49
+ const keyring = new Keyring();
50
+ const order = [];
51
+ const first = new Uint8Array(32).fill(1);
52
+ keyring.use(async () => { order.push('a'); return null; });
53
+ keyring.use(async () => { order.push('b'); return first; });
54
+ keyring.use(async () => { order.push('c'); return new Uint8Array(32).fill(2); });
55
+ const found = await keyring.getKeyFor('aes-256-gcm', 'x', undefined);
56
+ expect(found).to.equal(first);
57
+ expect(order).to.deep.equal(['a', 'b']); // 'c' never reached
58
+ });
59
+
60
+ it('[ENCKRC] store takes precedence over resolvers', async function () {
61
+ const stored = new Uint8Array(32).fill(5);
62
+ const keyring = new Keyring({ known: stored });
63
+ let resolverCalled = false;
64
+ keyring.use(async () => { resolverCalled = true; return new Uint8Array(32); });
65
+ const found = await keyring.getKeyFor('aes-256-gcm', 'known', undefined);
66
+ expect(found).to.equal(stored);
67
+ expect(resolverCalled).to.equal(false);
68
+ });
69
+
70
+ it('[ENCKRD] passes method, keyRef and hint through to resolvers', async function () {
71
+ const keyring = new Keyring();
72
+ let seen = null;
73
+ keyring.use(async (method, keyRef, hint) => { seen = { method, keyRef, hint }; return null; });
74
+ await keyring.getKeyFor('my-method', 'ref-1', { kid: 42 });
75
+ expect(seen).to.deep.equal({ method: 'my-method', keyRef: 'ref-1', hint: { kid: 42 } });
76
+ });
77
+
78
+ it('[ENCKRE] returns null when every resolver returns null', async function () {
79
+ const keyring = new Keyring();
80
+ keyring.use(async () => null);
81
+ keyring.use(async () => null);
82
+ const found = await keyring.getKeyFor('aes-256-gcm', 'x', undefined);
83
+ expect(found).to.equal(null);
84
+ });
85
+
86
+ it('[ENCKRF] use() returns the keyring for chaining', function () {
87
+ const keyring = new Keyring();
88
+ expect(keyring.use(async () => null)).to.equal(keyring);
89
+ });
90
+ });
91
+ });