@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.
- package/LICENSE +48 -0
- package/README.md +23 -0
- package/dist/index.cjs +2646 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +978 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +978 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +2644 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +2552 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +85 -0
- package/src/base/assertion.ts +179 -0
- package/src/base/assertions.ts +304 -0
- package/src/base/cbor.ts +122 -0
- package/src/base/digest.ts +204 -0
- package/src/base/elide.ts +526 -0
- package/src/base/envelope-decodable.ts +229 -0
- package/src/base/envelope-encodable.ts +71 -0
- package/src/base/envelope.ts +790 -0
- package/src/base/error.ts +421 -0
- package/src/base/index.ts +56 -0
- package/src/base/leaf.ts +226 -0
- package/src/base/queries.ts +374 -0
- package/src/base/walk.ts +241 -0
- package/src/base/wrap.ts +72 -0
- package/src/extension/attachment.ts +369 -0
- package/src/extension/compress.ts +293 -0
- package/src/extension/encrypt.ts +379 -0
- package/src/extension/expression.ts +404 -0
- package/src/extension/index.ts +72 -0
- package/src/extension/proof.ts +276 -0
- package/src/extension/recipient.ts +557 -0
- package/src/extension/salt.ts +223 -0
- package/src/extension/signature.ts +463 -0
- package/src/extension/types.ts +222 -0
- package/src/format/diagnostic.ts +116 -0
- package/src/format/hex.ts +25 -0
- package/src/format/index.ts +13 -0
- package/src/format/tree.ts +168 -0
- package/src/index.ts +32 -0
- package/src/utils/index.ts +8 -0
- 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();
|