Skip to content

Commit d823526

Browse files
committed
Move to using padding like TLS
1 parent 5e52338 commit d823526

File tree

7 files changed

+159
-68
lines changed

7 files changed

+159
-68
lines changed

circle.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ machine:
44
python:
55
version: 3.5.2
66
post:
7-
- pyenv global 2.7.12 3.4.4 3.5.2
7+
- pyenv global 2.7.12 3.4.4 3.5.2 3.6.0
88

99
dependencies:
1010
cache_directories:

nodejs/ece.js

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var saved = {
2525
keylabels: {}
2626
};
2727
var AES_GCM = 'aes-128-gcm';
28-
var PAD_SIZE = { 'aes128gcm': 2, 'aesgcm': 2, 'aesgcm128': 1 };
28+
var PAD_SIZE = { 'aes128gcm': 1, 'aesgcm': 2, 'aesgcm128': 1 };
2929
var TAG_LENGTH = 16;
3030
var KEY_LENGTH = 16;
3131
var NONCE_LENGTH = 12;
@@ -313,15 +313,8 @@ function readHeader(buffer, header) {
313313
return 21 + idsz;
314314
}
315315

316-
function decryptRecord(key, counter, buffer, header) {
317-
keylog('decrypt', buffer);
318-
var nonce = generateNonce(key.nonce, counter);
319-
var gcm = crypto.createDecipheriv(AES_GCM, key.key, nonce);
320-
gcm.setAuthTag(buffer.slice(buffer.length - TAG_LENGTH));
321-
var data = gcm.update(buffer.slice(0, buffer.length - TAG_LENGTH));
322-
data = Buffer.concat([data, gcm.final()]);
323-
keylog('decrypted', data);
324-
var padSize = PAD_SIZE[header.version];
316+
function unpadLegacy(data, version) {
317+
var padSize = PAD_SIZE[version];
325318
var pad = data.readUIntBE(0, padSize);
326319
if (pad + padSize > data.length) {
327320
throw new Error('padding exceeds block size');
@@ -335,6 +328,40 @@ function decryptRecord(key, counter, buffer, header) {
335328
return data.slice(padSize + pad);
336329
}
337330

331+
function unpad(data, last) {
332+
var i = data.length - 1;
333+
while(i > 0) {
334+
if (data[i]) {
335+
if (last) {
336+
if (data[i] !== 2) {
337+
throw new Error('last record needs to start padding with a 2');
338+
}
339+
} else {
340+
if (data[i] !== 1) {
341+
throw new Error('last record needs to start padding with a 2');
342+
}
343+
}
344+
return data.slice(0, i);
345+
}
346+
--i;
347+
}
348+
throw new Error('all zero plaintext');
349+
}
350+
351+
function decryptRecord(key, counter, buffer, header, last) {
352+
keylog('decrypt', buffer);
353+
var nonce = generateNonce(key.nonce, counter);
354+
var gcm = crypto.createDecipheriv(AES_GCM, key.key, nonce);
355+
gcm.setAuthTag(buffer.slice(buffer.length - TAG_LENGTH));
356+
var data = gcm.update(buffer.slice(0, buffer.length - TAG_LENGTH));
357+
data = Buffer.concat([data, gcm.final()]);
358+
keylog('decrypted', data);
359+
if (header.version !== 'aes128gcm') {
360+
return unpadLegacy(data, header.version);
361+
}
362+
return unpad(data, last);
363+
}
364+
338365
// TODO: this really should use the node streams stuff
339366

340367
/**
@@ -375,38 +402,51 @@ function decrypt(buffer, params) {
375402

376403
for (var i = 0; start < buffer.length; ++i) {
377404
var end = start + chunkSize;
378-
if (end === buffer.length) {
405+
if (header.version !== 'aes128gcm' && end === buffer.length) {
379406
throw new Error('Truncated payload');
380407
}
381408
end = Math.min(end, buffer.length);
382409
if (end - start <= TAG_LENGTH) {
383410
throw new Error('Invalid block: too small at ' + i);
384411
}
385412
var block = decryptRecord(key, i, buffer.slice(start, end),
386-
header);
413+
header, end >= buffer.length);
387414
result = Buffer.concat([result, block]);
388415
start = end;
389416
}
390417
return result;
391418
}
392419

393-
function encryptRecord(key, counter, buffer, pad, padSize) {
420+
function encryptRecord(key, counter, buffer, pad, header, last) {
394421
keylog('encrypt', buffer);
395422
pad = pad || 0;
396423
var nonce = generateNonce(key.nonce, counter);
397424
var gcm = crypto.createCipheriv(AES_GCM, key.key, nonce);
425+
426+
var ciphertext = [];
427+
var padSize = PAD_SIZE[header.version];
398428
var padding = new Buffer(pad + padSize);
399429
padding.fill(0);
400-
padding.writeUIntBE(pad, 0, padSize);
401-
keylog('padding', padding);
402-
var epadding = gcm.update(padding);
403-
var ebuffer = gcm.update(buffer);
430+
431+
if (header.version !== 'aes128gcm') {
432+
padding.writeUIntBE(pad, 0, padSize);
433+
keylog('padding', padding);
434+
ciphertext.push(gcm.update(padding));
435+
ciphertext.push(gcm.update(buffer));
436+
} else {
437+
ciphertext.push(gcm.update(buffer));
438+
padding.writeUIntBE(last ? 2 : 1, 0, 1);
439+
keylog('padding', padding);
440+
ciphertext.push(gcm.update(padding));
441+
}
442+
404443
gcm.final();
405444
var tag = gcm.getAuthTag();
406445
if (tag.length !== TAG_LENGTH) {
407446
throw new Error('invalid tag generated');
408447
}
409-
return keylog('encrypted', Buffer.concat([epadding, ebuffer, tag]));
448+
ciphertext.push(tag);
449+
return keylog('encrypted', Buffer.concat(ciphertext));
410450
}
411451

412452
function writeHeader(header) {
@@ -471,19 +511,30 @@ function encrypt(buffer, params) {
471511
}
472512
var pad = isNaN(parseInt(params.pad, 10)) ? 0 : parseInt(params.pad, 10);
473513

474-
// Note the <= here ensures that we write out a padding-only block at the end
475-
// of a buffer.
476-
for (var i = 0; start <= buffer.length; ++i) {
514+
var counter = 0;
515+
var last = false;
516+
while (!last) {
477517
// Pad so that at least one data byte is in a block.
478-
var recordPad = Math.min((1 << (padSize * 8)) - 1, // maximum padding
479-
Math.min(header.rs - overhead - 1, pad));
518+
var recordPad = Math.min(header.rs - overhead - 1, pad);
519+
if (header.version !== 'aes128gcm') {
520+
recordPad = Math.min((1 << (padSize * 8)) - 1, recordPad);
521+
}
480522
pad -= recordPad;
481523

482-
var end = Math.min(start + header.rs - overhead - recordPad, buffer.length);
483-
var block = encryptRecord(key, i, buffer.slice(start, end),
484-
recordPad, padSize);
524+
var end = start + header.rs - overhead - recordPad;
525+
if (header.version !== 'aes128gcm') {
526+
// The > here ensures that we write out a padding-only block at the end
527+
// of a buffer.
528+
last = end > buffer.length;
529+
} else {
530+
last = end >= buffer.length && pad <= 0;
531+
}
532+
var block = encryptRecord(key, counter, buffer.slice(start, end),
533+
recordPad, header, last);
485534
result = Buffer.concat([result, block]);
486-
start += header.rs - overhead - recordPad;
535+
536+
start = end;
537+
++counter;
487538
}
488539
if (pad) {
489540
throw new Error('Unable to pad by requested amount, ' + pad + ' remaining');

nodejs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "http_ece",
3-
"version": "0.6.4",
3+
"version": "0.6.5",
44
"description": "Encrypted Content-Encoding for HTTP",
55
"homepage": "https://github.com/martinthomson/encrypted-content-encoding",
66
"bugs": "https://github.com/martinthomson/encrypted-content-encoding/issues",

nodejs/test.js

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ args.forEach(function(arg) {
2626
if (arg === 'verbose') {
2727
log = console.log.bind(console);
2828
} else if (arg.substring(0, 5) === 'text=') {
29-
plaintext = arg.substring(5);
29+
plaintext = Buffer.from(arg.substring(5), 'utf8');
3030
} else if (arg.substring(0, 4) === 'max=') {
3131
var v = parseInt(arg.substring(4), 10);
3232
if (!isNaN(v) && v > minLen) {
@@ -136,7 +136,6 @@ function encryptDecrypt(input, encryptParams, decryptParams, keys) {
136136
var decrypted = ece.decrypt(encrypted, decryptParams);
137137
logbuf('Decrypted', decrypted);
138138
assert.equal(Buffer.compare(input, decrypted), 0);
139-
log('----- OK');
140139

141140
saveDump({
142141
version: encryptParams.version,
@@ -183,6 +182,9 @@ function exactlyOneRecord(version) {
183182
}
184183

185184
function detectTruncation(version) {
185+
if (version === 'aes128gcm') {
186+
return;
187+
}
186188
var input = generateInput(2);
187189
var params = {
188190
version: version,
@@ -281,37 +283,39 @@ function useDH(version) {
281283
}
282284

283285
// Use the examples from the draft as a sanity check.
284-
function checkExamples() {
286+
function checkExamples(version) {
285287
[
286288
{
287289
args: {
288290
version: 'aes128gcm',
289-
key: base64.decode('6Aqf1aDH8lSxLyCpoCnAqg'),
291+
key: base64.decode('yqdlZ-tYemfogSmv7Ws5PQ'),
290292
keyid: '',
291-
salt: base64.decode('sJvlboCWzB5jr8hI_q9cOQ'),
293+
salt: base64.decode('I1BsxtFttlv3u_Oo94xnmw'),
292294
rs: 4096
293295
},
294296
plaintext: Buffer.from('I am the walrus'),
295-
ciphertext: base64.decode('sJvlboCWzB5jr8hI_q9cOQAAEAAANSmx' +
296-
'kSVa0-MiNNuF77YHSs-iwaNe_OK0qfmO' +
297-
'c7NT5WSW'),
297+
ciphertext: base64.decode('I1BsxtFttlv3u_Oo94xnmwAAEAAA-NAV' +
298+
'ub2qFgBEuQKRapoZu-IxkIva3MEB1PD-' +
299+
'ly8Thjg'),
298300
},
299301
{
300302
args: {
301303
version: 'aes128gcm',
302304
key: base64.decode('BO3ZVPxUlnLORbVGMpbT1Q'),
303305
keyid: 'a1',
304306
salt: base64.decode('uNCkWiNYzKTnBN9ji3-qWA'),
305-
rs: 26,
307+
rs: 25,
306308
pad: 1
307309
},
308310
plaintext: Buffer.from('I am the walrus'),
309-
ciphertext: base64.decode('uNCkWiNYzKTnBN9ji3-qWAAAABoCYTGH' +
310-
'OqYFz-0in3dpb-VE2GfBngkaPy6bZus_' +
311-
'qLF79s6zQyTSsA0iLOKyd3JqVIwprNzV' +
312-
'atRCWZGUx_qsFbJBCQu62RqQuR2d')
311+
ciphertext: base64.decode('uNCkWiNYzKTnBN9ji3-qWAAAABkCYTHO' +
312+
'G8chz_gnvgOqdGYovxyjuqRyJFjEDyoF' +
313+
'1Fvkj6hQPdPHI51OEUKEpgz3SsLWIqS_' +
314+
'uA')
313315
}
314-
].forEach(function (v, i) {
316+
].filter(function(v) {
317+
return v.args.version === version;
318+
}).forEach(function (v, i) {
315319
log('decrypt ' + v.args.version + ' example ' + (i + 1));
316320
var decrypted = ece.decrypt(v.ciphertext, v.args);
317321
logbuf('decrypted', decrypted);
@@ -333,17 +337,17 @@ filterTests([ 'aesgcm128', 'aesgcm', 'aes128gcm' ])
333337
detectTruncation,
334338
useKeyId,
335339
useDH,
340+
checkExamples,
336341
])
337342
.forEach(function(test) {
338343
log(version + ' Test: ' + test.name);
339344
test(version);
345+
log('----- OK');
340346
});
341347
});
342-
checkExamples();
343348

344349
log('All tests passed.');
345350

346-
347351
if (dumpFile) {
348352
require('fs').writeFileSync(dumpFile, JSON.stringify(dumpData, undefined, ' '));
349353
}

python/http_ece/__init__.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
# Valid content types (ordered from newest, to most obsolete)
2424
versions = {
25-
"aes128gcm": {"pad": 2},
25+
"aes128gcm": {"pad": 1},
2626
"aesgcm": {"pad": 2},
2727
"aesgcm128": {"pad": 1},
2828
}
@@ -217,16 +217,29 @@ def decrypt_record(key, nonce, counter, content):
217217
modes.GCM(iv(nonce, counter), tag=content[-TAG_LENGTH:]),
218218
backend=default_backend()
219219
).decryptor()
220-
data = decryptor.update(content[:-TAG_LENGTH]) + decryptor.finalize()
220+
return decryptor.update(content[:-TAG_LENGTH]) + decryptor.finalize()
221+
222+
def unpad_legacy(data):
221223
pad = functools.reduce(
222224
lambda x, y: x << 8 | y, struct.unpack(
223225
"!" + ("B" * pad_size), data[0:pad_size])
224226
)
225227
if pad_size + pad > len(data) or \
226228
data[pad_size:pad_size+pad] != (b"\x00" * pad):
227229
raise ECEException(u"Bad padding")
228-
data = data[pad_size + pad:]
229-
return data
230+
return data[pad_size + pad:]
231+
232+
def unpad(data, last):
233+
i = len(data) - 1
234+
for i in range(len(data) - 1, -1, -1):
235+
if data[i] != 0:
236+
if not last and data[i] != 1:
237+
raise ECEException(u'record delimiter != 1')
238+
if last and data[i] != 2:
239+
raise ECEException(u'last record delimiter != 2')
240+
return data[0:i]
241+
i -= 1
242+
raise ECEException(u'all zero record plaintext')
230243

231244
if version not in versions:
232245
raise ECEException(u"Invalid version")
@@ -269,14 +282,19 @@ def decrypt_record(key, nonce, counter, content):
269282
chunk = rs + 16 # account for tags in old versions
270283
else:
271284
chunk = rs
272-
if len(content) % chunk == 0:
285+
if len(content) % chunk == 0 and version != 'aes128gcm':
273286
raise ECEException(u"Message truncated")
274287

275288
result = b''
276289
counter = 0
277290
try:
278291
for i in list(range(0, len(content), chunk)):
279-
result += decrypt_record(key_, nonce_, counter, content[i:i + chunk])
292+
data = decrypt_record(key_, nonce_, counter, content[i:i + chunk])
293+
if version == 'aes128gcm':
294+
last = (i + chunk) >= len(content)
295+
result += unpad(data, last)
296+
else:
297+
result += unpad_legacy(data)
280298
counter += 1
281299
except InvalidTag as ex:
282300
raise ECEException("Decryption error: {}".format(repr(ex)))
@@ -312,14 +330,17 @@ def encrypt(content, salt=None, key=None,
312330
:rtype str
313331
314332
"""
315-
def encrypt_record(key, nonce, counter, buf):
333+
def encrypt_record(key, nonce, counter, buf, last):
316334
encryptor = Cipher(
317335
algorithms.AES(key),
318336
modes.GCM(iv(nonce, counter)),
319337
backend=default_backend()
320338
).encryptor()
321339

322-
data = encryptor.update((b"\0" * pad_size) + buf)
340+
if version == 'aes128gcm':
341+
data = encryptor.update(buf + (b'\x02' if last else b'\x01'))
342+
else:
343+
data = encryptor.update((b"\0" * pad_size) + buf)
323344
data += encryptor.finalize()
324345
data += encryptor.tag
325346
return data
@@ -372,6 +393,9 @@ def compose_aes128gcm(salt, content, rs, keyid):
372393
overhead = pad_size
373394
if version == 'aes128gcm':
374395
overhead += 16
396+
end = len(content)
397+
else:
398+
end = len(content) + 1
375399
if rs <= pad_size:
376400
raise ECEException(u"Record size too small")
377401
chunk_size = rs - overhead
@@ -381,9 +405,10 @@ def compose_aes128gcm(salt, content, rs, keyid):
381405

382406
# the extra one on the loop ensures that we produce a padding only
383407
# record if the data length is an exact multiple of the chunk size
384-
for i in list(range(0, len(content) + 1, chunk_size)):
408+
for i in list(range(0, end, chunk_size)):
385409
result += encrypt_record(key_, nonce_, counter,
386-
content[i:i + chunk_size])
410+
content[i:i + chunk_size],
411+
(i + chunk_size) >= end)
387412
counter += 1
388413
if version == "aes128gcm":
389414
if keyid is None and private_key is not None:

0 commit comments

Comments
 (0)