@hpke/cose 0.0.0-stage → 0.0.1

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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -2
  3. package/esm/mod.d.ts +16 -0
  4. package/esm/mod.d.ts.map +1 -0
  5. package/esm/mod.js +11 -0
  6. package/esm/package.json +3 -0
  7. package/esm/src/alg.d.ts +60 -0
  8. package/esm/src/alg.d.ts.map +1 -0
  9. package/esm/src/alg.js +96 -0
  10. package/esm/src/cbor/decoder.d.ts +4 -0
  11. package/esm/src/cbor/decoder.d.ts.map +1 -0
  12. package/esm/src/cbor/decoder.js +148 -0
  13. package/esm/src/cbor/encoder.d.ts +6 -0
  14. package/esm/src/cbor/encoder.d.ts.map +1 -0
  15. package/esm/src/cbor/encoder.js +109 -0
  16. package/esm/src/cbor/types.d.ts +8 -0
  17. package/esm/src/cbor/types.d.ts.map +1 -0
  18. package/esm/src/cbor/types.js +1 -0
  19. package/esm/src/contentAesGcm.d.ts +3 -0
  20. package/esm/src/contentAesGcm.d.ts.map +1 -0
  21. package/esm/src/contentAesGcm.js +30 -0
  22. package/esm/src/coseKey.d.ts +56 -0
  23. package/esm/src/coseKey.d.ts.map +1 -0
  24. package/esm/src/coseKey.js +185 -0
  25. package/esm/src/encrypt.d.ts +155 -0
  26. package/esm/src/encrypt.d.ts.map +1 -0
  27. package/esm/src/encrypt.js +317 -0
  28. package/esm/src/encrypt0.d.ts +91 -0
  29. package/esm/src/encrypt0.d.ts.map +1 -0
  30. package/esm/src/encrypt0.js +203 -0
  31. package/esm/src/errors.d.ts +5 -0
  32. package/esm/src/errors.d.ts.map +1 -0
  33. package/esm/src/errors.js +7 -0
  34. package/esm/src/hpke0.d.ts +8 -0
  35. package/esm/src/hpke0.d.ts.map +1 -0
  36. package/esm/src/hpke0.js +23 -0
  37. package/esm/src/hpke1.d.ts +8 -0
  38. package/esm/src/hpke1.d.ts.map +1 -0
  39. package/esm/src/hpke1.js +23 -0
  40. package/esm/src/hpke2.d.ts +8 -0
  41. package/esm/src/hpke2.d.ts.map +1 -0
  42. package/esm/src/hpke2.js +23 -0
  43. package/esm/src/hpke3.d.ts +8 -0
  44. package/esm/src/hpke3.d.ts.map +1 -0
  45. package/esm/src/hpke3.js +23 -0
  46. package/esm/src/hpke4.d.ts +8 -0
  47. package/esm/src/hpke4.d.ts.map +1 -0
  48. package/esm/src/hpke4.js +39 -0
  49. package/esm/src/hpke5.d.ts +8 -0
  50. package/esm/src/hpke5.d.ts.map +1 -0
  51. package/esm/src/hpke5.js +24 -0
  52. package/esm/src/hpke6.d.ts +8 -0
  53. package/esm/src/hpke6.d.ts.map +1 -0
  54. package/esm/src/hpke6.js +40 -0
  55. package/esm/src/hpke7.d.ts +8 -0
  56. package/esm/src/hpke7.d.ts.map +1 -0
  57. package/esm/src/hpke7.js +23 -0
  58. package/esm/src/structures.d.ts +41 -0
  59. package/esm/src/structures.d.ts.map +1 -0
  60. package/esm/src/structures.js +63 -0
  61. package/package.json +48 -3
