@hpke/jose 0.0.0-stage → 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/esm/mod.d.ts +17 -0
  4. package/esm/mod.d.ts.map +1 -0
  5. package/esm/mod.js +11 -0
  6. package/esm/package.json +4 -0
  7. package/esm/src/alg.d.ts +58 -0
  8. package/esm/src/alg.d.ts.map +1 -0
  9. package/esm/src/alg.js +96 -0
  10. package/esm/src/encrypt.d.ts +83 -0
  11. package/esm/src/encrypt.d.ts.map +1 -0
  12. package/esm/src/encrypt.js +228 -0
  13. package/esm/src/encrypt0.d.ts +64 -0
  14. package/esm/src/encrypt0.d.ts.map +1 -0
  15. package/esm/src/encrypt0.js +128 -0
  16. package/esm/src/errors.d.ts +7 -0
  17. package/esm/src/errors.d.ts.map +1 -0
  18. package/esm/src/errors.js +18 -0
  19. package/esm/src/hpke0.d.ts +16 -0
  20. package/esm/src/hpke0.d.ts.map +1 -0
  21. package/esm/src/hpke0.js +30 -0
  22. package/esm/src/hpke1.d.ts +16 -0
  23. package/esm/src/hpke1.d.ts.map +1 -0
  24. package/esm/src/hpke1.js +30 -0
  25. package/esm/src/hpke2.d.ts +16 -0
  26. package/esm/src/hpke2.d.ts.map +1 -0
  27. package/esm/src/hpke2.js +30 -0
  28. package/esm/src/hpke3.d.ts +16 -0
  29. package/esm/src/hpke3.d.ts.map +1 -0
  30. package/esm/src/hpke3.js +30 -0
  31. package/esm/src/hpke4.d.ts +4 -0
  32. package/esm/src/hpke4.d.ts.map +1 -0
  33. package/esm/src/hpke4.js +13 -0
  34. package/esm/src/hpke5.d.ts +16 -0
  35. package/esm/src/hpke5.d.ts.map +1 -0
  36. package/esm/src/hpke5.js +31 -0
  37. package/esm/src/hpke6.d.ts +4 -0
  38. package/esm/src/hpke6.d.ts.map +1 -0
  39. package/esm/src/hpke6.js +14 -0
  40. package/esm/src/hpke7.d.ts +16 -0
  41. package/esm/src/hpke7.d.ts.map +1 -0
  42. package/esm/src/hpke7.js +30 -0
  43. package/esm/src/jwk.d.ts +18 -0
  44. package/esm/src/jwk.d.ts.map +1 -0
  45. package/esm/src/jwk.js +77 -0
  46. package/esm/src/serialization.d.ts +61 -0
  47. package/esm/src/serialization.d.ts.map +1 -0
  48. package/esm/src/serialization.js +107 -0
  49. package/esm/src/structures.d.ts +14 -0
  50. package/esm/src/structures.d.ts.map +1 -0
  51. package/esm/src/structures.js +31 -0
  52. package/esm/src/utils.d.ts +5 -0
  53. package/esm/src/utils.d.ts.map +1 -0
  54. package/esm/src/utils.js +26 -0
  55. package/package.json +45 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Ajitomi Daisuke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # @hpke/jose
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A TypeScript implementation of
4
+ [JOSE-HPKE](https://datatracker.ietf.org/doc/draft-ietf-jose-hpke-encrypt/)
5
+ encryption based on [@hpke/core](https://github.com/dajiaji/hpke-js).
6
+
7
+ ## Features
8
+
9
+ - **JWE Integrated Encryption** — direct HPKE encryption for single-recipient
10
+ JWE.
11
+ - **JWE Key Encryption** — HPKE key wrapping with standard JWE content
12
+ encryption (A128GCM, A256GCM) for multi-recipient JWE.
13
+ - `seal()` produces the Compact Serialization for Integrated Encryption and the
14
+ General JSON Serialization for Key Encryption. `open()` accepts the Compact,
15
+ Flattened JSON and General JSON Serializations for both.
16
+ - PSK mode. The `psk_id` header parameter carries `BASE64URL(id)`.
17
+
18
+ ## Supported Environments
19
+
20
+ This package is distributed as ESM only. On Node.js, version 20 or later is
21
+ required.
22
+
23
+ ## Supported Algorithms
24
+
25
+ Implements
26
+ [draft-ietf-jose-hpke-encrypt-22](https://datatracker.ietf.org/doc/html/draft-ietf-jose-hpke-encrypt-22).
27
+
28
+ | Integrated | Key Encryption | KEM | KDF | AEAD |
29
+ | ---------- | -------------- | -------------------------- | ----------- | ---------------- |
30
+ | HPKE-0 | HPKE-0-KE | DHKEM(P-256, HKDF-SHA256) | HKDF-SHA256 | AES-128-GCM |
31
+ | HPKE-1 | HPKE-1-KE | DHKEM(P-384, HKDF-SHA384) | HKDF-SHA384 | AES-256-GCM |
32
+ | HPKE-2 | HPKE-2-KE | DHKEM(P-521, HKDF-SHA512) | HKDF-SHA512 | AES-256-GCM |
33
+ | HPKE-3 | HPKE-3-KE | DHKEM(X25519, HKDF-SHA256) | HKDF-SHA256 | AES-128-GCM |
34
+ | HPKE-4 | - | DHKEM(X25519, HKDF-SHA256) | HKDF-SHA256 | ChaCha20Poly1305 |
35
+ | HPKE-5 | HPKE-5-KE | DHKEM(X448, HKDF-SHA512) | HKDF-SHA512 | AES-256-GCM |
36
+ | HPKE-6 | - | DHKEM(X448, HKDF-SHA512) | HKDF-SHA512 | ChaCha20Poly1305 |
37
+ | HPKE-7 | HPKE-7-KE | DHKEM(P-256, HKDF-SHA256) | HKDF-SHA256 | AES-256-GCM |
38
+
39
+ ## Usage
40
+
41
+ ### Integrated Encryption (Compact JWE)
42
+
43
+ ```typescript
44
+ import { createJoseHpke3 } from "@hpke/jose";
45
+
46
+ const enc0 = createJoseHpke3();
47
+ const rkp = await enc0.generateKemKeyPair();
48
+
49
+ const plaintext = new TextEncoder().encode("Hello, JOSE-HPKE!");
50
+ const jwe = await enc0.seal(rkp.publicKey, plaintext);
51
+
52
+ const pt = await enc0.open(rkp, jwe);
53
+ // pt === plaintext
54
+ ```
55
+
56
+ ### Key Encryption (JSON JWE) with A128GCM
57
+
58
+ ```typescript
59
+ import { Aes128Gcm, createJoseHpke3Ke } from "@hpke/jose";
60
+
61
+ const enc = createJoseHpke3Ke(new Aes128Gcm());
62
+ const rkp = await enc.generateKemKeyPair();
63
+
64
+ const plaintext = new TextEncoder().encode("Hello, JOSE-HPKE KE!");
65
+ const jwe = await enc.seal(
66
+ [{ recipientPublicKey: rkp.publicKey }],
67
+ plaintext,
68
+ );
69
+
70
+ const pt = await enc.open(rkp, jwe);
71
+ // pt === plaintext
72
+ ```
73
+
74
+ The factory selects the HPKE suite for key wrapping; its AEAD argument selects
75
+ content encryption independently. This example uses AES-128-GCM for key wrapping
76
+ and AES-256-GCM for content encryption:
77
+
78
+ ```typescript
79
+ import { Aes256Gcm, createJoseHpke3Ke } from "@hpke/jose";
80
+
81
+ const enc = createJoseHpke3Ke(new Aes256Gcm());
82
+ ```
83
+
84
+ Supported content AEADs are `Aes128Gcm` and `Aes256Gcm`; other AEADs are
85
+ rejected when constructing the instance.
86
+
87
+ ### Key Encryption with Multiple Recipients
88
+
89
+ ```typescript
90
+ import { Aes128Gcm, createJoseHpke3Ke } from "@hpke/jose";
91
+
92
+ const enc = createJoseHpke3Ke(new Aes128Gcm());
93
+ const rkp1 = await enc.generateKemKeyPair();
94
+ const rkp2 = await enc.generateKemKeyPair();
95
+
96
+ const plaintext = new TextEncoder().encode("Multi-recipient!");
97
+ const jwe = await enc.seal(
98
+ [
99
+ { recipientPublicKey: rkp1.publicKey },
100
+ { recipientPublicKey: rkp2.publicKey },
101
+ ],
102
+ plaintext,
103
+ );
104
+
105
+ // Both recipients can decrypt
106
+ const pt1 = await enc.open(rkp1, jwe);
107
+ const pt2 = await enc.open(rkp2, jwe);
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT
package/esm/mod.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type { AeadInterface } from "@hpke/core";
2
+ export { Aes128Gcm, Aes256Gcm } from "@hpke/core";
3
+ export { ContentEncAlg, JoseHpkeAlg } from "./src/alg.js";
4
+ export type { JoseEncrypt0, JoseEncrypt0Options } from "./src/encrypt0.js";
5
+ export type { JoseEncrypt } from "./src/encrypt.js";
6
+ export type { JoseEncryptOpenOptions, JoseEncryptSealOptions, JoseRecipient, } from "./src/encrypt.js";
7
+ export type { JweFlattenedJson, JweGeneralJson, JweGeneralRecipientJson, JweJson, JweRecipientJson, } from "./src/serialization.js";
8
+ export { JoseError } from "./src/errors.js";
9
+ export { createJoseHpke0, createJoseHpke0Ke } from "./src/hpke0.js";
10
+ export { createJoseHpke1, createJoseHpke1Ke } from "./src/hpke1.js";
11
+ export { createJoseHpke2, createJoseHpke2Ke } from "./src/hpke2.js";
12
+ export { createJoseHpke3, createJoseHpke3Ke } from "./src/hpke3.js";
13
+ export { createJoseHpke4 } from "./src/hpke4.js";
14
+ export { createJoseHpke5, createJoseHpke5Ke } from "./src/hpke5.js";
15
+ export { createJoseHpke6 } from "./src/hpke6.js";
16
+ export { createJoseHpke7, createJoseHpke7Ke } from "./src/hpke7.js";
17
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EACV,sBAAsB,EACtB,sBAAsB,EACtB,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,OAAO,EACP,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
package/esm/mod.js ADDED
@@ -0,0 +1,11 @@
1
+ export { Aes128Gcm, Aes256Gcm } from "@hpke/core";
2
+ export { ContentEncAlg, JoseHpkeAlg } from "./src/alg.js";
3
+ export { JoseError } from "./src/errors.js";
4
+ export { createJoseHpke0, createJoseHpke0Ke } from "./src/hpke0.js";
5
+ export { createJoseHpke1, createJoseHpke1Ke } from "./src/hpke1.js";
6
+ export { createJoseHpke2, createJoseHpke2Ke } from "./src/hpke2.js";
7
+ export { createJoseHpke3, createJoseHpke3Ke } from "./src/hpke3.js";
8
+ export { createJoseHpke4 } from "./src/hpke4.js";
9
+ export { createJoseHpke5, createJoseHpke5Ke } from "./src/hpke5.js";
10
+ export { createJoseHpke6 } from "./src/hpke6.js";
11
+ export { createJoseHpke7, createJoseHpke7Ke } from "./src/hpke7.js";
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": false
4
+ }
@@ -0,0 +1,58 @@
1
+ import type { AeadInterface } from "@hpke/core";
2
+ /**
3
+ * JOSE HPKE algorithm identifiers from draft-ietf-jose-hpke-encrypt-22.
4
+ *
5
+ * Integrated Encryption: "HPKE-0" through "HPKE-7".
6
+ * Key Encryption: "HPKE-0-KE" through "HPKE-7-KE", except "HPKE-4-KE" and
7
+ * "HPKE-6-KE", which the draft does not define.
8
+ */
9
+ export declare const JoseHpkeAlg: {
10
+ /** DHKEM(P-256), HKDF-SHA256, AES-128-GCM */
11
+ readonly HPKE_0: "HPKE-0";
12
+ /** DHKEM(P-384), HKDF-SHA384, AES-256-GCM */
13
+ readonly HPKE_1: "HPKE-1";
14
+ /** DHKEM(P-521), HKDF-SHA512, AES-256-GCM */
15
+ readonly HPKE_2: "HPKE-2";
16
+ /** DHKEM(X25519), HKDF-SHA256, AES-128-GCM */
17
+ readonly HPKE_3: "HPKE-3";
18
+ /** DHKEM(X25519), HKDF-SHA256, ChaCha20Poly1305 */
19
+ readonly HPKE_4: "HPKE-4";
20
+ /** DHKEM(X448), HKDF-SHA512, AES-256-GCM */
21
+ readonly HPKE_5: "HPKE-5";
22
+ /** DHKEM(X448), HKDF-SHA512, ChaCha20Poly1305 */
23
+ readonly HPKE_6: "HPKE-6";
24
+ /** DHKEM(P-256), HKDF-SHA256, AES-256-GCM */
25
+ readonly HPKE_7: "HPKE-7";
26
+ /** Key Encryption: DHKEM(P-256), HKDF-SHA256, AES-128-GCM */
27
+ readonly HPKE_0_KE: "HPKE-0-KE";
28
+ /** Key Encryption: DHKEM(P-384), HKDF-SHA384, AES-256-GCM */
29
+ readonly HPKE_1_KE: "HPKE-1-KE";
30
+ /** Key Encryption: DHKEM(P-521), HKDF-SHA512, AES-256-GCM */
31
+ readonly HPKE_2_KE: "HPKE-2-KE";
32
+ /** Key Encryption: DHKEM(X25519), HKDF-SHA256, AES-128-GCM */
33
+ readonly HPKE_3_KE: "HPKE-3-KE";
34
+ /** Key Encryption: DHKEM(X448), HKDF-SHA512, AES-256-GCM */
35
+ readonly HPKE_5_KE: "HPKE-5-KE";
36
+ /** Key Encryption: DHKEM(P-256), HKDF-SHA256, AES-256-GCM */
37
+ readonly HPKE_7_KE: "HPKE-7-KE";
38
+ };
39
+ export type JoseHpkeAlg = (typeof JoseHpkeAlg)[keyof typeof JoseHpkeAlg];
40
+ /** JWE content-encryption algorithm identifiers. */
41
+ export declare const ContentEncAlg: {
42
+ /** AES-128-GCM */
43
+ readonly A128GCM: "A128GCM";
44
+ /** AES-256-GCM */
45
+ readonly A256GCM: "A256GCM";
46
+ };
47
+ export type ContentEncAlg = (typeof ContentEncAlg)[keyof typeof ContentEncAlg];
48
+ /**
49
+ * Map an HPKE AEAD object to its supported JWE content-encryption algorithm.
50
+ *
51
+ * @throws {JoseError} if the AEAD has no supported JWE equivalent.
52
+ */
53
+ export declare function contentEncAlgFromAead(aead: AeadInterface): ContentEncAlg;
54
+ /** Whether the algorithm is for Key Encryption. */
55
+ export declare function isKeyEncryption(alg: JoseHpkeAlg): boolean;
56
+ /** Whether the algorithm is for Integrated Encryption. */
57
+ export declare function isIntegratedEncryption(alg: JoseHpkeAlg): boolean;
58
+ //# sourceMappingURL=alg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alg.d.ts","sourceRoot":"","sources":["../../src/src/alg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW;IACtB,6CAA6C;;IAE7C,6CAA6C;;IAE7C,6CAA6C;;IAE7C,8CAA8C;;IAE9C,mDAAmD;;IAEnD,4CAA4C;;IAE5C,iDAAiD;;IAEjD,6CAA6C;;IAE7C,6DAA6D;;IAE7D,6DAA6D;;IAE7D,6DAA6D;;IAE7D,8DAA8D;;IAE9D,4DAA4D;;IAE5D,6DAA6D;;CAErD,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEzE,oDAAoD;AACpD,eAAO,MAAM,aAAa;IACxB,kBAAkB;;IAElB,kBAAkB;;CAEV,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE/E;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CA2BxE;AAqBD,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED,0DAA0D;AAC1D,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAEhE"}
package/esm/src/alg.js ADDED
@@ -0,0 +1,96 @@
1
+ import { JoseError } from "./errors.js";
2
+ /**
3
+ * JOSE HPKE algorithm identifiers from draft-ietf-jose-hpke-encrypt-22.
4
+ *
5
+ * Integrated Encryption: "HPKE-0" through "HPKE-7".
6
+ * Key Encryption: "HPKE-0-KE" through "HPKE-7-KE", except "HPKE-4-KE" and
7
+ * "HPKE-6-KE", which the draft does not define.
8
+ */
9
+ export const JoseHpkeAlg = {
10
+ /** DHKEM(P-256), HKDF-SHA256, AES-128-GCM */
11
+ HPKE_0: "HPKE-0",
12
+ /** DHKEM(P-384), HKDF-SHA384, AES-256-GCM */
13
+ HPKE_1: "HPKE-1",
14
+ /** DHKEM(P-521), HKDF-SHA512, AES-256-GCM */
15
+ HPKE_2: "HPKE-2",
16
+ /** DHKEM(X25519), HKDF-SHA256, AES-128-GCM */
17
+ HPKE_3: "HPKE-3",
18
+ /** DHKEM(X25519), HKDF-SHA256, ChaCha20Poly1305 */
19
+ HPKE_4: "HPKE-4",
20
+ /** DHKEM(X448), HKDF-SHA512, AES-256-GCM */
21
+ HPKE_5: "HPKE-5",
22
+ /** DHKEM(X448), HKDF-SHA512, ChaCha20Poly1305 */
23
+ HPKE_6: "HPKE-6",
24
+ /** DHKEM(P-256), HKDF-SHA256, AES-256-GCM */
25
+ HPKE_7: "HPKE-7",
26
+ /** Key Encryption: DHKEM(P-256), HKDF-SHA256, AES-128-GCM */
27
+ HPKE_0_KE: "HPKE-0-KE",
28
+ /** Key Encryption: DHKEM(P-384), HKDF-SHA384, AES-256-GCM */
29
+ HPKE_1_KE: "HPKE-1-KE",
30
+ /** Key Encryption: DHKEM(P-521), HKDF-SHA512, AES-256-GCM */
31
+ HPKE_2_KE: "HPKE-2-KE",
32
+ /** Key Encryption: DHKEM(X25519), HKDF-SHA256, AES-128-GCM */
33
+ HPKE_3_KE: "HPKE-3-KE",
34
+ /** Key Encryption: DHKEM(X448), HKDF-SHA512, AES-256-GCM */
35
+ HPKE_5_KE: "HPKE-5-KE",
36
+ /** Key Encryption: DHKEM(P-256), HKDF-SHA256, AES-256-GCM */
37
+ HPKE_7_KE: "HPKE-7-KE",
38
+ };
39
+ /** JWE content-encryption algorithm identifiers. */
40
+ export const ContentEncAlg = {
41
+ /** AES-128-GCM */
42
+ A128GCM: "A128GCM",
43
+ /** AES-256-GCM */
44
+ A256GCM: "A256GCM",
45
+ };
46
+ /**
47
+ * Map an HPKE AEAD object to its supported JWE content-encryption algorithm.
48
+ *
49
+ * @throws {JoseError} if the AEAD has no supported JWE equivalent.
50
+ */
51
+ export function contentEncAlgFromAead(aead) {
52
+ let alg;
53
+ let keySize;
54
+ switch (aead.id) {
55
+ case 0x0001: // AeadId.Aes128Gcm
56
+ alg = ContentEncAlg.A128GCM;
57
+ keySize = 16;
58
+ break;
59
+ case 0x0002: // AeadId.Aes256Gcm
60
+ alg = ContentEncAlg.A256GCM;
61
+ keySize = 32;
62
+ break;
63
+ default:
64
+ throw new JoseError(`Unsupported content encryption AEAD: id=0x${aead.id.toString(16).padStart(4, "0")}`);
65
+ }
66
+ if (aead.keySize !== keySize || aead.nonceSize !== 12 || aead.tagSize !== 16) {
67
+ throw new JoseError(`Invalid content encryption AEAD parameters for ${alg}: expected keySize=${keySize}, nonceSize=12, tagSize=16`);
68
+ }
69
+ return alg;
70
+ }
71
+ const KEY_ENCRYPTION_ALGS = new Set([
72
+ "HPKE-0-KE",
73
+ "HPKE-1-KE",
74
+ "HPKE-2-KE",
75
+ "HPKE-3-KE",
76
+ "HPKE-5-KE",
77
+ "HPKE-7-KE",
78
+ ]);
79
+ const INTEGRATED_ENCRYPTION_ALGS = new Set([
80
+ "HPKE-0",
81
+ "HPKE-1",
82
+ "HPKE-2",
83
+ "HPKE-3",
84
+ "HPKE-4",
85
+ "HPKE-5",
86
+ "HPKE-6",
87
+ "HPKE-7",
88
+ ]);
89
+ /** Whether the algorithm is for Key Encryption. */
90
+ export function isKeyEncryption(alg) {
91
+ return KEY_ENCRYPTION_ALGS.has(alg);
92
+ }
93
+ /** Whether the algorithm is for Integrated Encryption. */
94
+ export function isIntegratedEncryption(alg) {
95
+ return INTEGRATED_ENCRYPTION_ALGS.has(alg);
96
+ }
@@ -0,0 +1,83 @@
1
+ import type { AeadInterface, CipherSuite } from "@hpke/core";
2
+ import { type JoseHpkeAlg } from "./alg.js";
3
+ import { type JweFlattenedJson, type JweGeneralJson, type JweJson } from "./serialization.js";
4
+ /** A recipient descriptor for JoseEncrypt.seal(). */
5
+ export interface JoseRecipient {
6
+ /** The recipient's public key. */
7
+ recipientPublicKey: CryptoKey;
8
+ /** Key identifier for the recipient. */
9
+ kid?: string;
10
+ /** Extra info embedded in Recipient_structure. */
11
+ extraInfo?: Uint8Array;
12
+ /**
13
+ * Pre-shared key for PSK mode. `id` is the HPKE psk_id; it is carried in the
14
+ * "psk_id" header parameter as BASE64URL(id).
15
+ */
16
+ psk?: {
17
+ id: Uint8Array;
18
+ key: Uint8Array;
19
+ };
20
+ }
21
+ /** Options for JoseEncrypt.seal(). */
22
+ export interface JoseEncryptSealOptions {
23
+ /** Additional authenticated data (JWE AAD). */
24
+ aad?: Uint8Array;
25
+ }
26
+ /** Options for JoseEncrypt.open(). */
27
+ export interface JoseEncryptOpenOptions {
28
+ /** Extra info for Recipient_structure. */
29
+ extraInfo?: Uint8Array;
30
+ /** Pre-shared key for PSK mode. `id` is the HPKE psk_id. */
31
+ psk?: {
32
+ id: Uint8Array;
33
+ key: Uint8Array;
34
+ };
35
+ /** Key identifier to match against recipients. */
36
+ kid?: string;
37
+ }
38
+ /**
39
+ * JWE Key Encryption using HPKE.
40
+ *
41
+ * Implements draft-ietf-jose-hpke-encrypt-22 Section 6. seal() produces the
42
+ * General JSON Serialization; open() accepts all JWE serializations.
43
+ */
44
+ export interface JoseEncrypt {
45
+ /** The underlying HPKE CipherSuite. */
46
+ readonly suite: CipherSuite;
47
+ /** Generate a KEM key pair. */
48
+ generateKemKeyPair(): Promise<CryptoKeyPair>;
49
+ /**
50
+ * Encrypt plaintext into a JWE JSON Serialization structure.
51
+ */
52
+ seal(recipients: JoseRecipient[], plaintext: Uint8Array, options?: JoseEncryptSealOptions): Promise<JweJson>;
53
+ /**
54
+ * Decrypt a JWE in the Compact, Flattened JSON or General JSON Serialization.
55
+ * Header parameters are read from the union of the protected, shared
56
+ * unprotected and per-recipient headers.
57
+ */
58
+ open(recipientKey: CryptoKeyPair | CryptoKey, jwe: string | JweFlattenedJson | JweGeneralJson, options?: JoseEncryptOpenOptions): Promise<Uint8Array>;
59
+ /**
60
+ * Import a public key from a JWK.
61
+ */
62
+ importPublicJwk(jwk: JsonWebKey): Promise<CryptoKey>;
63
+ /**
64
+ * Import a private key from a JWK.
65
+ */
66
+ importPrivateJwk(jwk: JsonWebKey): Promise<CryptoKey>;
67
+ }
68
+ /** @internal */
69
+ export declare class JoseEncryptImpl implements JoseEncrypt {
70
+ private _suite;
71
+ private _alg;
72
+ private _contentEncAlg;
73
+ private _contentAead;
74
+ constructor(suite: CipherSuite, alg: JoseHpkeAlg, contentAead: AeadInterface);
75
+ get suite(): CipherSuite;
76
+ generateKemKeyPair(): Promise<CryptoKeyPair>;
77
+ seal(recipients: JoseRecipient[], plaintext: Uint8Array, options?: JoseEncryptSealOptions): Promise<JweJson>;
78
+ open(recipientKey: CryptoKeyPair | CryptoKey, jwe: string | JweFlattenedJson | JweGeneralJson, options?: JoseEncryptOpenOptions): Promise<Uint8Array>;
79
+ private _wrapCek;
80
+ importPublicJwk(jwk: JsonWebKey): Promise<CryptoKey>;
81
+ importPrivateJwk(jwk: JsonWebKey): Promise<CryptoKey>;
82
+ }
83
+ //# sourceMappingURL=encrypt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/src/encrypt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE7D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,UAAU,CAAC;AAOlB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,OAAO,EAGb,MAAM,oBAAoB,CAAC;AAM5B,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,kBAAkB,EAAE,SAAS,CAAC;IAC9B,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE;QAAE,EAAE,EAAE,UAAU,CAAC;QAAC,GAAG,EAAE,UAAU,CAAA;KAAE,CAAC;CAC3C;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACrC,+CAA+C;IAC/C,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,4DAA4D;IAC5D,GAAG,CAAC,EAAE;QAAE,EAAE,EAAE,UAAU,CAAC;QAAC,GAAG,EAAE,UAAU,CAAA;KAAE,CAAC;IAC1C,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAE5B,+BAA+B;IAC/B,kBAAkB,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAE7C;;OAEG;IACH,IAAI,CACF,UAAU,EAAE,aAAa,EAAE,EAC3B,SAAS,EAAE,UAAU,EACrB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB;;;;OAIG;IACH,IAAI,CACF,YAAY,EAAE,aAAa,GAAG,SAAS,EACvC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,cAAc,EAC/C,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAAC;IAEvB;;OAEG;IACH,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAErD;;OAEG;IACH,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACvD;AAED,gBAAgB;AAChB,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,YAAY,CAAgB;gBAGlC,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,WAAW,EAChB,WAAW,EAAE,aAAa;IAa5B,IAAI,KAAK,IAAI,WAAW,CAEvB;IAEK,kBAAkB,IAAI,OAAO,CAAC,aAAa,CAAC;IAI5C,IAAI,CACR,UAAU,EAAE,aAAa,EAAE,EAC3B,SAAS,EAAE,UAAU,EACrB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,OAAO,CAAC;IAsEb,IAAI,CACR,YAAY,EAAE,aAAa,GAAG,SAAS,EACvC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,cAAc,EAC/C,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,UAAU,CAAC;YAwGR,QAAQ;IA0ChB,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IASpD,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;CAQ5D"}
@@ -0,0 +1,228 @@
1
+ import { contentEncAlgFromAead, isKeyEncryption, } from "./alg.js";
2
+ import { decodeBase64Url, encodeBase64Url } from "./utils.js";
3
+ import { JoseError, withJoseError } from "./errors.js";
4
+ import { extractPrivateKeyBytesFromJwk, extractPublicKeyBytesFromJwk, } from "./jwk.js";
5
+ import { parseJwe, } from "./serialization.js";
6
+ import { buildRecipientStructure } from "./structures.js";
7
+ const EMPTY = new Uint8Array(0);
8
+ const te = new TextEncoder();
9
+ /** @internal */
10
+ export class JoseEncryptImpl {
11
+ constructor(suite, alg, contentAead) {
12
+ Object.defineProperty(this, "_suite", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: void 0
17
+ });
18
+ Object.defineProperty(this, "_alg", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: void 0
23
+ });
24
+ Object.defineProperty(this, "_contentEncAlg", {
25
+ enumerable: true,
26
+ configurable: true,
27
+ writable: true,
28
+ value: void 0
29
+ });
30
+ Object.defineProperty(this, "_contentAead", {
31
+ enumerable: true,
32
+ configurable: true,
33
+ writable: true,
34
+ value: void 0
35
+ });
36
+ if (!isKeyEncryption(alg)) {
37
+ throw new JoseError(`JoseEncrypt requires a Key Encryption algorithm, got ${alg}`);
38
+ }
39
+ this._suite = suite;
40
+ this._alg = alg;
41
+ this._contentEncAlg = contentEncAlgFromAead(contentAead);
42
+ this._contentAead = contentAead;
43
+ }
44
+ get suite() {
45
+ return this._suite;
46
+ }
47
+ async generateKemKeyPair() {
48
+ return await this._suite.kem.generateKeyPair();
49
+ }
50
+ async seal(recipients, plaintext, options) {
51
+ if (recipients.length === 0) {
52
+ throw new JoseError("At least one recipient is required");
53
+ }
54
+ const keySize = this._contentAead.keySize;
55
+ const nonceSize = this._contentAead.nonceSize;
56
+ const cek = crypto.getRandomValues(new Uint8Array(keySize));
57
+ try {
58
+ return await withJoseError("Encryption failed", async () => {
59
+ const nonce = crypto.getRandomValues(new Uint8Array(nonceSize));
60
+ // Build protected header: { "enc": "A128GCM" }
61
+ const protectedHeaderObj = {
62
+ enc: this._contentEncAlg,
63
+ };
64
+ const encodedProtectedHeader = encodeBase64Url(te.encode(JSON.stringify(protectedHeaderObj)));
65
+ // Content AAD = ASCII(EPH) or ASCII(EPH || '.' || BASE64URL(JWE AAD))
66
+ let contentAad;
67
+ let jweAad;
68
+ if (options?.aad && options.aad.length > 0) {
69
+ jweAad = encodeBase64Url(options.aad);
70
+ contentAad = `${encodedProtectedHeader}.${jweAad}`;
71
+ }
72
+ else {
73
+ contentAad = encodedProtectedHeader;
74
+ }
75
+ const aadBytes = te.encode(contentAad);
76
+ // Encrypt content with CEK
77
+ const contentCtx = this._contentAead.createEncryptionContext(cek);
78
+ const combined = new Uint8Array(await contentCtx.seal(nonce, plaintext, aadBytes));
79
+ // Split ciphertext and tag
80
+ const ct = combined.slice(0, combined.length - this._contentAead.tagSize);
81
+ const tag = combined.slice(combined.length - this._contentAead.tagSize);
82
+ // Wrap CEK for each recipient
83
+ const recipientEntries = [];
84
+ for (const r of recipients) {
85
+ const entry = await this._wrapCek(r, cek);
86
+ recipientEntries.push(entry);
87
+ }
88
+ const result = {
89
+ protected: encodedProtectedHeader,
90
+ iv: encodeBase64Url(nonce),
91
+ ciphertext: encodeBase64Url(ct),
92
+ tag: encodeBase64Url(tag),
93
+ recipients: recipientEntries,
94
+ };
95
+ if (jweAad) {
96
+ result.aad = jweAad;
97
+ }
98
+ return result;
99
+ });
100
+ }
101
+ finally {
102
+ // Best effort: crypto contexts may retain their own key copies.
103
+ cek.fill(0);
104
+ }
105
+ }
106
+ async open(recipientKey, jwe, options) {
107
+ const extraInfo = options?.extraInfo ?? EMPTY;
108
+ const parsed = parseJwe(jwe);
109
+ // Decode IV, ciphertext, tag
110
+ if (!parsed.iv) {
111
+ throw new JoseError("Missing IV");
112
+ }
113
+ if (!parsed.tag) {
114
+ throw new JoseError("Missing authentication tag");
115
+ }
116
+ const nonce = decodeBase64Url(parsed.iv);
117
+ const ct = decodeBase64Url(parsed.ciphertext);
118
+ const tag = decodeBase64Url(parsed.tag);
119
+ const privateKey = "privateKey" in recipientKey
120
+ ? recipientKey.privateKey
121
+ : recipientKey;
122
+ // Find matching recipient and unwrap CEK
123
+ let cek = null;
124
+ for (const { joseHeader, encryptedKey } of parsed.recipients) {
125
+ // All recipients of a JWE share one content encryption algorithm
126
+ if (joseHeader.enc !== this._contentEncAlg) {
127
+ throw new JoseError(`Content encryption algorithm mismatch: expected ${this._contentEncAlg}, got ${joseHeader.enc}`);
128
+ }
129
+ // Check algorithm matches
130
+ if (joseHeader.alg !== this._alg)
131
+ continue;
132
+ // Match by kid if specified
133
+ if (options?.kid && joseHeader.kid !== options.kid)
134
+ continue;
135
+ // Get ek (encapsulated key)
136
+ const rEk = joseHeader.ek;
137
+ if (!rEk || !encryptedKey)
138
+ continue;
139
+ // PSK consistency
140
+ if (options?.psk) {
141
+ if (joseHeader.psk_id !== encodeBase64Url(options.psk.id))
142
+ continue;
143
+ }
144
+ else {
145
+ if ("psk_id" in joseHeader)
146
+ continue;
147
+ }
148
+ const encBytes = decodeBase64Url(rEk);
149
+ const wrappedCek = decodeBase64Url(encryptedKey);
150
+ // Build HPKE info = Recipient_structure(enc_alg, extra_info)
151
+ const hpkeInfo = buildRecipientStructure(this._contentEncAlg, extraInfo);
152
+ try {
153
+ const recipientParams = {
154
+ recipientKey: privateKey,
155
+ enc: encBytes,
156
+ info: hpkeInfo,
157
+ };
158
+ if (options?.psk) {
159
+ recipientParams.psk = options.psk;
160
+ }
161
+ const ctx = await this._suite.createRecipientContext(recipientParams);
162
+ cek = new Uint8Array(await ctx.open(wrappedCek, EMPTY));
163
+ break;
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ }
169
+ if (cek === null) {
170
+ throw new JoseError("No matching recipient found or decryption failed");
171
+ }
172
+ try {
173
+ if (cek.length !== this._contentAead.keySize) {
174
+ throw new JoseError(`Invalid CEK length: expected ${this._contentAead.keySize}, got ${cek.length}`);
175
+ }
176
+ return await withJoseError("Content decryption failed", async () => {
177
+ // Combine ciphertext and tag for AES-GCM
178
+ const combined = new Uint8Array(ct.length + tag.length);
179
+ combined.set(ct);
180
+ combined.set(tag, ct.length);
181
+ const contentCtx = this._contentAead.createEncryptionContext(cek);
182
+ return new Uint8Array(await contentCtx.open(nonce, combined, parsed.additionalData));
183
+ });
184
+ }
185
+ finally {
186
+ // Best effort: crypto contexts may retain their own key copies.
187
+ cek.fill(0);
188
+ }
189
+ }
190
+ async _wrapCek(recipient, cek) {
191
+ const extraInfo = recipient.extraInfo ?? EMPTY;
192
+ // Build HPKE info = Recipient_structure(enc_alg, extra_info)
193
+ const hpkeInfo = buildRecipientStructure(this._contentEncAlg, extraInfo);
194
+ const senderParams = {
195
+ recipientPublicKey: recipient.recipientPublicKey,
196
+ info: hpkeInfo,
197
+ };
198
+ if (recipient.psk) {
199
+ senderParams.psk = recipient.psk;
200
+ }
201
+ const ctx = await this._suite.createSenderContext(senderParams);
202
+ const encBytes = new Uint8Array(ctx.enc);
203
+ const wrappedCek = new Uint8Array(await ctx.seal(cek, EMPTY));
204
+ // Per-recipient unprotected header
205
+ const rHeader = {
206
+ alg: this._alg,
207
+ ek: encodeBase64Url(encBytes),
208
+ };
209
+ if (recipient.kid) {
210
+ rHeader.kid = recipient.kid;
211
+ }
212
+ if (recipient.psk) {
213
+ rHeader.psk_id = encodeBase64Url(recipient.psk.id);
214
+ }
215
+ return {
216
+ header: rHeader,
217
+ encrypted_key: encodeBase64Url(wrappedCek),
218
+ };
219
+ }
220
+ async importPublicJwk(jwk) {
221
+ const rawBytes = extractPublicKeyBytesFromJwk(jwk, this._alg);
222
+ return await this._suite.kem.importKey("raw", rawBytes.buffer, true);
223
+ }
224
+ async importPrivateJwk(jwk) {
225
+ const rawBytes = extractPrivateKeyBytesFromJwk(jwk, this._alg);
226
+ return await this._suite.kem.importKey("raw", rawBytes.buffer, false);
227
+ }
228
+ }