@bcts/envelope 1.0.0-alpha.5

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 +978 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +978 -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 +85 -0
  14. package/src/base/assertion.ts +179 -0
  15. package/src/base/assertions.ts +304 -0
  16. package/src/base/cbor.ts +122 -0
  17. package/src/base/digest.ts +204 -0
  18. package/src/base/elide.ts +526 -0
  19. package/src/base/envelope-decodable.ts +229 -0
  20. package/src/base/envelope-encodable.ts +71 -0
  21. package/src/base/envelope.ts +790 -0
  22. package/src/base/error.ts +421 -0
  23. package/src/base/index.ts +56 -0
  24. package/src/base/leaf.ts +226 -0
  25. package/src/base/queries.ts +374 -0
  26. package/src/base/walk.ts +241 -0
  27. package/src/base/wrap.ts +72 -0
  28. package/src/extension/attachment.ts +369 -0
  29. package/src/extension/compress.ts +293 -0
  30. package/src/extension/encrypt.ts +379 -0
  31. package/src/extension/expression.ts +404 -0
  32. package/src/extension/index.ts +72 -0
  33. package/src/extension/proof.ts +276 -0
  34. package/src/extension/recipient.ts +557 -0
  35. package/src/extension/salt.ts +223 -0
  36. package/src/extension/signature.ts +463 -0
  37. package/src/extension/types.ts +222 -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,379 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { EnvelopeError } from "../base/error";
