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.
- package/README.md +84 -138
- package/dist/cjs/client.d.ts +13 -13
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +32 -52
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/derive.d.ts +63 -0
- package/dist/cjs/derive.d.ts.map +1 -0
- package/dist/cjs/derive.js +136 -0
- package/dist/cjs/derive.js.map +1 -0
- package/dist/cjs/identity.d.ts +37 -10
- package/dist/cjs/identity.d.ts.map +1 -1
- package/dist/cjs/identity.js +152 -146
- package/dist/cjs/identity.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +15 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/protocol.d.ts +1 -1
- package/dist/cjs/protocol.js +2 -2
- package/dist/cjs/protocol.js.map +1 -1
- package/dist/esm/client.d.ts +13 -13
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +32 -52
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/derive.d.ts +63 -0
- package/dist/esm/derive.d.ts.map +1 -0
- package/dist/esm/derive.js +127 -0
- package/dist/esm/derive.js.map +1 -0
- package/dist/esm/identity.d.ts +37 -10
- package/dist/esm/identity.d.ts.map +1 -1
- package/dist/esm/identity.js +152 -146
- package/dist/esm/identity.js.map +1 -1
- package/dist/esm/index.d.ts +4 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/protocol.d.ts +1 -1
- package/dist/esm/protocol.js +2 -2
- package/dist/esm/protocol.js.map +1 -1
- package/dist/esm/test/client.test.js +15 -16
- package/dist/esm/test/client.test.js.map +1 -1
- package/dist/esm/test/derive.test.d.ts +2 -0
- package/dist/esm/test/derive.test.d.ts.map +1 -0
- package/dist/esm/test/derive.test.js +164 -0
- package/dist/esm/test/derive.test.js.map +1 -0
- package/dist/esm/test/security.test.d.ts +10 -0
- package/dist/esm/test/security.test.d.ts.map +1 -0
- package/dist/esm/test/security.test.js +153 -0
- package/dist/esm/test/security.test.js.map +1 -0
- package/dist/esm/test/streaming.integration.d.ts +9 -0
- package/dist/esm/test/streaming.integration.d.ts.map +1 -0
- package/dist/esm/test/streaming.integration.js +190 -0
- package/dist/esm/test/streaming.integration.js.map +1 -0
- package/package.json +6 -7
- package/dist/esm/example.d.ts +0 -6
- package/dist/esm/example.d.ts.map +0 -1
- package/dist/esm/example.js +0 -115
- package/dist/esm/example.js.map +0 -1
- package/dist/esm/streaming-test.d.ts +0 -3
- package/dist/esm/streaming-test.d.ts.map +0 -1
- package/dist/esm/streaming-test.js +0 -102
- 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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"}
|
package/dist/cjs/identity.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { CipherSuite } from '
|
|
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:
|
|
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
|
|
31
|
+
* Get public key
|
|
24
32
|
*/
|
|
25
|
-
getPublicKey():
|
|
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
|
|
39
|
+
* Get private key
|
|
32
40
|
*/
|
|
33
|
-
getPrivateKey():
|
|
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
|
|
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
|
-
|
|
73
|
+
decryptResponseWithContext(response: Response, context: RequestContext): Promise<Response>;
|
|
47
74
|
/**
|
|
48
|
-
*
|
|
75
|
+
* Creates a ReadableStream that decrypts response chunks.
|
|
49
76
|
*/
|
|
50
|
-
|
|
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,
|
|
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"}
|
package/dist/cjs/identity.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Identity = void 0;
|
|
4
|
-
const
|
|
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 =
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 =
|
|
55
|
-
|
|
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(
|
|
52
|
+
privateKey: Array.from(privateKeyBytes),
|
|
60
53
|
});
|
|
61
54
|
}
|
|
62
55
|
/**
|
|
63
|
-
* Get public key
|
|
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
|
|
73
|
-
return
|
|
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
|
|
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 =
|
|
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) &
|
|
103
|
-
buffer[offset++] = kemId &
|
|
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) &
|
|
109
|
-
buffer[offset++] = cipherSuitesSize &
|
|
99
|
+
buffer[offset++] = (cipherSuitesSize >> 8) & 0xff;
|
|
100
|
+
buffer[offset++] = cipherSuitesSize & 0xff;
|
|
110
101
|
// KDF ID
|
|
111
|
-
buffer[offset++] = (kdfId >> 8) &
|
|
112
|
-
buffer[offset++] = kdfId &
|
|
102
|
+
buffer[offset++] = (kdfId >> 8) & 0xff;
|
|
103
|
+
buffer[offset++] = kdfId & 0xff;
|
|
113
104
|
// AEAD ID
|
|
114
|
-
buffer[offset++] = (aeadId >> 8) &
|
|
115
|
-
buffer[offset++] = aeadId &
|
|
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
|
|
151
|
-
const suite =
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
180
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
215
|
+
async decryptResponseWithContext(response, context) {
|
|
210
216
|
if (!response.body) {
|
|
211
217
|
return response;
|
|
212
218
|
}
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
buffer =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
}
|