ai-remote 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 +50 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +7 -0
- package/dist/protocols/index.js +35 -0
- package/dist/protocols/index.js.map +7 -0
- package/dist/protocols/rdp/buffer.js +170 -0
- package/dist/protocols/rdp/buffer.js.map +7 -0
- package/dist/protocols/rdp/caps.js +260 -0
- package/dist/protocols/rdp/caps.js.map +7 -0
- package/dist/protocols/rdp/cert.js +135 -0
- package/dist/protocols/rdp/cert.js.map +7 -0
- package/dist/protocols/rdp/client.js +1068 -0
- package/dist/protocols/rdp/client.js.map +7 -0
- package/dist/protocols/rdp/cliprdr.js +240 -0
- package/dist/protocols/rdp/cliprdr.js.map +7 -0
- package/dist/protocols/rdp/credssp.js +254 -0
- package/dist/protocols/rdp/credssp.js.map +7 -0
- package/dist/protocols/rdp/crypto.js +369 -0
- package/dist/protocols/rdp/crypto.js.map +7 -0
- package/dist/protocols/rdp/display.js +589 -0
- package/dist/protocols/rdp/display.js.map +7 -0
- package/dist/protocols/rdp/gcc.js +132 -0
- package/dist/protocols/rdp/gcc.js.map +7 -0
- package/dist/protocols/rdp/keymap.js +132 -0
- package/dist/protocols/rdp/keymap.js.map +7 -0
- package/dist/protocols/rdp/mcs.js +223 -0
- package/dist/protocols/rdp/mcs.js.map +7 -0
- package/dist/protocols/rdp/ntlm.js +305 -0
- package/dist/protocols/rdp/ntlm.js.map +7 -0
- package/dist/protocols/rdp/pdu.js +444 -0
- package/dist/protocols/rdp/pdu.js.map +7 -0
- package/dist/protocols/rdp/rail.js +383 -0
- package/dist/protocols/rdp/rail.js.map +7 -0
- package/dist/protocols/rdp/rle.js +276 -0
- package/dist/protocols/rdp/rle.js.map +7 -0
- package/dist/protocols/rdp/sec.js +156 -0
- package/dist/protocols/rdp/sec.js.map +7 -0
- package/dist/protocols/rdp/session.js +307 -0
- package/dist/protocols/rdp/session.js.map +7 -0
- package/dist/protocols/rdp/tls.js +354 -0
- package/dist/protocols/rdp/tls.js.map +7 -0
- package/dist/protocols/rdp/vchannel.js +59 -0
- package/dist/protocols/rdp/vchannel.js.map +7 -0
- package/dist/protocols/rdp/x224.js +110 -0
- package/dist/protocols/rdp/x224.js.map +7 -0
- package/dist/protocols/ssh/kex.js +177 -0
- package/dist/protocols/ssh/kex.js.map +7 -0
- package/dist/protocols/ssh/messages.js +55 -0
- package/dist/protocols/ssh/messages.js.map +7 -0
- package/dist/protocols/ssh/session.js +608 -0
- package/dist/protocols/ssh/session.js.map +7 -0
- package/dist/protocols/ssh/terminal.js +191 -0
- package/dist/protocols/ssh/terminal.js.map +7 -0
- package/dist/protocols/ssh/transport.js +611 -0
- package/dist/protocols/ssh/transport.js.map +7 -0
- package/dist/protocols/ssh/wire.js +189 -0
- package/dist/protocols/ssh/wire.js.map +7 -0
- package/dist/protocols/types.js +1 -0
- package/dist/protocols/types.js.map +7 -0
- package/dist/protocols/vnc/input.js +16 -0
- package/dist/protocols/vnc/input.js.map +7 -0
- package/dist/protocols/vnc/session.js +259 -0
- package/dist/protocols/vnc/session.js.map +7 -0
- package/dist/shared/connection.js +114 -0
- package/dist/shared/connection.js.map +7 -0
- package/dist/shared/device.js +1 -0
- package/dist/shared/device.js.map +7 -0
- package/dist/shared/hosts.js +133 -0
- package/dist/shared/hosts.js.map +7 -0
- package/dist/shared/icons.js +42 -0
- package/dist/shared/icons.js.map +7 -0
- package/dist/shared/protocol.js +40 -0
- package/dist/shared/protocol.js.map +7 -0
- package/dist/shared/signals.js +42 -0
- package/dist/shared/signals.js.map +7 -0
- package/package.json +82 -0
- package/src/index.ts +14 -0
- package/src/protocols/index.ts +72 -0
- package/src/protocols/rdp/buffer.ts +204 -0
- package/src/protocols/rdp/caps.ts +304 -0
- package/src/protocols/rdp/cert.ts +190 -0
- package/src/protocols/rdp/client.ts +1363 -0
- package/src/protocols/rdp/cliprdr.ts +344 -0
- package/src/protocols/rdp/credssp.ts +319 -0
- package/src/protocols/rdp/crypto.ts +416 -0
- package/src/protocols/rdp/display.ts +750 -0
- package/src/protocols/rdp/gcc.ts +162 -0
- package/src/protocols/rdp/keymap.ts +68 -0
- package/src/protocols/rdp/mcs.ts +283 -0
- package/src/protocols/rdp/ntlm.ts +380 -0
- package/src/protocols/rdp/pdu.ts +497 -0
- package/src/protocols/rdp/rail.ts +480 -0
- package/src/protocols/rdp/rle.ts +343 -0
- package/src/protocols/rdp/sec.ts +184 -0
- package/src/protocols/rdp/session.ts +393 -0
- package/src/protocols/rdp/tls.ts +452 -0
- package/src/protocols/rdp/vchannel.ts +89 -0
- package/src/protocols/rdp/x224.ts +139 -0
- package/src/protocols/ssh/kex.ts +227 -0
- package/src/protocols/ssh/messages.ts +54 -0
- package/src/protocols/ssh/session.ts +878 -0
- package/src/protocols/ssh/terminal.ts +242 -0
- package/src/protocols/ssh/transport.ts +732 -0
- package/src/protocols/ssh/wire.ts +220 -0
- package/src/protocols/types.ts +146 -0
- package/src/protocols/vnc/input.ts +13 -0
- package/src/protocols/vnc/novnc.d.ts +75 -0
- package/src/protocols/vnc/session.ts +367 -0
- package/src/shared/connection.ts +206 -0
- package/src/shared/device.ts +28 -0
- package/src/shared/hosts.ts +240 -0
- package/src/shared/icons.ts +49 -0
- package/src/shared/protocol.ts +55 -0
- package/src/shared/signals.ts +67 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// A single-purpose TLS 1.2 client, written because neither of the TLS stacks
|
|
2
|
+
// available to this gateway can be used for NLA:
|
|
3
|
+
//
|
|
4
|
+
// * Cloudflare Workers' socket.startTls() validates the host certificate
|
|
5
|
+
// against public CAs with no opt-out, and RDP hosts use self-signed
|
|
6
|
+
// certificates.
|
|
7
|
+
// * CredSSP has to encrypt the host certificate's public key to prove the
|
|
8
|
+
// channel is not being relayed (MS-CSSP 3.1.5), and no runtime here exposes
|
|
9
|
+
// the certificate of a TLS session it established for us.
|
|
10
|
+
//
|
|
11
|
+
// Doing the handshake in JavaScript solves both at once: we see the certificate
|
|
12
|
+
// and we decide what to do about it. Only what RDP needs is implemented -- TLS
|
|
13
|
+
// 1.2, RSA key transport, AES-GCM records -- so this is a few hundred lines
|
|
14
|
+
// rather than a library. The certificate is deliberately NOT validated, exactly
|
|
15
|
+
// as mstsc does not validate it by default; CredSSP's binding of that
|
|
16
|
+
// certificate to a password-derived key is what actually stops a MITM here.
|
|
17
|
+
|
|
18
|
+
import { Reader, Writer, concatBytes } from './buffer';
|
|
19
|
+
import { parseX509PublicKey } from './cert';
|
|
20
|
+
import { rsaEncryptPkcs1 } from './crypto';
|
|
21
|
+
|
|
22
|
+
const TLS_1_2 = 0x0303;
|
|
23
|
+
|
|
24
|
+
const RECORD = {
|
|
25
|
+
CHANGE_CIPHER_SPEC: 20,
|
|
26
|
+
ALERT: 21,
|
|
27
|
+
HANDSHAKE: 22,
|
|
28
|
+
APPLICATION_DATA: 23,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const HANDSHAKE = {
|
|
32
|
+
CLIENT_HELLO: 1,
|
|
33
|
+
SERVER_HELLO: 2,
|
|
34
|
+
NEW_SESSION_TICKET: 4,
|
|
35
|
+
CERTIFICATE: 11,
|
|
36
|
+
SERVER_KEY_EXCHANGE: 12,
|
|
37
|
+
CERTIFICATE_REQUEST: 13,
|
|
38
|
+
SERVER_HELLO_DONE: 14,
|
|
39
|
+
CLIENT_KEY_EXCHANGE: 16,
|
|
40
|
+
FINISHED: 20,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Both suites use RSA key transport, so there is no ServerKeyExchange to verify
|
|
45
|
+
* and no ephemeral key agreement to implement. They differ only in PRF hash and
|
|
46
|
+
* key size.
|
|
47
|
+
*/
|
|
48
|
+
const CIPHER_SUITES = {
|
|
49
|
+
0x009c: { name: 'TLS_RSA_WITH_AES_128_GCM_SHA256', keyLength: 16, hash: 'SHA-256' },
|
|
50
|
+
0x009d: { name: 'TLS_RSA_WITH_AES_256_GCM_SHA384', keyLength: 32, hash: 'SHA-384' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ALERT_DESCRIPTIONS = {
|
|
54
|
+
0: 'close_notify',
|
|
55
|
+
10: 'unexpected_message',
|
|
56
|
+
20: 'bad_record_mac',
|
|
57
|
+
40: 'handshake_failure',
|
|
58
|
+
42: 'bad_certificate',
|
|
59
|
+
46: 'certificate_unknown',
|
|
60
|
+
47: 'illegal_parameter',
|
|
61
|
+
48: 'unknown_ca',
|
|
62
|
+
50: 'decode_error',
|
|
63
|
+
51: 'decrypt_error',
|
|
64
|
+
70: 'protocol_version',
|
|
65
|
+
71: 'insufficient_security',
|
|
66
|
+
80: 'internal_error',
|
|
67
|
+
90: 'user_canceled',
|
|
68
|
+
109: 'missing_extension',
|
|
69
|
+
112: 'unrecognized_name',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const ascii = (text: string) => new TextEncoder().encode(text);
|
|
73
|
+
|
|
74
|
+
function u16(value) {
|
|
75
|
+
return new Uint8Array([(value >> 8) & 0xff, value & 0xff]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function u24(value) {
|
|
79
|
+
return new Uint8Array([(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function u64(value) {
|
|
83
|
+
const out = new Uint8Array(8);
|
|
84
|
+
new DataView(out.buffer).setBigUint64(0, BigInt(value));
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- TLS 1.2 PRF (RFC 5246 5) ----------------------------------------------
|
|
89
|
+
|
|
90
|
+
async function pHash(hash: Uint8Array, secret: Uint8Array, seed: Uint8Array, length: number) {
|
|
91
|
+
const key = await crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash }, false, ['sign']);
|
|
92
|
+
const hmac = async (data: Uint8Array) => new Uint8Array(await crypto.subtle.sign('HMAC', key, data));
|
|
93
|
+
|
|
94
|
+
const blocks = [];
|
|
95
|
+
let produced = 0;
|
|
96
|
+
let a = await hmac(seed);
|
|
97
|
+
|
|
98
|
+
while (produced < length) {
|
|
99
|
+
const block = await hmac(concatBytes(a, seed));
|
|
100
|
+
blocks.push(block);
|
|
101
|
+
produced += block.length;
|
|
102
|
+
a = await hmac(a);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return concatBytes(...blocks).subarray(0, length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const prf = (hash: Uint8Array, secret: Uint8Array, label: string, seed: Uint8Array, length: number) =>
|
|
109
|
+
pHash(hash, secret, concatBytes(ascii(label), seed), length);
|
|
110
|
+
|
|
111
|
+
export class Tls12Client {
|
|
112
|
+
/**
|
|
113
|
+
* @param {(bytes: Uint8Array) => void} send write raw bytes to the transport
|
|
114
|
+
* @param {(bytes: Uint8Array) => void} onData decrypted application data
|
|
115
|
+
* @param {(error: Error) => void} onError fatal protocol failure
|
|
116
|
+
*/
|
|
117
|
+
constructor({ send, onData, onError }) {
|
|
118
|
+
this.sendRaw = send;
|
|
119
|
+
this.onData = onData;
|
|
120
|
+
this.onError = onError;
|
|
121
|
+
|
|
122
|
+
this.inbound = new Uint8Array(0);
|
|
123
|
+
this.transcript = [];
|
|
124
|
+
this.established = false;
|
|
125
|
+
this.serverPublicKey = null;
|
|
126
|
+
this.suite = null;
|
|
127
|
+
this.certificates = [];
|
|
128
|
+
|
|
129
|
+
this.readSequence = 0n;
|
|
130
|
+
this.writeSequence = 0n;
|
|
131
|
+
this.readEncrypted = false;
|
|
132
|
+
this.writeEncrypted = false;
|
|
133
|
+
|
|
134
|
+
// Records must be processed strictly in order, and every step is async.
|
|
135
|
+
this.queue = Promise.resolve();
|
|
136
|
+
this.handshakeDone = new Promise((resolve, reject) => {
|
|
137
|
+
this.resolveHandshake = resolve;
|
|
138
|
+
this.rejectHandshake = reject;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- handshake ------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
async start() {
|
|
145
|
+
this.clientRandom = new Uint8Array(32);
|
|
146
|
+
crypto.getRandomValues(this.clientRandom);
|
|
147
|
+
|
|
148
|
+
const extensions = concatBytes(
|
|
149
|
+
// signature_algorithms: rsa with sha256, sha384, sha1
|
|
150
|
+
u16(0x000d), u16(10), u16(8),
|
|
151
|
+
new Uint8Array([0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x06, 0x01]),
|
|
152
|
+
// renegotiation_info: empty, which Schannel expects to see
|
|
153
|
+
u16(0xff01), u16(1), new Uint8Array([0x00])
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const body = concatBytes(
|
|
157
|
+
u16(TLS_1_2),
|
|
158
|
+
this.clientRandom,
|
|
159
|
+
new Uint8Array([0]), // no session id
|
|
160
|
+
u16(4), u16(0x009c), u16(0x009d), // cipher suites
|
|
161
|
+
new Uint8Array([1, 0]), // compression: null
|
|
162
|
+
u16(extensions.length), extensions
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await this.#sendHandshake(HANDSHAKE.CLIENT_HELLO, body);
|
|
166
|
+
return this.handshakeDone;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#record(type, payload: Uint8Array) {
|
|
170
|
+
return concatBytes(new Uint8Array([type]), u16(TLS_1_2), u16(payload.length), payload);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async #sendHandshake(type, body: Uint8Array) {
|
|
174
|
+
const message = concatBytes(new Uint8Array([type]), u24(body.length), body);
|
|
175
|
+
this.transcript.push(message);
|
|
176
|
+
await this.#sendRecord(RECORD.HANDSHAKE, message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async #sendRecord(type, payload: Uint8Array) {
|
|
180
|
+
if (!this.writeEncrypted) {
|
|
181
|
+
this.sendRaw(this.#record(type, payload));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.sendRaw(this.#record(type, await this.#seal(type, payload)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** AES-GCM record protection (RFC 5288): explicit nonce, then ciphertext. */
|
|
188
|
+
async #seal(type, plaintext: Uint8Array) {
|
|
189
|
+
const sequence = this.writeSequence++;
|
|
190
|
+
const explicitNonce = u64(sequence);
|
|
191
|
+
const iv = concatBytes(this.keys.clientIv, explicitNonce);
|
|
192
|
+
const aad = concatBytes(u64(sequence), new Uint8Array([type]), u16(TLS_1_2), u16(plaintext.length));
|
|
193
|
+
|
|
194
|
+
const sealed = await crypto.subtle.encrypt(
|
|
195
|
+
{ name: 'AES-GCM', iv, additionalData: aad, tagLength: 128 },
|
|
196
|
+
this.keys.clientKey,
|
|
197
|
+
plaintext
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return concatBytes(explicitNonce, new Uint8Array(sealed));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async #open(type, fragment) {
|
|
204
|
+
const sequence = this.readSequence++;
|
|
205
|
+
const explicitNonce = fragment.subarray(0, 8);
|
|
206
|
+
const ciphertext = fragment.subarray(8);
|
|
207
|
+
const iv = concatBytes(this.keys.serverIv, explicitNonce);
|
|
208
|
+
const aad = concatBytes(
|
|
209
|
+
u64(sequence),
|
|
210
|
+
new Uint8Array([type]),
|
|
211
|
+
u16(TLS_1_2),
|
|
212
|
+
u16(ciphertext.length - 16)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const opened = await crypto.subtle.decrypt(
|
|
216
|
+
{ name: 'AES-GCM', iv, additionalData: aad, tagLength: 128 },
|
|
217
|
+
this.keys.serverKey,
|
|
218
|
+
ciphertext
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return new Uint8Array(opened);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async #deriveKeys(preMasterSecret) {
|
|
225
|
+
const { hash, keyLength } = this.suite;
|
|
226
|
+
|
|
227
|
+
this.masterSecret = await prf(
|
|
228
|
+
hash,
|
|
229
|
+
preMasterSecret,
|
|
230
|
+
'master secret',
|
|
231
|
+
concatBytes(this.clientRandom, this.serverRandom),
|
|
232
|
+
48
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// AEAD suites need two keys and two 4-byte salts, and no MAC keys.
|
|
236
|
+
const keyBlock = await prf(
|
|
237
|
+
hash,
|
|
238
|
+
this.masterSecret,
|
|
239
|
+
'key expansion',
|
|
240
|
+
concatBytes(this.serverRandom, this.clientRandom),
|
|
241
|
+
keyLength * 2 + 8
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const reader = new Reader(keyBlock);
|
|
245
|
+
const clientKeyBytes = reader.raw(keyLength);
|
|
246
|
+
const serverKeyBytes = reader.raw(keyLength);
|
|
247
|
+
|
|
248
|
+
this.keys = {
|
|
249
|
+
clientKey: await crypto.subtle.importKey('raw', clientKeyBytes, 'AES-GCM', false, ['encrypt']),
|
|
250
|
+
serverKey: await crypto.subtle.importKey('raw', serverKeyBytes, 'AES-GCM', false, ['decrypt']),
|
|
251
|
+
clientIv: reader.raw(4).slice(),
|
|
252
|
+
serverIv: reader.raw(4).slice(),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async #transcriptHash() {
|
|
257
|
+
const digest = await crypto.subtle.digest(this.suite.hash, concatBytes(...this.transcript));
|
|
258
|
+
return new Uint8Array(digest);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async #verifyData(label: string) {
|
|
262
|
+
return prf(this.suite.hash, this.masterSecret, label, await this.#transcriptHash(), 12);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- inbound --------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
/** Feed transport bytes. Calls are serialized, so order is preserved. */
|
|
268
|
+
receive(bytes: Uint8Array) {
|
|
269
|
+
this.queue = this.queue
|
|
270
|
+
.then(() => this.#consume(bytes))
|
|
271
|
+
.catch((error: unknown) => this.#fail(error));
|
|
272
|
+
return this.queue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#fail(error: unknown) {
|
|
276
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
277
|
+
this.rejectHandshake(failure);
|
|
278
|
+
if (this.onError) this.onError(failure);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #consume(bytes: Uint8Array) {
|
|
282
|
+
this.inbound = this.inbound.length === 0 ? bytes : concatBytes(this.inbound, bytes);
|
|
283
|
+
|
|
284
|
+
while (this.inbound.length >= 5) {
|
|
285
|
+
const type = this.inbound[0];
|
|
286
|
+
const length = (this.inbound[3] << 8) | this.inbound[4];
|
|
287
|
+
if (this.inbound.length < 5 + length) return;
|
|
288
|
+
|
|
289
|
+
const fragment = this.inbound.subarray(5, 5 + length);
|
|
290
|
+
this.inbound = this.inbound.subarray(5 + length).slice();
|
|
291
|
+
|
|
292
|
+
const payload = this.readEncrypted && type !== RECORD.CHANGE_CIPHER_SPEC
|
|
293
|
+
? await this.#open(type, fragment)
|
|
294
|
+
: fragment;
|
|
295
|
+
|
|
296
|
+
await this.#handleRecord(type, payload);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async #handleRecord(type, payload: Uint8Array) {
|
|
301
|
+
if (type === RECORD.ALERT) {
|
|
302
|
+
const level = payload[0];
|
|
303
|
+
const description = payload[1];
|
|
304
|
+
const name = ALERT_DESCRIPTIONS[description] || `alert ${description}`;
|
|
305
|
+
if (name === 'close_notify') {
|
|
306
|
+
this.established = false;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`TLS: the host sent a fatal alert: ${name}${level === 1 ? ' (warning)' : ''}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (type === RECORD.CHANGE_CIPHER_SPEC) {
|
|
313
|
+
// Everything the host sends after this point is encrypted.
|
|
314
|
+
this.readEncrypted = true;
|
|
315
|
+
this.readSequence = 0n;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (type === RECORD.APPLICATION_DATA) {
|
|
320
|
+
if (this.onData && payload.length > 0) this.onData(payload);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (type !== RECORD.HANDSHAKE) return;
|
|
325
|
+
|
|
326
|
+
// One record can carry several handshake messages.
|
|
327
|
+
let offset = 0;
|
|
328
|
+
while (offset + 4 <= payload.length) {
|
|
329
|
+
const messageType = payload[offset];
|
|
330
|
+
const length = (payload[offset + 1] << 16) | (payload[offset + 2] << 8) | payload[offset + 3];
|
|
331
|
+
const message = payload.subarray(offset, offset + 4 + length);
|
|
332
|
+
const body = payload.subarray(offset + 4, offset + 4 + length);
|
|
333
|
+
offset += 4 + length;
|
|
334
|
+
|
|
335
|
+
// The Finished check covers every handshake message before it, so the
|
|
336
|
+
// transcript must include this one before it is verified.
|
|
337
|
+
if (messageType !== HANDSHAKE.FINISHED) this.transcript.push(message);
|
|
338
|
+
await this.#handleHandshake(messageType, body, message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async #handleHandshake(type, body: Uint8Array, message: string) {
|
|
343
|
+
switch (type) {
|
|
344
|
+
case HANDSHAKE.SERVER_HELLO: {
|
|
345
|
+
const reader = new Reader(body);
|
|
346
|
+
const version = reader.u16be();
|
|
347
|
+
if (version !== TLS_1_2) {
|
|
348
|
+
throw new Error(`TLS: the host chose an unsupported version 0x${version.toString(16)}`);
|
|
349
|
+
}
|
|
350
|
+
this.serverRandom = reader.raw(32).slice();
|
|
351
|
+
reader.raw(reader.u8()); // session id
|
|
352
|
+
const suiteId = reader.u16be();
|
|
353
|
+
this.suite = CIPHER_SUITES[suiteId];
|
|
354
|
+
if (!this.suite) {
|
|
355
|
+
throw new Error(`TLS: the host chose cipher suite 0x${suiteId.toString(16)}, which this client does not implement`);
|
|
356
|
+
}
|
|
357
|
+
reader.u8(); // compression method
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case HANDSHAKE.CERTIFICATE: {
|
|
362
|
+
const reader = new Reader(body);
|
|
363
|
+
const totalLength = (reader.u8() << 16) | reader.u16be();
|
|
364
|
+
const end = reader.offset + totalLength;
|
|
365
|
+
while (reader.offset < end) {
|
|
366
|
+
const certificateLength = (reader.u8() << 16) | reader.u16be();
|
|
367
|
+
this.certificates.push(reader.raw(certificateLength).slice());
|
|
368
|
+
}
|
|
369
|
+
if (this.certificates.length === 0) throw new Error('TLS: the host sent no certificate');
|
|
370
|
+
|
|
371
|
+
// The leaf comes first, and its key is what CredSSP will bind to.
|
|
372
|
+
this.serverPublicKey = parseX509PublicKey(this.certificates[0]);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
case HANDSHAKE.CERTIFICATE_REQUEST:
|
|
377
|
+
this.certificateRequested = true;
|
|
378
|
+
return;
|
|
379
|
+
|
|
380
|
+
case HANDSHAKE.SERVER_KEY_EXCHANGE:
|
|
381
|
+
throw new Error('TLS: the host chose an ephemeral key exchange, which this client does not implement');
|
|
382
|
+
|
|
383
|
+
case HANDSHAKE.SERVER_HELLO_DONE:
|
|
384
|
+
await this.#finishClientHandshake();
|
|
385
|
+
return;
|
|
386
|
+
|
|
387
|
+
case HANDSHAKE.NEW_SESSION_TICKET:
|
|
388
|
+
return;
|
|
389
|
+
|
|
390
|
+
case HANDSHAKE.FINISHED: {
|
|
391
|
+
const expected = await this.#verifyData('server finished');
|
|
392
|
+
const actual = body.subarray(0, 12);
|
|
393
|
+
const matches = expected.length === actual.length && expected.every((byte, i) => byte === actual[i]);
|
|
394
|
+
if (!matches) throw new Error('TLS: the host Finished message did not verify');
|
|
395
|
+
|
|
396
|
+
this.transcript.push(message);
|
|
397
|
+
this.established = true;
|
|
398
|
+
this.resolveHandshake({
|
|
399
|
+
cipherSuite: this.suite.name,
|
|
400
|
+
certificates: this.certificates,
|
|
401
|
+
publicKey: this.serverPublicKey,
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
default:
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async #finishClientHandshake() {
|
|
412
|
+
if (this.certificateRequested) {
|
|
413
|
+
// An empty certificate list is the legal way to decline.
|
|
414
|
+
await this.#sendHandshake(HANDSHAKE.CERTIFICATE, u24(0));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// The premaster secret starts with the version the client first offered,
|
|
418
|
+
// which the host checks to detect a downgrade.
|
|
419
|
+
const preMasterSecret = new Uint8Array(48);
|
|
420
|
+
crypto.getRandomValues(preMasterSecret);
|
|
421
|
+
preMasterSecret[0] = 0x03;
|
|
422
|
+
preMasterSecret[1] = 0x03;
|
|
423
|
+
|
|
424
|
+
const encrypted = rsaEncryptPkcs1(
|
|
425
|
+
preMasterSecret,
|
|
426
|
+
this.serverPublicKey.modulus,
|
|
427
|
+
this.serverPublicKey.exponent
|
|
428
|
+
);
|
|
429
|
+
await this.#sendHandshake(HANDSHAKE.CLIENT_KEY_EXCHANGE, concatBytes(u16(encrypted.length), encrypted));
|
|
430
|
+
|
|
431
|
+
await this.#deriveKeys(preMasterSecret);
|
|
432
|
+
|
|
433
|
+
await this.#sendRecord(RECORD.CHANGE_CIPHER_SPEC, new Uint8Array([1]));
|
|
434
|
+
this.writeEncrypted = true;
|
|
435
|
+
this.writeSequence = 0n;
|
|
436
|
+
|
|
437
|
+
await this.#sendHandshake(HANDSHAKE.FINISHED, await this.#verifyData('client finished'));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// --- outbound -------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
/** Send application data once the handshake has completed. */
|
|
443
|
+
write(plaintext: Uint8Array) {
|
|
444
|
+
this.queue = this.queue
|
|
445
|
+
.then(async () => {
|
|
446
|
+
if (!this.established) throw new Error('TLS: write before the handshake finished');
|
|
447
|
+
await this.#sendRecord(RECORD.APPLICATION_DATA, plaintext);
|
|
448
|
+
})
|
|
449
|
+
.catch((error: unknown) => this.#fail(error));
|
|
450
|
+
return this.queue;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Static virtual channel framing (MS-RDPBCGR 2.2.6.1).
|
|
2
|
+
//
|
|
3
|
+
// A virtual channel PDU is a CHANNEL_PDU_HEADER followed by data, split across
|
|
4
|
+
// as many MCS send-data PDUs as the chunk size requires. The header repeats the
|
|
5
|
+
// total length in every chunk, so the receiver knows how much to expect before
|
|
6
|
+
// the last one arrives.
|
|
7
|
+
|
|
8
|
+
import { Reader, Writer, concatBytes } from './buffer';
|
|
9
|
+
|
|
10
|
+
/** Channel option bits used when the channel is declared in Client Network Data. */
|
|
11
|
+
export const CHANNEL_OPTION = {
|
|
12
|
+
INITIALIZED: 0x80000000,
|
|
13
|
+
ENCRYPT_RDP: 0x40000000,
|
|
14
|
+
COMPRESS_RDP: 0x00800000,
|
|
15
|
+
SHOW_PROTOCOL: 0x00200000,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CHANNEL_FLAG_FIRST = 0x00000001;
|
|
19
|
+
const CHANNEL_FLAG_LAST = 0x00000002;
|
|
20
|
+
export const CHANNEL_FLAG_SHOW_PROTOCOL = 0x00000010;
|
|
21
|
+
|
|
22
|
+
/** What RDP itself uses; a host may accept more, but never needs to. */
|
|
23
|
+
export const CHANNEL_CHUNK_LENGTH = 1600;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Split one logical channel message into wire chunks, each already carrying its
|
|
27
|
+
* CHANNEL_PDU_HEADER. An empty message still produces one chunk, because the
|
|
28
|
+
* header is what tells the host anything happened at all.
|
|
29
|
+
*/
|
|
30
|
+
export function chunkChannelData(data: Uint8Array, chunkSize = CHANNEL_CHUNK_LENGTH, extraFlags = 0) {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
|
|
33
|
+
for (let offset = 0; offset < data.length || offset === 0; offset += chunkSize) {
|
|
34
|
+
const slice = data.subarray(offset, offset + chunkSize);
|
|
35
|
+
let flags = extraFlags;
|
|
36
|
+
if (offset === 0) flags |= CHANNEL_FLAG_FIRST;
|
|
37
|
+
if (offset + chunkSize >= data.length) flags |= CHANNEL_FLAG_LAST;
|
|
38
|
+
|
|
39
|
+
const writer = new Writer(slice.length + 8);
|
|
40
|
+
writer.u32le(data.length).u32le(flags).raw(slice);
|
|
41
|
+
chunks.push(writer.take());
|
|
42
|
+
|
|
43
|
+
if (data.length === 0) break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return chunks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reassemble inbound chunks into whole messages. A chunk that arrives without a
|
|
51
|
+
* preceding FIRST is treated as its own message rather than dropped: some hosts
|
|
52
|
+
* send small PDUs with both flags set and others with neither.
|
|
53
|
+
*/
|
|
54
|
+
export class ChannelReassembler {
|
|
55
|
+
private parts: Uint8Array[];
|
|
56
|
+
private expectedLength: number;
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
this.parts = [];
|
|
60
|
+
this.expectedLength = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Returns the completed message, or null while more chunks are pending. */
|
|
64
|
+
push(bytes: Uint8Array) {
|
|
65
|
+
if (bytes.length < 8) return null;
|
|
66
|
+
|
|
67
|
+
const reader = new Reader(bytes);
|
|
68
|
+
const totalLength = reader.u32le();
|
|
69
|
+
const flags = reader.u32le();
|
|
70
|
+
const data = reader.rest();
|
|
71
|
+
|
|
72
|
+
if (flags & CHANNEL_FLAG_FIRST) {
|
|
73
|
+
this.parts = [];
|
|
74
|
+
this.expectedLength = totalLength;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.parts.push(data.slice());
|
|
78
|
+
|
|
79
|
+
const complete = (flags & CHANNEL_FLAG_LAST) !== 0;
|
|
80
|
+
if (!complete) return null;
|
|
81
|
+
|
|
82
|
+
const message = concatBytes(...this.parts);
|
|
83
|
+
this.parts = [];
|
|
84
|
+
// The declared length is authoritative: a final chunk may be padded.
|
|
85
|
+
return this.expectedLength && this.expectedLength < message.length
|
|
86
|
+
? message.subarray(0, this.expectedLength)
|
|
87
|
+
: message;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// TPKT (RFC 1006) and X.224 framing, plus the RDP security negotiation that
|
|
2
|
+
// rides on the very first exchange (MS-RDPBCGR 2.2.1.1 / 2.2.1.2).
|
|
3
|
+
|
|
4
|
+
import { Reader, Writer } from './buffer';
|
|
5
|
+
|
|
6
|
+
export const TPKT_VERSION = 0x03;
|
|
7
|
+
export const TPKT_HEADER_LENGTH = 4;
|
|
8
|
+
|
|
9
|
+
const X224_CONNECTION_REQUEST = 0xe0;
|
|
10
|
+
const X224_CONNECTION_CONFIRM = 0xd0;
|
|
11
|
+
const X224_DATA = 0xf0;
|
|
12
|
+
|
|
13
|
+
const RDP_NEG_REQ = 0x01;
|
|
14
|
+
const RDP_NEG_RSP = 0x02;
|
|
15
|
+
const RDP_NEG_FAILURE = 0x03;
|
|
16
|
+
|
|
17
|
+
export const NEG_FAILURE = {
|
|
18
|
+
SSL_REQUIRED: 0x00000001,
|
|
19
|
+
SSL_NOT_ALLOWED: 0x00000002,
|
|
20
|
+
SSL_CERT_NOT_ON_SERVER: 0x00000003,
|
|
21
|
+
INCONSISTENT_FLAGS: 0x00000004,
|
|
22
|
+
HYBRID_REQUIRED: 0x00000005,
|
|
23
|
+
SSL_WITH_USER_AUTH_REQUIRED: 0x00000006,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const NEG_PROTOCOL = {
|
|
27
|
+
RDP: 0x00000000,
|
|
28
|
+
SSL: 0x00000001,
|
|
29
|
+
HYBRID: 0x00000002,
|
|
30
|
+
RDSTLS: 0x00000004,
|
|
31
|
+
HYBRID_EX: 0x00000008,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const NEG_FAILURE_MESSAGES = {
|
|
35
|
+
0x00000001: 'The host requires TLS. Choose the TLS security option for this target.',
|
|
36
|
+
0x00000002: 'The host does not allow TLS. Choose the "RDP (standard)" security option.',
|
|
37
|
+
0x00000003: 'The host has no certificate installed, so it cannot negotiate TLS.',
|
|
38
|
+
0x00000004: 'The host rejected the requested protocol flags as inconsistent.',
|
|
39
|
+
0x00000005: 'The host requires Network Level Authentication (NLA/CredSSP). Turn NLA off on the host, or set its security layer to "RDP".',
|
|
40
|
+
0x00000006: 'The host requires TLS with early user authentication, which this gateway does not support.',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** X.224 Connection Request, carrying the protocols this client will accept. */
|
|
44
|
+
export function connectionRequest(requestedProtocols: number) {
|
|
45
|
+
const writer = new Writer(32);
|
|
46
|
+
writer.u8(TPKT_VERSION).u8(0).u16be(19);
|
|
47
|
+
writer.u8(14).u8(X224_CONNECTION_REQUEST).u16be(0).u16be(0).u8(0);
|
|
48
|
+
writer.u8(RDP_NEG_REQ).u8(0).u16le(8).u32le(requestedProtocols);
|
|
49
|
+
return writer.take();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read the X.224 Connection Confirm. Throws with the host's own explanation
|
|
54
|
+
* when it refuses the requested protocol.
|
|
55
|
+
*/
|
|
56
|
+
export function parseConnectionConfirm(frame: Uint8Array) {
|
|
57
|
+
const reader = new Reader(frame);
|
|
58
|
+
reader.skip(TPKT_HEADER_LENGTH);
|
|
59
|
+
reader.u8();
|
|
60
|
+
const code = reader.u8();
|
|
61
|
+
if (code !== X224_CONNECTION_CONFIRM) {
|
|
62
|
+
throw new Error(`RDP: expected an X.224 connection confirm, got TPDU code 0x${code.toString(16)}`);
|
|
63
|
+
}
|
|
64
|
+
reader.skip(5); // dst-ref, src-ref, class
|
|
65
|
+
|
|
66
|
+
// Servers that predate the negotiation extension simply stop here, which
|
|
67
|
+
// means "plain RDP security, as asked".
|
|
68
|
+
if (reader.remaining < 8) return { selectedProtocol: NEG_PROTOCOL.RDP, negotiated: false };
|
|
69
|
+
|
|
70
|
+
const type = reader.u8();
|
|
71
|
+
reader.u8();
|
|
72
|
+
reader.u16le();
|
|
73
|
+
|
|
74
|
+
if (type === RDP_NEG_FAILURE) {
|
|
75
|
+
const failureCode = reader.u32le();
|
|
76
|
+
const error = new Error(
|
|
77
|
+
NEG_FAILURE_MESSAGES[failureCode] ||
|
|
78
|
+
`The host refused the RDP connection (negotiation failure 0x${failureCode.toString(16)}).`
|
|
79
|
+
);
|
|
80
|
+
// A client negotiating automatically needs the code, not just the sentence.
|
|
81
|
+
error.negotiationFailure = failureCode;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
if (type !== RDP_NEG_RSP) {
|
|
85
|
+
return { selectedProtocol: NEG_PROTOCOL.RDP, negotiated: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { selectedProtocol: reader.u32le(), negotiated: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Wrap a payload in TPKT + X.224 Data. */
|
|
92
|
+
export function dataFrame(payload: Uint8Array) {
|
|
93
|
+
const writer = new Writer(payload.length + 7);
|
|
94
|
+
writer.u8(TPKT_VERSION).u8(0).u16be(payload.length + 7);
|
|
95
|
+
writer.u8(2).u8(X224_DATA).u8(0x80); // li, DT code, EOT
|
|
96
|
+
writer.raw(payload);
|
|
97
|
+
return writer.take();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Strip TPKT + X.224 Data headers from a complete frame. */
|
|
101
|
+
export function dataPayload(frame: Uint8Array) {
|
|
102
|
+
if (frame.length < 7) throw new Error('RDP: X.224 data frame is too short');
|
|
103
|
+
return frame.subarray(7);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Split a byte stream into whole frames. RDP interleaves two framings on one
|
|
108
|
+
* connection: TPKT (first byte 0x03) and fast-path output (any other value),
|
|
109
|
+
* each with its own length encoding.
|
|
110
|
+
*/
|
|
111
|
+
export function splitFrames(buffer: Uint8Array) {
|
|
112
|
+
const frames = [];
|
|
113
|
+
let offset = 0;
|
|
114
|
+
|
|
115
|
+
while (buffer.length - offset >= 4) {
|
|
116
|
+
let frameLength;
|
|
117
|
+
let fastPath = false;
|
|
118
|
+
|
|
119
|
+
if (buffer[offset] === TPKT_VERSION) {
|
|
120
|
+
frameLength = (buffer[offset + 2] << 8) | buffer[offset + 3];
|
|
121
|
+
if (frameLength < TPKT_HEADER_LENGTH) {
|
|
122
|
+
throw new Error('RDP: malformed TPKT length');
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
fastPath = true;
|
|
126
|
+
// Fast-path length is 1 or 2 bytes; the high bit of the first marks which.
|
|
127
|
+
frameLength = buffer[offset + 1] & 0x80
|
|
128
|
+
? ((buffer[offset + 1] & 0x7f) << 8) | buffer[offset + 2]
|
|
129
|
+
: buffer[offset + 1];
|
|
130
|
+
if (frameLength < 2) throw new Error('RDP: malformed fast-path length');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (buffer.length - offset < frameLength) break;
|
|
134
|
+
frames.push({ fastPath, frame: buffer.subarray(offset, offset + frameLength) });
|
|
135
|
+
offset += frameLength;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { frames, rest: buffer.subarray(offset) };
|
|
139
|
+
}
|