3
+ import { type Digest } from "../base/digest";
4
+ import { cborData, decodeCbor } from "@bcts/dcbor";
5
+ import {
6
+ aeadChaCha20Poly1305EncryptWithAad,
7
+ aeadChaCha20Poly1305DecryptWithAad,
8
+ SYMMETRIC_KEY_SIZE,
9
+ SYMMETRIC_NONCE_SIZE,
10
+ } from "@bcts/crypto";
11
+ import { SecureRandomNumberGenerator, rngRandomData, type RandomNumberGenerator } from "@bcts/rand";
12
+
13
+ /// Extension for encrypting and decrypting envelopes using symmetric encryption.
14
+ ///
15
+ /// This module extends Gordian Envelope with functions for symmetric encryption
16
+ /// and decryption using the IETF-ChaCha20-Poly1305 construct. It enables
17
+ /// privacy-enhancing operations by allowing envelope elements to be encrypted
18
+ /// without changing the envelope's digest, similar to elision.
19
+ ///
20
+ /// The encryption process preserves the envelope's digest tree structure, which
21
+ /// means signatures, proofs, and other cryptographic artifacts remain valid
22
+ /// even when parts of the envelope are encrypted.
23
+ ///
24
+ /// @example
25
+ /// ```typescript
26
+ /// // Create an envelope
27
+ /// const envelope = Envelope.new("Hello world");
28
+ ///
29
+ /// // Generate a symmetric key for encryption
30
+ /// const key = SymmetricKey.generate();
31
+ ///
32
+ /// // Encrypt the envelope's subject
33
+ /// const encrypted = envelope.encryptSubject(key);
34
+ ///
35
+ /// // The encrypted envelope has the same digest as the original
36
+ /// console.log(envelope.digest().equals(encrypted.digest())); // true
37
+ ///
38
+ /// // The subject is now encrypted
39
+ /// console.log(encrypted.subject().isEncrypted()); // true
40
+ ///
41
+ /// // Decrypt the envelope
42
+ /// const decrypted = encrypted.decryptSubject(key);
43
+ ///
44
+ /// // The decrypted envelope is equivalent to the original
45
+ /// console.log(envelope.digest().equals(decrypted.digest())); // true
46
+ /// ```
47
+
48
+ /// Helper function to create a secure RNG
49
+ function createSecureRng(): RandomNumberGenerator {
50
+ return new SecureRandomNumberGenerator();
51
+ }
52
+
53
+ /// Represents a symmetric encryption key (256-bit)
54
+ /// Matches bc-components-rust/src/symmetric/symmetric_key.rs
55
+ export class SymmetricKey {
56
+ readonly #key: Uint8Array;
57
+
58
+ constructor(key: Uint8Array) {
59
+ if (key.length !== SYMMETRIC_KEY_SIZE) {
60
+ throw new Error(`Symmetric key must be ${SYMMETRIC_KEY_SIZE} bytes`);
61
+ }
62
+ this.#key = key;
63
+ }
64
+
65
+ /// Generates a new random symmetric key
66
+ static generate(): SymmetricKey {
67
+ const rng = createSecureRng();
68
+ const key = rngRandomData(rng, SYMMETRIC_KEY_SIZE);
69
+ return new SymmetricKey(key);
70
+ }
71
+
72
+ /// Creates a symmetric key from existing bytes
73
+ static from(key: Uint8Array): SymmetricKey {
74
+ return new SymmetricKey(key);
75
+ }
76
+
77
+ /// Returns the raw key bytes
78
+ data(): Uint8Array {
79
+ return this.#key;
80
+ }
81
+
82
+ /// Encrypts data with associated digest (AAD)
83
+ /// Uses IETF ChaCha20-Poly1305 with 12-byte nonce
84
+ encrypt(plaintext: Uint8Array, digest: Digest): EncryptedMessage {
85
+ const rng = createSecureRng();
86
+
87
+ // Generate a random nonce (12 bytes for IETF ChaCha20-Poly1305)
88
+ const nonce = rngRandomData(rng, SYMMETRIC_NONCE_SIZE);
89
+
90
+ // Use digest as additional authenticated data (AAD)
91
+ const aad = digest.data();
92
+
93
+ // Encrypt using IETF ChaCha20-Poly1305
94
+ const [ciphertext, authTag] = aeadChaCha20Poly1305EncryptWithAad(
95
+ plaintext,
96
+ this.#key,
97
+ nonce,
98
+ aad,
99
+ );
100
+
101
+ return new EncryptedMessage(ciphertext, nonce, authTag, digest);
102
+ }
103
+
104
+ /// Decrypts an encrypted message
105
+ decrypt(message: EncryptedMessage): Uint8Array {
106
+ const digest = message.aadDigest();
107
+ if (digest === undefined) {
108
+ throw EnvelopeError.general("Missing digest in encrypted message");
109
+ }
110
+
111
+ const aad = digest.data();
112
+
113
+ try {
114
+ const plaintext = aeadChaCha20Poly1305DecryptWithAad(
115
+ message.ciphertext(),
116
+ this.#key,
117
+ message.nonce(),
118
+ aad,
119
+ message.authTag(),
120
+ );
121
+
122
+ return plaintext;
123
+ } catch (_error) {
124
+ throw EnvelopeError.general("Decryption failed: invalid key or corrupted data");
125
+ }
126
+ }
127
+ }
128
+
129
+ /// Represents an encrypted message with nonce, auth tag, and optional AAD digest
130
+ /// Matches bc-components-rust/src/symmetric/encrypted_message.rs
131
+ export class EncryptedMessage {
132
+ readonly #ciphertext: Uint8Array;
133
+ readonly #nonce: Uint8Array;
134
+ readonly #authTag: Uint8Array;
135
+ readonly #aadDigest?: Digest;
136
+
137
+ constructor(ciphertext: Uint8Array, nonce: Uint8Array, authTag: Uint8Array, aadDigest?: Digest) {
138
+ this.#ciphertext = ciphertext;
139
+ this.#nonce = nonce;
140
+ this.#authTag = authTag;
141
+ if (aadDigest !== undefined) {
142
+ this.#aadDigest = aadDigest;
143
+ }
144
+ }
145
+
146
+ /// Returns the ciphertext
147
+ ciphertext(): Uint8Array {
148
+ return this.#ciphertext;
149
+ }
150
+
151
+ /// Returns the nonce
152
+ nonce(): Uint8Array {
153
+ return this.#nonce;
154
+ }
155
+
156
+ /// Returns the authentication tag
157
+ authTag(): Uint8Array {
158
+ return this.#authTag;
159
+ }
160
+
161
+ /// Returns the optional AAD digest
162
+ aadDigest(): Digest | undefined {
163
+ return this.#aadDigest;
164
+ }
165
+
166
+ /// Returns the digest of this encrypted message (the AAD digest)
167
+ digest(): Digest {
168
+ if (this.#aadDigest === undefined) {
169
+ throw new Error("Encrypted message missing AAD digest");
170
+ }
171
+ return this.#aadDigest;
172
+ }
173
+ }
174
+
175
+ declare module "../base/envelope" {
176
+ interface Envelope {
177
+ /// Returns a new envelope with its subject encrypted.
178
+ ///
179
+ /// Encrypts only the subject of the envelope, leaving assertions
180
+ /// unencrypted. To encrypt an entire envelope including its assertions,
181
+ /// it must first be wrapped using the `wrap()` method, or you
182
+ /// can use the `encrypt()` convenience method.
183
+ ///
184
+ /// The encryption uses IETF ChaCha20-Poly1305 and preserves the envelope's
185
+ /// digest, allowing for features like selective disclosure and
186
+ /// signature verification to work even on encrypted envelopes.
187
+ ///
188
+ /// @param key - The SymmetricKey to use for encryption
189
+ /// @returns A new envelope with its subject encrypted
190
+ /// @throws {EnvelopeError} If the envelope is already encrypted or elided
191
+ ///
192
+ /// @example
193
+ /// ```typescript
194
+ /// const envelope = Envelope.new("Secret data");
195
+ /// const key = SymmetricKey.generate();
196
+ /// const encrypted = envelope.encryptSubject(key);
197
+ /// console.log(encrypted.subject().isEncrypted()); // true
198
+ /// ```
199
+ encryptSubject(key: SymmetricKey): Envelope;
200
+
201
+ /// Returns a new envelope with its subject decrypted.
202
+ ///
203
+ /// Decrypts the subject of an envelope that was previously encrypted using
204
+ /// `encryptSubject()`. The symmetric key used must be the same one
205
+ /// used for encryption.
206
+ ///
207
+ /// @param key - The SymmetricKey to use for decryption
208
+ /// @returns A new envelope with its subject decrypted
209
+ /// @throws {EnvelopeError} If the envelope's subject is not encrypted, key is incorrect, or digest mismatch
210
+ ///
211
+ /// @example
212
+ /// ```typescript
213
+ /// const decrypted = encrypted.decryptSubject(key);
214
+ /// console.log(decrypted.asText()); // "Secret data"
215
+ /// ```
216
+ decryptSubject(key: SymmetricKey): Envelope;
217
+
218
+ /// Convenience method to encrypt an entire envelope including its assertions.
219
+ ///
220
+ /// This method wraps the envelope and then encrypts its subject, which has
221
+ /// the effect of encrypting the entire original envelope including all
222
+ /// its assertions.
223
+ ///
224
+ /// @param key - The SymmetricKey to use for encryption
225
+ /// @returns A new envelope with the entire original envelope encrypted
226
+ ///
227
+ /// @example
228
+ /// ```typescript
229
+ /// const envelope = Envelope.new("Alice").addAssertion("knows", "Bob");
230
+ /// const key = SymmetricKey.generate();
231
+ /// const encrypted = envelope.encrypt(key);
232
+ /// ```
233
+ encrypt(key: SymmetricKey): Envelope;
234
+
235
+ /// Convenience method to decrypt an entire envelope that was encrypted
236
+ /// using the `encrypt()` method.
237
+ ///
238
+ /// This method decrypts the subject and then unwraps the resulting
239
+ /// envelope, returning the original envelope with all its assertions.
240
+ ///
241
+ /// @param key - The SymmetricKey to use for decryption
242
+ /// @returns The original decrypted envelope
243
+ /// @throws {EnvelopeError} If envelope is not encrypted, key is incorrect, digest mismatch, or cannot unwrap
244
+ ///
245
+ /// @example
246
+ /// ```typescript
247
+ /// const decrypted = encrypted.decrypt(key);
248
+ /// console.log(envelope.digest().equals(decrypted.digest())); // true
249
+ /// ```
250
+ decrypt(key: SymmetricKey): Envelope;
251
+
252
+ /// Checks if this envelope is encrypted
253
+ isEncrypted(): boolean;
254
+ }
255
+ }
256
+
257
+ /// Register encryption extension methods on Envelope prototype
258
+ /// This function is exported and called during module initialization
259
+ /// to ensure Envelope is fully defined before attaching methods.
260
+ export function registerEncryptExtension(): void {
261
+ if (Envelope?.prototype === undefined) {
262
+ return;
263
+ }
264
+
265
+ // Skip if already registered
266
+ if (typeof Envelope.prototype.encryptSubject === "function") {
267
+ return;
268
+ }
269
+
270
+ Envelope.prototype.encryptSubject = function (this: Envelope, key: SymmetricKey): Envelope {
271
+ const c = this.case();
272
+
273
+ // Can't encrypt if already encrypted or elided
274
+ if (c.type === "encrypted") {
275
+ throw EnvelopeError.general("Envelope is already encrypted");
276
+ }
277
+ if (c.type === "elided") {
278
+ throw EnvelopeError.general("Cannot encrypt elided envelope");
279
+ }
280
+
281
+ // For node case, encrypt just the subject
282
+ if (c.type === "node") {
283
+ if (c.subject.isEncrypted()) {
284
+ throw EnvelopeError.general("Subject is already encrypted");
285
+ }
286
+
287
+ // Get the subject's CBOR data
288
+ const subjectCbor = c.subject.taggedCbor();
289
+ const encodedCbor = cborData(subjectCbor);
290
+ const subjectDigest = c.subject.digest();
291
+
292
+ // Encrypt the subject
293
+ const encryptedMessage = key.encrypt(encodedCbor, subjectDigest);
294
+
295
+ // Create encrypted envelope
296
+ const encryptedSubject = Envelope.fromCase({
297
+ type: "encrypted",
298
+ message: encryptedMessage,
299
+ });
300
+
301
+ // Rebuild the node with encrypted subject and same assertions
302
+ return Envelope.newWithAssertions(encryptedSubject, c.assertions);
303
+ }
304
+
305
+ // For other cases, encrypt the entire envelope
306
+ const cbor = this.taggedCbor();
307
+ const encodedCbor = cborData(cbor);
308
+ const digest = this.digest();
309
+
310
+ const encryptedMessage = key.encrypt(encodedCbor, digest);
311
+
312
+ return Envelope.fromCase({
313
+ type: "encrypted",
314
+ message: encryptedMessage,
315
+ });
316
+ };
317
+
318
+ /// Implementation of decryptSubject()
319
+ Envelope.prototype.decryptSubject = function (this: Envelope, key: SymmetricKey): Envelope {
320
+ const subjectCase = this.subject().case();
321
+
322
+ if (subjectCase.type !== "encrypted") {
323
+ throw EnvelopeError.general("Subject is not encrypted");
324
+ }
325
+
326
+ const message = subjectCase.message;
327
+ const subjectDigest = message.aadDigest();
328
+
329
+ if (subjectDigest === undefined) {
330
+ throw EnvelopeError.general("Missing digest in encrypted message");
331
+ }
332
+
333
+ // Decrypt the subject
334
+ const decryptedData = key.decrypt(message);
335
+
336
+ // Parse back to envelope
337
+ const cbor = decodeCbor(decryptedData);
338
+ const resultSubject = Envelope.fromTaggedCbor(cbor);
339
+
340
+ // Verify digest
341
+ if (!resultSubject.digest().equals(subjectDigest)) {
342
+ throw EnvelopeError.general("Invalid digest after decryption");
343
+ }
344
+
345
+ const c = this.case();
346
+
347
+ // If this is a node, rebuild with decrypted subject
348
+ if (c.type === "node") {
349
+ const result = Envelope.newWithAssertions(resultSubject, c.assertions);
350
+ if (!result.digest().equals(c.digest)) {
351
+ throw EnvelopeError.general("Invalid envelope digest after decryption");
352
+ }
353
+ return result;
354
+ }
355
+
356
+ // Otherwise just return the decrypted subject
357
+ return resultSubject;
358
+ };
359
+
360
+ /// Implementation of encrypt() - convenience method
361
+ Envelope.prototype.encrypt = function (this: Envelope, key: SymmetricKey): Envelope {
362
+ return this.wrap().encryptSubject(key);
363
+ };
364
+
365
+ /// Implementation of decrypt() - convenience method
366
+ Envelope.prototype.decrypt = function (this: Envelope, key: SymmetricKey): Envelope {
367
+ const decrypted = this.decryptSubject(key);
368
+ return decrypted.unwrap();
369
+ };
370
+
371
+ /// Implementation of isEncrypted()
372
+ Envelope.prototype.isEncrypted = function (this: Envelope): boolean {
373
+ return this.case().type === "encrypted";
374
+ };
375
+ }
376
+
377
+ // Auto-register on module load - will be called again from index.ts
378
+ // to ensure proper ordering after all modules are loaded
379
+ registerEncryptExtension();