forked from quicwg/base-drafts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinitial-protection.js
executable file
·278 lines (245 loc) · 8.67 KB
/
initial-protection.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/bin/sh
':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
// This script performs simple encryption and decryption for Initial packets.
// It's crude, but it should be sufficient to generate examples.
'use strict';
var buffer = require('buffer');
var crypto = require('crypto');
var assert = require('assert');
var INITIAL_SALT = Buffer.from('ef4fb0abb47470c41befcf8031334fae485e09a0', 'hex');
var SHA256 = 'sha256';
var AES_GCM = 'aes-128-gcm';
var AES_ECB = 'aes-128-ecb';
function log(m, k) {
console.log(m + ' [' + k.length + ']: ' + k.toString('hex'));
};
class HMAC {
constructor(hash) {
this.hash = hash;
}
digest(key, input) {
var hmac = crypto.createHmac(this.hash, key);
hmac.update(input);
return hmac.digest();
}
}
/* HKDF as defined in RFC5869, with HKDF-Expand-Label from RFC8446. */
class QHKDF {
constructor(hmac, prk) {
this.hmac = hmac;
this.prk = prk;
}
static extract(hash, salt, ikm) {
var hmac = new HMAC(hash);
return new QHKDF(hmac, hmac.digest(salt, ikm));
}
expand(info, len) {
var output = Buffer.alloc(0);
var T = Buffer.alloc(0);
info = Buffer.from(info, 'ascii');
var counter = 0;
var cbuf = Buffer.alloc(1);
while (output.length < len) {
cbuf.writeUIntBE(++counter, 0, 1);
T = this.hmac.digest(this.prk, Buffer.concat([T, info, cbuf]));
output = Buffer.concat([output, T]);
}
return output.slice(0, len);
}
expand_label(label, len) {
const prefix = "tls13 ";
var info = Buffer.alloc(2 + 1 + prefix.length + label.length + 1);
// Note that Buffer.write returns the number of bytes written, whereas
// Buffer.writeUIntBE returns the end offset of the write. Consistency FTW.
var offset = info.writeUIntBE(len, 0, 2);
offset = info.writeUIntBE(prefix.length + label.length, offset, 1);
offset += info.write(prefix + label, offset);
info.writeUIntBE(0, offset, 1);
log('info for ' + label, info);
return this.expand(info, len);
}
}
class InitialProtection {
constructor(label, cid) {
var qhkdf = QHKDF.extract(SHA256, INITIAL_SALT, cid);
log('initial_secret', qhkdf.prk);
qhkdf = new QHKDF(qhkdf.hmac, qhkdf.expand_label(label, 32));
log(label + ' secret', qhkdf.prk);
this.key = qhkdf.expand_label("quic key", 16);
log(label + ' key', this.key);
this.iv = qhkdf.expand_label("quic iv", 12);
log(label + ' iv', this.iv);
this.hp = qhkdf.expand_label("quic hp", 16);
log(label + ' hp', this.hp);
}
generateNonce(counter) {
var nonce = Buffer.from(this.iv);
var m = nonce.readUIntBE(nonce.length - 6, 6);
var x = ((m ^ counter) & 0xffffff) +
((((m / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000);
nonce.writeUIntBE(x, nonce.length - 6, 6);
return nonce;
}
// Returns the encrypted data with authentication tag appended. The AAD is
// used, but not added to the output.
encipher(pn, aad, data) {
console.log('encipher pn', pn);
log('encipher aad', aad);
log('encipher data', data);
var nonce = this.generateNonce(pn);
var gcm = crypto.createCipheriv(AES_GCM, this.key, nonce);
gcm.setAAD(aad);
var e = gcm.update(data);
gcm.final();
e = Buffer.concat([e, gcm.getAuthTag()]);
log('enciphered', e);
return e;
}
decipher(pn, aad, data) {
console.log('decipher pn', pn);
log('decipher aad', aad);
log('decipher data', data);
var nonce = this.generateNonce(pn);
var gcm = crypto.createDecipheriv(AES_GCM, this.key, nonce);
gcm.setAAD(aad);
gcm.setAuthTag(data.slice(data.length - 16));
var d = gcm.update(data.slice(0, data.length - 16));
gcm.final();
log('deciphered', d);
return d;
}
// Calculates the header protection mask. Returns 16 bytes of output.
hpMask(sample) {
log('hp sample', sample);
// var ctr = crypto.createCipheriv('aes-128-ctr', this.hp, sample);
// var mask = ctr.update(Buffer.alloc(5));
var ecb = crypto.createCipheriv('aes-128-ecb', this.hp, Buffer.alloc(0));
var mask = ecb.update(sample);
log('hp mask', mask);
return mask;
}
// XOR b into a.
xor(a, b) {
a.forEach((_, i) => {
a[i] ^= b[i];
});
}
// hdr is everything before the length field
// hdr[0] has the packet number length already in place
// pn is the packet number
// data is the payload (i.e., encoded frames)
encrypt(hdr, pn, data) {
var pn_len = 1 + (hdr[0] & 0x3);
if (pn_len + data.length < 4) {
throw new Error('insufficient length of packet number and payload');
}
var aad = Buffer.alloc(hdr.length + 2 + pn_len);
var offset = hdr.copy(aad);
// Add a length that covers the packet number encoding and the auth tag.
offset = aad.writeUIntBE(0x4000 | (pn_len + data.length + 16), offset, 2);
var pn_offset = offset;
var pn_mask = 0xffffffff >> (8 * (4 - pn_len));
offset = aad.writeUIntBE(pn & pn_mask, offset, pn_len)
log('header', aad);
var payload = this.encipher(pn, aad, data);
var mask = this.hpMask(payload.slice(4 - pn_len, 20 - pn_len));
aad[0] ^= mask[0] & (0x1f >> (aad[0] >> 7));
this.xor(aad.slice(pn_offset), mask.slice(1));
log('masked header', aad);
return Buffer.concat([aad, payload]);
}
cidLen(v) {
if (!v) {
return 0;
}
return v + 3;
}
decrypt(data) {
log('decrypt', data);
if (data[0] & 0x40 !== 0x40) {
throw new Error('missing QUIC bit');
}
if (data[0] & 0x80 === 0) {
throw new Error('short header unsupported');
}
var hdr_len = 1 + 4 + 1 +
this.cidLen(data[5]&0xf) + this.cidLen(data[5]>>4);
if ((data[0] & 0x30) === 0) { // Initial packet: token.
if ((data[hdr_len] & 0xc0) !== 0) {
throw new Error('multi-byte token length unsupported');
}
hdr_len += 1 + data[hdr_len]; // oops: this only handles single octet lengths.
}
// Skip the length.
hdr_len += 1 << (data[hdr_len] >> 6);
// Now we're at the encrypted bit.
var mask = this.hpMask(data.slice(hdr_len + 4, hdr_len + 20));
var octet0 = data[0] ^ (mask[0] & (0x1f >> (data[0] >> 7)));
var pn_len = (octet0 & 3) + 1;
var hdr = Buffer.from(data.slice(0, hdr_len + pn_len));
hdr[0] = octet0;
log('header', hdr);
this.xor(hdr.slice(hdr_len), mask.slice(1));
log('unmasked header', hdr);
var pn = hdr.readUIntBE(hdr_len, pn_len);
// Important: this doesn't recover PN based on expected value.
// The expectation being that Initial packets won't ever need that.
return this.decipher(pn, hdr, data.slice(hdr.length));
}
}
function pad(hdr, body) {
var pn_len = (hdr[0] & 3) + 1;
var size = 1200 - hdr.length - 2 - pn_len - 16; // Assume 2 byte length.
if (size < 0) {
return body;
}
var padded = Buffer.allocUnsafe(size);
console.log('pad amount', size);
body.copy(padded);
padded.fill(0, body.length);
log('padded', padded);
return padded;
}
function test(role, cid, hdr, pn, body) {
cid = Buffer.from(cid, 'hex');
log('connection ID', cid);
hdr = Buffer.from(hdr, 'hex');
log('header', hdr);
console.log('packet number = ' + pn);
body = Buffer.from(body, 'hex');
log('body', hdr);
if (role === 'client' && (hdr[0] & 0x30) === 0) {
body = pad(hdr, body);
}
var endpoint = new InitialProtection(role + ' in', cid);
var packet = endpoint.encrypt(hdr, pn, body);
log('encrypted packet', packet);
var content = endpoint.decrypt(packet);
log('decrypted content', content);
if (content.compare(body) !== 0) {
throw new Error('decrypted result not the same as the original');
}
}
var version = 'ff000012'
var cid = '8394c8f03e515708';
var ci_hdr = 'c3' + version + '50' + cid + '00';
// This is a client Initial. Unfortunately, the ClientHello currently omits
// the transport_parameters extension.
var crypto_frame = '060040c4' +
'010000c003036660261ff947cea49cce6cfad687f457cf1b14531ba14131a0e8' +
'f309a1d0b9c4000006130113031302010000910000000b000900000673657276' +
'6572ff01000100000a00140012001d0017001800190100010101020103010400' +
'230000003300260024001d00204cfdfcd178b784bf328cae793b136f2aedce00' +
'5ff183d7bb1495207236647037002b0003020304000d0020001e040305030603' +
'020308040805080604010501060102010402050206020202002d00020101001c' +
'00024001';
test('client', cid, ci_hdr, 2, crypto_frame);
// This should be a valid server Initial.
var frames = '0d0000000018410a' +
'020000560303eefce7f7b37ba1d163' +
'2e96677825ddf73988cfc79825df566dc5430b9a04' +
'5a1200130100002e00330024001d00209d3c940d89' +
'690b84d08a60993c144eca684d1081287c834d5311' +
'bcf32bb9da1a002b00020304';
var si_hdr = 'c1' + version + '05' + 'f067a5502a4262b5' + '00';
test('server', cid, si_hdr, 1, frames);