ehbp 0.0.3 → 0.0.5
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/dist/cjs/client.d.ts +51 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +160 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/identity.d.ts +52 -0
- package/dist/cjs/identity.d.ts.map +1 -0
- package/dist/cjs/identity.js +275 -0
- package/dist/cjs/identity.js.map +1 -0
- package/{src/index.ts → dist/cjs/index.d.ts} +2 -4
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/protocol.d.ts +19 -0
- package/dist/cjs/protocol.d.ts.map +1 -0
- package/dist/cjs/protocol.js +22 -0
- package/dist/cjs/protocol.js.map +1 -0
- package/dist/esm/client.d.ts +51 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +155 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/example.d.ts +6 -0
- package/dist/esm/example.d.ts.map +1 -0
- package/dist/esm/example.js +115 -0
- package/dist/esm/example.js.map +1 -0
- package/dist/esm/identity.d.ts +52 -0
- package/dist/esm/identity.d.ts.map +1 -0
- package/dist/esm/identity.js +271 -0
- package/dist/esm/identity.js.map +1 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/protocol.d.ts +19 -0
- package/dist/esm/protocol.d.ts.map +1 -0
- package/dist/esm/protocol.js +19 -0
- package/dist/esm/protocol.js.map +1 -0
- package/dist/esm/streaming-test.d.ts +3 -0
- package/dist/esm/streaming-test.d.ts.map +1 -0
- package/dist/esm/streaming-test.js +102 -0
- package/dist/esm/streaming-test.js.map +1 -0
- package/dist/esm/test/client.test.d.ts +2 -0
- package/dist/esm/test/client.test.d.ts.map +1 -0
- package/dist/esm/test/client.test.js +71 -0
- package/dist/esm/test/client.test.js.map +1 -0
- package/dist/esm/test/identity.test.d.ts +2 -0
- package/dist/esm/test/identity.test.d.ts.map +1 -0
- package/dist/esm/test/identity.test.js +39 -0
- package/dist/esm/test/identity.test.js.map +1 -0
- package/dist/esm/test/streaming.test.d.ts +2 -0
- package/dist/esm/test/streaming.test.d.ts.map +1 -0
- package/dist/esm/test/streaming.test.js +71 -0
- package/dist/esm/test/streaming.test.js.map +1 -0
- package/package.json +7 -2
- package/build-browser.js +0 -54
- package/chat.html +0 -285
- package/src/client.ts +0 -181
- package/src/example.ts +0 -126
- package/src/identity.ts +0 -339
- package/src/protocol.ts +0 -19
- package/src/streaming-test.ts +0 -118
- package/src/test/client.test.ts +0 -93
- package/src/test/identity.test.ts +0 -46
- package/src/test/streaming.test.ts +0 -85
- package/test.html +0 -271
- package/tsconfig.cjs.json +0 -8
- package/tsconfig.esm.json +0 -7
- package/tsconfig.json +0 -19
package/src/example.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Example usage of the EHBP JavaScript client
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Identity, createTransport } from './index.js';
|
|
8
|
-
|
|
9
|
-
async function main() {
|
|
10
|
-
console.log('EHBP JavaScript Client Example');
|
|
11
|
-
console.log('==============================');
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
// Create client identity
|
|
15
|
-
console.log('Creating client identity...');
|
|
16
|
-
const clientIdentity = await Identity.generate();
|
|
17
|
-
console.log('Client public key:', await clientIdentity.getPublicKeyHex());
|
|
18
|
-
|
|
19
|
-
// Create transport (this will fetch server public key)
|
|
20
|
-
console.log('Creating transport...');
|
|
21
|
-
const serverURL = 'http://localhost:8080'; // Adjust as needed
|
|
22
|
-
const transport = await createTransport(serverURL, clientIdentity);
|
|
23
|
-
console.log('Transport created successfully');
|
|
24
|
-
|
|
25
|
-
// Example 1: GET request to secure endpoint
|
|
26
|
-
console.log('\n--- GET Request ---');
|
|
27
|
-
try {
|
|
28
|
-
const getResponse = await transport.get(`${serverURL}/secure`);
|
|
29
|
-
console.log('GET Response status:', getResponse.status);
|
|
30
|
-
if (getResponse.ok) {
|
|
31
|
-
const getData = await getResponse.text();
|
|
32
|
-
console.log('GET Response:', getData);
|
|
33
|
-
} else {
|
|
34
|
-
console.log('GET Request failed with status:', getResponse.status);
|
|
35
|
-
}
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.log('GET Request failed:', error instanceof Error ? error.message : String(error));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Example 2: POST request with JSON data
|
|
41
|
-
console.log('\n--- POST Request ---');
|
|
42
|
-
try {
|
|
43
|
-
const postData = { message: 'Hello from JavaScript client!', timestamp: new Date().toISOString() };
|
|
44
|
-
const postResponse = await transport.post(
|
|
45
|
-
`${serverURL}/secure`,
|
|
46
|
-
JSON.stringify(postData),
|
|
47
|
-
{ headers: { 'Content-Type': 'application/json' } }
|
|
48
|
-
);
|
|
49
|
-
console.log('POST Response status:', postResponse.status);
|
|
50
|
-
if (postResponse.ok) {
|
|
51
|
-
const responseData = await postResponse.text();
|
|
52
|
-
console.log('POST Response:', responseData);
|
|
53
|
-
} else {
|
|
54
|
-
console.log('POST Request failed with status:', postResponse.status);
|
|
55
|
-
}
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.log('POST Request failed:', error instanceof Error ? error.message : String(error));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Example 3: PUT request
|
|
61
|
-
console.log('\n--- PUT Request ---');
|
|
62
|
-
try {
|
|
63
|
-
const putData = { id: 1, name: 'Updated Item' };
|
|
64
|
-
const putResponse = await transport.put(
|
|
65
|
-
`${serverURL}/secure`,
|
|
66
|
-
JSON.stringify(putData),
|
|
67
|
-
{ headers: { 'Content-Type': 'application/json' } }
|
|
68
|
-
);
|
|
69
|
-
console.log('PUT Response status:', putResponse.status);
|
|
70
|
-
if (putResponse.ok) {
|
|
71
|
-
const putResponseData = await putResponse.text();
|
|
72
|
-
console.log('PUT Response:', putResponseData);
|
|
73
|
-
} else {
|
|
74
|
-
console.log('PUT Request failed with status:', putResponse.status);
|
|
75
|
-
}
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.log('PUT Request failed:', error instanceof Error ? error.message : String(error));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Example 4: Streaming request
|
|
81
|
-
console.log('\n--- Streaming Request ---');
|
|
82
|
-
try {
|
|
83
|
-
const streamResponse = await transport.get(`${serverURL}/stream`);
|
|
84
|
-
console.log('Stream Response status:', streamResponse.status);
|
|
85
|
-
if (streamResponse.ok) {
|
|
86
|
-
console.log('Streaming response (should show numbers 1-20):');
|
|
87
|
-
const reader = streamResponse.body?.getReader();
|
|
88
|
-
if (reader) {
|
|
89
|
-
const decoder = new TextDecoder();
|
|
90
|
-
let chunkCount = 0;
|
|
91
|
-
|
|
92
|
-
while (true) {
|
|
93
|
-
const { done, value } = await reader.read();
|
|
94
|
-
if (done) break;
|
|
95
|
-
|
|
96
|
-
const text = decoder.decode(value, { stream: true });
|
|
97
|
-
process.stdout.write(text);
|
|
98
|
-
chunkCount++;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
console.log(`\nStream completed with ${chunkCount} chunks`);
|
|
102
|
-
} else {
|
|
103
|
-
console.log('No readable stream available');
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
console.log('Stream Request failed with status:', streamResponse.status);
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.log('Stream Request failed:', error instanceof Error ? error.message : String(error));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
console.log('\nExample completed successfully!');
|
|
113
|
-
console.log('\nTo test with a real server:');
|
|
114
|
-
console.log('1. Start the Go server: go run pkg/server/main.go');
|
|
115
|
-
console.log('2. Run this example: npm run example');
|
|
116
|
-
|
|
117
|
-
} catch (error) {
|
|
118
|
-
console.error('Error:', error);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Run the example
|
|
124
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
125
|
-
main().catch(console.error);
|
|
126
|
-
}
|
package/src/identity.ts
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256, Aes256Gcm } from '@hpke/core';
|
|
2
|
-
import { PROTOCOL, HPKE_CONFIG } from './protocol.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Identity class for managing HPKE key pairs and encryption/decryption
|
|
6
|
-
*/
|
|
7
|
-
export class Identity {
|
|
8
|
-
private suite: CipherSuite;
|
|
9
|
-
private publicKey: CryptoKey;
|
|
10
|
-
private privateKey: CryptoKey;
|
|
11
|
-
|
|
12
|
-
constructor(suite: CipherSuite, publicKey: CryptoKey, privateKey: CryptoKey) {
|
|
13
|
-
this.suite = suite;
|
|
14
|
-
this.publicKey = publicKey;
|
|
15
|
-
this.privateKey = privateKey;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Generate a new identity with X25519 key pair
|
|
20
|
-
*/
|
|
21
|
-
static async generate(): Promise<Identity> {
|
|
22
|
-
const suite = new CipherSuite({
|
|
23
|
-
kem: new DhkemX25519HkdfSha256(),
|
|
24
|
-
kdf: new HkdfSha256(),
|
|
25
|
-
aead: new Aes256Gcm()
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const { publicKey, privateKey } = await suite.kem.generateKeyPair();
|
|
29
|
-
|
|
30
|
-
// Make sure the public key is extractable for serialization
|
|
31
|
-
const extractablePublicKey = await crypto.subtle.importKey(
|
|
32
|
-
'raw',
|
|
33
|
-
await crypto.subtle.exportKey('raw', publicKey),
|
|
34
|
-
{ name: 'X25519' },
|
|
35
|
-
true, // extractable
|
|
36
|
-
[]
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
return new Identity(suite, extractablePublicKey, privateKey);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Create identity from JSON string
|
|
45
|
-
*/
|
|
46
|
-
static async fromJSON(json: string): Promise<Identity> {
|
|
47
|
-
const data = JSON.parse(json);
|
|
48
|
-
const suite = new CipherSuite({
|
|
49
|
-
kem: new DhkemX25519HkdfSha256(),
|
|
50
|
-
kdf: new HkdfSha256(),
|
|
51
|
-
aead: new Aes256Gcm()
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Import public key
|
|
55
|
-
const publicKey = await crypto.subtle.importKey(
|
|
56
|
-
'raw',
|
|
57
|
-
new Uint8Array(data.publicKey),
|
|
58
|
-
{ name: 'X25519' },
|
|
59
|
-
true, // extractable
|
|
60
|
-
[]
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
// Deserialize private key using HPKE library
|
|
64
|
-
const privateKey = await suite.kem.deserializePrivateKey(new Uint8Array(data.privateKey).buffer);
|
|
65
|
-
|
|
66
|
-
return new Identity(suite, publicKey, privateKey);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Convert identity to JSON string
|
|
72
|
-
*/
|
|
73
|
-
async toJSON(): Promise<string> {
|
|
74
|
-
const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', this.publicKey));
|
|
75
|
-
|
|
76
|
-
// For X25519, we need to use the HPKE library's serialization for private keys
|
|
77
|
-
const privateKeyBytes = await this.suite.kem.serializePrivateKey(this.privateKey);
|
|
78
|
-
|
|
79
|
-
return JSON.stringify({
|
|
80
|
-
publicKey: Array.from(publicKeyBytes),
|
|
81
|
-
privateKey: Array.from(new Uint8Array(privateKeyBytes))
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Get public key as CryptoKey
|
|
87
|
-
*/
|
|
88
|
-
getPublicKey(): CryptoKey {
|
|
89
|
-
return this.publicKey;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get public key as hex string
|
|
94
|
-
*/
|
|
95
|
-
async getPublicKeyHex(): Promise<string> {
|
|
96
|
-
const exported = await crypto.subtle.exportKey('raw', this.publicKey);
|
|
97
|
-
return Array.from(new Uint8Array(exported))
|
|
98
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
99
|
-
.join('');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Get private key as CryptoKey
|
|
104
|
-
*/
|
|
105
|
-
getPrivateKey(): CryptoKey {
|
|
106
|
-
return this.privateKey;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Marshal public key configuration for server key distribution
|
|
111
|
-
* Implements RFC 9458 format
|
|
112
|
-
*/
|
|
113
|
-
async marshalConfig(): Promise<Uint8Array> {
|
|
114
|
-
const kemId = HPKE_CONFIG.KEM;
|
|
115
|
-
const kdfId = HPKE_CONFIG.KDF;
|
|
116
|
-
const aeadId = HPKE_CONFIG.AEAD;
|
|
117
|
-
|
|
118
|
-
// Export public key as raw bytes
|
|
119
|
-
const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', this.publicKey));
|
|
120
|
-
|
|
121
|
-
// Key ID (1 byte) + KEM ID (2 bytes) + Public Key + Cipher Suites
|
|
122
|
-
const keyId = 0;
|
|
123
|
-
const publicKeySize = publicKeyBytes.length;
|
|
124
|
-
const cipherSuitesSize = 2 + 2; // KDF ID + AEAD ID
|
|
125
|
-
|
|
126
|
-
const buffer = new Uint8Array(1 + 2 + publicKeySize + 2 + cipherSuitesSize);
|
|
127
|
-
let offset = 0;
|
|
128
|
-
|
|
129
|
-
// Key ID
|
|
130
|
-
buffer[offset++] = keyId;
|
|
131
|
-
|
|
132
|
-
// KEM ID
|
|
133
|
-
buffer[offset++] = (kemId >> 8) & 0xFF;
|
|
134
|
-
buffer[offset++] = kemId & 0xFF;
|
|
135
|
-
|
|
136
|
-
// Public Key
|
|
137
|
-
buffer.set(publicKeyBytes, offset);
|
|
138
|
-
offset += publicKeySize;
|
|
139
|
-
|
|
140
|
-
// Cipher Suites Length (2 bytes)
|
|
141
|
-
buffer[offset++] = (cipherSuitesSize >> 8) & 0xFF;
|
|
142
|
-
buffer[offset++] = cipherSuitesSize & 0xFF;
|
|
143
|
-
|
|
144
|
-
// KDF ID
|
|
145
|
-
buffer[offset++] = (kdfId >> 8) & 0xFF;
|
|
146
|
-
buffer[offset++] = kdfId & 0xFF;
|
|
147
|
-
|
|
148
|
-
// AEAD ID
|
|
149
|
-
buffer[offset++] = (aeadId >> 8) & 0xFF;
|
|
150
|
-
buffer[offset++] = aeadId & 0xFF;
|
|
151
|
-
|
|
152
|
-
return buffer;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Unmarshal public configuration from server
|
|
157
|
-
*/
|
|
158
|
-
static async unmarshalPublicConfig(data: Uint8Array): Promise<Identity> {
|
|
159
|
-
let offset = 0;
|
|
160
|
-
|
|
161
|
-
// Read Key ID
|
|
162
|
-
const keyId = data[offset++];
|
|
163
|
-
|
|
164
|
-
// Read KEM ID
|
|
165
|
-
const kemId = (data[offset++] << 8) | data[offset++];
|
|
166
|
-
|
|
167
|
-
// Read Public Key (32 bytes for X25519)
|
|
168
|
-
const publicKeySize = 32;
|
|
169
|
-
const publicKeyBytes = data.slice(offset, offset + publicKeySize);
|
|
170
|
-
offset += publicKeySize;
|
|
171
|
-
|
|
172
|
-
// Read Cipher Suites Length
|
|
173
|
-
const cipherSuitesLength = (data[offset++] << 8) | data[offset++];
|
|
174
|
-
|
|
175
|
-
// Read KDF ID
|
|
176
|
-
const kdfId = (data[offset++] << 8) | data[offset++];
|
|
177
|
-
|
|
178
|
-
// Read AEAD ID
|
|
179
|
-
const aeadId = (data[offset++] << 8) | data[offset++];
|
|
180
|
-
|
|
181
|
-
// Create suite (assuming X25519 for now)
|
|
182
|
-
const suite = new CipherSuite({
|
|
183
|
-
kem: new DhkemX25519HkdfSha256(),
|
|
184
|
-
kdf: new HkdfSha256(),
|
|
185
|
-
aead: new Aes256Gcm()
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// Import public key using HPKE library
|
|
189
|
-
const publicKey = await suite.kem.deserializePublicKey(publicKeyBytes.buffer);
|
|
190
|
-
|
|
191
|
-
// For server config, we only have the public key, no private key
|
|
192
|
-
// We'll create a dummy private key that won't be used
|
|
193
|
-
const dummyPrivateKey = await suite.kem.deserializePrivateKey(new Uint8Array(32).buffer);
|
|
194
|
-
|
|
195
|
-
return new Identity(suite, publicKey, dummyPrivateKey);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Encrypt request body and set appropriate headers
|
|
200
|
-
*/
|
|
201
|
-
async encryptRequest(request: Request, serverPublicKey: CryptoKey): Promise<Request> {
|
|
202
|
-
const body = await request.arrayBuffer();
|
|
203
|
-
if (body.byteLength === 0) {
|
|
204
|
-
// No body to encrypt, just set client public key header
|
|
205
|
-
const headers = new Headers(request.headers);
|
|
206
|
-
headers.set(PROTOCOL.CLIENT_PUBLIC_KEY_HEADER, await this.getPublicKeyHex());
|
|
207
|
-
return new Request(request.url, {
|
|
208
|
-
method: request.method,
|
|
209
|
-
headers,
|
|
210
|
-
body: null
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Create sender for encryption
|
|
215
|
-
const sender = await this.suite.createSenderContext({
|
|
216
|
-
recipientPublicKey: serverPublicKey
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Encrypt the body
|
|
220
|
-
const encrypted = await sender.seal(body);
|
|
221
|
-
|
|
222
|
-
// Get encapsulated key
|
|
223
|
-
const encapKey = sender.enc;
|
|
224
|
-
|
|
225
|
-
// Create chunked format: 4-byte length header + encrypted data
|
|
226
|
-
const chunkLength = new Uint8Array(4);
|
|
227
|
-
const view = new DataView(chunkLength.buffer);
|
|
228
|
-
view.setUint32(0, encrypted.byteLength, false); // Big-endian
|
|
229
|
-
|
|
230
|
-
const chunkedData = new Uint8Array(4 + encrypted.byteLength);
|
|
231
|
-
chunkedData.set(chunkLength, 0);
|
|
232
|
-
chunkedData.set(new Uint8Array(encrypted), 4);
|
|
233
|
-
|
|
234
|
-
// Create new request with encrypted body and headers
|
|
235
|
-
const headers = new Headers(request.headers);
|
|
236
|
-
headers.set(PROTOCOL.CLIENT_PUBLIC_KEY_HEADER, await this.getPublicKeyHex());
|
|
237
|
-
headers.set(PROTOCOL.ENCAPSULATED_KEY_HEADER, Array.from(new Uint8Array(encapKey))
|
|
238
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
239
|
-
.join(''));
|
|
240
|
-
|
|
241
|
-
return new Request(request.url, {
|
|
242
|
-
method: request.method,
|
|
243
|
-
headers,
|
|
244
|
-
body: chunkedData,
|
|
245
|
-
duplex: 'half'
|
|
246
|
-
} as RequestInit);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Decrypt response body
|
|
251
|
-
*/
|
|
252
|
-
async decryptResponse(response: Response, serverEncapKey: Uint8Array): Promise<Response> {
|
|
253
|
-
if (!response.body) {
|
|
254
|
-
return response;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Create receiver for decryption
|
|
258
|
-
const receiver = await this.suite.createRecipientContext({
|
|
259
|
-
recipientKey: this.privateKey,
|
|
260
|
-
enc: serverEncapKey.buffer as ArrayBuffer
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Create a readable stream that decrypts chunks as they arrive
|
|
264
|
-
const decryptedStream = new ReadableStream({
|
|
265
|
-
start(controller) {
|
|
266
|
-
const reader = response.body!.getReader();
|
|
267
|
-
let buffer = new Uint8Array(0);
|
|
268
|
-
let offset = 0;
|
|
269
|
-
|
|
270
|
-
async function pump() {
|
|
271
|
-
try {
|
|
272
|
-
while (true) {
|
|
273
|
-
const { done, value } = await reader.read();
|
|
274
|
-
if (done) break;
|
|
275
|
-
|
|
276
|
-
// Append new data to buffer
|
|
277
|
-
const newBuffer = new Uint8Array(buffer.length + value.length);
|
|
278
|
-
newBuffer.set(buffer);
|
|
279
|
-
newBuffer.set(value, buffer.length);
|
|
280
|
-
buffer = newBuffer;
|
|
281
|
-
|
|
282
|
-
// Process complete chunks
|
|
283
|
-
while (offset + 4 <= buffer.length) {
|
|
284
|
-
// Read chunk length (4 bytes big-endian)
|
|
285
|
-
const chunkLength = (buffer[offset] << 24) |
|
|
286
|
-
(buffer[offset + 1] << 16) |
|
|
287
|
-
(buffer[offset + 2] << 8) |
|
|
288
|
-
buffer[offset + 3];
|
|
289
|
-
offset += 4;
|
|
290
|
-
|
|
291
|
-
if (chunkLength === 0) {
|
|
292
|
-
continue; // Empty chunk
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Check if we have the complete chunk
|
|
296
|
-
if (offset + chunkLength > buffer.length) {
|
|
297
|
-
// Not enough data yet, wait for more
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Extract and decrypt the chunk
|
|
302
|
-
const encryptedChunk = buffer.slice(offset, offset + chunkLength);
|
|
303
|
-
offset += chunkLength;
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
const decryptedChunk = await receiver.open(encryptedChunk.buffer);
|
|
307
|
-
controller.enqueue(new Uint8Array(decryptedChunk));
|
|
308
|
-
} catch (error) {
|
|
309
|
-
controller.error(new Error(`Failed to decrypt chunk: ${error}`));
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Remove processed data from buffer
|
|
315
|
-
if (offset > 0) {
|
|
316
|
-
buffer = buffer.slice(offset);
|
|
317
|
-
offset = 0;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
controller.close();
|
|
322
|
-
} catch (error) {
|
|
323
|
-
controller.error(error);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
pump();
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Create new response with decrypted stream
|
|
332
|
-
return new Response(decryptedStream, {
|
|
333
|
-
status: response.status,
|
|
334
|
-
statusText: response.statusText,
|
|
335
|
-
headers: response.headers
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
}
|
package/src/protocol.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protocol constants for EHBP (Encrypted HTTP Body Protocol)
|
|
3
|
-
*/
|
|
4
|
-
export const PROTOCOL = {
|
|
5
|
-
ENCAPSULATED_KEY_HEADER: 'Ehbp-Encapsulated-Key',
|
|
6
|
-
CLIENT_PUBLIC_KEY_HEADER: 'Ehbp-Client-Public-Key',
|
|
7
|
-
KEYS_MEDIA_TYPE: 'application/ohttp-keys',
|
|
8
|
-
KEYS_PATH: '/.well-known/hpke-keys',
|
|
9
|
-
FALLBACK_HEADER: 'Ehbp-Fallback'
|
|
10
|
-
} as const;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* HPKE suite configuration matching the Go implementation
|
|
14
|
-
*/
|
|
15
|
-
export const HPKE_CONFIG = {
|
|
16
|
-
KEM: 0x0020, // X25519 HKDF SHA256
|
|
17
|
-
KDF: 0x0001, // HKDF SHA256
|
|
18
|
-
AEAD: 0x0002 // AES-256-GCM
|
|
19
|
-
} as const;
|
package/src/streaming-test.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Identity, createTransport } from './index.js';
|
|
4
|
-
|
|
5
|
-
async function streamingTest() {
|
|
6
|
-
console.log('EHBP Streaming Test');
|
|
7
|
-
console.log('===================');
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
// Create client identity
|
|
11
|
-
console.log('Creating client identity...');
|
|
12
|
-
const clientIdentity = await Identity.generate();
|
|
13
|
-
console.log('Client identity created');
|
|
14
|
-
|
|
15
|
-
// Create transport
|
|
16
|
-
console.log('Creating transport...');
|
|
17
|
-
const serverURL = 'http://localhost:8080';
|
|
18
|
-
const transport = await createTransport(serverURL, clientIdentity);
|
|
19
|
-
console.log('Transport created');
|
|
20
|
-
|
|
21
|
-
// Test 1: Basic streaming request
|
|
22
|
-
console.log('\n--- Test 1: Basic Streaming ---');
|
|
23
|
-
const streamResponse = await transport.get(`${serverURL}/stream`);
|
|
24
|
-
console.log('Stream request sent, status:', streamResponse.status);
|
|
25
|
-
|
|
26
|
-
if (streamResponse.ok) {
|
|
27
|
-
console.log('Reading stream data...');
|
|
28
|
-
const reader = streamResponse.body?.getReader();
|
|
29
|
-
if (reader) {
|
|
30
|
-
const decoder = new TextDecoder();
|
|
31
|
-
|
|
32
|
-
while (true) {
|
|
33
|
-
const { done, value } = await reader.read();
|
|
34
|
-
if (done) break;
|
|
35
|
-
|
|
36
|
-
const text = decoder.decode(value, { stream: true });
|
|
37
|
-
process.stdout.write(text);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
console.log('\nStream completed');
|
|
41
|
-
} else {
|
|
42
|
-
console.log('No readable stream available');
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
console.log('Stream request failed with status:', streamResponse.status);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Test 2: Multiple concurrent streams
|
|
49
|
-
console.log('\n--- Test 2: Concurrent Streams ---');
|
|
50
|
-
const concurrentStreams = 3;
|
|
51
|
-
const streamPromises = [];
|
|
52
|
-
|
|
53
|
-
for (let i = 0; i < concurrentStreams; i++) {
|
|
54
|
-
streamPromises.push(
|
|
55
|
-
(async (streamId: number) => {
|
|
56
|
-
const response = await transport.get(`${serverURL}/stream`);
|
|
57
|
-
if (response.ok && response.body) {
|
|
58
|
-
const reader = response.body.getReader();
|
|
59
|
-
const decoder = new TextDecoder();
|
|
60
|
-
|
|
61
|
-
while (true) {
|
|
62
|
-
const { done, value } = await reader.read();
|
|
63
|
-
if (done) break;
|
|
64
|
-
|
|
65
|
-
const text = decoder.decode(value, { stream: true });
|
|
66
|
-
|
|
67
|
-
// Prefix with stream ID to show concurrency
|
|
68
|
-
process.stdout.write(`[Stream ${streamId}] ${text}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { streamId };
|
|
72
|
-
}
|
|
73
|
-
return { streamId };
|
|
74
|
-
})(i + 1)
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const results = await Promise.all(streamPromises);
|
|
79
|
-
console.log('\nConcurrent streams completed:');
|
|
80
|
-
results.forEach(result => {
|
|
81
|
-
console.log(` - Stream ${result.streamId}: completed`);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Test 3: Large data streaming (if server supports it)
|
|
85
|
-
console.log('\n--- Test 3: Large Data Stream ---');
|
|
86
|
-
try {
|
|
87
|
-
const largeStreamResponse = await transport.get(`${serverURL}/stream`);
|
|
88
|
-
if (largeStreamResponse.ok) {
|
|
89
|
-
console.log('Reading large data stream...');
|
|
90
|
-
const reader = largeStreamResponse.body?.getReader();
|
|
91
|
-
if (reader) {
|
|
92
|
-
const decoder = new TextDecoder();
|
|
93
|
-
|
|
94
|
-
while (true) {
|
|
95
|
-
const { done, value } = await reader.read();
|
|
96
|
-
if (done) break;
|
|
97
|
-
|
|
98
|
-
const text = decoder.decode(value, { stream: true });
|
|
99
|
-
process.stdout.write(text);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.log('Large data stream test failed:', error instanceof Error ? error.message : String(error));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log('\nAll streaming tests completed successfully!');
|
|
108
|
-
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error('Streaming test failed:', error);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Run the streaming test
|
|
116
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
117
|
-
streamingTest().catch(console.error);
|
|
118
|
-
}
|
package/src/test/client.test.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import { Identity, Transport, createTransport } from '../index.js';
|
|
4
|
-
import { PROTOCOL } from '../protocol.js';
|
|
5
|
-
|
|
6
|
-
describe('Transport', () => {
|
|
7
|
-
let clientIdentity: Identity;
|
|
8
|
-
let serverIdentity: Identity;
|
|
9
|
-
|
|
10
|
-
before(async () => {
|
|
11
|
-
clientIdentity = await Identity.generate();
|
|
12
|
-
serverIdentity = await Identity.generate();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should create transport with server public key', () => {
|
|
16
|
-
const transport = new Transport(
|
|
17
|
-
clientIdentity,
|
|
18
|
-
'localhost:8080',
|
|
19
|
-
serverIdentity.getPublicKey()
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
assert(transport instanceof Transport, 'Should create transport instance');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should encrypt and decrypt request', async () => {
|
|
26
|
-
const serverPublicKey = serverIdentity.getPublicKey();
|
|
27
|
-
const originalBody = new TextEncoder().encode('Hello, World!');
|
|
28
|
-
const request = new Request('http://localhost:8080/test', {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
body: originalBody
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const encryptedRequest = await clientIdentity.encryptRequest(request, serverPublicKey);
|
|
34
|
-
|
|
35
|
-
// Check that headers are set
|
|
36
|
-
assert(encryptedRequest.headers.get(PROTOCOL.CLIENT_PUBLIC_KEY_HEADER), 'Client public key header should be set');
|
|
37
|
-
assert(encryptedRequest.headers.get(PROTOCOL.ENCAPSULATED_KEY_HEADER), 'Encapsulated key header should be set');
|
|
38
|
-
|
|
39
|
-
// Check that body is encrypted (different from original)
|
|
40
|
-
const encryptedBody = await encryptedRequest.arrayBuffer();
|
|
41
|
-
assert(encryptedBody.byteLength > 0, 'Encrypted body should not be empty');
|
|
42
|
-
assert(encryptedBody.byteLength !== originalBody.length, 'Encrypted body should have different length');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should handle request without body', async () => {
|
|
46
|
-
const serverPublicKey = serverIdentity.getPublicKey();
|
|
47
|
-
const request = new Request('http://localhost:8080/test', {
|
|
48
|
-
method: 'GET'
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const encryptedRequest = await clientIdentity.encryptRequest(request, serverPublicKey);
|
|
52
|
-
|
|
53
|
-
// Check that only client public key header is set
|
|
54
|
-
assert(encryptedRequest.headers.get(PROTOCOL.CLIENT_PUBLIC_KEY_HEADER), 'Client public key header should be set');
|
|
55
|
-
assert(!encryptedRequest.headers.get(PROTOCOL.ENCAPSULATED_KEY_HEADER), 'Encapsulated key header should not be set for empty body');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should connect to actual server and POST to /secure endpoint', async (t) => {
|
|
59
|
-
const serverURL = 'http://localhost:8080';
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const keysResponse = await fetch(`${serverURL}${PROTOCOL.KEYS_PATH}`);
|
|
63
|
-
if (!keysResponse.ok) {
|
|
64
|
-
t.skip('Server not running at localhost:8080');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
t.skip('Server not running at localhost:8080');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Create transport that will connect to the real server
|
|
73
|
-
const transport = await createTransport(serverURL, clientIdentity);
|
|
74
|
-
|
|
75
|
-
const testName = 'Integration Test User';
|
|
76
|
-
|
|
77
|
-
const serverPubKeyHex = await transport.getServerPublicKeyHex();
|
|
78
|
-
assert.strictEqual(serverPubKeyHex.length, 64, 'Server public key should be 64 bytes');
|
|
79
|
-
|
|
80
|
-
// Make actual POST request to /secure endpoint
|
|
81
|
-
const response = await transport.post(`${serverURL}/secure`, testName, {
|
|
82
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Verify response
|
|
86
|
-
assert(response.ok, `Response should be ok, got status: ${response.status}`);
|
|
87
|
-
|
|
88
|
-
const responseText = await response.text();
|
|
89
|
-
assert.strictEqual(responseText, `Hello, ${testName}`, 'Server should respond with Hello, {name}');
|
|
90
|
-
|
|
91
|
-
console.log(`✓ Integration test passed: ${responseText}`);
|
|
92
|
-
});
|
|
93
|
-
});
|