ehbp 0.0.7 → 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 (62) hide show
  1. package/README.md +84 -138
  2. package/dist/cjs/client.d.ts +13 -13
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +32 -52
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/derive.d.ts +63 -0
  7. package/dist/cjs/derive.d.ts.map +1 -0
  8. package/dist/cjs/derive.js +136 -0
  9. package/dist/cjs/derive.js.map +1 -0
  10. package/dist/cjs/identity.d.ts +37 -10
  11. package/dist/cjs/identity.d.ts.map +1 -1
  12. package/dist/cjs/identity.js +152 -146
  13. package/dist/cjs/identity.js.map +1 -1
  14. package/dist/cjs/index.d.ts +4 -1
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +15 -1
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/protocol.d.ts +1 -1
  19. package/dist/cjs/protocol.js +2 -2
  20. package/dist/cjs/protocol.js.map +1 -1
  21. package/dist/esm/client.d.ts +13 -13
  22. package/dist/esm/client.d.ts.map +1 -1
  23. package/dist/esm/client.js +32 -52
  24. package/dist/esm/client.js.map +1 -1
  25. package/dist/esm/derive.d.ts +63 -0
  26. package/dist/esm/derive.d.ts.map +1 -0
  27. package/dist/esm/derive.js +127 -0
  28. package/dist/esm/derive.js.map +1 -0
  29. package/dist/esm/identity.d.ts +37 -10
  30. package/dist/esm/identity.d.ts.map +1 -1
  31. package/dist/esm/identity.js +152 -146
  32. package/dist/esm/identity.js.map +1 -1
  33. package/dist/esm/index.d.ts +4 -1
  34. package/dist/esm/index.d.ts.map +1 -1
  35. package/dist/esm/index.js +2 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/protocol.d.ts +1 -1
  38. package/dist/esm/protocol.js +2 -2
  39. package/dist/esm/protocol.js.map +1 -1
  40. package/dist/esm/test/client.test.js +15 -16
  41. package/dist/esm/test/client.test.js.map +1 -1
  42. package/dist/esm/test/derive.test.d.ts +2 -0
  43. package/dist/esm/test/derive.test.d.ts.map +1 -0
  44. package/dist/esm/test/derive.test.js +164 -0
  45. package/dist/esm/test/derive.test.js.map +1 -0
  46. package/dist/esm/test/security.test.d.ts +10 -0
  47. package/dist/esm/test/security.test.d.ts.map +1 -0
  48. package/dist/esm/test/security.test.js +153 -0
  49. package/dist/esm/test/security.test.js.map +1 -0
  50. package/dist/esm/test/streaming.integration.d.ts +9 -0
  51. package/dist/esm/test/streaming.integration.d.ts.map +1 -0
  52. package/dist/esm/test/streaming.integration.js +190 -0
  53. package/dist/esm/test/streaming.integration.js.map +1 -0
  54. package/package.json +6 -7
  55. package/dist/esm/example.d.ts +0 -6
  56. package/dist/esm/example.d.ts.map +0 -1
  57. package/dist/esm/example.js +0 -115
  58. package/dist/esm/example.js.map +0 -1
  59. package/dist/esm/streaming-test.d.ts +0 -3
  60. package/dist/esm/streaming-test.d.ts.map +0 -1
  61. package/dist/esm/streaming-test.js +0 -102
  62. package/dist/esm/streaming-test.js.map +0 -1
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Response key derivation for EHBP
4
+ *
5
+ * This module implements the key derivation matching the Go implementation.
6
+ *
7
+ * The derivation follows OHTTP (RFC 9458):
8
+ * salt = concat(enc, response_nonce)
9
+ * prk = Extract(salt, secret)
10
+ * aead_key = Expand(prk, "key", Nk)
11
+ * aead_nonce = Expand(prk, "nonce", Nn)
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.REQUEST_ENC_LENGTH = exports.AES_GCM_NONCE_LENGTH = exports.AES256_KEY_LENGTH = exports.RESPONSE_NONCE_LENGTH = exports.EXPORT_LENGTH = exports.EXPORT_LABEL = exports.HPKE_REQUEST_INFO = void 0;
15
+ exports.deriveResponseKeys = deriveResponseKeys;
16
+ exports.computeNonce = computeNonce;
17
+ exports.encryptChunk = encryptChunk;
18
+ exports.decryptChunk = decryptChunk;
19
+ exports.hexToBytes = hexToBytes;
20
+ exports.bytesToHex = bytesToHex;
21
+ const hpke_1 = require("hpke");
22
+ const kdf = (0, hpke_1.KDF_HKDF_SHA256)();
23
+ const aead = (0, hpke_1.AEAD_AES_256_GCM)();
24
+ exports.HPKE_REQUEST_INFO = 'ehbp request';
25
+ exports.EXPORT_LABEL = 'ehbp response';
26
+ exports.EXPORT_LENGTH = 32;
27
+ exports.RESPONSE_NONCE_LENGTH = 32; // max(Nn, Nk) = max(12, 32) = 32
28
+ exports.AES256_KEY_LENGTH = 32;
29
+ exports.AES_GCM_NONCE_LENGTH = 12;
30
+ exports.REQUEST_ENC_LENGTH = 32; // X25519 enc size
31
+ // Labels for HKDF-Expand
32
+ const RESPONSE_KEY_LABEL = new TextEncoder().encode('key');
33
+ const RESPONSE_NONCE_LABEL = new TextEncoder().encode('nonce');
34
+ /**
35
+ * Derives response encryption keys from the HPKE exported secret.
36
+ *
37
+ * salt = concat(enc, response_nonce)
38
+ * prk = Extract(salt, secret)
39
+ * key = Expand(prk, "key", 32)
40
+ * nonceBase = Expand(prk, "nonce", 12)
41
+ *
42
+ * @param exportedSecret - 32 bytes exported from HPKE context
43
+ * @param requestEnc - 32 bytes encapsulated key from request
44
+ * @param responseNonce - 32 bytes random nonce from response
45
+ * @returns Key material for response encryption/decryption
46
+ */
47
+ async function deriveResponseKeys(exportedSecret, requestEnc, responseNonce) {
48
+ // Validate inputs
49
+ if (exportedSecret.length !== exports.EXPORT_LENGTH) {
50
+ throw new Error(`exported secret must be ${exports.EXPORT_LENGTH} bytes, got ${exportedSecret.length}`);
51
+ }
52
+ if (requestEnc.length !== exports.REQUEST_ENC_LENGTH) {
53
+ throw new Error(`request enc must be ${exports.REQUEST_ENC_LENGTH} bytes, got ${requestEnc.length}`);
54
+ }
55
+ if (responseNonce.length !== exports.RESPONSE_NONCE_LENGTH) {
56
+ throw new Error(`response nonce must be ${exports.RESPONSE_NONCE_LENGTH} bytes, got ${responseNonce.length}`);
57
+ }
58
+ // salt = concat(enc, response_nonce)
59
+ const salt = new Uint8Array(requestEnc.length + responseNonce.length);
60
+ salt.set(requestEnc, 0);
61
+ salt.set(responseNonce, requestEnc.length);
62
+ // prk = Extract(salt, secret)
63
+ const prk = await kdf.Extract(salt, exportedSecret);
64
+ // key = Expand(prk, "key", 32)
65
+ const keyBytes = await kdf.Expand(prk, RESPONSE_KEY_LABEL, exports.AES256_KEY_LENGTH);
66
+ // nonceBase = Expand(prk, "nonce", 12)
67
+ const nonceBase = await kdf.Expand(prk, RESPONSE_NONCE_LABEL, exports.AES_GCM_NONCE_LENGTH);
68
+ return { keyBytes, nonceBase };
69
+ }
70
+ /**
71
+ * Computes the nonce for a specific sequence number.
72
+ * nonce = nonceBase XOR sequence_number (big-endian in last 8 bytes)
73
+ */
74
+ function computeNonce(nonceBase, seq) {
75
+ if (nonceBase.length !== exports.AES_GCM_NONCE_LENGTH) {
76
+ throw new Error(`nonce base must be ${exports.AES_GCM_NONCE_LENGTH} bytes`);
77
+ }
78
+ // Validate seq to prevent nonce reuse from integer overflow.
79
+ // JavaScript's >>> operator only works correctly for 32-bit unsigned integers.
80
+ // Values >= 2^32 wrap around (e.g., 2^32 >>> 0 === 0), causing nonce reuse.
81
+ // In practice, 2^32 chunks per response is impossible (~4PB minimum), but we validate defensively.
82
+ if (!Number.isInteger(seq) || seq < 0 || seq >= 0x100000000) {
83
+ throw new Error(`sequence number must be an integer in range [0, 2^32): got ${seq}`);
84
+ }
85
+ const nonce = new Uint8Array(exports.AES_GCM_NONCE_LENGTH);
86
+ nonce.set(nonceBase);
87
+ // XOR with sequence number in the last 8 bytes (big-endian)
88
+ for (let i = 0; i < 8; i++) {
89
+ const shift = i * 8;
90
+ if (shift < 32) {
91
+ nonce[exports.AES_GCM_NONCE_LENGTH - 1 - i] ^= (seq >>> shift) & 0xff;
92
+ }
93
+ }
94
+ return nonce;
95
+ }
96
+ /**
97
+ * Encrypts a chunk using the response key material
98
+ */
99
+ async function encryptChunk(km, seq, plaintext) {
100
+ const nonce = computeNonce(km.nonceBase, seq);
101
+ const ciphertext = await aead.Seal(km.keyBytes, nonce, new Uint8Array(0), plaintext);
102
+ return ciphertext;
103
+ }
104
+ /**
105
+ * Decrypts a chunk using the response key material
106
+ */
107
+ async function decryptChunk(km, seq, ciphertext) {
108
+ const nonce = computeNonce(km.nonceBase, seq);
109
+ const plaintext = await aead.Open(km.keyBytes, nonce, new Uint8Array(0), ciphertext);
110
+ return plaintext;
111
+ }
112
+ /**
113
+ * Utility: Convert hex string to Uint8Array
114
+ */
115
+ function hexToBytes(hex) {
116
+ if (hex.length % 2 !== 0) {
117
+ throw new Error('Hex string must have even length');
118
+ }
119
+ if (!/^[0-9a-fA-F]*$/.test(hex)) {
120
+ throw new Error('Invalid hex character');
121
+ }
122
+ const bytes = new Uint8Array(hex.length / 2);
123
+ for (let i = 0; i < bytes.length; i++) {
124
+ bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
125
+ }
126
+ return bytes;
127
+ }
128
+ /**
129
+ * Utility: Convert Uint8Array to hex string
130
+ */
131
+ function bytesToHex(bytes) {
132
+ return Array.from(bytes)
133
+ .map(b => b.toString(16).padStart(2, '0'))
134
+ .join('');
135
+ }
136
+ //# sourceMappingURL=derive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.js","sourceRoot":"","sources":["../../src/derive.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,12 @@
1
- import { CipherSuite } from '@hpke/core';
1
+ import { CipherSuite, type SenderContext, type Key } from 'hpke';
2
+ /**
3
+ * Request context for response decryption.
4
+ * Holds the HPKE sender context needed to derive response keys.
5
+ */
6
+ export interface RequestContext {
7
+ senderContext: SenderContext;
8
+ requestEnc: Uint8Array;
9
+ }
2
10
  /**
3
11
  * Identity class for managing HPKE key pairs and encryption/decryption
4
12
  */