@@ -0,0 +1,317 @@
1
+ import { encode, encodeTagged } from "./cbor/encoder.js";
2
+ import { decode } from "./cbor/decoder.js";
3
+ import { contentKeySize, contentNonceSize, isKeyEncryption, } from "./alg.js";
4
+ import { CoseError } from "./errors.js";
5
+ import { extractPrivateKeyBytes, extractPublicKeyBytes } from "./coseKey.js";
6
+ import { buildEncStructureEncrypt, buildRecipientStructure, HeaderLabel, } from "./structures.js";
7
+ const EMPTY = new Uint8Array(0);
8
+ function uint8ArrayEqual(a, b) {
9
+ if (a.length !== b.length)
10
+ return false;
11
+ for (let i = 0; i < a.length; i++) {
12
+ if (a[i] !== b[i])
13
+ return false;
14
+ }
15
+ return true;
16
+ }
17
+ /** @internal */
18
+ export class CoseEncryptImpl {
19
+ /**
20
+ * @param suite The HPKE CipherSuite to use for key wrapping.
21
+ * @param alg A COSE HPKE algorithm identifier for Key Encryption (46-53).
22
+ * @param contentAlg The content encryption algorithm (A128GCM=1, A256GCM=3, or CHACHA20POLY1305=24).
23
+ * @param contentCrypto Content encryption implementation for Layer 0.
24
+ */
25
+ constructor(suite, alg, contentAlg, contentCrypto) {
26
+ Object.defineProperty(this, "_suite", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: void 0
31
+ });
32
+ Object.defineProperty(this, "_alg", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ Object.defineProperty(this, "_contentAlg", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: void 0
43
+ });
44
+ Object.defineProperty(this, "_contentCrypto", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: void 0
49
+ });
50
+ if (!isKeyEncryption(alg)) {
51
+ throw new CoseError(`CoseEncrypt requires a Key Encryption algorithm (46-53), got ${alg}`);
52
+ }
53
+ this._suite = suite;
54
+ this._alg = alg;
55
+ this._contentAlg = contentAlg;
56
+ this._contentCrypto = contentCrypto;
57
+ }
58
+ /** The underlying HPKE CipherSuite. */
59
+ get suite() {
60
+ return this._suite;
61
+ }
62
+ /**
63
+ * Generate a KEM key pair.
64
+ */
65
+ async generateKemKeyPair() {
66
+ return await this._suite.kem.generateKeyPair();
67
+ }
68
+ /**
69
+ * Encrypt plaintext into a CBOR-encoded COSE_Encrypt structure.
70
+ *
71
+ * @param recipients One or more recipient descriptors.
72
+ * @param plaintext The data to encrypt.
73
+ * @param options Optional seal parameters.
74
+ * @returns CBOR-encoded COSE_Encrypt.
75
+ */
76
+ async seal(recipients, plaintext, options) {
77
+ const inner = await this._sealInner(recipients, plaintext, options);
78
+ const coseEncrypt = [
79
+ inner.protectedHeader,
80
+ inner.unprotectedMap,
81
+ inner.ciphertext,
82
+ inner.recipientEntries,
83
+ ];
84
+ if (options?.tagged) {
85
+ return encodeTagged(96, coseEncrypt);
86
+ }
87
+ return encode(coseEncrypt);
88
+ }
89
+ async sealDetached(recipients, plaintext, options) {
90
+ const inner = await this._sealInner(recipients, plaintext, options);
91
+ const coseEncrypt = [
92
+ inner.protectedHeader,
93
+ inner.unprotectedMap,
94
+ null,
95
+ inner.recipientEntries,
96
+ ];
97
+ const message = options?.tagged
98
+ ? encodeTagged(96, coseEncrypt)
99
+ : encode(coseEncrypt);
100
+ return { message, payload: inner.ciphertext };
101
+ }
102
+ async _sealInner(recipients, plaintext, options) {
103
+ if (recipients.length === 0) {
104
+ throw new CoseError("At least one recipient is required");
105
+ }
106
+ const extAad = options?.externalAad ?? EMPTY;
107
+ const keySize = contentKeySize(this._contentAlg);
108
+ const nonceSize = contentNonceSize(this._contentAlg);
109
+ const cek = crypto.getRandomValues(new Uint8Array(keySize));
110
+ const nonce = crypto.getRandomValues(new Uint8Array(nonceSize));
111
+ const protectedMap = new Map();
112
+ protectedMap.set(HeaderLabel.ALG, this._contentAlg);
113
+ const protectedHeader = encode(protectedMap);
114
+ const unprotectedMap = new Map();
115
+ unprotectedMap.set(HeaderLabel.IV, nonce);
116
+ const aad = buildEncStructureEncrypt(protectedHeader, extAad);
117
+ const ciphertext = await this._contentEncrypt(cek, nonce, plaintext, aad);
118
+ const recipientEntries = [];
119
+ for (const r of recipients) {
120
+ const entry = await this._wrapCek(r, cek);
121
+ recipientEntries.push(entry);
122
+ }
123
+ return { protectedHeader, unprotectedMap, ciphertext, recipientEntries };
124
+ }
125
+ /**
126
+ * Decrypt a CBOR-encoded COSE_Encrypt structure.
127
+ *
128
+ * @param recipientKey The recipient's key pair or private key.
129
+ * @param data CBOR-encoded COSE_Encrypt.
130
+ * @param options Optional parameters.
131
+ * @returns Decrypted plaintext.
132
+ */
133
+ async open(recipientKey, data, options) {
134
+ const extAad = options?.externalAad ?? EMPTY;
135
+ const extraInfo = options?.extraInfo ?? EMPTY;
136
+ const hpkeAad = options?.aad ?? EMPTY;
137
+ const decoded = decode(data);
138
+ if (!Array.isArray(decoded) || decoded.length !== 4) {
139
+ throw new CoseError("Invalid COSE_Encrypt structure");
140
+ }
141
+ const protectedHeader = decoded[0];
142
+ const unprotectedHeader = decoded[1];
143
+ const rawCiphertext = decoded[2];
144
+ const recipientsList = decoded[3];
145
+ if (!(protectedHeader instanceof Uint8Array)) {
146
+ throw new CoseError("Invalid protected header");
147
+ }
148
+ // Support detached payload: if ciphertext in structure is null,
149
+ // use detachedPayload from options.
150
+ let ciphertext;
151
+ if (rawCiphertext === null) {
152
+ if (!options?.detachedPayload) {
153
+ throw new CoseError("Ciphertext is null (detached) but no detachedPayload provided");
154
+ }
155
+ ciphertext = options.detachedPayload;
156
+ }
157
+ else if (rawCiphertext instanceof Uint8Array) {
158
+ ciphertext = rawCiphertext;
159
+ }
160
+ else {
161
+ throw new CoseError("Invalid ciphertext");
162
+ }
163
+ if (!Array.isArray(recipientsList)) {
164
+ throw new CoseError("Invalid recipients array");
165
+ }
166
+ // Parse Layer 0 protected header
167
+ const headerMap = decode(protectedHeader);
168
+ if (!(headerMap instanceof Map)) {
169
+ throw new CoseError("Protected header is not a map");
170
+ }
171
+ // If alg is present, verify it matches. If absent, proceed with
172
+ // the instance's configured content algorithm (Postel's principle).
173
+ const algValue = headerMap.get(HeaderLabel.ALG);
174
+ if (algValue !== undefined && algValue !== this._contentAlg) {
175
+ throw new CoseError(`Content algorithm mismatch: expected ${this._contentAlg}, got ${algValue}`);
176
+ }
177
+ // Read IV from unprotected header
178
+ if (!(unprotectedHeader instanceof Map)) {
179
+ throw new CoseError("Invalid unprotected header");
180
+ }
181
+ const nonce = unprotectedHeader.get(HeaderLabel.IV);
182
+ if (!(nonce instanceof Uint8Array)) {
183
+ throw new CoseError("Missing or invalid IV in unprotected header");
184
+ }
185
+ const privateKey = "privateKey" in recipientKey
186
+ ? recipientKey.privateKey
187
+ : recipientKey;
188
+ // Find matching recipient and unwrap CEK
189
+ let cek = null;
190
+ for (const entry of recipientsList) {
191
+ if (!Array.isArray(entry) || entry.length !== 3)
192
+ continue;
193
+ const rProtected = entry[0];
194
+ const rUnprotected = entry[1];
195
+ const rCiphertext = entry[2];
196
+ if (!(rProtected instanceof Uint8Array))
197
+ continue;
198
+ if (!(rCiphertext instanceof Uint8Array))
199
+ continue;
200
+ const rHeader = decode(rProtected);
201
+ if (!(rHeader instanceof Map))
202
+ continue;
203
+ // If alg is present in recipient header, verify it matches.
204
+ // If absent, try this recipient (Postel's principle).
205
+ const rAlg = rHeader.get(HeaderLabel.ALG);
206
+ if (rAlg !== undefined && rAlg !== this._alg)
207
+ continue;
208
+ // Match by kid if specified
209
+ if (options?.kid) {
210
+ const rKid = rHeader.get(HeaderLabel.KID);
211
+ if (!(rKid instanceof Uint8Array))
212
+ continue;
213
+ if (!uint8ArrayEqual(rKid, options.kid))
214
+ continue;
215
+ }
216
+ // ek is in the unprotected header
217
+ if (!(rUnprotected instanceof Map))
218
+ continue;
219
+ const rEnc = rUnprotected.get(HeaderLabel.EK);
220
+ if (!(rEnc instanceof Uint8Array))
221
+ continue;
222
+ // Validate psk_id / mode consistency (draft-ietf-cose-hpke §5)
223
+ // psk_id MUST be in the recipient protected header per spec.
224
+ if (options?.psk) {
225
+ const storedPskId = rHeader.get(HeaderLabel.PSK_ID);
226
+ if (!(storedPskId instanceof Uint8Array))
227
+ continue;
228
+ if (!uint8ArrayEqual(storedPskId, options.psk.id))
229
+ continue;
230
+ }
231
+ else {
232
+ // In base mode, psk_id MUST NOT be present
233
+ if (rHeader.has(HeaderLabel.PSK_ID)) {
234
+ continue;
235
+ }
236
+ }
237
+ // Build HPKE info = Recipient_structure(contentAlg, protected, extraInfo)
238
+ const hpkeInfo = buildRecipientStructure(this._contentAlg, rProtected, extraInfo);
239
+ try {
240
+ // Build recipient context params
241
+ const recipientParams = {
242
+ recipientKey: privateKey,
243
+ enc: rEnc,
244
+ info: hpkeInfo,
245
+ };
246
+ if (options?.psk) {
247
+ recipientParams.psk = {
248
+ id: options.psk.id,
249
+ key: options.psk.key,
250
+ };
251
+ }
252
+ const ctx = await this._suite.createRecipientContext(recipientParams);
253
+ cek = new Uint8Array(await ctx.open(rCiphertext, hpkeAad));
254
+ break;
255
+ }
256
+ catch {
257
+ continue;
258
+ }
259
+ }
260
+ if (cek === null) {
261
+ throw new CoseError("No matching recipient found or decryption failed");
262
+ }
263
+ // Decrypt content with CEK
264
+ const contentAad = buildEncStructureEncrypt(protectedHeader, extAad);
265
+ return await this._contentDecrypt(cek, nonce, ciphertext, contentAad);
266
+ }
267
+ async _contentEncrypt(cek, nonce, plaintext, aad) {
268
+ return await this._contentCrypto.seal(cek, nonce, plaintext, aad);
269
+ }
270
+ async _contentDecrypt(cek, nonce, ciphertext, aad) {
271
+ return await this._contentCrypto.open(cek, nonce, ciphertext, aad);
272
+ }
273
+ async _wrapCek(recipient, cek) {
274
+ const extraInfo = recipient.extraInfo ?? EMPTY;
275
+ const hpkeAad = recipient.aad ?? EMPTY;
276
+ // Recipient protected header: { alg, [kid], [psk_id] }
277
+ const rProtectedMap = new Map();
278
+ rProtectedMap.set(HeaderLabel.ALG, this._alg);
279
+ if (recipient.kid) {
280
+ rProtectedMap.set(HeaderLabel.KID, recipient.kid);
281
+ }
282
+ if (recipient.psk) {
283
+ rProtectedMap.set(HeaderLabel.PSK_ID, recipient.psk.id);
284
+ }
285
+ const rProtectedHeader = encode(rProtectedMap);
286
+ // HPKE info = Recipient_structure(contentAlg, protected, extraInfo)
287
+ const hpkeInfo = buildRecipientStructure(this._contentAlg, rProtectedHeader, extraInfo);
288
+ // Build sender context params
289
+ const senderParams = {
290
+ recipientPublicKey: recipient.recipientPublicKey,
291
+ info: hpkeInfo,
292
+ };
293
+ if (recipient.psk) {
294
+ senderParams.psk = {
295
+ id: recipient.psk.id,
296
+ key: recipient.psk.key,
297
+ };
298
+ }
299
+ // Create sender context with the info
300
+ const ctx = await this._suite.createSenderContext(senderParams);
301
+ const encBytes = new Uint8Array(ctx.enc);
302
+ const wrappedCek = new Uint8Array(await ctx.seal(cek, hpkeAad));
303
+ // Recipient unprotected header: { ek: enc }
304
+ const rUnprotectedMap = new Map();
305
+ rUnprotectedMap.set(HeaderLabel.EK, encBytes);
306
+ // COSE_recipient = [protected, unprotected, ciphertext]
307
+ return [rProtectedHeader, rUnprotectedMap, wrappedCek];
308
+ }
309
+ async importPublicCoseKey(coseKeyBytes) {
310
+ const rawBytes = extractPublicKeyBytes(coseKeyBytes);
311
+ return await this._suite.kem.importKey("raw", rawBytes.buffer, true);
312
+ }
313
+ async importPrivateCoseKey(coseKeyBytes) {
314
+ const rawBytes = extractPrivateKeyBytes(coseKeyBytes);
315
+ return await this._suite.kem.importKey("raw", rawBytes.buffer, false);
316
+ }
317
+ }
@@ -0,0 +1,91 @@
1
+ import type { CipherSuite } from "@hpke/core";
2
+ import { type CoseHpkeAlg } from "./alg.js";
3
+ /** Options for CoseEncrypt0 seal/open operations. */
4
+ export interface CoseEncrypt0Options {
5
+ /** External AAD included in the Enc_structure. */
6
+ externalAad?: Uint8Array;
7
+ /** HPKE info parameter. */
8
+ info?: Uint8Array;
9
+ /** Pre-shared key for PSK mode. */
10
+ psk?: {
11
+ id: Uint8Array;
12
+ key: Uint8Array;
13
+ };
14
+ /** Key identifier to include in the protected header (bstr per RFC 9052). */
15
+ kid?: Uint8Array;
16
+ /** If true, output is wrapped in CBOR Tag 16 (COSE_Encrypt0). */
17
+ tagged?: boolean;
18
+ /**
19
+ * Detached ciphertext for open(). When provided, the ciphertext field
20
+ * in the COSE structure (which should be null) is replaced by this value.
21
+ */
22
+ detachedPayload?: Uint8Array;
23
+ }
24
+ /** Result of a detached seal operation. */
25
+ export interface CoseEncrypt0DetachedResult {
26
+ /** CBOR-encoded COSE_Encrypt0 with null ciphertext. */
27
+ message: Uint8Array;
28
+ /** The ciphertext transported separately. */
29
+ payload: Uint8Array;
30
+ }
31
+ /**
32
+ * COSE_Encrypt0 (Integrated Encryption) using HPKE.
33
+ *
34
+ * Implements draft-ietf-cose-hpke-27 Section 3.2 (HPKE Integrated Encryption Mode).
35
+ */
36
+ export interface CoseEncrypt0 {
37
+ /** The underlying HPKE CipherSuite. */
38
+ readonly suite: CipherSuite;
39
+ /**
40
+ * Encrypt a plaintext into a CBOR-encoded COSE_Encrypt0 structure.
41
+ */
42
+ seal(recipientPublicKey: CryptoKey, plaintext: Uint8Array, options?: CoseEncrypt0Options): Promise<Uint8Array>;
43
+ /**
44
+ * Encrypt a plaintext into a detached COSE_Encrypt0 structure.
45
+ * The ciphertext field is null; the actual payload is returned separately.
46
+ */
47
+ sealDetached(recipientPublicKey: CryptoKey, plaintext: Uint8Array, options?: CoseEncrypt0Options): Promise<CoseEncrypt0DetachedResult>;
48
+ /**
49
+ * Decrypt a CBOR-encoded COSE_Encrypt0 structure.
50
+ * For detached payloads, pass the ciphertext via options.detachedPayload.
51
+ */
52
+ open(recipientKey: CryptoKeyPair | CryptoKey, data: Uint8Array, options?: CoseEncrypt0Options): Promise<Uint8Array>;
53
+ /**
54
+ * Import a public key from a CBOR-encoded COSE_Key.
55
+ * Validates kty, crv, and key length.
56
+ */
57
+ importPublicCoseKey(coseKeyBytes: Uint8Array): Promise<CryptoKey>;
58
+ /**
59
+ * Import a private key from a CBOR-encoded COSE_Key.
60
+ * Validates kty, crv, and key length.
61
+ */
62
+ importPrivateCoseKey(coseKeyBytes: Uint8Array): Promise<CryptoKey>;
63
+ }
64
+ /** @internal */
65
+ export declare class CoseEncrypt0Impl implements CoseEncrypt0 {
66
+ private _alg;
67
+ private _suite;
68
+ /**
69
+ * @param suite The HPKE CipherSuite to use.
70
+ * @param alg A COSE HPKE algorithm identifier for Integrated Encryption (35-45).
71
+ * @throws {CoseError} if alg is not an Integrated Encryption algorithm.
72
+ */
73
+ constructor(suite: CipherSuite, alg: CoseHpkeAlg);
74
+ /** The underlying HPKE CipherSuite. */
75
+ get suite(): CipherSuite;
76
+ seal(recipientPublicKey: CryptoKey, plaintext: Uint8Array, options?: CoseEncrypt0Options): Promise<Uint8Array>;
77
+ sealDetached(recipientPublicKey: CryptoKey, plaintext: Uint8Array, options?: CoseEncrypt0Options): Promise<CoseEncrypt0DetachedResult>;
78
+ private _sealInner;
79
+ /**
80
+ * Decrypt a CBOR-encoded COSE_Encrypt0 structure.
81
+ *
82
+ * @param recipientKey The recipient's key pair or CryptoKey (private).
83
+ * @param data CBOR-encoded COSE_Encrypt0.
84
+ * @param options Optional external AAD, HPKE info, and PSK.
85
+ * @returns Decrypted plaintext.
86
+ */
87
+ open(recipientKey: CryptoKeyPair | CryptoKey, data: Uint8Array, options?: CoseEncrypt0Options): Promise<Uint8Array>;
88
+ importPublicCoseKey(coseKeyBytes: Uint8Array): Promise<CryptoKey>;
89
+ importPrivateCoseKey(coseKeyBytes: Uint8Array): Promise<CryptoKey>;
90
+ }
91
+ //# sourceMappingURL=encrypt0.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypt0.d.ts","sourceRoot":"","sources":["../../src/src/encrypt0.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C,OAAO,EAAE,KAAK,WAAW,EAA0B,MAAM,UAAU,CAAC;AAepE,qDAAqD;AACrD,MAAM,WAAW,mBAAmB;IAClC,kDAAkD;IAClD,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,mCAAmC;IACnC,GAAG,CAAC,EAAE;QAAE,EAAE,EAAE,UAAU,CAAC;QAAC,GAAG,EAAE,UAAU,CAAA;KAAE,CAAC;IAC1C,6EAA6E;IAC7E,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,UAAU,CAAC;CAC9B;AAED,2CAA2C;AAC3C,MAAM,WAAW,0BAA0B;IACzC,uDAAuD;IACvD,OAAO,EAAE,UAAU,CAAC;IACpB,6CAA6C;IAC7C,OAAO,EAAE,UAAU,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAE5B;;OAEG;IACH,IAAI,CACF,kBAAkB,EAAE,SAAS,EAC7B,SAAS,EAAE,UAAU,EACrB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAAC;IAEvB;;;OAGG;IACH,YAAY,CACV,kBAAkB,EAAE,SAAS,EAC7B,SAAS,EAAE,UAAU,EACrB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAEvC;;;OAGG;IACH,IAAI,CACF,YAAY,EAAE,aAAa,GAAG,SAAS,EACvC,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAAC;IAEvB;;;OAGG;IACH,mBAAmB,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAElE;;;OAGG;IACH,oBAAoB,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACpE;AAED,gBAAgB;AAChB,qBAAa,gBAAiB,YAAW,YAAY;IACnD,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,MAAM,CAAc;IAE5B;;;;OAIG;gBACS,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW;IAUhD,uCAAuC;IACvC,IAAI,KAAK,IAAI,WAAW,CAEvB;IAEK,IAAI,CACR,kBAAkB,EAAE,SAAS,EAC7B,SAAS,EAAE,UAAU,EACrB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,UAAU,CAAC;IAchB,YAAY,CAChB,kBAAkB,EAAE,SAAS,EAC7B,SAAS,EAAE,UAAU,EACrB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,0BAA0B,CAAC;YAcxB,UAAU;IA6CxB;;;;;;;OAOG;IACG,IAAI,CACR,YAAY,EAAE,aAAa,GAAG,SAAS,EACvC,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,UAAU,CAAC;IAyGhB,mBAAmB,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IASjE,oBAAoB,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;CAQzE"}
@@ -0,0 +1,203 @@
1
+ import { encode, encodeTagged } from "./cbor/encoder.js";
2
+ import { decode } from "./cbor/decoder.js";
3
+ import { isIntegratedEncryption } from "./alg.js";
4
+ import { extractPrivateKeyBytes, extractPublicKeyBytes } from "./coseKey.js";
5
+ import { CoseError } from "./errors.js";
6
+ import { buildEncStructure, HeaderLabel } from "./structures.js";
7
+ const EMPTY = new Uint8Array(0);
8
+ function uint8ArrayEqual(a, b) {
9
+ if (a.length !== b.length)
10
+ return false;
11
+ for (let i = 0; i < a.length; i++) {
12
+ if (a[i] !== b[i])
13
+ return false;
14
+ }
15
+ return true;
16
+ }
17
+ /** @internal */
18
+ export class CoseEncrypt0Impl {
19
+ /**
20
+ * @param suite The HPKE CipherSuite to use.
21
+ * @param alg A COSE HPKE algorithm identifier for Integrated Encryption (35-45).
22
+ * @throws {CoseError} if alg is not an Integrated Encryption algorithm.
23
+ */
24
+ constructor(suite, alg) {
25
+ Object.defineProperty(this, "_alg", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: void 0
30
+ });
31
+ Object.defineProperty(this, "_suite", {
32
+ enumerable: true,
33
+ configurable: true,
34
+ writable: true,
35
+ value: void 0
36
+ });
37
+ if (!isIntegratedEncryption(alg)) {
38
+ throw new CoseError("CoseEncrypt0 requires an Integrated Encryption algorithm (35-45)");
39
+ }
40
+ this._alg = alg;
41
+ this._suite = suite;
42
+ }
43
+ /** The underlying HPKE CipherSuite. */
44
+ get suite() {
45
+ return this._suite;
46
+ }
47
+ async seal(recipientPublicKey, plaintext, options) {
48
+ const { protectedHeader, unprotectedMap, ciphertext } = await this
49
+ ._sealInner(recipientPublicKey, plaintext, options);
50
+ const coseEncrypt0 = [
51
+ protectedHeader,
52
+ unprotectedMap,
53
+ ciphertext,
54
+ ];
55
+ if (options?.tagged) {
56
+ return encodeTagged(16, coseEncrypt0);
57
+ }
58
+ return encode(coseEncrypt0);
59
+ }
60
+ async sealDetached(recipientPublicKey, plaintext, options) {
61
+ const { protectedHeader, unprotectedMap, ciphertext } = await this
62
+ ._sealInner(recipientPublicKey, plaintext, options);
63
+ const coseEncrypt0 = [
64
+ protectedHeader,
65
+ unprotectedMap,
66
+ null,
67
+ ];
68
+ const message = options?.tagged
69
+ ? encodeTagged(16, coseEncrypt0)
70
+ : encode(coseEncrypt0);
71
+ return { message, payload: ciphertext };
72
+ }
73
+ async _sealInner(recipientPublicKey, plaintext, options) {
74
+ const externalAad = options?.externalAad ?? EMPTY;
75
+ const info = options?.info ?? EMPTY;
76
+ const senderParams = {
77
+ recipientPublicKey,
78
+ info,
79
+ };
80
+ if (options?.psk) {
81
+ senderParams.psk = {
82
+ id: options.psk.id,
83
+ key: options.psk.key,
84
+ };
85
+ }
86
+ const ctx = await this._suite.createSenderContext(senderParams);
87
+ const encBytes = new Uint8Array(ctx.enc);
88
+ const protectedMap = new Map();
89
+ protectedMap.set(HeaderLabel.ALG, this._alg);
90
+ if (options?.kid) {
91
+ protectedMap.set(HeaderLabel.KID, options.kid);
92
+ }
93
+ if (options?.psk) {
94
+ protectedMap.set(HeaderLabel.PSK_ID, options.psk.id);
95
+ }
96
+ const protectedHeader = encode(protectedMap);
97
+ const unprotectedMap = new Map();
98
+ unprotectedMap.set(HeaderLabel.EK, encBytes);
99
+ const aad = buildEncStructure(protectedHeader, externalAad);
100
+ const ciphertext = new Uint8Array(await ctx.seal(plaintext, aad));
101
+ return { protectedHeader, unprotectedMap, ciphertext };
102
+ }
103
+ /**
104
+ * Decrypt a CBOR-encoded COSE_Encrypt0 structure.
105
+ *
106
+ * @param recipientKey The recipient's key pair or CryptoKey (private).
107
+ * @param data CBOR-encoded COSE_Encrypt0.
108
+ * @param options Optional external AAD, HPKE info, and PSK.
109
+ * @returns Decrypted plaintext.
110
+ */
111
+ async open(recipientKey, data, options) {
112
+ const externalAad = options?.externalAad ?? EMPTY;
113
+ const info = options?.info ?? EMPTY;
114
+ const decoded = decode(data);
115
+ if (!Array.isArray(decoded) || decoded.length !== 3) {
116
+ throw new CoseError("Invalid COSE_Encrypt0 structure");
117
+ }
118
+ const protectedHeader = decoded[0];
119
+ const unprotectedHeader = decoded[1];
120
+ const rawCiphertext = decoded[2];
121
+ if (!(protectedHeader instanceof Uint8Array)) {
122
+ throw new CoseError("Invalid protected header");
123
+ }
124
+ // Support detached payload: if ciphertext in structure is null,
125
+ // use detachedPayload from options.
126
+ let ciphertext;
127
+ if (rawCiphertext === null) {
128
+ if (!options?.detachedPayload) {
129
+ throw new CoseError("Ciphertext is null (detached) but no detachedPayload provided");
130
+ }
131
+ ciphertext = options.detachedPayload;
132
+ }
133
+ else if (rawCiphertext instanceof Uint8Array) {
134
+ ciphertext = rawCiphertext;
135
+ }
136
+ else {
137
+ throw new CoseError("Invalid ciphertext");
138
+ }
139
+ // Parse protected header to extract alg
140
+ const headerMap = decode(protectedHeader);
141
+ if (!(headerMap instanceof Map)) {
142
+ throw new CoseError("Protected header is not a map");
143
+ }
144
+ // If alg is present, verify it matches. If absent, proceed with
145
+ // the instance's configured algorithm (Postel's principle).
146
+ const algValue = headerMap.get(HeaderLabel.ALG);
147
+ if (algValue !== undefined && algValue !== this._alg) {
148
+ throw new CoseError(`Algorithm mismatch: expected ${this._alg}, got ${algValue}`);
149
+ }
150
+ // Read ek from unprotected header
151
+ if (!(unprotectedHeader instanceof Map)) {
152
+ throw new CoseError("Invalid unprotected header");
153
+ }
154
+ const encBytes = unprotectedHeader.get(HeaderLabel.EK);
155
+ if (!(encBytes instanceof Uint8Array)) {
156
+ throw new CoseError("Missing or invalid ek in unprotected header");
157
+ }
158
+ // Validate psk_id / mode consistency (draft-ietf-cose-hpke §4)
159
+ // psk_id MUST be in the protected header per spec.
160
+ if (options?.psk) {
161
+ const storedPskId = headerMap.get(HeaderLabel.PSK_ID);
162
+ if (!(storedPskId instanceof Uint8Array)) {
163
+ throw new CoseError("Missing psk_id in protected header for PSK mode");
164
+ }
165
+ if (!uint8ArrayEqual(storedPskId, options.psk.id)) {
166
+ throw new CoseError("psk_id mismatch");
167
+ }
168
+ }
169
+ else {
170
+ // In base mode, psk_id MUST NOT be present
171
+ if (headerMap.has(HeaderLabel.PSK_ID)) {
172
+ throw new CoseError("psk_id present but PSK mode was not selected");
173
+ }
174
+ }
175
+ // Reconstruct AAD = Enc_structure("Encrypt0", protected, external_aad)
176
+ const aad = buildEncStructure(protectedHeader, externalAad);
177
+ const privateKey = "privateKey" in recipientKey
178
+ ? recipientKey.privateKey
179
+ : recipientKey;
180
+ // Build recipient context params
181
+ const recipientParams = {
182
+ recipientKey: privateKey,
183
+ enc: encBytes,
184
+ info,
185
+ };
186
+ if (options?.psk) {
187
+ recipientParams.psk = {
188
+ id: options.psk.id,
189
+ key: options.psk.key,
190
+ };
191
+ }
192
+ const ctx = await this._suite.createRecipientContext(recipientParams);
193
+ return new Uint8Array(await ctx.open(ciphertext, aad));
194
+ }
195
+ async importPublicCoseKey(coseKeyBytes) {
196
+ const rawBytes = extractPublicKeyBytes(coseKeyBytes);
197
+ return await this._suite.kem.importKey("raw", rawBytes.buffer, true);
198
+ }
199
+ async importPrivateCoseKey(coseKeyBytes) {
200
+ const rawBytes = extractPrivateKeyBytes(coseKeyBytes);
201
+ return await this._suite.kem.importKey("raw", rawBytes.buffer, false);
202
+ }
203
+ }
@@ -0,0 +1,5 @@
1
+ /** Base error for @hpke/cose operations. */
2
+ export declare class CoseError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,qBAAa,SAAU,SAAQ,KAAK;gBACf,OAAO,EAAE,MAAM;CAInC"}
@@ -0,0 +1,7 @@
1
+ /** Base error for @hpke/cose operations. */
2
+ export class CoseError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "CoseError";
6
+ }
7
+ }
@@ -0,0 +1,8 @@
1
+ import { type ContentAlg } from "./alg.js";
2
+ import type { CoseEncrypt0 } from "./encrypt0.js";
3
+ import type { CoseEncrypt } from "./encrypt.js";
4
+ /** DHKEM(P-256), HKDF-SHA256, AES-128-GCM — Integrated Encryption. */
5
+ export declare function createHpke0(): CoseEncrypt0;
6
+ /** DHKEM(P-256), HKDF-SHA256, AES-128-GCM — Key Encryption. */
7
+ export declare function createHpke0Ke(contentAlg: ContentAlg): CoseEncrypt;
8
+ //# sourceMappingURL=hpke0.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hpke0.d.ts","sourceRoot":"","sources":["../../src/src/hpke0.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,UAAU,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,sEAAsE;AACtE,wBAAgB,WAAW,IAAI,YAAY,CAO1C;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,WAAW,CAYjE"}
@@ -0,0 +1,23 @@
1
+ import { Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256, } from "@hpke/core";
2
+ import { CoseHpkeAlg } from "./alg.js";
3
+ import { aesGcmContentCrypto } from "./contentAesGcm.js";
4
+ import { CoseEncrypt0Impl } from "./encrypt0.js";
5
+ import { CoseEncryptImpl } from "./encrypt.js";
6
+ /** DHKEM(P-256), HKDF-SHA256, AES-128-GCM — Integrated Encryption. */
7
+ export function createHpke0() {
8
+ const suite = new CipherSuite({
9
+ kem: new DhkemP256HkdfSha256(),
10
+ kdf: new HkdfSha256(),
11
+ aead: new Aes128Gcm(),
12
+ });
13
+ return new CoseEncrypt0Impl(suite, CoseHpkeAlg.HPKE_0);
14
+ }
15
+ /** DHKEM(P-256), HKDF-SHA256, AES-128-GCM — Key Encryption. */
16
+ export function createHpke0Ke(contentAlg) {
17
+ const suite = new CipherSuite({
18
+ kem: new DhkemP256HkdfSha256(),
19
+ kdf: new HkdfSha256(),
20
+ aead: new Aes128Gcm(),
21
+ });
22
+ return new CoseEncryptImpl(suite, CoseHpkeAlg.HPKE_0_KE, contentAlg, aesGcmContentCrypto);
23
+ }
@@ -0,0 +1,8 @@
1
+ import { type ContentAlg } from "./alg.js";
2
+ import type { CoseEncrypt0 } from "./encrypt0.js";
3
+ import type { CoseEncrypt } from "./encrypt.js";
4
+ /** DHKEM(P-384), HKDF-SHA384, AES-256-GCM — Integrated Encryption. */
5
+ export declare function createHpke1(): CoseEncrypt0;
6
+ /** DHKEM(P-384), HKDF-SHA384, AES-256-GCM — Key Encryption. */
7
+ export declare function createHpke1Ke(contentAlg: ContentAlg): CoseEncrypt;
8
+ //# sourceMappingURL=hpke1.d.ts.map