|
1 | 1 | //@ts-self-types = "../type/clienthello.d.ts" |
2 | | -import { |
3 | | - Uint8, Uint16, Uint24, Version, Constrained, Cipher, Struct, |
4 | | - Extension, ExtensionType, |
5 | | - NamedGroupList, NamedGroup, RecordSizeLimit, |
6 | | - KeyShareClientHello, SupportedVersions, ServerNameList, PskKeyExchangeModes, |
7 | | - Cookie, Supported_signature_algorithms, |
8 | | - HandshakeType, |
9 | | - OfferedPsks, |
10 | | - EarlyDataIndication, |
11 | | - Padding, |
12 | | - safeuint8array, |
13 | | - Handshake, |
14 | | -} from "./dep.ts" |
| 2 | +import { Cipher, Constrained, ContentType, Cookie, EarlyDataIndication, Extension, ExtensionType, HandshakeType, KeyShareClientHello, NamedGroupList, OfferedPsks, Padding, parseItems, PskKeyExchangeModes, RecordSizeLimit, safeuint8array, ServerNameList, Supported_signature_algorithms, SupportedVersions, Uint16, Uint24, Version } from "./dep.ts"; |
15 | 3 |
|
16 | | -export class ClientHello extends Struct { |
17 | | - legacy_version; |
18 | | - random; // offset 2, length 32 |
19 | | - legacy_session;// offset = 32, length = 1; |
20 | | - cipher_suites; // offset = 33; |
21 | | - legacy_compression_methods; // offset = 33 + cipher_suites.length |
22 | | - extensions; |
23 | | - ext = new Map; |
24 | | - static fromHandShake(handshake) { |
25 | | - const copy = Uint8Array.from(handshake); |
26 | | - let offset = 0; |
27 | | - const _type = HandshakeType.from(copy); offset += 1; |
28 | | - const lengthOf = Uint24.from(copy.subarray(offset)).value; offset += 3; |
29 | | - return ClientHello.from(copy.subarray(offset, offset + lengthOf)); |
| 4 | +export class ClientHello extends Uint8Array { |
| 5 | + #version |
| 6 | + #random |
| 7 | + #legacy_session_id |
| 8 | + #ciphers |
| 9 | + #legacy_compression_methods |
| 10 | + #extensions |
| 11 | + static create(...args){ |
| 12 | + return new ClientHello(...args) |
30 | 13 | } |
31 | | - static fromHandshake = ClientHello.fromHandShake |
32 | | - static from(array) { |
33 | | - const copy = Uint8Array.from(array); |
34 | | - let offset = 0 |
35 | | - const legacy_version = Version.from(copy.subarray(offset)); offset += 2; |
36 | | - const random = copy.subarray(offset, offset + 32); offset += 32; |
37 | | - const legacy_session = Legacy_session_id.from(copy.subarray(offset)); offset += legacy_session.length; |
38 | | - const cipher_suites = Cipher_suites.from(copy.subarray(offset)); offset += cipher_suites.length; |
39 | | - const _legacy_compression_methods = Legacy_compression_methods.from(copy.subarray(offset)); offset += _legacy_compression_methods.length; |
40 | | - const extensions = Extensions.from(copy.subarray(offset)); |
41 | | - return new ClientHello(legacy_version, random, legacy_session, cipher_suites, ...extensions.extensions) |
| 14 | + static from = ClientHello.create |
| 15 | + constructor(...args) { |
| 16 | + args = (args.at(0) instanceof Uint8Array)? sanitize(...args): args |
| 17 | + super(...args) |
42 | 18 | } |
43 | | - constructor( |
44 | | - legacy_version = Version.legacy.protocolVersion(), |
45 | | - random = crypto.getRandomValues(new Uint8Array(32)), |
46 | | - legacy_session = new Legacy_session_id, |
47 | | - cipher_suites = new Cipher_suites(Cipher.AES_128_GCM_SHA256, Cipher.AES_256_GCM_SHA384), |
48 | | - ...extensions |
49 | | - ) { |
50 | | - |
51 | | - const legacy_compression_methods = new Legacy_compression_methods; |
52 | | - |
53 | | - super( |
54 | | - legacy_version instanceof Uint8Array ? legacy_version : legacy_version.Uint16, |
55 | | - random, |
56 | | - legacy_session, |
57 | | - cipher_suites, |
58 | | - legacy_compression_methods, |
59 | | - Extensions.fromExtension(...extensions) |
60 | | - ) |
61 | | - this.legacy_version = legacy_version; |
62 | | - this.random = random; |
63 | | - this.legacy_session = legacy_session; |
64 | | - this.cipher_suites = cipher_suites; |
65 | | - this.legacy_compression_methods = legacy_compression_methods |
66 | | - let offset = legacy_version.length + random.length + legacy_session.length |
67 | | - + cipher_suites.length + legacy_compression_methods.length; |
68 | | - this.extensions = extensions; |
69 | | - for (const ex of extensions) { |
70 | | - this.ext.set(ex.extension_type?.name, { pos: offset + 2, data: ex.extension_data }); |
71 | | - offset += ex.length; |
| 19 | + get version() { |
| 20 | + this.#version ||= Version.from(this.subarray(0, 2)) |
| 21 | + return this.#version |
| 22 | + } |
| 23 | + get random() { |
| 24 | + this.#random ||= this.subarray(2, 34); |
| 25 | + return this.#random; |
| 26 | + } |
| 27 | + get legacy_session_id() { |
| 28 | + if (this.#legacy_session_id) return this.#legacy_session_id |
| 29 | + const lengthOf = this.at(34); |
| 30 | + if (lengthOf == 0) { |
| 31 | + this.#legacy_session_id = this.subarray(34, 35); |
| 32 | + } else { |
| 33 | + this.#legacy_session_id = this.subarray(35, 35 + lengthOf) |
72 | 34 | } |
| 35 | + return this.#legacy_session_id |
73 | 36 | } |
74 | | - static fromServerName(serverName) { |
75 | | - return new ClientHello(undefined, undefined, undefined, undefined, |
76 | | - Uint8Array.of( |
77 | | - 0, 10, 0, 12, 0, 10, 0, 29, 0, 23, 0, 24, 0, 30, 0, 25, // ExtensionType.SUPPORTED_GROUPS.extension(NamedGroupList.default()); |
78 | | - 0, 13, 0, 18, 0, 16, 8, 6, 8, 5, 8, 4, 8, 7, 8, 8, 6, 3, 5, 3, 4, 3, // ExtensionType.SIGNATURE_ALGORITHMS.extension(Supported_signature_algorithms.default()); |
79 | | - 0, 43, 0, 3, 2, 3, 4, // ExtensionType.SUPPORTED_VERSIONS.extension(SupportedVersions.forClient_hello()) |
80 | | - 0, 45, 0, 2, 1, 1 // ExtensionType.PSK_KEY_EXCHANGE_MODES.extension(PskKeyExchangeModes.default()) |
81 | | - ), |
82 | | - ExtensionType.SERVER_NAME.extension(ServerNameList.fromName(serverName)), |
83 | | - ExtensionType.KEY_SHARE.extension(KeyShareClientHello.fromKeyShareEntries( |
84 | | - NamedGroup.X25519.keyShareEntry(), |
85 | | - NamedGroup.SECP256R1.keyShareEntry() |
86 | | - )) |
87 | | - ) |
| 37 | + get ciphers() { |
| 38 | + this.#ciphers ||= Cipher_suites.from(this.subarray(35 + this.at(34))); |
| 39 | + return this.#ciphers; |
| 40 | + } |
| 41 | + /** |
| 42 | + * For every TLS 1.3 ClientHello, this vector |
| 43 | + MUST contain exactly one byte, set to zero, which corresponds to |
| 44 | + the "null" compression method in prior versions of TLS. If a |
| 45 | + TLS 1.3 ClientHello is received with any other value in this |
| 46 | + field, the server MUST abort the handshake with an |
| 47 | + "illegal_parameter" alert. |
| 48 | + */ |
| 49 | + get legacy_compression_methods() { |
| 50 | + if (this.#legacy_compression_methods) return this.#legacy_compression_methods |
| 51 | + const offset = 34 + this.legacy_session_id.length + this.ciphers.length; |
| 52 | + this.#legacy_compression_methods ||= this.subarray(offset, offset + 2); |
| 53 | + return this.#legacy_compression_methods |
| 54 | + } |
| 55 | + get extensions() { |
| 56 | + if (this.#extensions) return this.#extensions; |
| 57 | + const copy = this.subarray(34 + this.legacy_session_id.length + this.ciphers.length + 2) |
| 58 | + const lengthOf = Uint16.from(copy).value; |
| 59 | + if (lengthOf < 8) throw TypeError(`Length min. 8 bytes`) |
| 60 | + if (lengthOf > 2 ** 16 - 2) throw TypeError(`Length max: ${2 ** 16 - 2}`) |
| 61 | + const output = new Map; |
| 62 | + let offset = 2; |
| 63 | + while (true) { |
| 64 | + const extension = Extension.from(copy.subarray(offset)); offset += extension.length |
| 65 | + parseExtension(extension); |
| 66 | + extension.extension_data.pos = offset + 2; |
| 67 | + output.set(extension.extension_type, extension.extension_data) |
| 68 | + if (offset >= lengthOf) break; |
| 69 | + if (offset >= copy.length) break; |
| 70 | + } |
| 71 | + this.#extensions ||= output; |
| 72 | + return this.#extensions; |
88 | 73 | } |
89 | | - |
90 | | - get handshake() { return new Handshake(HandshakeType.CLIENT_HELLO, this) } |
91 | | - get record() { return this.handshake.record } |
92 | | - |
93 | 74 | addBinders(binders) { |
94 | | - const psk = this.ext.get('PRE_SHARED_KEY'); |
95 | | - const lengthOf = psk.data.length + binders.length; |
| 75 | + const psk = this.extensions.get(ExtensionType.PRE_SHARED_KEY); |
| 76 | + const lengthOf = psk.length + binders.length; |
96 | 77 | const uint16 = Uint16.fromValue(lengthOf) |
97 | 78 | const array = safeuint8array(this, binders); |
98 | 79 | array.set(uint16, psk.pos + 2); |
99 | | - return ClientHello.from(array) |
| 80 | + return new ClientHello(array) |
100 | 81 | } |
101 | 82 | binderPos() { |
102 | | - const psk = this.ext.get('PRE_SHARED_KEY'); |
103 | | - return psk.pos + 4 + psk.data.identities.length |
| 83 | + const psk = this.extensions.get(ExtensionType.PRE_SHARED_KEY); |
| 84 | + return psk.pos + 4 + psk.identities.length |
104 | 85 | } |
105 | 86 | } |
106 | 87 |
|
107 | | -export class Cipher_suites extends Constrained { |
108 | | - static from(array) { |
109 | | - const copy = Uint8Array.from(array); |
110 | | - const lengthOf = Uint16.from(copy).value; |
111 | | - const ciphers = []; |
112 | | - for (let offset = 2; offset < lengthOf + 2; offset += 2) { |
113 | | - const cipher = Cipher.from(copy.subarray(offset)); |
114 | | - ciphers.push(cipher) |
| 88 | +function sanitize(...args) { |
| 89 | + try { |
| 90 | + if(Version.from(...args) instanceof Version) return args |
| 91 | + throw Error |
| 92 | + } catch (_error) { |
| 93 | + try { |
| 94 | + if(HandshakeType.from(args[0]) == HandshakeType.CLIENT_HELLO){ |
| 95 | + const lengthOf = Uint24.from(args[0].subarray(1)).value; |
| 96 | + return [args[0].subarray(4, 4 + lengthOf)] |
| 97 | + } |
| 98 | + throw Error |
| 99 | + } catch (_error) { |
| 100 | + try { |
| 101 | + const contentType = ContentType.from(args[0]); |
| 102 | + const handshakeType = HandshakeType.from(args[0].subarray(5)); |
| 103 | + const lengthOf = Uint24.from(args[0].subarray(6)).value; |
| 104 | + const conditions = [ |
| 105 | + contentType == ContentType.HANDSHAKE, |
| 106 | + handshakeType == HandshakeType.CLIENT_HELLO |
| 107 | + ] |
| 108 | + if(conditions.every(e=>e==true))return [args[0].subarray(9, 9 + lengthOf)] |
| 109 | + throw Error; |
| 110 | + } catch (error) { |
| 111 | + throw error; |
| 112 | + } |
115 | 113 | } |
116 | | - return new Cipher_suites(...ciphers) |
117 | | - } |
118 | | - constructor(...ciphers) { |
119 | | - super(2, 2 ** 16 - 2, ...ciphers.map(e => e.Uint16)) |
120 | | - this.ciphers = new Set(ciphers); |
121 | 114 | } |
122 | 115 | } |
123 | 116 |
|
124 | | -class Extensions extends Constrained { |
125 | | - static fromExtension(...extensions) { return new Extensions(...extensions) } |
| 117 | +//const test_0 = new ClientHello(HandshakeType.CLIENT_HELLO.Uint8) |
| 118 | + |
| 119 | +export class Cipher_suites extends Constrained { |
126 | 120 | static from(array) { |
127 | 121 | const copy = Uint8Array.from(array); |
128 | 122 | const lengthOf = Uint16.from(copy).value; |
129 | | - const extensions = []; |
130 | | - let offset = 2; |
131 | | - //for (let offset = 2; offset < lengthOf + 2;) { |
132 | | - while (true) { |
133 | | - const extension = Extension.from(copy.subarray(offset)); offset += extension.length |
134 | | - parseExtension(extension); |
135 | | - extensions.push(extension) |
136 | | - if (offset >= lengthOf) break; |
137 | | - if (offset >= copy.length) break; |
138 | | - } |
139 | | - return new Extensions(...extensions) |
| 123 | + const ciphers = parseItems(copy, 2, lengthOf, Cipher); |
| 124 | + return new Cipher_suites(...ciphers) |
140 | 125 | } |
141 | | - constructor(...extensions) { |
142 | | - super(8, 2 ** 16 - 1, ...extensions) |
143 | | - this.extensions = extensions |
| 126 | + constructor(...ciphers) { |
| 127 | + super(2, 2 ** 16 - 2, ...ciphers.map(e => e.Uint16)) |
| 128 | + this.ciphers = ciphers; |
144 | 129 | } |
145 | 130 | } |
146 | 131 |
|
@@ -185,28 +170,3 @@ function parseExtension(extension) { |
185 | 170 | } |
186 | 171 | } |
187 | 172 |
|
188 | | -class Legacy_session_id extends Constrained { |
189 | | - static from(array) { |
190 | | - const copy = Uint8Array.from(array); |
191 | | - const lengthOf = Uint8.from(copy).value; |
192 | | - if (lengthOf == 0) return new Legacy_session_id; |
193 | | - return new Legacy_session_id(copy.subarray(1, 1 + lengthOf)) |
194 | | - } |
195 | | - constructor(opaque = new Uint8Array) { |
196 | | - super(0, 32, opaque) |
197 | | - this.opaque = opaque |
198 | | - } |
199 | | -} |
200 | | - |
201 | | -class Legacy_compression_methods extends Constrained { |
202 | | - static from(array) { |
203 | | - const copy = Uint8Array.from(array); |
204 | | - const lengthOf = Uint8.from(copy).value; |
205 | | - return new Legacy_compression_methods(copy.subarray(1, 1 + lengthOf)) |
206 | | - } |
207 | | - constructor(opaque = Uint8Array.of(0)) { |
208 | | - super(0, 2 ** 8 - 1, opaque) |
209 | | - this.opaque = opaque |
210 | | - } |
211 | | -} |
212 | | - |
0 commit comments