@@ -6,7 +14,7 @@ export declare class Identity {
6
14
  private suite;
7
15
  private publicKey;
8
16
  private privateKey;
9
- constructor(suite: CipherSuite, publicKey: CryptoKey, privateKey: CryptoKey);
17
+ constructor(suite: CipherSuite, publicKey: Key, privateKey: Key);
10
18
  /**
11
19
  * Generate a new identity with X25519 key pair
12
20
  */
@@ -20,17 +28,17 @@ export declare class Identity {
20
28
  */
21
29
  toJSON(): Promise<string>;
22
30
  /**
23
- * Get public key as CryptoKey
31
+ * Get public key
24
32
  */
25
- getPublicKey(): CryptoKey;
33
+ getPublicKey(): Key;
26
34
  /**
27
35
  * Get public key as hex string
28
36
  */
29
37
  getPublicKeyHex(): Promise<string>;
30
38
  /**
31
- * Get private key as CryptoKey
39
+ * Get private key
32
40
  */
33
- getPrivateKey(): CryptoKey;
41
+ getPrivateKey(): Key;
34
42
  /**
35
43
  * Marshal public key configuration for server key distribution
36
44
  * Implements RFC 9458 format
@@ -41,12 +49,31 @@ export declare class Identity {
41
49
  */
42
50
  static unmarshalPublicConfig(data: Uint8Array): Promise<Identity>;
43
51
  /**
44
- * Encrypt request body and set appropriate headers
52
+ * Encrypt request body and return context for response decryption.
53
+ *
54
+ * This method is called on the SERVER's identity (public key only).
55
+ * It:
56
+ * 1. Creates an HPKE sender context to this identity's public key
57
+ * 2. Encrypts the request body
58
+ * 3. Returns a RequestContext that must be used to decrypt the response
59
+ */
60
+ encryptRequestWithContext(request: Request): Promise<{
61
+ request: Request;
62
+ context: RequestContext | null;
63
+ }>;
64
+ /**
65
+ * Decrypt response using keys derived from request context.
66
+ *
67
+ * This method:
68
+ * 1. Reads the response nonce from Ehbp-Response-Nonce header
69
+ * 2. Exports a secret from the HPKE sender context
70
+ * 3. Derives response keys using HKDF
71
+ * 4. Decrypts the response body
45
72
  */
46
- encryptRequest(request: Request, serverPublicKey: CryptoKey): Promise<Request>;
73
+ decryptResponseWithContext(response: Response, context: RequestContext): Promise<Response>;
47
74
  /**
48
- * Decrypt response body
75
+ * Creates a ReadableStream that decrypts response chunks.
49
76
  */
50
- decryptResponse(response: Response, serverEncapKey: Uint8Array): Promise<Response>;
77
+ private createDecryptStream;
51
78
  }
52
79
  //# sourceMappingURL=identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgD,MAAM,YAAY,CAAC;AAGvF;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAY;gBAElB,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS;IAM3E;;OAEG;WACU,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAsB1C;;OAEG;WACU,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBtD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAY/B;;OAEG;IACH,YAAY,IAAI,SAAS;IAIzB;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAOxC;;OAEG;IACH,aAAa,IAAI,SAAS;IAI1B;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC;IA0C1C;;OAEG;WACU,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;IAuDvE;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAgDpF;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;CAwFzF"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAIX,KAAK,aAAa,EAClB,KAAK,GAAG,EACT,MAAM,MAAM,CAAC;AAcd;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,UAAU,CAAC;CACxB;AAaD;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,UAAU,CAAM;gBAEZ,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG;IAM/D;;OAEG;WACU,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAO1C;;OAEG;WACU,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAWtD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAU/B;;OAEG;IACH,YAAY,IAAI,GAAG;IAInB;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAKxC;;OAEG;IACH,aAAa,IAAI,GAAG;IAIpB;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC;IA0C1C;;OAEG;WACU,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqDvE;;;;;;;;OAQG;IACG,yBAAyB,CAC7B,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;KAAE,CAAC;IAwDhE;;;;;;;;OAQG;IACG,0BAA0B,CAC9B,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,QAAQ,CAAC;IAmCpB;;OAEG;IACH,OAAO,CAAC,mBAAmB;CAyD5B"}
@@ -1,8 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Identity = void 0;
4
- const core_1 = require("@hpke/core");
4
+ const hpke_1 = require("hpke");
5
5
  const protocol_js_1 = require("./protocol.js");
6
+ const derive_js_1 = require("./derive.js");
7
+ /**
8
+ * Creates a new CipherSuite for X25519/HKDF-SHA256/AES-256-GCM
9
+ */
10
+ function createSuite() {
11
+ return new hpke_1.CipherSuite(hpke_1.KEM_DHKEM_X25519_HKDF_SHA256, hpke_1.KDF_HKDF_SHA256, hpke_1.AEAD_AES_256_GCM);
12
+ }
6
13
  /**
7
14
  * Identity class for managing HPKE key pairs and encryption/decryption
8
15
  */
@@ -19,48 +26,34 @@ class Identity {
19
26
  * Generate a new identity with X25519 key pair
20
27
  */
21
28
  static async generate() {
22
- const suite = new core_1.CipherSuite({
23
- kem: new core_1.DhkemX25519HkdfSha256(),
24
- kdf: new core_1.HkdfSha256(),
25
- aead: new core_1.Aes256Gcm()
26
- });
27
- const { publicKey, privateKey } = await suite.kem.generateKeyPair();
28
- // Make sure the public key is extractable for serialization
29
- const extractablePublicKey = await crypto.subtle.importKey('raw', await crypto.subtle.exportKey('raw', publicKey), { name: 'X25519' }, true, // extractable
30
- []);
31
- return new Identity(suite, extractablePublicKey, privateKey);
29
+ const suite = createSuite();
30
+ const { publicKey, privateKey } = await suite.GenerateKeyPair(true); // extractable
31
+ return new Identity(suite, publicKey, privateKey);
32
32
  }
33
33
  /**
34
34
  * Create identity from JSON string
35
35
  */
36
36
  static async fromJSON(json) {
37
37
  const data = JSON.parse(json);
38
- const suite = new core_1.CipherSuite({
39
- kem: new core_1.DhkemX25519HkdfSha256(),
40
- kdf: new core_1.HkdfSha256(),
41
- aead: new core_1.Aes256Gcm()
42
- });
43
- // Import public key
44
- const publicKey = await crypto.subtle.importKey('raw', new Uint8Array(data.publicKey), { name: 'X25519' }, true, // extractable
45
- []);
46
- // Deserialize private key using HPKE library
47
- const privateKey = await suite.kem.deserializePrivateKey(new Uint8Array(data.privateKey).buffer);
38
+ const suite = createSuite();
39
+ // Deserialize keys using the suite
40
+ const publicKey = await suite.DeserializePublicKey(new Uint8Array(data.publicKey));
41
+ const privateKey = await suite.DeserializePrivateKey(new Uint8Array(data.privateKey), true);
48
42
  return new Identity(suite, publicKey, privateKey);
49
43
  }
50
44
  /**
51
45
  * Convert identity to JSON string
52
46
  */
53
47
  async toJSON() {
54
- const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', this.publicKey));
55
- // For X25519, we need to use the HPKE library's serialization for private keys
56
- const privateKeyBytes = await this.suite.kem.serializePrivateKey(this.privateKey);
48
+ const publicKeyBytes = await this.suite.SerializePublicKey(this.publicKey);
49
+ const privateKeyBytes = await this.suite.SerializePrivateKey(this.privateKey);
57
50
  return JSON.stringify({
58
51
  publicKey: Array.from(publicKeyBytes),
59
- privateKey: Array.from(new Uint8Array(privateKeyBytes))
52
+ privateKey: Array.from(privateKeyBytes),
60
53
  });
61
54
  }
62
55
  /**
63
- * Get public key as CryptoKey
56
+ * Get public key
64
57
  */
65
58
  getPublicKey() {
66
59
  return this.publicKey;
@@ -69,13 +62,11 @@ class Identity {
69
62
  * Get public key as hex string
70
63
  */
71
64
  async getPublicKeyHex() {
72
- const exported = await crypto.subtle.exportKey('raw', this.publicKey);
73
- return Array.from(new Uint8Array(exported))
74
- .map(b => b.toString(16).padStart(2, '0'))
75
- .join('');
65
+ const exported = await this.suite.SerializePublicKey(this.publicKey);
66
+ return (0, derive_js_1.bytesToHex)(exported);
76
67
  }
77
68
  /**
78
- * Get private key as CryptoKey
69
+ * Get private key
79
70
  */
80
71
  getPrivateKey() {
81
72
  return this.privateKey;
@@ -89,7 +80,7 @@ class Identity {
89
80
  const kdfId = protocol_js_1.HPKE_CONFIG.KDF;
90
81
  const aeadId = protocol_js_1.HPKE_CONFIG.AEAD;
91
82
  // Export public key as raw bytes
92
- const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', this.publicKey));
83
+ const publicKeyBytes = await this.suite.SerializePublicKey(this.publicKey);
93
84
  // Key ID (1 byte) + KEM ID (2 bytes) + Public Key + Cipher Suites
94
85
  const keyId = 0;
95
86
  const publicKeySize = publicKeyBytes.length;
@@ -99,20 +90,20 @@ class Identity {
99
90
  // Key ID
100
91
  buffer[offset++] = keyId;
101
92
  // KEM ID
102
- buffer[offset++] = (kemId >> 8) & 0xFF;
103
- buffer[offset++] = kemId & 0xFF;
93
+ buffer[offset++] = (kemId >> 8) & 0xff;
94
+ buffer[offset++] = kemId & 0xff;
104
95
  // Public Key
105
96
  buffer.set(publicKeyBytes, offset);
106
97
  offset += publicKeySize;
107
98
  // Cipher Suites Length (2 bytes)
108
- buffer[offset++] = (cipherSuitesSize >> 8) & 0xFF;
109
- buffer[offset++] = cipherSuitesSize & 0xFF;
99
+ buffer[offset++] = (cipherSuitesSize >> 8) & 0xff;
100
+ buffer[offset++] = cipherSuitesSize & 0xff;
110
101
  // KDF ID
111
- buffer[offset++] = (kdfId >> 8) & 0xFF;
112
- buffer[offset++] = kdfId & 0xFF;
102
+ buffer[offset++] = (kdfId >> 8) & 0xff;
103
+ buffer[offset++] = kdfId & 0xff;
113
104
  // AEAD ID
114
- buffer[offset++] = (aeadId >> 8) & 0xFF;
115
- buffer[offset++] = aeadId & 0xFF;
105
+ buffer[offset++] = (aeadId >> 8) & 0xff;
106
+ buffer[offset++] = aeadId & 0xff;
116
107
  return buffer;
117
108
  }
118
109
  /**
@@ -147,140 +138,155 @@ class Identity {
147
138
  if (firstSuite.kdfId !== protocol_js_1.HPKE_CONFIG.KDF || firstSuite.aeadId !== protocol_js_1.HPKE_CONFIG.AEAD) {
148
139
  throw new Error(`Unsupported cipher suite: KDF=0x${firstSuite.kdfId.toString(16)}, AEAD=0x${firstSuite.aeadId.toString(16)}`);
149
140
  }
150
- // Create cipher suite (currently only supports X25519/HKDF-SHA256/AES-256-GCM)
151
- const suite = new core_1.CipherSuite({
152
- kem: new core_1.DhkemX25519HkdfSha256(),
153
- kdf: new core_1.HkdfSha256(),
154
- aead: new core_1.Aes256Gcm()
155
- });
156
- // Import public key using HPKE library
157
- const publicKey = await suite.kem.deserializePublicKey(publicKeyBytes.buffer);
141
+ // Create cipher suite
142
+ const suite = createSuite();
143
+ // Import public key
144
+ const publicKey = await suite.DeserializePublicKey(publicKeyBytes);
158
145
  // For server config, we only have the public key, no private key
159
146
  // We'll create a dummy private key that won't be used
160
- const dummyPrivateKey = await suite.kem.deserializePrivateKey(new Uint8Array(32).buffer);
147
+ const dummyPrivateKey = await suite.DeserializePrivateKey(new Uint8Array(32), false);
161
148
  return new Identity(suite, publicKey, dummyPrivateKey);
162
149
  }
163
150
  /**
164
- * Encrypt request body and set appropriate headers
151
+ * Encrypt request body and return context for response decryption.
152
+ *
153
+ * This method is called on the SERVER's identity (public key only).
154
+ * It:
155
+ * 1. Creates an HPKE sender context to this identity's public key
156
+ * 2. Encrypts the request body
157
+ * 3. Returns a RequestContext that must be used to decrypt the response
165
158
  */
166
- async encryptRequest(request, serverPublicKey) {
159
+ async encryptRequestWithContext(request) {
167
160
  const body = await request.arrayBuffer();
161
+ // Bodyless requests pass through unmodified - no HPKE context needed.
162
+ // See SPEC.md Section 5.1: "When the request has no payload body, an encrypted
163
+ // response is not possible (since there is no HPKE context to derive response
164
+ // keys from). Such requests pass through unmodified."
168
165
  if (body.byteLength === 0) {
169
- // No body to encrypt, just set client public key header
170
- const headers = new Headers(request.headers);
171
- headers.set(protocol_js_1.PROTOCOL.CLIENT_PUBLIC_KEY_HEADER, await this.getPublicKeyHex());
172
- return new Request(request.url, {
173
- method: request.method,
174
- headers,
175
- body: null
176
- });
166
+ return {
167
+ request: new Request(request.url, {
168
+ method: request.method,
169
+ headers: request.headers,
170
+ body: null,
171
+ }),
172
+ context: null,
173
+ };
177
174
  }
178
- // Create sender for encryption
179
- const sender = await this.suite.createSenderContext({
180
- recipientPublicKey: serverPublicKey
175
+ // Create sender context for encryption with info parameter for domain separation
176
+ const infoBytes = new TextEncoder().encode(derive_js_1.HPKE_REQUEST_INFO);
177
+ const { encapsulatedSecret, ctx } = await this.suite.SetupSender(this.publicKey, {
178
+ info: infoBytes,
181
179
  });
180
+ // Store context for response decryption
181
+ const context = {
182
+ senderContext: ctx,
183
+ requestEnc: encapsulatedSecret,
184
+ };
185
+ // Set headers - only encapsulated key for requests with body
186
+ const headers = new Headers(request.headers);
187
+ headers.set(protocol_js_1.PROTOCOL.ENCAPSULATED_KEY_HEADER, (0, derive_js_1.bytesToHex)(context.requestEnc));
182
188
  // Encrypt the body
183
- const encrypted = await sender.seal(body);
184
- // Get encapsulated key
185
- const encapKey = sender.enc;
189
+ const encrypted = await ctx.Seal(new Uint8Array(body));
186
190
  // Create chunked format: 4-byte length header + encrypted data
187
191
  const chunkLength = new Uint8Array(4);
188
- const view = new DataView(chunkLength.buffer);
189
- view.setUint32(0, encrypted.byteLength, false); // Big-endian
192
+ new DataView(chunkLength.buffer).setUint32(0, encrypted.byteLength, false);
190
193
  const chunkedData = new Uint8Array(4 + encrypted.byteLength);
191
194
  chunkedData.set(chunkLength, 0);
192
- chunkedData.set(new Uint8Array(encrypted), 4);
193
- // Create new request with encrypted body and headers
194
- const headers = new Headers(request.headers);
195
- headers.set(protocol_js_1.PROTOCOL.CLIENT_PUBLIC_KEY_HEADER, await this.getPublicKeyHex());
196
- headers.set(protocol_js_1.PROTOCOL.ENCAPSULATED_KEY_HEADER, Array.from(new Uint8Array(encapKey))
197
- .map(b => b.toString(16).padStart(2, '0'))
198
- .join(''));
199
- return new Request(request.url, {
200
- method: request.method,
201
- headers,
202
- body: chunkedData,
203
- duplex: 'half'
204
- });
195
+ chunkedData.set(encrypted, 4);
196
+ return {
197
+ request: new Request(request.url, {
198
+ method: request.method,
199
+ headers,
200
+ body: chunkedData,
201
+ duplex: 'half',
202
+ }),
203
+ context,
204
+ };
205
205
  }
206
206
  /**
207
- * Decrypt response body
207
+ * Decrypt response using keys derived from request context.
208
+ *
209
+ * This method:
210
+ * 1. Reads the response nonce from Ehbp-Response-Nonce header
211
+ * 2. Exports a secret from the HPKE sender context
212
+ * 3. Derives response keys using HKDF
213
+ * 4. Decrypts the response body
208
214
  */
209
- async decryptResponse(response, serverEncapKey) {
215
+ async decryptResponseWithContext(response, context) {
210
216
  if (!response.body) {
211
217
  return response;
212
218
  }
213
- // Create receiver for decryption
214
- const receiver = await this.suite.createRecipientContext({
215
- recipientKey: this.privateKey,
216
- enc: serverEncapKey.buffer
219
+ // Get response nonce from header
220
+ const responseNonceHex = response.headers.get(protocol_js_1.PROTOCOL.RESPONSE_NONCE_HEADER);
221
+ if (!responseNonceHex) {
222
+ throw new Error(`Missing ${protocol_js_1.PROTOCOL.RESPONSE_NONCE_HEADER} header`);
223
+ }
224
+ const responseNonce = (0, derive_js_1.hexToBytes)(responseNonceHex);
225
+ if (responseNonce.length !== derive_js_1.RESPONSE_NONCE_LENGTH) {
226
+ throw new Error(`Invalid response nonce length: expected ${derive_js_1.RESPONSE_NONCE_LENGTH}, got ${responseNonce.length}`);
227
+ }
228
+ // Export secret from request context
229
+ const exportLabelBytes = new TextEncoder().encode(derive_js_1.EXPORT_LABEL);
230
+ const exportedSecret = await context.senderContext.Export(exportLabelBytes, derive_js_1.EXPORT_LENGTH);
231
+ // Derive response keys
232
+ const km = await (0, derive_js_1.deriveResponseKeys)(exportedSecret, context.requestEnc, responseNonce);
233
+ // Create decrypting stream
234
+ const decryptedStream = this.createDecryptStream(response.body, km);
235
+ return new Response(decryptedStream, {
236
+ status: response.status,
237
+ statusText: response.statusText,
238
+ headers: response.headers,
217
239
  });
218
- // Create a readable stream that decrypts chunks as they arrive
219
- const decryptedStream = new ReadableStream({
220
- start(controller) {
221
- const reader = response.body.getReader();
222
- let buffer = new Uint8Array(0);
223
- let offset = 0;
224
- async function pump() {
225
- try {
226
- while (true) {
227
- const { done, value } = await reader.read();
228
- if (done)
229
- break;
230
- // Append new data to buffer
231
- const newBuffer = new Uint8Array(buffer.length + value.length);
232
- newBuffer.set(buffer);
233
- newBuffer.set(value, buffer.length);
234
- buffer = newBuffer;
235
- // Process complete chunks
236
- while (offset + 4 <= buffer.length) {
237
- // Read chunk length (4 bytes big-endian)
238
- const chunkLength = (buffer[offset] << 24) |
239
- (buffer[offset + 1] << 16) |
240
- (buffer[offset + 2] << 8) |
241
- buffer[offset + 3];
242
- offset += 4;
243
- if (chunkLength === 0) {
244
- continue; // Empty chunk
245
- }
246
- // Check if we have the complete chunk
247
- if (offset + chunkLength > buffer.length) {
248
- // Not enough data yet, rewind offset and wait for more
249
- offset -= 4;
250
- break;
251
- }
252
- // Extract and decrypt the chunk
253
- const encryptedChunk = buffer.slice(offset, offset + chunkLength);
254
- offset += chunkLength;
255
- try {
256
- const decryptedChunk = await receiver.open(encryptedChunk.buffer);
257
- controller.enqueue(new Uint8Array(decryptedChunk));
258
- }
259
- catch (error) {
260
- controller.error(new Error(`Failed to decrypt chunk: ${error}`));
261
- return;
262
- }
240
+ }
241
+ /**
242
+ * Creates a ReadableStream that decrypts response chunks.
243
+ */
244
+ createDecryptStream(body, km) {
245
+ let buffer = new Uint8Array(0);
246
+ let seq = 0;
247
+ const reader = body.getReader();
248
+ return new ReadableStream({
249
+ async pull(controller) {
250
+ while (true) {
251
+ // Try to read a complete chunk from buffer
252
+ if (buffer.length >= 4) {
253
+ const chunkLength = (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];
254
+ if (chunkLength === 0) {
255
+ // Skip empty chunk
256
+ buffer = buffer.slice(4);
257
+ continue;
258
+ }
259
+ if (buffer.length >= 4 + chunkLength) {
260
+ const ciphertext = buffer.slice(4, 4 + chunkLength);
261
+ buffer = buffer.slice(4 + chunkLength);
262
+ try {
263
+ const plaintext = await (0, derive_js_1.decryptChunk)(km, seq++, ciphertext);
264
+ controller.enqueue(plaintext);
265
+ return;
263
266
  }
264
- // Remove processed data from buffer
265
- if (offset > 0) {
266
- buffer = buffer.slice(offset);
267
- offset = 0;
267
+ catch (error) {
268
+ controller.error(new Error(`Decryption failed at chunk ${seq - 1}: ${error}`));
269
+ return;
268
270
  }
269
271
  }
270
- controller.close();
271
272
  }
272
- catch (error) {
273
- controller.error(error);
273
+ // Need more data
274
+ const { done, value } = await reader.read();
275
+ if (done) {
276
+ controller.close();
277
+ return;
274
278
  }
279
+ // Append to buffer
280
+ const newBuffer = new Uint8Array(buffer.length + value.length);
281
+ newBuffer.set(buffer);
282
+ newBuffer.set(value, buffer.length);
283
+ buffer = newBuffer;
275
284
  }
276
- pump();
277
- }
278
- });
279
- // Create new response with decrypted stream
280
- return new Response(decryptedStream, {
281
- status: response.status,
282
- statusText: response.statusText,
283
- headers: response.headers
285
+ },
286
+ cancel(reason) {
287
+ // Release the underlying reader when the stream is cancelled
288
+ return reader.cancel(reason);
289
+ },
284
290
  });
285
291
  }
286
292
  }