Skip to content

Commit ae2f1f8

Browse files
committed
Move to a |version| parameter
1 parent 4b18253 commit ae2f1f8

6 files changed

Lines changed: 195 additions & 144 deletions

File tree

nodejs/decrypt-dh.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ receiver.generateKeys();
1919
receiver.setPublicKey(base64.decode(process.argv[4]));
2020
receiver.setPrivateKey(base64.decode(process.argv[3]));
2121
var keymap = {};
22-
keymap[''] = receiver;
2322

2423
var params = {
24+
type: 'aes128gcm',
2525
keyid: '',
2626
authSecret: process.argv[2],
2727
dh: process.argv[5],
@@ -34,6 +34,7 @@ if (process.argv.length > 7) {
3434
params[k] = extra[k];
3535
});
3636
}
37+
keymap[params.keyid] = receiver;
3738

3839
console.log("Params: " + JSON.stringify(params, null, 2));
3940
var result = ece.decrypt(base64.decode(process.argv[6]), params);

nodejs/decrypt.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ if (process.argv.length < 4) {
1111
}
1212

1313
var params = {
14+
version: 'aes128gcm',
1415
key: process.argv[2]
1516
};
1617

nodejs/ece.js

Lines changed: 104 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,26 @@
44
*
55
* === Note about versions ===
66
*
7-
* This code supports multiple versions of the draft:
7+
* This code supports multiple versions of the draft. This is selected using
8+
* the |version| parameter.
89
*
910
* aes128gcm: The most recent version, the salt, record size and key identifier
10-
* are included in a header that is part of the encrypted content coding. In
11-
* order to select this version, the |salt| parameter is omitted from calls
12-
* to encrypt() and decrypt(). A value for the salt is automatically
13-
* generated by this API when encrypting, which avoids errors.
11+
* are included in a header that is part of the encrypted content coding.
1412
*
15-
* aesgcm: The version that is widely deployed with WebPush (as of 2016-10).
16-
* This version is selected by default if you provide a |salt| parameter and
17-
* either omit the |padSize| parameter, or set it to 2.
13+
* aesgcm: The version that is widely deployed with WebPush (as of 2016-11).
14+
* This version is selected by default, unless you specify a |padSize| of 1.
1815
*
1916
* aesgcm128: This version is old and will be removed in an upcoming release.
20-
* Select this version by providing |salt| and a |padSize| of 1.
17+
* This version is selected by providing a |padSize| parameter of 1.
2118
*/
2219

2320
var crypto = require('crypto');
2421
var base64 = require('urlsafe-base64');
2522

26-
var savedKeys = {};
27-
var keyLabels = {};
23+
var saved = {
24+
keymap: {},
25+
keylabels: {}
26+
};
2827
var AES_GCM = 'aes-128-gcm';
2928
var PAD_SIZE = { 'aes128gcm': 2, 'aesgcm': 2, 'aesgcm128': 1 };
3029
var TAG_LENGTH = 16;
@@ -44,6 +43,14 @@ if (process.env.ECE_KEYLOG === '1') {
4443
keylog = function(m, k) { return k; };
4544
}
4645

