@interop/minimal-cipher 7.2.0 → 7.4.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 +192 -18
- package/dist/Cipher.d.ts +34 -5
- package/dist/Cipher.d.ts.map +1 -1
- package/dist/Cipher.js +38 -2
- package/dist/Cipher.js.map +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -8,7 +8,6 @@ library, secure algs only, browser-compatible.
|
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
10
|
|
|
11
|
-
- [Security](#security)
|
|
12
11
|
- [Background](#background)
|
|
13
12
|
- [Install](#install)
|
|
14
13
|
- [Usage](#usage)
|
|
@@ -16,10 +15,6 @@ library, secure algs only, browser-compatible.
|
|
|
16
15
|
- [Commercial Support](#commercial-support)
|
|
17
16
|
- [License](#license)
|
|
18
17
|
|
|
19
|
-
## Security
|
|
20
|
-
|
|
21
|
-
TBD
|
|
22
|
-
|
|
23
18
|
## Background
|
|
24
19
|
|
|
25
20
|
Every version of this library will only offer at most two algorithms for
|
|
@@ -84,8 +79,25 @@ Pick a Cipher interface (`recommended` or `fips`) and create an instance:
|
|
|
84
79
|
import { Cipher } from '@interop/minimal-cipher'
|
|
85
80
|
|
|
86
81
|
const cipher = new Cipher() // by default {version: 'recommended'}
|
|
82
|
+
|
|
83
|
+
// or, to use FIPS-validated algorithms:
|
|
84
|
+
const fipsCipher = new Cipher({ version: 'fips' })
|
|
87
85
|
```
|
|
88
86
|
|
|
87
|
+
Both versions produce the same JWE envelope structure -- ECDH-ES key agreement
|
|
88
|
+
that wraps the content encryption key (CEK) with AES Key Wrap (`A256KW`) -- and
|
|
89
|
+
differ only in the content-encryption cipher and the key-agreement curve:
|
|
90
|
+
|
|
91
|
+
| Version | Content encryption (`enc`) | Key agreement | Notes |
|
|
92
|
+
| ------------- | ---------------------------- | ------------- | ------------------------------------------------- |
|
|
93
|
+
| `recommended` | `XC20P` (XChaCha20-Poly1305) | X25519 | Default. Modern, fast; not on the NIST/FIPS list. |
|
|
94
|
+
| `fips` | `A256GCM` (AES-256-GCM) | NIST P-256 | Uses only FIPS 140-validated algorithms. |
|
|
95
|
+
|
|
96
|
+
Use `recommended` (the default) unless you have a specific FIPS requirement, in
|
|
97
|
+
which case use `fips`. Note that `version` controls the algorithms used to
|
|
98
|
+
**encrypt**, so it must match the kind of key agreement keys your recipients
|
|
99
|
+
have: X25519 keys for `recommended`, P-256 keys for `fips`.
|
|
100
|
+
|
|
89
101
|
### Encrypting
|
|
90
102
|
|
|
91
103
|
To encrypt something (to create a cipher, serialized as a JWE JSON document),
|
|
@@ -107,11 +119,11 @@ public/private key pairs that will be used to encrypt/decrypt the message):
|
|
|
107
119
|
const keyAgreementKey = await fetchFromSomewhere()
|
|
108
120
|
|
|
109
121
|
// or derive them from an existing Ed25519 signing key
|
|
110
|
-
import { X25519KeyAgreementKey2020 } from '@
|
|
111
|
-
import {
|
|
112
|
-
const keyPair = await
|
|
122
|
+
import { X25519KeyAgreementKey2020 } from '@interop/x25519-key-agreement-key'
|
|
123
|
+
import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
|
|
124
|
+
const keyPair = await Ed25519VerificationKey.generate()
|
|
113
125
|
|
|
114
|
-
const keyAgreementKey =
|
|
126
|
+
const keyAgreementKey = X25519KeyAgreementKey2020.fromEd25519VerificationKey2020({
|
|
115
127
|
keyPair
|
|
116
128
|
})
|
|
117
129
|
// If the source key pair didn't have a controller set, don't forget to set one:
|
|
@@ -123,8 +135,10 @@ const didDoc = await veresDriver.get({ did })
|
|
|
123
135
|
const authnKey = didDoc.getVerificationMethod({
|
|
124
136
|
proofPurpose: 'authentication'
|
|
125
137
|
})
|
|
126
|
-
const edKeyPair = await
|
|
127
|
-
const
|
|
138
|
+
const edKeyPair = await Ed25519VerificationKey.from(authnKey)
|
|
139
|
+
const keyAgreementKey = X25519KeyAgreementKey2020.fromEd25519VerificationKey2020({
|
|
140
|
+
keyPair: edKeyPair
|
|
141
|
+
})
|
|
128
142
|
|
|
129
143
|
const recipient = {
|
|
130
144
|
header: {
|
|
@@ -157,21 +171,46 @@ const keyResolver = async () => publicKeyNode
|
|
|
157
171
|
// A more advanced resolver based on DID doc authentication keys
|
|
158
172
|
const keyResolver = async ({ id }) => {
|
|
159
173
|
// Use veres driver to fetch the authn key directly
|
|
160
|
-
const keyPair = await
|
|
174
|
+
const keyPair = await Ed25519VerificationKey.from(
|
|
161
175
|
await veresDriver.get({ did: id })
|
|
162
176
|
)
|
|
163
177
|
// Convert authn key to key agreement key
|
|
164
|
-
return
|
|
178
|
+
return X25519KeyAgreementKey2020.fromEd25519VerificationKey2020({ keyPair })
|
|
165
179
|
}
|
|
166
180
|
```
|
|
167
181
|
|
|
168
182
|
```js
|
|
169
|
-
// Using did
|
|
170
|
-
|
|
183
|
+
// Using the did:key method driver as a key resolver
|
|
184
|
+
import { driver } from '@interop/did-method-key'
|
|
185
|
+
import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
|
|
186
|
+
|
|
187
|
+
const didKeyDriver = driver()
|
|
188
|
+
// Register Ed25519 keys, and derive an X25519 keyAgreement key when resolving
|
|
189
|
+
// (did:key identities are Ed25519 signing keys; encryption needs the derived
|
|
190
|
+
// X25519 key agreement key)
|
|
191
|
+
didKeyDriver.use({
|
|
192
|
+
keyPairClass: Ed25519VerificationKey,
|
|
193
|
+
enableEncryptionKeyDerivation: true
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// The resolver is called with a key agreement key id (a did:key URL with a
|
|
197
|
+
// fragment) and returns the matching public key node
|
|
198
|
+
const keyResolver = async ({ id }) => didKeyDriver.get({ url: id })
|
|
171
199
|
```
|
|
172
200
|
|
|
201
|
+
#### Shortcut: `createRecipients`
|
|
202
|
+
|
|
203
|
+
If you already hold the recipient key-agreement keys (each with an `id` and the
|
|
204
|
+
public key material the algorithm reads, e.g. `X25519KeyAgreementKey2020`
|
|
205
|
+
instances), `cipher.createRecipients({ keys })` builds both the `recipients`
|
|
206
|
+
array and a matching by-id `keyResolver` for you -- no need to assemble the
|
|
207
|
+
headers or write a resolver. The header `alg` is taken from the cipher's key
|
|
208
|
+
agreement algorithm, so it always matches the version:
|
|
209
|
+
|
|
173
210
|
```js
|
|
174
|
-
|
|
211
|
+
const keys = [aliceKeyAgreementKey, bobKeyAgreementKey]
|
|
212
|
+
const { recipients, keyResolver } = cipher.createRecipients({ keys })
|
|
213
|
+
const jweDoc = await cipher.encryptObject({ obj, recipients, keyResolver })
|
|
175
214
|
```
|
|
176
215
|
|
|
177
216
|
Create the JWE:
|
|
@@ -186,6 +225,91 @@ const obj = { key: 'value' }
|
|
|
186
225
|
const jweDoc = await cipher.encryptObject({ obj, recipients, keyResolver })
|
|
187
226
|
```
|
|
188
227
|
|
|
228
|
+
To encrypt a binary blob, pass the bytes directly as a `Uint8Array`. There is no
|
|
229
|
+
need to text-encode the data first -- a `Uint8Array` is passed through to the
|
|
230
|
+
content-encryption step as-is, while a string is UTF-8 encoded for you:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
// To encrypt a binary blob
|
|
234
|
+
const data = new Uint8Array(await blob.arrayBuffer()) // e.g. from a browser Blob
|
|
235
|
+
const jweDoc = await cipher.encrypt({ data, recipients, keyResolver })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Streaming encryption
|
|
239
|
+
|
|
240
|
+
`encrypt()` buffers the whole input in memory and produces a single JWE, which
|
|
241
|
+
is fine for small payloads. For large blobs or files, use the streaming API
|
|
242
|
+
instead: `createEncryptStream()` returns a WHATWG
|
|
243
|
+
[`TransformStream`][Streams API] that breaks the incoming data into chunks
|
|
244
|
+
(default `chunkSize` of 1 MiB) and emits one `{ jwe }` object per chunk, so the
|
|
245
|
+
data is never fully held in memory.
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
// `stream` is a WHATWG ReadableStream of Uint8Array chunks
|
|
249
|
+
const encryptStream = await cipher.createEncryptStream({
|
|
250
|
+
recipients,
|
|
251
|
+
keyResolver,
|
|
252
|
+
chunkSize: 1048576 // optional; bytes per chunk, defaults to 1 MiB
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Each chunk read from the resulting stream is an object: { jwe }
|
|
256
|
+
const readable = stream.pipeThrough(encryptStream)
|
|
257
|
+
const reader = readable.getReader()
|
|
258
|
+
let done
|
|
259
|
+
let value
|
|
260
|
+
while (!done) {
|
|
261
|
+
;({ value, done } = await reader.read())
|
|
262
|
+
if (value) {
|
|
263
|
+
// store or upload value.jwe ...
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
This is how [`edv-client`](https://github.com/digitalbazaar/edv-client) encrypts
|
|
269
|
+
large documents: it pipes a user-supplied `ReadableStream` through
|
|
270
|
+
`createEncryptStream()` and stores each emitted `jwe` as a separate chunk.
|
|
271
|
+
|
|
272
|
+
### Encrypting with the FIPS version
|
|
273
|
+
|
|
274
|
+
The examples above use the default `recommended` version, whose key agreement
|
|
275
|
+
keys are X25519. To encrypt with FIPS-validated algorithms, construct the cipher
|
|
276
|
+
with `{ version: 'fips' }` and use NIST P-256 key agreement keys. Everything
|
|
277
|
+
else -- the `recipients` array, `keyResolver`, and the `encrypt*` calls -- works
|
|
278
|
+
the same way.
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
import { Cipher } from '@interop/minimal-cipher'
|
|
282
|
+
import * as EcdsaMultikey from '@interop/ecdsa-multikey'
|
|
283
|
+
|
|
284
|
+
const cipher = new Cipher({ version: 'fips' })
|
|
285
|
+
|
|
286
|
+
// Generate (or load) a P-256 key agreement key for each recipient
|
|
287
|
+
const keyAgreementKey = await EcdsaMultikey.generate({
|
|
288
|
+
id: 'urn:123',
|
|
289
|
+
curve: 'P-256',
|
|
290
|
+
keyAgreement: true
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// The recipient header is the same shape as the recommended version
|
|
294
|
+
const recipients = [
|
|
295
|
+
{ header: { kid: keyAgreementKey.id, alg: 'ECDH-ES+A256KW' } }
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
// `keyResolver` resolves each `kid` to the recipient's P-256 public key
|
|
299
|
+
const publicKeyNode = await keyAgreementKey.export({ publicKey: true })
|
|
300
|
+
const keyResolver = async () => publicKeyNode
|
|
301
|
+
|
|
302
|
+
const obj = { key: 'value' }
|
|
303
|
+
const jweDoc = await cipher.encryptObject({ obj, recipients, keyResolver })
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
To decrypt, pass the P-256 key agreement key (which can derive the shared
|
|
307
|
+
secret) as `keyAgreementKey`, exactly as with the recommended version:
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
const object = await cipher.decryptObject({ jwe: jweDoc, keyAgreementKey })
|
|
311
|
+
```
|
|
312
|
+
|
|
189
313
|
### Decrypting
|
|
190
314
|
|
|
191
315
|
Decrypt a JWE JSON Document, using a private `keyAgreementKey`:
|
|
@@ -196,8 +320,58 @@ const data = await cipher.decrypt({ jwe, keyAgreementKey })
|
|
|
196
320
|
const object = await cipher.decryptObject({ jwe, keyAgreementKey })
|
|
197
321
|
```
|
|
198
322
|
|
|
199
|
-
|
|
200
|
-
`
|
|
323
|
+
To decrypt streamed (chunked) data, use `createDecryptStream()`. It returns a
|
|
324
|
+
[`TransformStream`][Streams API] that takes `{ jwe }` chunks (as produced by
|
|
325
|
+
`createEncryptStream()`) and outputs the decrypted `Uint8Array` chunks:
|
|
326
|
+
|
|
327
|
+
```js
|
|
328
|
+
// `stream` is a ReadableStream of { jwe } chunks
|
|
329
|
+
const decryptStream = await cipher.createDecryptStream({ keyAgreementKey })
|
|
330
|
+
const readable = stream.pipeThrough(decryptStream)
|
|
331
|
+
const reader = readable.getReader()
|
|
332
|
+
let done
|
|
333
|
+
let value
|
|
334
|
+
while (!done) {
|
|
335
|
+
;({ value, done } = await reader.read())
|
|
336
|
+
if (value) {
|
|
337
|
+
// value is a Uint8Array of decrypted bytes ...
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Key wrapping (the KEK interface)
|
|
343
|
+
|
|
344
|
+
Encryption uses two layers of keys. A randomly generated **content encryption
|
|
345
|
+
key (CEK)** encrypts the payload (with `A256GCM` or `XC20P`). That CEK is then
|
|
346
|
+
wrapped, once per recipient, by a **key encryption key (KEK)**, and the wrapped
|
|
347
|
+
result is stored as each recipient's `encrypted_key`. Decryption reverses this:
|
|
348
|
+
the KEK unwraps the CEK, which then decrypts the payload.
|
|
349
|
+
|
|
350
|
+
You do not construct or pass a KEK yourself. Because the cipher uses ECDH-ES key
|
|
351
|
+
agreement, each KEK is derived internally, per recipient, from a shared secret
|
|
352
|
+
between an ephemeral key and a recipient's static key agreement key. The
|
|
353
|
+
recipient is identified in the JWE by its key agreement key id (the `kid` in the
|
|
354
|
+
recipient header), which `keyResolver` maps to a public key -- the KEK itself is
|
|
355
|
+
ephemeral and has no identity of its own.
|
|
356
|
+
|
|
357
|
+
A KEK object implements the following interface:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
interface KEK {
|
|
361
|
+
// The key-wrapping algorithm, e.g. { name: 'A256KW' }.
|
|
362
|
+
algorithm: { name: string }
|
|
363
|
+
|
|
364
|
+
// Wraps the CEK bytes; resolves to the base64url-encoded wrapped key.
|
|
365
|
+
wrapKey(options: { unwrappedKey: Uint8Array }): Promise<string>
|
|
366
|
+
|
|
367
|
+
// Unwraps a base64url-encoded wrapped key; resolves to the CEK bytes,
|
|
368
|
+
// or null if unwrapping fails (e.g. this KEK does not match the recipient).
|
|
369
|
+
unwrapKey(options: { wrappedKey: string }): Promise<Uint8Array | null>
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The built-in implementation wraps the CEK with AES Key Wrap (`A256KW`) using the
|
|
374
|
+
Web Crypto API; see `src/algorithms/aeskw.ts`.
|
|
201
375
|
|
|
202
376
|
## Contribute
|
|
203
377
|
|
package/dist/Cipher.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { DecryptTransformer } from './DecryptTransformer.js';
|
|
2
2
|
import { EncryptTransformer } from './EncryptTransformer.js';
|
|
3
|
-
import type { IJWE, IKeyAgreementKey, IKeyResolver, IRecipient } from '@interop/data-integrity-core';
|
|
3
|
+
import type { IJWE, IKeyAgreementKey, IKeyResolver, IRecipient, IRecipientTemplate } from '@interop/data-integrity-core';
|
|
4
4
|
import type { CipherAlgorithm, EphemeralKeyPair, KeyAgreementAlgorithm } from './types.js';
|
|
5
5
|
interface EncryptOptions {
|
|
6
6
|
data?: Uint8Array | string;
|
|
7
|
-
recipients:
|
|
7
|
+
recipients: IRecipientTemplate[];
|
|
8
8
|
keyResolver: IKeyResolver;
|
|
9
9
|
}
|
|
10
10
|
interface EncryptObjectOptions {
|
|
11
11
|
obj: object;
|
|
12
|
-
recipients:
|
|
12
|
+
recipients: IRecipientTemplate[];
|
|
13
13
|
keyResolver: IKeyResolver;
|
|
14
14
|
}
|
|
15
15
|
interface CreateEncryptOptions {
|
|
16
|
-
recipients:
|
|
16
|
+
recipients: IRecipientTemplate[];
|
|
17
17
|
keyResolver: IKeyResolver;
|
|
18
18
|
chunkSize?: number;
|
|
19
19
|
}
|
|
@@ -36,6 +36,35 @@ export declare class Cipher {
|
|
|
36
36
|
constructor({ version }?: {
|
|
37
37
|
version?: string;
|
|
38
38
|
});
|
|
39
|
+
/**
|
|
40
|
+
* Builds the `recipients` array and a matching `keyResolver` from a set of
|
|
41
|
+
* recipient key-agreement keys, for use with `encrypt`/`encryptObject` (and
|
|
42
|
+
* the stream/transformer variants). Saves callers from assembling the
|
|
43
|
+
* recipient headers and a by-id resolver by hand.
|
|
44
|
+
*
|
|
45
|
+
* Each key must expose an `id` (used as the JWE recipient `kid`) and the
|
|
46
|
+
* public key material the key agreement algorithm reads -- for X25519, a
|
|
47
|
+
* `publicKeyMultibase` (e.g. an `X25519KeyAgreementKey2020` instance or its
|
|
48
|
+
* exported public key). The recipient header `alg` is taken from this
|
|
49
|
+
* cipher's key agreement algorithm (`this.keyAgreement.JWE_ALG`), so it
|
|
50
|
+
* always matches the cipher version. The returned `keyResolver` looks a
|
|
51
|
+
* recipient up by `id` and throws when asked for an unknown key.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} options - Options.
|
|
54
|
+
* @param {Array} options.keys - Recipient key-agreement keys, each with an
|
|
55
|
+
* `id` and the public key material the algorithm reads.
|
|
56
|
+
*
|
|
57
|
+
* @returns {{recipients: IRecipientTemplate[], keyResolver: IKeyResolver}}
|
|
58
|
+
* The `recipients` array and the `keyResolver` to pass to `encrypt`.
|
|
59
|
+
*/
|
|
60
|
+
createRecipients({ keys }: {
|
|
61
|
+
keys: {
|
|
62
|
+
id: string;
|
|
63
|
+
}[];
|
|
64
|
+
}): {
|
|
65
|
+
recipients: IRecipientTemplate[];
|
|
66
|
+
keyResolver: IKeyResolver;
|
|
67
|
+
};
|
|
39
68
|
/**
|
|
40
69
|
* Creates a TransformStream that will encrypt some data for one or more
|
|
41
70
|
* recipients and output a stream of chunks, each containing an object
|
|
@@ -201,7 +230,7 @@ export declare class Cipher {
|
|
|
201
230
|
* @returns {Promise<object>} A JWE recipient object.
|
|
202
231
|
*/
|
|
203
232
|
_createRecipient({ recipient, ephemeralKeyPair, cek, keyResolver }: {
|
|
204
|
-
recipient:
|
|
233
|
+
recipient: IRecipientTemplate;
|
|
205
234
|
ephemeralKeyPair: EphemeralKeyPair;
|
|
206
235
|
cek: Uint8Array;
|
|
207
236
|
keyResolver: IKeyResolver;
|
package/dist/Cipher.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cipher.d.ts","sourceRoot":"","sources":["../src/Cipher.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D,OAAO,KAAK,EACV,IAAI,EACJ,gBAAgB,EAChB,YAAY,EACZ,UAAU,
|
|
1
|
+
{"version":3,"file":"Cipher.d.ts","sourceRoot":"","sources":["../src/Cipher.ts"],"names":[],"mappings":"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"}
|
package/dist/Cipher.js
CHANGED
|
@@ -41,6 +41,42 @@ export class Cipher {
|
|
|
41
41
|
this.keyAgreement = recAlgorithm.keyAgreement;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Builds the `recipients` array and a matching `keyResolver` from a set of
|
|
46
|
+
* recipient key-agreement keys, for use with `encrypt`/`encryptObject` (and
|
|
47
|
+
* the stream/transformer variants). Saves callers from assembling the
|
|
48
|
+
* recipient headers and a by-id resolver by hand.
|
|
49
|
+
*
|
|
50
|
+
* Each key must expose an `id` (used as the JWE recipient `kid`) and the
|
|
51
|
+
* public key material the key agreement algorithm reads -- for X25519, a
|
|
52
|
+
* `publicKeyMultibase` (e.g. an `X25519KeyAgreementKey2020` instance or its
|
|
53
|
+
* exported public key). The recipient header `alg` is taken from this
|
|
54
|
+
* cipher's key agreement algorithm (`this.keyAgreement.JWE_ALG`), so it
|
|
55
|
+
* always matches the cipher version. The returned `keyResolver` looks a
|
|
56
|
+
* recipient up by `id` and throws when asked for an unknown key.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} options - Options.
|
|
59
|
+
* @param {Array} options.keys - Recipient key-agreement keys, each with an
|
|
60
|
+
* `id` and the public key material the algorithm reads.
|
|
61
|
+
*
|
|
62
|
+
* @returns {{recipients: IRecipientTemplate[], keyResolver: IKeyResolver}}
|
|
63
|
+
* The `recipients` array and the `keyResolver` to pass to `encrypt`.
|
|
64
|
+
*/
|
|
65
|
+
createRecipients({ keys }) {
|
|
66
|
+
const alg = this.keyAgreement.JWE_ALG;
|
|
67
|
+
const byId = new Map(keys.map(key => [key.id, key]));
|
|
68
|
+
const recipients = keys.map(key => ({
|
|
69
|
+
header: { kid: key.id, alg }
|
|
70
|
+
}));
|
|
71
|
+
const keyResolver = async ({ id }) => {
|
|
72
|
+
const key = id ? byId.get(id) : undefined;
|
|
73
|
+
if (!key) {
|
|
74
|
+
throw new Error(`No public key for recipient "${id}".`);
|
|
75
|
+
}
|
|
76
|
+
return key;
|
|
77
|
+
};
|
|
78
|
+
return { recipients, keyResolver };
|
|
79
|
+
}
|
|
44
80
|
/**
|
|
45
81
|
* Creates a TransformStream that will encrypt some data for one or more
|
|
46
82
|
* recipients and output a stream of chunks, each containing an object
|
|
@@ -223,7 +259,7 @@ export class Cipher {
|
|
|
223
259
|
const cek = await cipher.generateKey();
|
|
224
260
|
// derive ephemeral ECDH key pair to use with all recipients
|
|
225
261
|
const ephemeralKeyPair = await keyAgreement.generateEphemeralKeyPair();
|
|
226
|
-
|
|
262
|
+
const wrappedRecipients = await Promise.all(recipients.map(recipient => this._createRecipient({ recipient, cek, ephemeralKeyPair, keyResolver })));
|
|
227
263
|
// create shared protected header as associated authenticated data (aad)
|
|
228
264
|
// ASCII(BASE64URL(UTF8(JWE Protected Header)))
|
|
229
265
|
const enc = cipher.JWE_ENC;
|
|
@@ -232,7 +268,7 @@ export class Cipher {
|
|
|
232
268
|
// UTF8-encoding a base64url-encoded string is the same as ASCII
|
|
233
269
|
const additionalData = stringToUint8Array(encodedProtectedHeader);
|
|
234
270
|
return new EncryptTransformer({
|
|
235
|
-
recipients,
|
|
271
|
+
recipients: wrappedRecipients,
|
|
236
272
|
encodedProtectedHeader,
|
|
237
273
|
cipher,
|
|
238
274
|
additionalData,
|
package/dist/Cipher.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cipher.js","sourceRoot":"","sources":["../src/Cipher.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,aAAa,MAAM,sBAAsB,CAAA;AACrD,OAAO,KAAK,YAAY,MAAM,6BAA6B,CAAA;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"Cipher.js","sourceRoot":"","sources":["../src/Cipher.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interop/minimal-cipher",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "Minimal encryption/decryption JWE library.",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"type": "module",
|
|
@@ -30,17 +30,18 @@
|
|
|
30
30
|
"dev": "vite",
|
|
31
31
|
"fix": "eslint --fix src test && pnpm run format",
|
|
32
32
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.md\"",
|
|
33
|
-
"lint": "eslint src test",
|
|
33
|
+
"lint": "eslint src test && pnpm run typecheck",
|
|
34
34
|
"prepare": "pnpm run build",
|
|
35
35
|
"rebuild": "pnpm run clear && pnpm run build",
|
|
36
36
|
"test": "pnpm run fix && pnpm run lint && pnpm run test:node && pnpm run test:browser",
|
|
37
37
|
"test:browser": "playwright test",
|
|
38
38
|
"test:node": "vitest run",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.dev.json --noEmit",
|
|
39
40
|
"test:coverage": "vitest run --coverage"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"@interop/data-integrity-core": "^
|
|
43
|
-
"@interop/ecdsa-multikey": "^2.3.
|
|
43
|
+
"@interop/data-integrity-core": "^8.0.0",
|
|
44
|
+
"@interop/ecdsa-multikey": "^2.3.1",
|
|
44
45
|
"@noble/curves": "^2.2.0",
|
|
45
46
|
"@scure/base": "^2.2.0",
|
|
46
47
|
"@stablelib/chacha": "^2.0.1",
|
|
@@ -48,10 +49,10 @@
|
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@eslint/js": "^10.0.1",
|
|
51
|
-
"@interop/did-io": "^4.0.
|
|
52
|
-
"@interop/did-method-key": "^7.3.
|
|
52
|
+
"@interop/did-io": "^4.0.5",
|
|
53
|
+
"@interop/did-method-key": "^7.3.2",
|
|
53
54
|
"@interop/ed25519-verification-key": "^8.0.0",
|
|
54
|
-
"@interop/x25519-key-agreement-key": "^
|
|
55
|
+
"@interop/x25519-key-agreement-key": "^5.1.0",
|
|
55
56
|
"@playwright/test": "^1.60.0",
|
|
56
57
|
"@types/node": "^25.9.1",
|
|
57
58
|
"@vitest/coverage-v8": "^4.1.7",
|