@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.
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # @pryv/encryption
2
+
3
+ Client-side encryption and decryption of [Pryv.io](https://pryv.com) events.
4
+
5
+ Events are encrypted **on the client** before they are sent to a Pryv.io core,
6
+ and decrypted **on the client** after they are read back. The core stores and
7
+ serves the encrypted form and never sees the key or the plaintext.
8
+
9
+ An encrypted event keeps its envelope in plaintext and moves the sensitive
10
+ `type` / `content` into an opaque payload:
11
+
12
+ ```jsonc
13
+ {
14
+ "id": "ck…",
15
+ "streamIds": ["journal"],
16
+ "time": 1700000000,
17
+ "type": "encrypted/aes-256-gcm",
18
+ "content": { "payload": "…base64…", "keyRef": "journal-2026" }
19
+ }
20
+ ```
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @pryv/encryption
26
+ ```
27
+
28
+ Requires Node.js >= 20 or a modern browser — it uses the standard WebCrypto API
29
+ (`globalThis.crypto.subtle`) and has no runtime dependencies.
30
+
31
+ ## Usage
32
+
33
+ ```js
34
+ const { EventsCipher, Keyring } = require('@pryv/encryption');
35
+
36
+ // A Keyring holds key material and can defer to async resolvers.
37
+ // Key material may be raw bytes (Uint8Array), a base64 string, or a CryptoKey.
38
+ const keyring = new Keyring({ 'journal-2026': keyMaterial });
39
+
40
+ // Optional: resolvers are tried, in registration order, when the store misses.
41
+ keyring.use(async (method, keyRef, hint) => fetchKeySomehow(keyRef) /* or null */);
42
+
43
+ const cipher = new EventsCipher(keyring, {
44
+ // optional; default is silent
45
+ onDecryptError: (event, error) => console.warn('could not decrypt', event.id, error)
46
+ });
47
+
48
+ // Encrypt before writing to a Pryv.io connection:
49
+ const encrypted = await cipher.encryptEvent(
50
+ { streamIds: ['journal'], type: 'note/txt', content: 'a private note' },
51
+ { method: 'aes-256-gcm', keyRef: 'journal-2026' }
52
+ );
53
+
54
+ // Decrypt after reading:
55
+ const decrypted = await cipher.decryptEvent(encrypted);
56
+ // decrypted.type === 'note/txt', decrypted.content === 'a private note'
57
+ // decrypted.decryptedFrom === encrypted (the original, untouched)
58
+
59
+ // Batch, and streaming (forEachEvent) helpers:
60
+ const all = await cipher.decryptEvents(events);
61
+ const onEvent = cipher.wrapForEachEvent((event) => { /* receives decrypted events */ });
62
+
63
+ // Recover the stored (encrypted) form of a decrypted event, e.g. before an update:
64
+ const stored = cipher.stripDecrypted(decrypted); // === encrypted
65
+
66
+ // Update an encrypted event: re-encrypt the new material, send it as the update
67
+ // (updates are always full re-encryptions — see Limitations):
68
+ const update = await cipher.encryptEventContent(
69
+ { type: 'note/txt', content: 'the edited note' },
70
+ { method: 'aes-256-gcm', keyRef: 'journal-2026' }
71
+ );
72
+ await conn.api([{ method: 'events.update', params: { id: stored.id, update } }]);
73
+ ```
74
+
75
+ ### Keyring
76
+
77
+ - `new Keyring({ keyRef: material, … })` — seed the key/value store.
78
+ - `keyring.set(keyRef, material)` — add/replace a key.
79
+ - `keyring.use(resolver)` — append an async resolver
80
+ `(method, keyRef, hint) => material | null`.
81
+ - `keyring.getKeyFor(method, keyRef, hint)` — resolve material: the store is
82
+ consulted first (by `keyRef`), then resolvers in registration order; the first
83
+ non-null result wins, otherwise `null`.
84
+
85
+ ### Decryption is non-destructive
86
+
87
+ `decryptEvent` never throws and never drops an event:
88
+
89
+ - If the event `type` is not `encrypted/<registered-method>`, the **same event
90
+ reference** is returned untouched.
91
+ - On **any** failure — no key, a resolver throwing, a wrong key / tampered tag,
92
+ a malformed payload, or plaintext that is not JSON with `type` and `content` —
93
+ the **same event reference** is returned, and `onDecryptError(event, error)`
94
+ is called when provided.
95
+ - On success a **new** object is returned: the envelope fields are preserved,
96
+ `type` / `content` are the decrypted values, and `decryptedFrom` points back
97
+ at the original encrypted event.
98
+
99
+ `encryptEvent`, in contrast, **throws** on an unknown method or a missing key.
100
+
101
+ ### Methods
102
+
103
+ Each method interprets **key material** in its own way — a keyring simply stores
104
+ whatever material a method expects and hands it back untouched. The three
105
+ built-in methods are:
106
+
107
+ #### `aes-256-gcm` (symmetric, encrypt + decrypt)
108
+
109
+ AES-256 in GCM mode. `content.payload` is `base64(iv ‖ ciphertext ‖ gcm-tag)`
110
+ where the IV is 12 random bytes and the tag is the 16-byte GCM authentication
111
+ tag. **Key material** is the 32 raw key bytes, accepted as a `Uint8Array`, a
112
+ **base64 string of those 32 bytes**, or an AES-GCM `CryptoKey`.
113
+
114
+ #### `aes-text-base64` (legacy, **decrypt-only**)
115
+
116
+ Reads the historical ciphertext produced by
117
+ `CryptoJS.AES.encrypt(text, passphrase).toString()` — the OpenSSL "salted"
118
+ envelope `base64("Salted__" ‖ salt[8] ‖ AES-256-CBC(text, key, iv))`. The
119
+ 32-byte key and 16-byte IV are derived from the passphrase and salt via
120
+ OpenSSL's `EVP_BytesToKey` (MD5, one iteration); the cipher is AES-256-CBC with
121
+ PKCS#7 padding.
122
+
123
+ **Key material for this method is the passphrase STRING itself** — *not* base64
124
+ of key bytes (unlike `aes-256-gcm`). Put the passphrase directly into the
125
+ keyring:
126
+
127
+ ```js
128
+ const keyring = new Keyring({ 'legacy-2019': 'the original passphrase' });
129
+ ```
130
+
131
+ This method has **no `encrypt`**: it exists only to read pre-existing legacy
132
+ events, so `encryptEvent` / `encryptEventContent` throw for it. New events should
133
+ use a modern method. (MD5 — required only to reproduce the legacy key
134
+ derivation — is vendored internally; it is broken and is used for nothing else.)
135
+
136
+ #### `ecies-aes-256-gcm` (asymmetric, encrypt + decrypt)
137
+
138
+ ECIES over NIST P-256 with AES-256-GCM. A sender encrypts to a recipient's
139
+ **public** key; only the holder of the matching **private** key can decrypt — so
140
+ an encrypted event can be shared without ever moving a shared secret.
141
+
142
+ `content.payload` is
143
+ `base64(ephemeralPublicKey[65] ‖ iv[12] ‖ ciphertext ‖ gcm-tag[16])`, where the
144
+ ephemeral public key is a fresh P-256 point in SEC1 uncompressed form
145
+ (`0x04 ‖ X ‖ Y`). The AES-256-GCM key is
146
+ `HKDF-SHA-256(ECDH(ephemeral, recipient), salt = <empty>, info = "encrypted/ecies-aes-256-gcm")`.
147
+
148
+ **Key material** (each operation picks the side it needs):
149
+
150
+ - to **encrypt** — the recipient PUBLIC key as a `CryptoKey`, a JWK (no `d`), a
151
+ raw 65-byte `Uint8Array` (SEC1 uncompressed) or its base64;
152
+ - to **decrypt** — the recipient PRIVATE key as a `CryptoKey` (ECDH), a JWK
153
+ (has `d`), or base64 / `Uint8Array` PKCS#8;
154
+ - either operation also accepts a `{ publicKey, privateKey }` pair object holding
155
+ any of the shapes above.
156
+
157
+ Mint a key pair (exportable JWK objects) with the method's `generateKeyPair()`:
158
+
159
+ ```js
160
+ const { methods } = require('@pryv/encryption');
161
+ const { publicKey, privateKey } = await methods['ecies-aes-256-gcm'].generateKeyPair();
162
+
163
+ // sender only needs the public key:
164
+ const senderRing = new Keyring({ 'alice': publicKey });
165
+ const enc = await senderCipher.encryptEvent(plain, { method: 'ecies-aes-256-gcm', keyRef: 'alice' });
166
+
167
+ // alice decrypts with her private key (in her own keyring):
168
+ const aliceRing = new Keyring({ 'alice': privateKey });
169
+ const dec = await aliceCipher.decryptEvent(enc);
170
+ ```
171
+
172
+ ### Encrypted attachments
173
+
174
+ An event's attached files are encrypted with the **same method and key as the
175
+ event's `content`**, and stored as **raw binary in the method's payload byte
176
+ layout** — i.e. exactly the bytes that Base64-decoding a `content.payload` would
177
+ yield, but **without** the Base64 wrapping:
178
+
179
+ - `aes-256-gcm` — `iv[12] ‖ ciphertext ‖ gcm-tag[16]`
180
+ - `ecies-aes-256-gcm` — `ephemeralPublicKey[65] ‖ iv[12] ‖ ciphertext ‖ gcm-tag[16]`
181
+
182
+ Two `EventsCipher` helpers move raw bytes:
183
+
184
+ ```js
185
+ // Encrypt the file bytes with the SAME method/key as the event content:
186
+ const cipherBytes = await cipher.encryptAttachmentData(fileBytes, {
187
+ method: 'aes-256-gcm', keyRef: 'journal-2026'
188
+ });
189
+
190
+ // Upload cipherBytes as the file body (isomorphic — a Blob works in Node >= 20
191
+ // and the browser):
192
+ const blob = new Blob([cipherBytes], { type: 'application/octet-stream' });
193
+ const created = (await conn.createEventWithFileFromBuffer(encryptedEvent, blob, 'secret.bin')).event;
194
+
195
+ // Download the raw bytes and decrypt them against the event they belong to:
196
+ const att = created.attachments[0];
197
+ const url = conn.endpoint + 'events/' + created.id + '/' + att.id + '?readToken=' + att.readToken;
198
+ const downloaded = new Uint8Array(await (await fetch(url)).arrayBuffer());
199
+ const fileBytesBack = await cipher.decryptAttachmentData(created, downloaded);
200
+ ```
201
+
202
+ - `encryptAttachmentData(bytes, { method, keyRef, hint })` → `Uint8Array` —
203
+ resolves the key exactly like `encryptEventContent`. The input is not mutated.
204
+ - `decryptAttachmentData(event, bytes)` → `Uint8Array` — derives the method from
205
+ the event's `encrypted/<method>` type and the key from its `content.keyRef` /
206
+ `content.hint`. `event` may be the encrypted event **or** an already-decrypted
207
+ one (carrying `decryptedFrom`); the encrypted form is used either way.
208
+
209
+ **Throw asymmetry.** Unlike the passive, never-throw event decryption
210
+ (`decryptEvent`), attachment decryption is an **explicit request** and
211
+ **throws** on any failure (event not encrypted, unknown method, a method with no
212
+ byte support, a missing key, or bad / tampered / truncated bytes). Both helpers
213
+ throw on error, matching `encryptEvent`.
214
+
215
+ **The legacy `aes-text-base64` method has no byte support** — there are no known
216
+ legacy encrypted attachments, so it exposes neither `encryptBytes` nor
217
+ `decryptBytes`, and both attachment helpers throw for it.
218
+
219
+ The core stores the (already-encrypted) file verbatim and reports its **encrypted**
220
+ size on `attachments[0].size` — it never sees the key or the plaintext.
221
+
222
+ ### Custom methods
223
+
224
+ Register or override a method with:
225
+
226
+ ```js
227
+ cipher.registerMethod('my-method', {
228
+ encrypt: async (material, key) => ({ payload: /* … */ }),
229
+ decrypt: async (content, key) => material,
230
+ // optional — enable encrypted attachments for this method; each returns the
231
+ // raw payload-layout / plaintext bytes as a Uint8Array:
232
+ encryptBytes: async (bytes, key) => /* Uint8Array */,
233
+ decryptBytes: async (bytes, key) => /* Uint8Array */
234
+ });
235
+ ```
236
+
237
+ `encryptBytes` / `decryptBytes` are optional; when present they must be
238
+ functions (validated on registration). A method without them cannot encrypt or
239
+ decrypt attachments.
240
+
241
+ ## Limitations
242
+
243
+ - **Updates are full re-encryptions.** There is no partial/streaming update of
244
+ an encrypted `content`; changing it means encrypting the new value and
245
+ replacing the event's `content`.
246
+ - **The server cannot query encrypted content.** Because `type` and `content`
247
+ are opaque to the core, server-side filtering, type queries, aggregation and
248
+ full-text search do not apply to encrypted fields. Envelope fields
249
+ (`streamIds`, `time`, …) stay in plaintext and remain queryable.
250
+ - **Key management is the application's responsibility.** This library never
251
+ transmits, persists or derives keys; losing a key means losing access to the
252
+ data it protected.
253
+
254
+ ## License
255
+
256
+ [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@pryv/encryption",
3
+ "version": "3.10.0",
4
+ "description": "Client-side encryption and decryption of Pryv.io events",
5
+ "keywords": [
6
+ "Pryv",
7
+ "Pryv.io",
8
+ "Encryption",
9
+ "Decryption",
10
+ "Privacy"
11
+ ],
12
+ "homepage": "https://github.com/pryv/lib-js/tree/master/components/pryv-encryption#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/pryv/lib-js/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git://github.com/pryv/lib-js.git"
19
+ },
20
+ "license": "BSD-3-Clause",
21
+ "author": "Pryv <info@pryv.com> (https://pryv.com)",
22
+ "main": "src/index.js",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=20.0.0"
28
+ }
29
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /**
6
+ * Encrypts and decrypts Pryv.io events.
7
+ *
8
+ * An encrypted event carries `type: "encrypted/<method>"` and a
9
+ * `content: { payload, keyRef?, hint? }`. All other envelope fields
10
+ * (id, streamIds, time, created, …) stay in plaintext.
11
+ *
12
+ * Decryption is defensive: any failure leaves the event untouched (the exact
13
+ * same reference is returned) and, when configured, is reported through
14
+ * `onDecryptError` — a failed decryption never throws and never drops an event.
15
+ * Encryption, by contrast, throws on error.
16
+ *
17
+ * Attachments are handled by the byte helpers `encryptAttachmentData` /
18
+ * `decryptAttachmentData`. An attachment is encrypted with the SAME method and
19
+ * key as its event's content, stored as the method's raw payload byte layout
20
+ * (WITHOUT Base64). Unlike passive event decryption, attachment decryption is an
21
+ * explicit request and THROWS on failure.
22
+ */
23
+ const Keyring = require('./Keyring');
24
+ const builtinMethods = require('./methods');
25
+
26
+ const ENCRYPTED_TYPE_PREFIX = 'encrypted/';
27
+
28
+ class EventsCipher {
29
+ /**
30
+ * @param {Keyring} keyring
31
+ * @param {Object} [options]
32
+ * @param {(event: Object, error: Error) => void} [options.onDecryptError] called on any decryption failure. Default: silent.
33
+ */
34
+ constructor (keyring, options = {}) {
35
+ if (!(keyring instanceof Keyring)) {
36
+ throw new Error('EventsCipher requires a Keyring');
37
+ }
38
+ this.keyring = keyring;
39
+ this.onDecryptError = (options && options.onDecryptError) || null;
40
+ // Clone the built-ins so registerMethod() never mutates the shared map.
41
+ this._methods = Object.assign({}, builtinMethods);
42
+ }
43
+
44
+ /**
45
+ * Register (or override) an encryption method. `decrypt` is required;
46
+ * `encrypt` is optional (a decrypt-only method supports reading existing
47
+ * encrypted events but cannot produce new ones).
48
+ * @param {string} name
49
+ * @param {{ decrypt: Function, encrypt?: Function }} method
50
+ * @returns {EventsCipher} this, for chaining.
51
+ */
52
+ registerMethod (name, method) {
53
+ if (!method || typeof method.decrypt !== 'function' ||
54
+ (method.encrypt != null && typeof method.encrypt !== 'function') ||
55
+ (method.encryptBytes != null && typeof method.encryptBytes !== 'function') ||
56
+ (method.decryptBytes != null && typeof method.decryptBytes !== 'function')) {
57
+ throw new Error('registerMethod expects a { decrypt } function and optional { encrypt, encryptBytes, decryptBytes } functions');
58
+ }
59
+ this._methods[name] = method;
60
+ return this;
61
+ }
62
+
63
+ /**
64
+ * Encrypt event material into the `{ type, content }` an implementer passes to
65
+ * an `events.create` / `events.update` call. Only the material's `type` and
66
+ * `content` are encrypted; any other fields are ignored. Throws on unknown or
67
+ * decrypt-only method, or on a missing key. The input is not mutated.
68
+ * @param {Object} material - `{ type, content, ... }` (same shape decryptEvent restores).
69
+ * @param {Object} params
70
+ * @param {string} params.method
71
+ * @param {string} [params.keyRef]
72
+ * @param {*} [params.hint]
73
+ * @returns {Promise<{ type: string, content: { payload: string, keyRef?: string, hint?: * } }>}
74
+ */
75
+ async encryptEventContent (material, params = {}) {
76
+ const { method: methodName, keyRef, hint } = params;
77
+ const method = this._methods[methodName];
78
+ if (!method) {
79
+ throw new Error(`Unknown encryption method: ${methodName}`);
80
+ }
81
+ if (typeof method.encrypt !== 'function') {
82
+ throw new Error(`Method "${methodName}" is decrypt-only and cannot encrypt`);
83
+ }
84
+ const key = await this.keyring.getKeyFor(methodName, keyRef, hint);
85
+ if (key == null) {
86
+ throw new Error(`No key available for method "${methodName}"${keyRef != null ? ` (keyRef "${keyRef}")` : ''}`);
87
+ }
88
+
89
+ const source = { type: material.type, content: material.content };
90
+ const content = await method.encrypt(source, key);
91
+ if (keyRef != null) content.keyRef = keyRef;
92
+ if (hint != null) content.hint = hint;
93
+
94
+ return { type: ENCRYPTED_TYPE_PREFIX + methodName, content };
95
+ }
96
+
97
+ /**
98
+ * Encrypt a plain event. Throws on unknown method or missing key.
99
+ * @param {Object} plainEvent
100
+ * @param {Object} params
101
+ * @param {string} params.method
102
+ * @param {string} [params.keyRef]
103
+ * @param {*} [params.hint]
104
+ * @returns {Promise<Object>} a new encrypted event (input is not mutated).
105
+ */
106
+ async encryptEvent (plainEvent, params = {}) {
107
+ const { type, content } = await this.encryptEventContent(plainEvent, params);
108
+ const encrypted = Object.assign({}, plainEvent);
109
+ encrypted.type = type;
110
+ encrypted.content = content;
111
+ return encrypted;
112
+ }
113
+
114
+ /**
115
+ * Decrypt a single event. Never throws.
116
+ * - Non-encrypted or unregistered-method events are returned untouched (same ref).
117
+ * - On any failure the original event is returned (same ref) and
118
+ * `onDecryptError(event, error)` is called when configured.
119
+ * - On success a NEW object is returned with restored `type`/`content`,
120
+ * preserved envelope fields, and a `decryptedFrom` back-reference.
121
+ * @param {Object} event
122
+ * @returns {Promise<Object>}
123
+ */
124
+ async decryptEvent (event) {
125
+ const methodName = methodNameFromType(event && event.type);
126
+ if (methodName == null) return event;
127
+ const method = this._methods[methodName];
128
+ if (!method) return event;
129
+
130
+ try {
131
+ const content = event.content || {};
132
+ const key = await this.keyring.getKeyFor(methodName, content.keyRef, content.hint);
133
+ if (key == null) {
134
+ throw new Error(`No key available for method "${methodName}"`);
135
+ }
136
+ const material = await method.decrypt(content, key);
137
+ if (material == null || material.type == null || material.content == null) {
138
+ throw new Error('Decrypted payload is missing "type" or "content"');
139
+ }
140
+ return Object.assign({}, event, material, { decryptedFrom: event });
141
+ } catch (error) {
142
+ if (this.onDecryptError) this.onDecryptError(event, error);
143
+ return event;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Decrypt an array of events. Never throws; failures pass through untouched.
149
+ * @param {Object[]} events
150
+ * @returns {Promise<Object[]>}
151
+ */
152
+ async decryptEvents (events) {
153
+ return Promise.all(events.map((event) => this.decryptEvent(event)));
154
+ }
155
+
156
+ /**
157
+ * Wrap a `forEachEvent`-style callback so each event is decrypted before it
158
+ * is forwarded to the user callback.
159
+ * @param {(event: Object) => *} callback
160
+ * @returns {(event: Object) => Promise<*>}
161
+ */
162
+ wrapForEachEvent (callback) {
163
+ return async (event) => {
164
+ const decrypted = await this.decryptEvent(event);
165
+ return callback(decrypted);
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Encrypt raw attachment bytes with the SAME method and key an event's
171
+ * `content` uses. The result is the method's raw payload byte layout (exactly
172
+ * the bytes Base64-decoding a `content.payload` would yield, WITHOUT Base64) —
173
+ * upload it verbatim as the file's binary body. Like `encryptEventContent`,
174
+ * this THROWS on an unknown method, a method without byte support, or a missing
175
+ * key. The input bytes are not mutated.
176
+ * @param {Uint8Array} bytes - raw attachment bytes to encrypt.
177
+ * @param {Object} params
178
+ * @param {string} params.method
179
+ * @param {string} [params.keyRef]
180
+ * @param {*} [params.hint]
181
+ * @returns {Promise<Uint8Array>} raw payload-layout bytes.
182
+ */
183
+ async encryptAttachmentData (bytes, params = {}) {
184
+ const { method: methodName, keyRef, hint } = params;
185
+ const method = this._methods[methodName];
186
+ if (!method) {
187
+ throw new Error(`Unknown encryption method: ${methodName}`);
188
+ }
189
+ if (typeof method.encryptBytes !== 'function') {
190
+ throw new Error(`Method "${methodName}" does not support attachment (byte) encryption`);
191
+ }
192
+ const key = await this.keyring.getKeyFor(methodName, keyRef, hint);
193
+ if (key == null) {
194
+ throw new Error(`No key available for method "${methodName}"${keyRef != null ? ` (keyRef "${keyRef}")` : ''}`);
195
+ }
196
+ return method.encryptBytes(bytes, key);
197
+ }
198
+
199
+ /**
200
+ * Decrypt raw attachment bytes that belong to `event`. The method is derived
201
+ * from the event's `encrypted/<method>` type and the key from its
202
+ * `content.keyRef` / `content.hint` — the SAME material used for its content.
203
+ *
204
+ * `event` may be the encrypted event OR an already-decrypted one (carrying a
205
+ * `decryptedFrom` back-reference); the encrypted form is used either way.
206
+ *
207
+ * Unlike the passive, never-throw event decryption (`decryptEvent`),
208
+ * attachment decryption is an EXPLICIT request and THROWS on failure (event
209
+ * not encrypted, unknown method, no byte support, missing key, bad key /
210
+ * tampered / truncated bytes).
211
+ * @param {Object} event - the (encrypted or decrypted) event the attachment belongs to.
212
+ * @param {Uint8Array} bytes - raw payload-layout bytes downloaded from the core.
213
+ * @returns {Promise<Uint8Array>} raw plaintext bytes.
214
+ */
215
+ async decryptAttachmentData (event, bytes) {
216
+ const source = this.stripDecrypted(event);
217
+ const methodName = methodNameFromType(source && source.type);
218
+ if (methodName == null) {
219
+ throw new Error('Event is not encrypted (type is not "encrypted/<method>")');
220
+ }
221
+ const method = this._methods[methodName];
222
+ if (!method) {
223
+ throw new Error(`Unknown encryption method: ${methodName}`);
224
+ }
225
+ if (typeof method.decryptBytes !== 'function') {
226
+ throw new Error(`Method "${methodName}" does not support attachment (byte) decryption`);
227
+ }
228
+ const content = (source && source.content) || {};
229
+ const key = await this.keyring.getKeyFor(methodName, content.keyRef, content.hint);
230
+ if (key == null) {
231
+ throw new Error(`No key available for method "${methodName}"${content.keyRef != null ? ` (keyRef "${content.keyRef}")` : ''}`);
232
+ }
233
+ return method.decryptBytes(bytes, key);
234
+ }
235
+
236
+ /**
237
+ * Return the original encrypted event a decrypted event came from, or the
238
+ * event itself when it was never decrypted.
239
+ * @param {Object} event
240
+ * @returns {Object}
241
+ */
242
+ stripDecrypted (event) {
243
+ if (event && event.decryptedFrom != null) return event.decryptedFrom;
244
+ return event;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Extract the method name from an `encrypted/<method>` type, or null.
250
+ * @param {*} type
251
+ * @returns {?string}
252
+ */
253
+ function methodNameFromType (type) {
254
+ if (typeof type !== 'string' || !type.startsWith(ENCRYPTED_TYPE_PREFIX)) return null;
255
+ const name = type.slice(ENCRYPTED_TYPE_PREFIX.length);
256
+ return name.length > 0 ? name : null;
257
+ }
258
+
259
+ module.exports = EventsCipher;
package/src/Keyring.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /**
6
+ * A key store with a pluggable chain of asynchronous resolvers.
7
+ *
8
+ * Resolution order for `getKeyFor(method, keyRef, hint)`:
9
+ * 1. the key/value store, looked up by `keyRef`;
10
+ * 2. each registered resolver, in registration order — the first one that
11
+ * returns a non-null value wins.
12
+ *
13
+ * Key material is stored and returned as-is (raw `Uint8Array`, base64 string
14
+ * or `CryptoKey`); interpreting it is the encryption method's responsibility.
15
+ */
16
+ class Keyring {
17
+ /**
18
+ * @param {Object<string, (Uint8Array|string|CryptoKey)>} [keys] initial keyRef → material map.
19
+ */
20
+ constructor (keys = {}) {
21
+ this._store = new Map();
22
+ if (keys != null) {
23
+ for (const keyRef of Object.keys(keys)) {
24
+ this._store.set(keyRef, keys[keyRef]);
25
+ }
26
+ }
27
+ this._resolvers = [];
28
+ }
29
+
30
+ /**
31
+ * Add or replace a key in the store.
32
+ * @param {string} keyRef
33
+ * @param {Uint8Array|string|CryptoKey} material
34
+ * @returns {Keyring} this, for chaining.
35
+ */
36
+ set (keyRef, material) {
37
+ this._store.set(keyRef, material);
38
+ return this;
39
+ }
40
+
41
+ /**
42
+ * Register an asynchronous resolver, tried after the store misses.
43
+ * @param {(method: string, keyRef: ?string, hint: *) => Promise<?(Uint8Array|string|CryptoKey)>} resolver
44
+ * @returns {Keyring} this, for chaining.
45
+ */
46
+ use (resolver) {
47
+ if (typeof resolver !== 'function') {
48
+ throw new Error('Keyring.use() expects a function');
49
+ }
50
+ this._resolvers.push(resolver);
51
+ return this;
52
+ }
53
+
54
+ /**
55
+ * Resolve key material for a given method / keyRef / hint.
56
+ * The store is consulted first, then each resolver in registration order.
57
+ * @param {string} method
58
+ * @param {?string} keyRef
59
+ * @param {*} [hint]
60
+ * @returns {Promise<?(Uint8Array|string|CryptoKey)>} resolved material, or null.
61
+ */
62
+ async getKeyFor (method, keyRef, hint) {
63
+ if (keyRef != null && this._store.has(keyRef)) {
64
+ return this._store.get(keyRef);
65
+ }
66
+ for (const resolver of this._resolvers) {
67
+ const material = await resolver(method, keyRef, hint);
68
+ if (material != null) return material;
69
+ }
70
+ return null;
71
+ }
72
+ }
73
+
74
+ module.exports = Keyring;
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /**
6
+ * @pryv/encryption — client-side encryption and decryption of Pryv.io events.
7
+ *
8
+ * - `Keyring`: a key store with a pluggable chain of async key resolvers.
9
+ * - `EventsCipher`: encrypts / decrypts events using registered methods.
10
+ * - `methods`: the built-in encryption methods (`aes-256-gcm`,
11
+ * `aes-text-base64`, `ecies-aes-256-gcm`).
12
+ */
13
+ const EventsCipher = require('./EventsCipher');
14
+ const Keyring = require('./Keyring');
15
+ const methods = require('./methods');
16
+
17
+ module.exports = { EventsCipher, Keyring, methods };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /**
6
+ * Isomorphic base64 helpers built on the standard `btoa`/`atob` globals,
7
+ * which are available in browsers and in Node.js (>= 16). No `Buffer`, so the
8
+ * same code path runs unchanged in both environments.
9
+ */
10
+
11
+ /**
12
+ * Encode raw bytes to a standard (non-URL-safe) base64 string.
13
+ * @param {Uint8Array} bytes
14
+ * @returns {string}
15
+ */
16
+ function bytesToBase64 (bytes) {
17
+ let binary = '';
18
+ for (let i = 0; i < bytes.length; i++) {
19
+ binary += String.fromCharCode(bytes[i]);
20
+ }
21
+ return btoa(binary);
22
+ }
23
+
24
+ /**
25
+ * Decode a standard base64 string to raw bytes.
26
+ * @param {string} b64
27
+ * @returns {Uint8Array}
28
+ */
29
+ function base64ToBytes (b64) {
30
+ const binary = atob(b64);
31
+ const bytes = new Uint8Array(binary.length);
32
+ for (let i = 0; i < binary.length; i++) {
33
+ bytes[i] = binary.charCodeAt(i);
34
+ }
35
+ return bytes;
36
+ }
37
+
38
+ module.exports = { bytesToBase64, base64ToBytes };