@bcts/envelope 1.0.0-alpha.10

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.
Files changed (44) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +23 -0
  3. package/dist/index.cjs +2646 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +782 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +782 -0
  8. package/dist/index.d.mts.map +1 -0
  9. package/dist/index.iife.js +2644 -0
  10. package/dist/index.iife.js.map +1 -0
  11. package/dist/index.mjs +2552 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +84 -0
  14. package/src/base/assertion.ts +179 -0
  15. package/src/base/assertions.ts +153 -0
  16. package/src/base/cbor.ts +122 -0
  17. package/src/base/digest.ts +204 -0
  18. package/src/base/elide.ts +390 -0
  19. package/src/base/envelope-decodable.ts +186 -0
  20. package/src/base/envelope-encodable.ts +71 -0
  21. package/src/base/envelope.ts +988 -0
  22. package/src/base/error.ts +421 -0
  23. package/src/base/index.ts +56 -0
  24. package/src/base/leaf.ts +147 -0
  25. package/src/base/queries.ts +244 -0
  26. package/src/base/walk.ts +215 -0
  27. package/src/base/wrap.ts +26 -0
  28. package/src/extension/attachment.ts +280 -0
  29. package/src/extension/compress.ts +176 -0
  30. package/src/extension/encrypt.ts +297 -0
  31. package/src/extension/expression.ts +404 -0
  32. package/src/extension/index.ts +72 -0
  33. package/src/extension/proof.ts +227 -0
  34. package/src/extension/recipient.ts +440 -0
  35. package/src/extension/salt.ts +114 -0
  36. package/src/extension/signature.ts +398 -0
  37. package/src/extension/types.ts +92 -0
  38. package/src/format/diagnostic.ts +116 -0
  39. package/src/format/hex.ts +25 -0
  40. package/src/format/index.ts +13 -0
  41. package/src/format/tree.ts +168 -0
  42. package/src/index.ts +32 -0
  43. package/src/utils/index.ts +8 -0
  44. package/src/utils/string.ts +48 -0