46+
/* Optionally base64 decode something. */
47+
function decode(b) {
48+
if (typeof b === 'string') {
49+
return base64.decode(b);
50+
}
51+
return b;
52+
}
53+
4754
function HMAC_hash(key, input) {
4855
var hmac = crypto.createHmac('sha256', key);
4956
hmac.update(input);
@@ -93,29 +100,30 @@ function lengthPrefix(buffer) {
93100
return b;
94101
}
95102

96-
function extractDH(keyid, share, mode) {
97-
if (!savedKeys[keyid]) {
98-
throw new Error('No known DH key for ' + keyid);
103+
function extractDH(header, mode) {
104+
if (!header.keymap[header.keyid]) {
105+
throw new Error('No known DH key for ' + header.keyid);
99106
}
100-
if (!keyLabels[keyid]) {
101-
throw new Error('No known DH key label for ' + keyid);
107+
if (!header.keylabels[header.keyid]) {
108+
throw new Error('No known DH key label for ' + header.keyid);
102109
}
103-
var key = savedKeys[keyid];
110+
var key = header.keymap[header.keyid];
104111
var senderPubKey, receiverPubKey;
105112
if (mode === MODE_ENCRYPT) {
106113
senderPubKey = key.getPublicKey();
107-
receiverPubKey = share;
114+
receiverPubKey = header.dh;
108115
} else if (mode === MODE_DECRYPT) {
109-
senderPubKey = share;
116+
senderPubKey = header.dh;
110117
receiverPubKey = key.getPublicKey();
111118
} else {
112119
throw new Error('Unknown mode only ' + MODE_ENCRYPT +
113120
' and ' + MODE_DECRYPT + ' supported');
114121
}
115122
return {
116-
secret: key.computeSecret(share),
123+
secret: key.computeSecret(header.dh),
117124
context: Buffer.concat([
118-
keyLabels[keyid],
125+
decode(header.keylabels[header.keyid]),
126+
Buffer.from([0]),
119127
lengthPrefix(receiverPubKey),
120128
lengthPrefix(senderPubKey)
121129
])
@@ -130,9 +138,9 @@ function extractSecretAndContext(header, mode) {
130138
throw new Error('An explicit key must be ' + KEY_LENGTH + ' bytes');
131139
}
132140
} else if (header.dh) { // receiver/decrypt
133-
result = extractDH(header.keyid, header.dh, mode);
141+
result = extractDH(header, mode);
134142
} else if (header.keyid) {
135-
result.secret = savedKeys[header.keyid];
143+
result.secret = header.keymap[header.keyid];
136144
}
137145
if (!result.secret) {
138146
throw new Error('Unable to determine key');
@@ -192,29 +200,29 @@ function extractSecret(header, mode) {
192200

193201
function deriveKeyAndNonce(header, mode) {
194202
if (!header.salt) {
195-
throw new Error('must include a salt parameter for ' + header.type);
203+
throw new Error('must include a salt parameter for ' + header.version);
196204
}
197205
var keyInfo;
198206
var nonceInfo;
199207
var secret;
200-
if (header.type === 'aesgcm128') {
208+
if (header.version === 'aesgcm128') {
201209
// really old
202210
keyInfo = 'Content-Encoding: aesgcm128';
203211
nonceInfo = 'Content-Encoding: nonce';
204212
secret = extractSecretAndContext(header, mode).secret;
205-
} else if (header.type === 'aesgcm') {
213+
} else if (header.version === 'aesgcm') {
206214
// old
207215
var s = extractSecretAndContext(header, mode);
208216
keyInfo = info('aesgcm', s.context);
209217
nonceInfo = info('nonce', s.context);
210218
secret = s.secret;
211-
} else if (header.type === 'aes128gcm') {
219+
} else if (header.version === 'aes128gcm') {
212220
// latest
213221
keyInfo = Buffer.from('Content-Encoding: aes128gcm\0');
214222
nonceInfo = Buffer.from('Content-Encoding: nonce\0');
215223
secret = extractSecret(header, mode);
216224
} else {
217-
throw new Error('Unable to set context for mode ' + params.type);
225+
throw new Error('Unable to set context for mode ' + params.version);
218226
}
219227
var prk = HKDF_extract(header.salt, secret);
220228
var result = {
@@ -227,55 +235,47 @@ function deriveKeyAndNonce(header, mode) {
227235
}
228236

229237
function fillCommonParams(header, params) {
230-
if (params.key) {
231-
header.key = base64.decode(params.key);
232-
}
233-
header.keymap = params.keymap;
234-
if (params.dh) {
235-
header.dh = base64.decode(params.dh);
236-
}
237-
if (params.authSecret) {
238-
header.authSecret = base64.decode(params.authSecret);
239-
}
240238
return header;
241239
}
242240

243-
/* Used when decrypting aes128gcm to populate the header values. */
244-
function readHeader(buffer, params) {
245-
var idsz = buffer.readUIntBE(20, 1);
246-
var header = {
247-
type: 'aes128gcm',
248-
salt: buffer.slice(0, KEY_LENGTH),
249-
rs: buffer.readUIntBE(KEY_LENGTH, 4),
250-
keyid: buffer.slice(21, 21 + idsz).toString('utf-8'),
251-
headerLength: 21 + idsz
252-
};
253-
return fillCommonParams(header, params);
254-
}
255-
256-
/* Used when decrypting to populate the header values for aesgcm[128]. Used also
257-
* to parse the |param| argument when encrypting. */
241+
/* Parse command-line arguments. */
258242
function parseParams(params) {
259-
var header = {
260-
type: (params.padSize === 1) ? 'aesgcm128' : 'aesgcm',
261-
keyid: params.keyid,
262-
headerLength: 0
263-
};
243+
var header = {};
244+
if (params.version) {
245+
header.version = params.version;
246+
} else {
247+
header.version = (params.padSize === 1) ? 'aesgcm128' : 'aesgcm';
248+
}
249+
264250
header.rs = parseInt(params.rs, 10);
265251
if (isNaN(header.rs)) {
266252
header.rs = 4096;
267253
}
268-
if (header.rs <= PAD_SIZE[header.type]) {
254+
if (header.rs <= PAD_SIZE[header.version]) {
269255
throw new Error('The rs parameter has to be greater than ' +
270-
PAD_SIZE[header.type]);
256+
PAD_SIZE[header.version]);
271257
}
258+
272259
if (params.salt) {
273-
header.salt = base64.decode(params.salt);
260+
header.salt = decode(params.salt);
274261
if (header.salt.length !== KEY_LENGTH) {
275262
throw new Error('The salt parameter must be ' + KEY_LENGTH + ' bytes');
276263
}
277264
}
278-
return fillCommonParams(header, params);
265+
header.keyid = params.keyid;
266+
if (params.key) {
267+
header.key = decode(params.key);
268+
} else {
269+
header.keymap = params.keymap || saved.keymap;
270+
header.keylabels = params.keylabels || saved.keylabels;
271+
if (params.dh) {
272+
header.dh = decode(params.dh);
273+
}
274+
}
275+
if (params.authSecret) {
276+
header.authSecret = decode(params.authSecret);
277+
}
278+
return header;
279279
}
280280

281281
function generateNonce(base, counter) {
@@ -288,6 +288,16 @@ function generateNonce(base, counter) {
288288
return nonce;
289289
}
290290

291+
/* Used when decrypting aes128gcm to populate the header values. Modifies the
292+
* header values in place and returns the size of the header. */
293+
function readHeader(buffer, header) {
294+
var idsz = buffer.readUIntBE(20, 1);
295+
header.salt = buffer.slice(0, KEY_LENGTH);
296+
header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
297+
header.keyid = buffer.slice(21, 21 + idsz).toString('utf-8');
298+
return 21 + idsz;
299+
}
300+
291301
function decryptRecord(key, counter, buffer, header) {
292302
keylog('decrypt', buffer);
293303
var nonce = generateNonce(key.nonce, counter);
@@ -296,11 +306,12 @@ function decryptRecord(key, counter, buffer, header) {
296306
var data = gcm.update(buffer.slice(0, buffer.length - TAG_LENGTH));
297307
data = Buffer.concat([data, gcm.final()]);
298308
keylog('decrypted', data);
299-
var padSize = PAD_SIZE[header.type];
309+
var padSize = PAD_SIZE[header.version];
300310
var pad = data.readUIntBE(0, padSize);
301311
if (pad + padSize > data.length) {
302312
throw new Error('padding exceeds block size');
303313
}
314+
keylog('padding', data.slice(0, padSize + pad));
304315
var padCheck = new Buffer(pad);
305316
padCheck.fill(0);
306317
if (padCheck.compare(data.slice(padSize, padSize + pad)) !== 0) {
@@ -315,6 +326,11 @@ function decryptRecord(key, counter, buffer, header) {
315326
* Decrypt some bytes. This uses the parameters to determine the key and block
316327
* size, which are described in the draft. Binary values are base64url encoded.
317328
*
329+
* |params.version| contains the version of encoding to use: aes128gcm is the latest,
330+
* but aesgcm and aesgcm128 are also accepted (though the latter two might
331+
* disappear in a future release). If omitted, assume aesgcm, unless
332+
* |params.padSize| is set to 1, which means aesgcm128.
333+
*
318334
* If |params.key| is specified, that value is used as the key.
319335
*
320336
* If |params.keyid| is specified without |params.dh|, the keyid value is used
@@ -324,14 +340,12 @@ function decryptRecord(key, counter, buffer, header) {
324340
* pair used to decrypt is looked up using |params.keymap[params.keyid]|.
325341
*/
326342
function decrypt(buffer, params) {
327-
var header;
328-
if (params.salt) {
329-
header = parseParams(params);
330-
} else {
331-
header = readHeader(buffer, params);
343+
var header = parseParams(params);
344+
if (header.version === 'aes128gcm') {
345+
var headerLength = readHeader(buffer, header);
346+
buffer = buffer.slice(headerLength);
332347
}
333348
var key = deriveKeyAndNonce(header, MODE_DECRYPT);
334-
buffer = buffer.slice(header.headerLength || 0);
335349
var start = 0;
336350
var result = new Buffer(0);
337351

@@ -360,6 +374,7 @@ function encryptRecord(key, counter, buffer, pad, padSize) {
360374
var padding = new Buffer(pad + padSize);
361375
padding.fill(0);
362376
padding.writeUIntBE(pad, 0, padSize);
377+
keylog('padding', padding);
363378
var epadding = gcm.update(padding);
364379
var ebuffer = gcm.update(buffer);
365380
gcm.final();
@@ -370,9 +385,9 @@ function encryptRecord(key, counter, buffer, pad, padSize) {
370385
return keylog('encrypted', Buffer.concat([epadding, ebuffer, tag]));
371386
}
372387

373-
function encodeHeader(header) {
388+
function writeHeader(header) {
374389
var ints = new Buffer(5);
375-
var keyid = Buffer.from(header.keyid || '');
390+
var keyid = Buffer.from(header.keyid || []);
376391
if (keyid.length > 255) {
377392
throw new Error('keyid is too large');
378393
}
@@ -385,6 +400,11 @@ function encodeHeader(header) {
385400
* Encrypt some bytes. This uses the parameters to determine the key and block
386401
* size, which are described in the draft.
387402
*
403+
* |params.version| contains the version of encoding to use: aes128gcm is the latest,
404+
* but aesgcm and aesgcm128 are also accepted (though the latter two might
405+
* disappear in a future release). If omitted, assume aesgcm, unless
406+
* |params.padSize| is set to 1, which means aesgcm128.
407+
*
388408
* If |params.key| is specified, that value is used as the key.
389409
*
390410
* If |params.keyid| is specified without |params.dh|, the keyid value is used
@@ -398,20 +418,22 @@ function encrypt(buffer, params) {
398418
if (!Buffer.isBuffer(buffer)) {
399419
throw new Error('buffer argument must be a Buffer');
400420
}
421+
var header = parseParams(params);
422+
if (!header.salt) {
423+
header.salt = crypto.randomBytes(KEY_LENGTH);
424+
}
425+
401426
var result;
402-
var header;
403-
header = parseParams(params);
404-
if (params.salt) { // old versions
405-
result = new Buffer(0);
427+
if (header.version === 'aes128gcm') {
428+
result = writeHeader(header);
406429
} else {
407-
header.type = 'aes128gcm';
408-
header.salt = crypto.randomBytes(KEY_LENGTH);
409-
result = encodeHeader(header);
430+
// No header on other versions
431+
result = new Buffer(0);
410432
}
411433

412434
var key = deriveKeyAndNonce(header, MODE_ENCRYPT);
413435
var start = 0;
414-
var padSize = PAD_SIZE[header.type];
436+
var padSize = PAD_SIZE[header.version];
415437
var pad = isNaN(parseInt(params.pad, 10)) ? 0 : parseInt(params.pad, 10);
416438

417439
// Note the <= here ensures that we write out a padding-only block at the end
@@ -435,19 +457,12 @@ function encrypt(buffer, params) {
435457
}
436458

437459
/**
438-
* This function saves a key under the provided identifier. This is only used
439-
* by aesgcm and aesgcm128 codings. For the aes128gcm coding, use the |keymap|
440-
* parameter to encrypt() and decrypt().
441-
*
442-
* This is used to save the keys that are used to decrypt and encrypt blobs that
443-
* are identified by a 'keyid'. DH or ECDH keys that are used with the 'dh'
444-
* parameter need to include a label (included in 'dhLabel') that identifies
445-
* them.
460+
* Deprecated. Use the keymap and keylabels arguments to encrypt()/decrypt().
446461
*/
447462
function saveKey(id, key, dhLabel) {
448-
savedKeys[id] = key;
463+
saved.keymap[id] = key;
449464
if (dhLabel) {
450-
keyLabels[id] = new Buffer(dhLabel + '\0', 'ascii');
465+
saved.keylabels[id] = dhLabel;
451466
}
452467
}
453468

0 commit comments

Comments
 (0)