@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/esm/src/jwk.js ADDED
@@ -0,0 +1,77 @@
1
+ import { decodeBase64Url } from "./utils.js";
2
+ import { JoseError } from "./errors.js";
3
+ /** Expected JWK key type and curve for a given JOSE HPKE algorithm. */
4
+ export function getExpectedJwkParams(alg) {
5
+ switch (alg) {
6
+ case "HPKE-0":
7
+ case "HPKE-0-KE":
8
+ case "HPKE-7":
9
+ case "HPKE-7-KE":
10
+ return { kty: "EC", crv: "P-256" };
11
+ case "HPKE-1":
12
+ case "HPKE-1-KE":
13
+ return { kty: "EC", crv: "P-384" };
14
+ case "HPKE-2":
15
+ case "HPKE-2-KE":
16
+ return { kty: "EC", crv: "P-521" };
17
+ case "HPKE-3":
18
+ case "HPKE-3-KE":
19
+ case "HPKE-4":
20
+ return { kty: "OKP", crv: "X25519" };
21
+ case "HPKE-5":
22
+ case "HPKE-5-KE":
23
+ case "HPKE-6":
24
+ return { kty: "OKP", crv: "X448" };
25
+ default:
26
+ throw new JoseError(`Unknown algorithm: ${alg}`);
27
+ }
28
+ }
29
+ /**
30
+ * Extract raw public key bytes from a JWK.
31
+ * For EC keys: uncompressed point (0x04 || x || y).
32
+ * For OKP keys: raw x coordinate bytes.
33
+ */
34
+ export function extractPublicKeyBytesFromJwk(jwk, alg) {
35
+ const expected = getExpectedJwkParams(alg);
36
+ if (jwk.kty !== expected.kty) {
37
+ throw new JoseError(`JWK kty mismatch: expected ${expected.kty}, got ${jwk.kty}`);
38
+ }
39
+ if (jwk.crv !== expected.crv) {
40
+ throw new JoseError(`JWK crv mismatch: expected ${expected.crv}, got ${jwk.crv}`);
41
+ }
42
+ if (!jwk.x) {
43
+ throw new JoseError("JWK missing x parameter");
44
+ }
45
+ if (expected.kty === "EC") {
46
+ if (!jwk.y) {
47
+ throw new JoseError("JWK missing y parameter for EC key");
48
+ }
49
+ const x = decodeBase64Url(jwk.x);
50
+ const y = decodeBase64Url(jwk.y);
51
+ // Uncompressed point: 0x04 || x || y
52
+ const raw = new Uint8Array(1 + x.length + y.length);
53
+ raw[0] = 0x04;
54
+ raw.set(x, 1);
55
+ raw.set(y, 1 + x.length);
56
+ return raw;
57
+ }
58
+ // OKP: raw x bytes
59
+ return decodeBase64Url(jwk.x);
60
+ }
61
+ /**
62
+ * Extract raw private key bytes from a JWK.
63
+ * Returns the d parameter bytes.
64
+ */
65
+ export function extractPrivateKeyBytesFromJwk(jwk, alg) {
66
+ const expected = getExpectedJwkParams(alg);
67
+ if (jwk.kty !== expected.kty) {
68
+ throw new JoseError(`JWK kty mismatch: expected ${expected.kty}, got ${jwk.kty}`);
69
+ }
70
+ if (jwk.crv !== expected.crv) {
71
+ throw new JoseError(`JWK crv mismatch: expected ${expected.crv}, got ${jwk.crv}`);
72
+ }
73
+ if (!jwk.d) {
74
+ throw new JoseError("JWK missing d parameter (private key)");
75
+ }
76
+ return decodeBase64Url(jwk.d);
77
+ }
@@ -0,0 +1,61 @@
1
+ /** A recipient entry of a JWE General JSON Serialization, as accepted by open(). */
2
+ export interface JweGeneralRecipientJson {
3
+ header?: Record<string, unknown>;
4
+ encrypted_key?: string;
5
+ }
6
+ /** JWE General JSON Serialization, as accepted by open(). */
7
+ export interface JweGeneralJson {
8
+ protected: string;
9
+ unprotected?: Record<string, unknown>;
10
+ iv?: string;
11
+ aad?: string;
12
+ ciphertext: string;
13
+ tag?: string;
14
+ recipients: JweGeneralRecipientJson[];
15
+ }
16
+ /** JWE Flattened JSON Serialization, as accepted by open(). */
17
+ export interface JweFlattenedJson {
18
+ protected: string;
19
+ unprotected?: Record<string, unknown>;
20
+ header?: Record<string, unknown>;
21
+ encrypted_key?: string;
22
+ iv?: string;
23
+ aad?: string;
24
+ ciphertext: string;
25
+ tag?: string;
26
+ }
27
+ /** A recipient entry of the JWE produced by JoseEncrypt.seal(). */
28
+ export interface JweRecipientJson extends JweGeneralRecipientJson {
29
+ header: Record<string, unknown>;
30
+ encrypted_key: string;
31
+ }
32
+ /** The JWE General JSON Serialization produced by JoseEncrypt.seal(). */
33
+ export interface JweJson extends JweGeneralJson {
34
+ iv: string;
35
+ tag: string;
36
+ recipients: JweRecipientJson[];
37
+ }
38
+ /** @internal */
39
+ export interface ParsedRecipient {
40
+ /** The union of the protected, shared and per-recipient headers. */
41
+ joseHeader: Record<string, unknown>;
42
+ encryptedKey: string;
43
+ }
44
+ /** @internal */
45
+ export interface ParsedJwe {
46
+ protectedHeader: Record<string, unknown>;
47
+ /** The Additional Authenticated Data encryption parameter. */
48
+ additionalData: Uint8Array;
49
+ iv: string;
50
+ ciphertext: string;
51
+ tag: string;
52
+ recipients: ParsedRecipient[];
53
+ }
54
+ /**
55
+ * Parse a JWE in any serialization into the values used by the message
56
+ * decryption process of draft-ietf-jose-hpke-encrypt-22 Section 7.2.
57
+ *
58
+ * @internal
59
+ */
60
+ export declare function parseJwe(jwe: string | JweFlattenedJson | JweGeneralJson): ParsedJwe;
61
+ //# sourceMappingURL=serialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../../src/src/serialization.ts"],"names":[],"mappings":"AAKA,oFAAoF;AACpF,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,uBAAuB,EAAE,CAAC;CACvC;AAED,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,gBAAiB,SAAQ,uBAAuB;IAC/D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,yEAAyE;AACzE,MAAM,WAAW,OAAQ,SAAQ,cAAc;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED,gBAAgB;AAChB,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,gBAAgB;AAChB,MAAM,WAAW,SAAS;IACxB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,8DAA8D;IAC9D,cAAc,EAAE,UAAU,CAAC;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAgHD;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,cAAc,GAC9C,SAAS,CAEX"}
@@ -0,0 +1,107 @@
1
+ import { decodeBase64Url } from "./utils.js";
2
+ import { JoseError } from "./errors.js";
3
+ const te = new TextEncoder();
4
+ function isJsonObject(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function parseProtectedHeader(encoded) {
8
+ let header;
9
+ try {
10
+ header = JSON.parse(new TextDecoder().decode(decodeBase64Url(encoded)));
11
+ }
12
+ catch {
13
+ throw new JoseError("Invalid protected header");
14
+ }
15
+ if (!isJsonObject(header)) {
16
+ throw new JoseError("Invalid protected header");
17
+ }
18
+ return header;
19
+ }
20
+ /** Build the JOSE Header, rejecting names that occur in more than one part. */
21
+ function buildJoseHeader(...parts) {
22
+ // No prototype: "__proto__" in a header must stay an ordinary name.
23
+ const joseHeader = Object.create(null);
24
+ for (const part of parts) {
25
+ if (part === undefined)
26
+ continue;
27
+ if (!isJsonObject(part)) {
28
+ throw new JoseError("Invalid JOSE Header");
29
+ }
30
+ for (const [name, value] of Object.entries(part)) {
31
+ if (Object.prototype.hasOwnProperty.call(joseHeader, name)) {
32
+ throw new JoseError(`Duplicate Header Parameter: ${name}`);
33
+ }
34
+ joseHeader[name] = value;
35
+ }
36
+ }
37
+ return joseHeader;
38
+ }
39
+ function parseCompact(jwe) {
40
+ const parts = jwe.split(".");
41
+ if (parts.length !== 5) {
42
+ throw new JoseError("Invalid JWE Compact Serialization");
43
+ }
44
+ const [encodedHeader, encryptedKey, iv, ciphertext, tag] = parts;
45
+ if (!encodedHeader || !encryptedKey) {
46
+ throw new JoseError("Invalid JWE Compact Serialization");
47
+ }
48
+ const protectedHeader = parseProtectedHeader(encodedHeader);
49
+ return {
50
+ protectedHeader,
51
+ additionalData: te.encode(encodedHeader),
52
+ iv,
53
+ ciphertext,
54
+ tag,
55
+ recipients: [{
56
+ joseHeader: buildJoseHeader(protectedHeader),
57
+ encryptedKey,
58
+ }],
59
+ };
60
+ }
61
+ function parseJson(jwe) {
62
+ if (!jwe.protected) {
63
+ throw new JoseError("Missing protected header");
64
+ }
65
+ const protectedHeader = parseProtectedHeader(jwe.protected);
66
+ let recipients;
67
+ if ("recipients" in jwe) {
68
+ if (!Array.isArray(jwe.recipients)) {
69
+ throw new JoseError("Missing or invalid recipients array");
70
+ }
71
+ if ("header" in jwe || "encrypted_key" in jwe) {
72
+ throw new JoseError("recipients must not be used with header or encrypted_key");
73
+ }
74
+ recipients = jwe.recipients;
75
+ }
76
+ else {
77
+ recipients = [{ header: jwe.header, encrypted_key: jwe.encrypted_key }];
78
+ }
79
+ if (typeof jwe.ciphertext !== "string") {
80
+ throw new JoseError("Missing ciphertext");
81
+ }
82
+ return {
83
+ protectedHeader,
84
+ additionalData: te.encode(jwe.aad !== undefined ? `${jwe.protected}.${jwe.aad}` : jwe.protected),
85
+ iv: jwe.iv ?? "",
86
+ ciphertext: jwe.ciphertext,
87
+ tag: jwe.tag ?? "",
88
+ recipients: recipients.map((entry) => {
89
+ if (!isJsonObject(entry)) {
90
+ throw new JoseError("Invalid recipient");
91
+ }
92
+ return {
93
+ joseHeader: buildJoseHeader(protectedHeader, jwe.unprotected, entry.header),
94
+ encryptedKey: entry.encrypted_key ?? "",
95
+ };
96
+ }),
97
+ };
98
+ }
99
+ /**
100
+ * Parse a JWE in any serialization into the values used by the message
101
+ * decryption process of draft-ietf-jose-hpke-encrypt-22 Section 7.2.
102
+ *
103
+ * @internal
104
+ */
105
+ export function parseJwe(jwe) {
106
+ return typeof jwe === "string" ? parseCompact(jwe) : parseJson(jwe);
107
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build the HPKE info parameter for Key Encryption recipients
3
+ * using the JOSE Recipient_structure.
4
+ *
5
+ * Recipient_structure = ASCII("JOSE-HPKE rcpt") ||
6
+ * BYTE(255) ||
7
+ * ASCII(content_encryption_alg) ||
8
+ * BYTE(255) ||
9
+ * recipient_extra_info
10
+ *
11
+ * See draft-ietf-jose-hpke-encrypt-22 Section 6.1.
12
+ */
13
+ export declare function buildRecipientStructure(contentEncAlg: string, recipientExtraInfo?: Uint8Array): Uint8Array;
14
+ //# sourceMappingURL=structures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structures.d.ts","sourceRoot":"","sources":["../../src/src/structures.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,EACrB,kBAAkB,GAAE,UAAkB,GACrC,UAAU,CAoBZ"}
@@ -0,0 +1,31 @@
1
+ const EMPTY = new Uint8Array(0);
2
+ const SEPARATOR = new Uint8Array([0xFF]);
3
+ /**
4
+ * Build the HPKE info parameter for Key Encryption recipients
5
+ * using the JOSE Recipient_structure.
6
+ *
7
+ * Recipient_structure = ASCII("JOSE-HPKE rcpt") ||
8
+ * BYTE(255) ||
9
+ * ASCII(content_encryption_alg) ||
10
+ * BYTE(255) ||
11
+ * recipient_extra_info
12
+ *
13
+ * See draft-ietf-jose-hpke-encrypt-22 Section 6.1.
14
+ */
15
+ export function buildRecipientStructure(contentEncAlg, recipientExtraInfo = EMPTY) {
16
+ const te = new TextEncoder();
17
+ const context = te.encode("JOSE-HPKE rcpt");
18
+ const algBytes = te.encode(contentEncAlg);
19
+ const result = new Uint8Array(context.length + 1 + algBytes.length + 1 + recipientExtraInfo.length);
20
+ let offset = 0;
21
+ result.set(context, offset);
22
+ offset += context.length;
23
+ result.set(SEPARATOR, offset);
24
+ offset += 1;
25
+ result.set(algBytes, offset);
26
+ offset += algBytes.length;
27
+ result.set(SEPARATOR, offset);
28
+ offset += 1;
29
+ result.set(recipientExtraInfo, offset);
30
+ return result;
31
+ }
@@ -0,0 +1,5 @@
1
+ /** Encode a Uint8Array to a base64url string (no padding). */
2
+ export declare function encodeBase64Url(data: Uint8Array): string;
3
+ /** Decode a base64url string to a Uint8Array. */
4
+ export declare function decodeBase64Url(str: string): Uint8Array;
5
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/src/utils.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CASxD;AAED,iDAAiD;AACjD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAWvD"}
@@ -0,0 +1,26 @@
1
+ /** Encode a Uint8Array to a base64url string (no padding). */
2
+ export function encodeBase64Url(data) {
3
+ let binary = "";
4
+ for (let i = 0; i < data.length; i++) {
5
+ binary += String.fromCharCode(data[i]);
6
+ }
7
+ return btoa(binary)
8
+ .replace(/\+/g, "-")
9
+ .replace(/\//g, "_")
10
+ .replace(/=+$/, "");
11
+ }
12
+ /** Decode a base64url string to a Uint8Array. */
13
+ export function decodeBase64Url(str) {
14
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
15
+ const pad = base64.length % 4;
16
+ if (pad === 2)
17
+ base64 += "==";
18
+ else if (pad === 3)
19
+ base64 += "=";
20
+ const binary = atob(base64);
21
+ const bytes = new Uint8Array(binary.length);
22
+ for (let i = 0; i < binary.length; i++) {
23
+ bytes[i] = binary.charCodeAt(i);
24
+ }
25
+ return bytes;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@hpke/jose",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "JOSE-HPKE (draft-ietf-jose-hpke-encrypt) encryption for JWE Integrated and Key Encryption",
5
+ "keywords": [
6
+ "hpke",
7
+ "jose",
8
+ "jwe",
9
+ "rfc9180",
10
+ "encryption",
11
+ "security"
12
+ ],
13
+ "author": "Ajitomi Daisuke",
14
+ "homepage": "https://github.com/dajiaji/hpke-js/tree/main/packages/jose#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/dajiaji/hpke-js.git"
18
+ },
19
+ "license": "MIT",
20
+ "bugs": {
21
+ "url": "https://github.com/dajiaji/hpke-js/issues"
22
+ },
23
+ "main": "./esm/mod.js",
24
+ "module": "./esm/mod.js",
25
+ "types": "./esm/mod.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./esm/mod.js",
29
+ "types": "./esm/mod.d.ts"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "funding": {
34
+ "type": "github",
35
+ "url": "https://github.com/sponsors/dajiaji"
36
+ },
37
+ "sideEffects": false,
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@hpke/chacha20poly1305": "^1.8.1",
43
+ "@hpke/core": "^1.10.1",
44
+ "@hpke/dhkem-x448": "^1.8.1"
45
+ },
46
+ "_generatedBy": "dnt@dev"
47
+ }