@@ -0,0 +1,440 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { EnvelopeError } from "../base/error";
3
+ import { SymmetricKey } from "./encrypt";
4
+ import {
5
+ x25519NewPrivateKeyUsing,
6
+ x25519PublicKeyFromPrivateKey,
7
+ x25519SharedKey,
8
+ aeadChaCha20Poly1305EncryptWithAad,
9
+ aeadChaCha20Poly1305DecryptWithAad,
10
+ hkdfHmacSha256,
11
+ X25519_PUBLIC_KEY_SIZE,
12
+ X25519_PRIVATE_KEY_SIZE,
13
+ SYMMETRIC_NONCE_SIZE,
14
+ SYMMETRIC_AUTH_SIZE,
15
+ } from "@bcts/crypto";
16
+ import { SecureRandomNumberGenerator, rngRandomData } from "@bcts/rand";
17
+
18
+ /// Extension for public-key encryption to specific recipients.
19
+ ///
20
+ /// This module implements multi-recipient public key encryption using the
21
+ /// Gordian Envelope sealed message pattern. Each recipient gets a sealed
22
+ /// message containing an encrypted content key, allowing multiple recipients
23
+ /// to decrypt the same envelope using their private keys.
24
+ ///
25
+ /// ## How it works:
26
+ ///
27
+ /// 1. A random symmetric content key is generated
28
+ /// 2. The envelope's subject is encrypted with the content key
29
+ /// 3. The content key is encrypted to each recipient's public key using X25519 key agreement
30
+ /// 4. Each encrypted content key is added as a `hasRecipient` assertion
31
+ ///
32
+ /// ## Sealed Box Security:
33
+ ///
34
+ /// Sealed boxes use ephemeral X25519 key pairs. The ephemeral private key
35
+ /// is discarded after encryption, ensuring that even the sender cannot
36
+ /// decrypt the message later. Recipients must try each sealed message
37
+ /// until one decrypts successfully.
38
+ ///
39
+ /// Uses @bcts/crypto functions for X25519 and ChaCha20-Poly1305.
40
+ ///
41
+ /// @example
42
+ /// ```typescript
43
+ /// // Generate recipient keys
44
+ /// const alice = PrivateKeyBase.generate();
45
+ /// const bob = PrivateKeyBase.generate();
46
+ ///
47
+ /// // Encrypt to multiple recipients
48
+ /// const envelope = Envelope.new("Secret message")
49
+ /// .encryptSubjectToRecipients([alice.publicKeys(), bob.publicKeys()]);
50
+ ///
51
+ /// // Alice decrypts
52
+ /// const aliceDecrypted = envelope.decryptSubjectToRecipient(alice);
53
+ ///
54
+ /// // Bob decrypts
55
+ /// const bobDecrypted = envelope.decryptSubjectToRecipient(bob);
56
+ /// ```
57
+
58
+ /// Predicate constant for recipient assertions
59
+ export const HAS_RECIPIENT = "hasRecipient";
60
+
61
+ /// Represents an X25519 public key for encryption
62
+ export class PublicKeyBase {
63
+ readonly #publicKey: Uint8Array;
64
+
65
+ constructor(publicKey: Uint8Array) {
66
+ if (publicKey.length !== X25519_PUBLIC_KEY_SIZE) {
67
+ throw new Error(`Public key must be ${X25519_PUBLIC_KEY_SIZE} bytes`);
68
+ }
69
+ this.#publicKey = publicKey;
70
+ }
71
+
72
+ /// Returns the raw public key bytes
73
+ data(): Uint8Array {
74
+ return this.#publicKey;
75
+ }
76
+
77
+ /// Returns the public key as a hex string
78
+ hex(): string {
79
+ return Array.from(this.#publicKey)
80
+ .map((b) => b.toString(16).padStart(2, "0"))
81
+ .join("");
82
+ }
83
+
84
+ /// Creates a public key from hex string
85
+ static fromHex(hex: string): PublicKeyBase {
86
+ if (hex.length !== 64) {
87
+ throw new Error("Hex string must be 64 characters (32 bytes)");
88
+ }
89
+ const bytes = new Uint8Array(32);
90
+ for (let i = 0; i < 32; i++) {
91
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
92
+ }
93
+ return new PublicKeyBase(bytes);
94
+ }
95
+ }
96
+
97
+ /// Represents an X25519 private key for decryption
98
+ export class PrivateKeyBase {
99
+ readonly #privateKey: Uint8Array;
100
+ readonly #publicKey: PublicKeyBase;
101
+
102
+ private constructor(privateKey: Uint8Array, publicKey: Uint8Array) {
103
+ this.#privateKey = privateKey;
104
+ this.#publicKey = new PublicKeyBase(publicKey);
105
+ }
106
+
107
+ /// Generates a new random X25519 key pair
108
+ static generate(): PrivateKeyBase {
109
+ const rng = new SecureRandomNumberGenerator();
110
+ const privateKey = x25519NewPrivateKeyUsing(rng);
111
+ const publicKey = x25519PublicKeyFromPrivateKey(privateKey);
112
+ return new PrivateKeyBase(privateKey, publicKey);
113
+ }
114
+
115
+ /// Creates a private key from existing bytes
116
+ static fromBytes(privateKey: Uint8Array, publicKey: Uint8Array): PrivateKeyBase {
117
+ if (privateKey.length !== X25519_PRIVATE_KEY_SIZE) {
118
+ throw new Error(`Private key must be ${X25519_PRIVATE_KEY_SIZE} bytes`);
119
+ }
120
+ if (publicKey.length !== X25519_PUBLIC_KEY_SIZE) {
121
+ throw new Error(`Public key must be ${X25519_PUBLIC_KEY_SIZE} bytes`);
122
+ }
123
+ return new PrivateKeyBase(privateKey, publicKey);
124
+ }
125
+
126
+ /// Creates a private key from hex strings
127
+ static fromHex(privateHex: string, publicHex: string): PrivateKeyBase {
128
+ const privateBytes = new Uint8Array(32);
129
+ const publicBytes = new Uint8Array(32);
130
+
131
+ for (let i = 0; i < 32; i++) {
132
+ privateBytes[i] = parseInt(privateHex.substr(i * 2, 2), 16);
133
+ publicBytes[i] = parseInt(publicHex.substr(i * 2, 2), 16);
134
+ }
135
+
136
+ return new PrivateKeyBase(privateBytes, publicBytes);
137
+ }
138
+
139
+ /// Returns the public key
140
+ publicKeys(): PublicKeyBase {
141
+ return this.#publicKey;
142
+ }
143
+
144
+ /// Returns the raw private key bytes
145
+ data(): Uint8Array {
146
+ return this.#privateKey;
147
+ }
148
+
149
+ /// Returns the private key as hex string
150
+ hex(): string {
151
+ return Array.from(this.#privateKey)
152
+ .map((b) => b.toString(16).padStart(2, "0"))
153
+ .join("");
154
+ }
155
+
156
+ /// Decrypts a sealed message to get the content key
157
+ unseal(sealedMessage: SealedMessage): SymmetricKey {
158
+ try {
159
+ const decrypted = sealedMessage.decrypt(this.#privateKey, this.#publicKey.data());
160
+ return SymmetricKey.from(decrypted);
161
+ } catch (_error) {
162
+ throw EnvelopeError.general("Failed to unseal message: not a recipient");
163
+ }
164
+ }
165
+ }
166
+
167
+ /// Represents a sealed message - encrypted content key for a recipient
168
+ /// Uses X25519 key agreement + ChaCha20-Poly1305 AEAD
169
+ ///
170
+ /// Format: ephemeral_public_key (32 bytes) || nonce (12 bytes) || ciphertext || auth_tag (16 bytes)
171
+ export class SealedMessage {
172
+ readonly #data: Uint8Array;
173
+
174
+ constructor(data: Uint8Array) {
175
+ this.#data = data;
176
+ }
177
+
178
+ /// Creates a sealed message by encrypting a symmetric key to a recipient's public key
179
+ /// Uses X25519 ECDH for key agreement and ChaCha20-Poly1305 for encryption
180
+ static seal(contentKey: SymmetricKey, recipientPublicKey: PublicKeyBase): SealedMessage {
181
+ const rng = new SecureRandomNumberGenerator();
182
+
183
+ // Generate ephemeral key pair
184
+ const ephemeralPrivate = x25519NewPrivateKeyUsing(rng);
185
+ const ephemeralPublic = x25519PublicKeyFromPrivateKey(ephemeralPrivate);
186
+
187
+ // Compute shared secret using X25519 ECDH
188
+ const sharedSecret = x25519SharedKey(ephemeralPrivate, recipientPublicKey.data());
189
+
190
+ // Derive encryption key from shared secret using HKDF
191
+ const salt = new TextEncoder().encode("sealed_message");
192
+ const encryptionKey = hkdfHmacSha256(sharedSecret, salt, 32);
193
+
194
+ // Generate random nonce
195
+ const nonce = rngRandomData(rng, SYMMETRIC_NONCE_SIZE);
196
+
197
+ // Encrypt content key using ChaCha20-Poly1305
198
+ const plaintext = contentKey.data();
199
+ const [ciphertext, authTag] = aeadChaCha20Poly1305EncryptWithAad(
200
+ plaintext,
201
+ encryptionKey,
202
+ nonce,
203
+ new Uint8Array(0), // No AAD for sealed box
204
+ );
205
+
206
+ // Format: ephemeral_public_key || nonce || ciphertext || auth_tag
207
+ const totalLength = ephemeralPublic.length + nonce.length + ciphertext.length + authTag.length;
208
+ const sealed = new Uint8Array(totalLength);
209
+ let offset = 0;
210
+
211
+ sealed.set(ephemeralPublic, offset);
212
+ offset += ephemeralPublic.length;
213
+
214
+ sealed.set(nonce, offset);
215
+ offset += nonce.length;
216
+
217
+ sealed.set(ciphertext, offset);
218
+ offset += ciphertext.length;
219
+
220
+ sealed.set(authTag, offset);
221
+
222
+ return new SealedMessage(sealed);
223
+ }
224
+
225
+ /// Decrypts this sealed message using recipient's private key
226
+ decrypt(recipientPrivate: Uint8Array, _recipientPublic: Uint8Array): Uint8Array {
227
+ // Parse sealed message format: ephemeral_public_key || nonce || ciphertext || auth_tag
228
+ const minLength = X25519_PUBLIC_KEY_SIZE + SYMMETRIC_NONCE_SIZE + SYMMETRIC_AUTH_SIZE;
229
+ if (this.#data.length < minLength) {
230
+ throw new Error("Sealed message too short");
231
+ }
232
+
233
+ let offset = 0;
234
+
235
+ // Extract ephemeral public key
236
+ const ephemeralPublic = this.#data.slice(offset, offset + X25519_PUBLIC_KEY_SIZE);
237
+ offset += X25519_PUBLIC_KEY_SIZE;
238
+
239
+ // Extract nonce
240
+ const nonce = this.#data.slice(offset, offset + SYMMETRIC_NONCE_SIZE);
241
+ offset += SYMMETRIC_NONCE_SIZE;
242
+
243
+ // Extract ciphertext and auth tag
244
+ const ciphertextAndTag = this.#data.slice(offset);
245
+ const ciphertext = ciphertextAndTag.slice(0, -SYMMETRIC_AUTH_SIZE);
246
+ const authTag = ciphertextAndTag.slice(-SYMMETRIC_AUTH_SIZE);
247
+
248
+ // Compute shared secret using X25519 ECDH
249
+ const sharedSecret = x25519SharedKey(recipientPrivate, ephemeralPublic);
250
+
251
+ // Derive decryption key from shared secret using HKDF
252
+ const salt = new TextEncoder().encode("sealed_message");
253
+ const decryptionKey = hkdfHmacSha256(sharedSecret, salt, 32);
254
+
255
+ // Decrypt using ChaCha20-Poly1305
256
+ const plaintext = aeadChaCha20Poly1305DecryptWithAad(
257
+ ciphertext,
258
+ decryptionKey,
259
+ nonce,
260
+ new Uint8Array(0), // No AAD for sealed box
261
+ authTag,
262
+ );
263
+
264
+ return plaintext;
265
+ }
266
+
267
+ /// Returns the raw sealed message bytes
268
+ data(): Uint8Array {
269
+ return this.#data;
270
+ }
271
+
272
+ /// Returns the sealed message as hex string
273
+ hex(): string {
274
+ return Array.from(this.#data)
275
+ .map((b) => b.toString(16).padStart(2, "0"))
276
+ .join("");
277
+ }
278
+
279
+ /// Creates a sealed message from hex string
280
+ static fromHex(hex: string): SealedMessage {
281
+ const bytes = new Uint8Array(hex.length / 2);
282
+ for (let i = 0; i < bytes.length; i++) {
283
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
284
+ }
285
+ return new SealedMessage(bytes);
286
+ }
287
+ }
288
+
289
+ /// Implementation of encryptSubjectToRecipient()
290
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
291
+ if (Envelope?.prototype) {
292
+ Envelope.prototype.encryptSubjectToRecipient = function (
293
+ this: Envelope,
294
+ recipientPublicKey: PublicKeyBase,
295
+ ): Envelope {
296
+ // Generate a random content key
297
+ const contentKey = SymmetricKey.generate();
298
+
299
+ // Encrypt the subject with the content key
300
+ const encrypted = this.encryptSubject(contentKey);
301
+
302
+ // Add the recipient
303
+ return encrypted.addRecipient(recipientPublicKey, contentKey);
304
+ };
305
+
306
+ /// Implementation of encryptSubjectToRecipients()
307
+ Envelope.prototype.encryptSubjectToRecipients = function (
308
+ this: Envelope,
309
+ recipients: PublicKeyBase[],
310
+ ): Envelope {
311
+ if (recipients.length === 0) {
312
+ throw EnvelopeError.general("Must provide at least one recipient");
313
+ }
314
+
315
+ // Generate a random content key
316
+ const contentKey = SymmetricKey.generate();
317
+
318
+ // Encrypt the subject with the content key
319
+ let result = this.encryptSubject(contentKey);
320
+
321
+ // Add each recipient
322
+ for (const recipient of recipients) {
323
+ result = result.addRecipient(recipient, contentKey);
324
+ }
325
+
326
+ return result;
327
+ };
328
+
329
+ /// Implementation of addRecipient()
330
+ Envelope.prototype.addRecipient = function (
331
+ this: Envelope,
332
+ recipientPublicKey: PublicKeyBase,
333
+ contentKey: SymmetricKey,
334
+ ): Envelope {
335
+ // Create a sealed message with the content key
336
+ const sealedMessage = SealedMessage.seal(contentKey, recipientPublicKey);
337
+
338
+ // Store the sealed message as bytes in the assertion
339
+ return this.addAssertion(HAS_RECIPIENT, sealedMessage.data());
340
+ };
341
+
342
+ /// Implementation of decryptSubjectToRecipient()
343
+ Envelope.prototype.decryptSubjectToRecipient = function (
344
+ this: Envelope,
345
+ recipientPrivateKey: PrivateKeyBase,
346
+ ): Envelope {
347
+ // Check that the subject is encrypted
348
+ const subjectCase = this.subject().case();
349
+ if (subjectCase.type !== "encrypted") {
350
+ throw EnvelopeError.general("Subject is not encrypted");
351
+ }
352
+
353
+ // Get all recipient assertions
354
+ const recipientAssertions = this.assertions().filter((assertion) => {
355
+ try {
356
+ const predicate = assertion.subject().asPredicate();
357
+ if (predicate === undefined) return false;
358
+ return predicate.asText() === HAS_RECIPIENT;
359
+ } catch {
360
+ return false;
361
+ }
362
+ });
363
+
364
+ if (recipientAssertions.length === 0) {
365
+ throw EnvelopeError.general("No recipients found");
366
+ }
367
+
368
+ // Try each recipient assertion until one unseals successfully
369
+ let contentKey: SymmetricKey | null = null;
370
+
371
+ for (const assertion of recipientAssertions) {
372
+ try {
373
+ const obj = assertion.subject().asObject();
374
+ if (obj === undefined) continue;
375
+ const sealedData = obj.asByteString();
376
+ if (sealedData === undefined) continue;
377
+ const sealedMessage = new SealedMessage(sealedData);
378
+
379
+ // Try to unseal with our private key
380
+ contentKey = recipientPrivateKey.unseal(sealedMessage);
381
+ break; // Success!
382
+ } catch {
383
+ // Not for us, try next one
384
+ continue;
385
+ }
386
+ }
387
+
388
+ if (contentKey === null) {
389
+ throw EnvelopeError.general("Not a valid recipient");
390
+ }
391
+
392
+ // Decrypt the subject using the content key
393
+ return this.decryptSubject(contentKey);
394
+ };
395
+
396
+ /// Implementation of decryptToRecipient()
397
+ Envelope.prototype.decryptToRecipient = function (
398
+ this: Envelope,
399
+ recipientPrivateKey: PrivateKeyBase,
400
+ ): Envelope {
401
+ const decrypted = this.decryptSubjectToRecipient(recipientPrivateKey);
402
+ return decrypted.unwrap();
403
+ };
404
+
405
+ /// Implementation of encryptToRecipients()
406
+ Envelope.prototype.encryptToRecipients = function (
407
+ this: Envelope,
408
+ recipients: PublicKeyBase[],
409
+ ): Envelope {
410
+ return this.wrap().encryptSubjectToRecipients(recipients);
411
+ };
412
+
413
+ /// Implementation of recipients()
414
+ Envelope.prototype.recipients = function (this: Envelope): SealedMessage[] {
415
+ const recipientAssertions = this.assertions().filter((assertion) => {
416
+ try {
417
+ const predicate = assertion.subject().asPredicate();
418
+ if (predicate === undefined) return false;
419
+ return predicate.asText() === HAS_RECIPIENT;
420
+ } catch {
421
+ return false;
422
+ }
423
+ });
424
+
425
+ return recipientAssertions.map((assertion) => {
426
+ const obj = assertion.subject().asObject();
427
+ if (obj === undefined) {
428
+ throw EnvelopeError.general("Invalid recipient assertion");
429
+ }
430
+ const sealedData = obj.asByteString();
431
+ if (sealedData === undefined) {
432
+ throw EnvelopeError.general("Invalid recipient data");
433
+ }
434
+ return new SealedMessage(sealedData);
435
+ });
436
+ };
437
+ }
438
+
439
+ // Import side-effect to register prototype extensions
440
+ export {};
@@ -0,0 +1,114 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { EnvelopeError } from "../base/error";
3
+ import {
4
+ SecureRandomNumberGenerator,
5
+ rngRandomData,
6
+ rngNextInClosedRangeI32,
7
+ type RandomNumberGenerator,
8
+ } from "@bcts/rand";
9
+
10
+ /// Extension for adding salt to envelopes to prevent correlation.
11
+ ///
12
+ /// This module provides functionality for decorrelating envelopes by adding
13
+ /// random salt. Salt is added as an assertion with the predicate 'salt' and
14
+ /// a random value. When an envelope is elided, this salt ensures that the
15
+ /// digest of the elided envelope cannot be correlated with other elided
16
+ /// envelopes containing the same information.
17
+ ///
18
+ /// Decorrelation is an important privacy feature that prevents third parties
19
+ /// from determining whether two elided envelopes originally contained the same
20
+ /// information by comparing their digests.
21
+ ///
22
+ /// Based on bc-envelope-rust/src/extension/salt.rs and bc-components-rust/src/salt.rs
23
+ ///
24
+ /// @example
25
+ /// ```typescript
26
+ /// // Create a simple envelope
27
+ /// const envelope = Envelope.new("Hello");
28
+ ///
29
+ /// // Create a decorrelated version by adding salt
30
+ /// const salted = envelope.addSalt();
31
+ ///
32
+ /// // The salted envelope has a different digest than the original
33
+ /// console.log(envelope.digest().equals(salted.digest())); // false
34
+ /// ```
35
+
36
+ /// The standard predicate for salt assertions
37
+ export const SALT = "salt";
38
+
39
+ /// Minimum salt size in bytes (64 bits)
40
+ const MIN_SALT_SIZE = 8;
41
+
42
+ /// Creates a new SecureRandomNumberGenerator instance
43
+ function createSecureRng(): RandomNumberGenerator {
44
+ return new SecureRandomNumberGenerator();
45
+ }
46
+
47
+ /// Generates random bytes using the rand package
48
+ function generateRandomBytes(length: number, rng?: RandomNumberGenerator): Uint8Array {
49
+ const actualRng = rng ?? createSecureRng();
50
+ return rngRandomData(actualRng, length);
51
+ }
52
+
53
+ /// Calculates salt size proportional to envelope size
54
+ /// This matches the Rust implementation in bc-components-rust/src/salt.rs
55
+ function calculateProportionalSaltSize(envelopeSize: number, rng?: RandomNumberGenerator): number {
56
+ const actualRng = rng ?? createSecureRng();
57
+ const count = envelopeSize;
58
+ const minSize = Math.max(8, Math.ceil(count * 0.05));
59
+ const maxSize = Math.max(minSize + 8, Math.ceil(count * 0.25));
60
+ return rngNextInClosedRangeI32(actualRng, minSize, maxSize);
61
+ }
62
+
63
+ /// Implementation of addSalt()
64
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
65
+ if (Envelope?.prototype) {
66
+ Envelope.prototype.addSalt = function (this: Envelope): Envelope {
67
+ const rng = createSecureRng();
68
+ const envelopeSize = this.cborBytes().length;
69
+ const saltSize = calculateProportionalSaltSize(envelopeSize, rng);
70
+ const saltBytes = generateRandomBytes(saltSize, rng);
71
+ return this.addAssertion(SALT, saltBytes);
72
+ };
73
+
74
+ /// Implementation of addSaltWithLength()
75
+ Envelope.prototype.addSaltWithLength = function (this: Envelope, count: number): Envelope {
76
+ if (count < MIN_SALT_SIZE) {
77
+ throw EnvelopeError.general(`Salt must be at least ${MIN_SALT_SIZE} bytes, got ${count}`);
78
+ }
79
+ const saltBytes = generateRandomBytes(count);
80
+ return this.addAssertion(SALT, saltBytes);
81
+ };
82
+
83
+ /// Implementation of addSaltBytes()
84
+ Envelope.prototype.addSaltBytes = function (this: Envelope, saltBytes: Uint8Array): Envelope {
85
+ if (saltBytes.length < MIN_SALT_SIZE) {
86
+ throw EnvelopeError.general(
87
+ `Salt must be at least ${MIN_SALT_SIZE} bytes, got ${saltBytes.length}`,
88
+ );
89
+ }
90
+ return this.addAssertion(SALT, saltBytes);
91
+ };
92
+
93
+ /// Implementation of addSaltInRange()
94
+ Envelope.prototype.addSaltInRange = function (
95
+ this: Envelope,
96
+ min: number,
97
+ max: number,
98
+ ): Envelope {
99
+ if (min < MIN_SALT_SIZE) {
100
+ throw EnvelopeError.general(
101
+ `Minimum salt size must be at least ${MIN_SALT_SIZE} bytes, got ${min}`,
102
+ );
103
+ }
104
+ if (max < min) {
105
+ throw EnvelopeError.general(
106
+ `Maximum salt size must be at least minimum, got min=${min} max=${max}`,
107
+ );
108
+ }
109
+ const rng = createSecureRng();
110
+ const saltSize = rngNextInClosedRangeI32(rng, min, max);
111
+ const saltBytes = generateRandomBytes(saltSize, rng);
112
+ return this.addAssertion(SALT, saltBytes);
113
+ };
114
+ }