Skip to content

Commit 593dc61

Browse files
committed
Merge pull request #14 from martinthomson/padfix
Padfix
2 parents fa8a798 + 9d09eae commit 593dc61

9 files changed

Lines changed: 426 additions & 187 deletions

File tree

nodejs/decrypt-dh.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
var base64 = require('urlsafe-base64');
4+
var crypto = require('crypto');
5+
var ece = require('./ece.js');
6+
7+
if (process.argv.length < 7) {
8+
console.warn('Usage: ' + process.argv.slice(0, 2).join(' ') +
9+
' <receiver-private> <receiver-public> <sender-public> <salt> <message> [JSON args]');
10+
process.exit(2);
11+
}
12+
13+
var receiver = crypto.createECDH('prime256v1');
14+
// node crypto is finicky about accessing the public key
15+
// 1. it can't generate the public key from the private key
16+
// 2. it barfs when you try to access the public key, even after you set it
17+
// This hack squelches the complaints at the cost of a few wasted cycles
18+
receiver.generateKeys();
19+
receiver.setPublicKey(base64.decode(process.argv[3]));
20+
receiver.setPrivateKey(base64.decode(process.argv[2]));
21+
ece.saveKey('keyid', receiver, "P-256");
22+
23+
var params = {
24+
keyid: 'keyid',
25+
dh: process.argv[4],
26+
salt: process.argv[5]
27+
};
28+
29+
if (process.argv.length > 7) {
30+
var extra = JSON.parse(process.argv[7]);
31+
Object.keys(extra).forEach(function(k) {
32+
params[k] = extra[k];
33+
});
34+
}
35+
36+
console.log("Params: " + JSON.stringify(params, null, 2));
37+
var result = ece.decrypt(base64.decode(process.argv[6]), params);
38+
39+
console.log(base64.encode(result));
40+
console.log(result.toString('utf-8'));

nodejs/decrypt.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@ var base64 = require('urlsafe-base64');
44
var crypto = require('crypto');
55
var ece = require('./ece.js');
66

7-
if (process.argv.length < 6) {
7+
if (process.argv.length < 5) {
88
console.warn('Usage: ' + process.argv.slice(0, 2).join(' ') +
9-
' <receiver-private> <sender-public> <salt> <message>');
9+
' <key> <salt> <message> [JSON args]');
1010
process.exit(2);
1111
}
1212

13-
var receiver = crypto.createECDH('prime256v1');
14-
receiver.setPrivateKey(base64.decode(process.argv[2]));
15-
ece.saveKey('keyid', receiver);
13+
var params = {
14+
key: process.argv[2],
15+
salt: process.argv[3]
16+
};
1617

17-
var result = ece.decrypt(base64.decode(process.argv[5]), {
18-
keyid: 'keyid',
19-
dh: process.argv[3],
20-
salt: process.argv[4]
21-
});
18+
if (process.argv.length > 5) {
19+
var extra = JSON.parse(process.argv[5]);
20+
Object.keys(extra).forEach(function(k) {
21+
params[k] = extra[k];
22+
});
23+
}
24+
25+
console.log("Params: " + JSON.stringify(params, null, 2));
26+
var result = ece.decrypt(base64.decode(process.argv[4]), params);
2227

2328
console.log(base64.encode(result));
2429
console.log(result.toString('utf-8'));

nodejs/ece.js

Lines changed: 124 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ var crypto = require('crypto');
44
var base64 = require('urlsafe-base64');
55

66
var savedKeys = {};
7+
var keyLabels = {};
78
var AES_GCM = 'id-aes128-GCM';
9+
var PAD_SIZE = 2;
810
var TAG_LENGTH = 16;
911
var KEY_LENGTH = 16;
1012
var NONCE_LENGTH = 12;
13+
var SHA_256_LENGTH = 32;
14+
var MODE_ENCRYPT = 'encrypt';
15+
var MODE_DECRYPT = 'decrypt';
1116

1217
function HMAC_hash(key, input) {
1318
var hmac = crypto.createHmac('sha256', key);
@@ -35,45 +40,105 @@ function HKDF_expand(prk, info, l) {
3540
return output.slice(0, l);
3641
}
3742

38-
function extractKey(params) {
39-
var secret;
43+
function HKDF(salt, ikm, info, len) {
44+
return HKDF_expand(HKDF_extract(salt, ikm), info, len);
45+
}
46+
47+
function info(base, context) {
48+
return Buffer.concat([
49+
new Buffer('Content-Encoding: ' + base + '\0', 'ascii'),
50+
context
51+
]);
52+
}
53+
54+
function extractSalt(salt) {
55+
if (!salt) {
56+
throw new Error('A salt is required');
57+
}
58+
salt = base64.decode(salt);
59+
if (salt.length !== KEY_LENGTH) {
60+
throw new Error('The salt parameter must be ' + KEY_LENGTH + ' bytes');
61+
}
62+
return salt;
63+
}
64+
65+
function lengthPrefix(buffer) {
66+
var b = Buffer.concat([ new Buffer(2), buffer ]);
67+
b.writeUIntBE(buffer.length, 0, 2);
68+
return b;
69+
}
70+
71+
function extractDH(keyid, dh, mode) {
72+
if (!savedKeys[keyid]) {
73+
throw new Error('No known DH key for ' + keyid);
74+
}
75+
if (!keyLabels[keyid]) {
76+
throw new Error('No known DH key label for ' + keyid);
77+
}
78+
var share = base64.decode(dh);
79+
var key = savedKeys[keyid];
80+
var senderPubKey, receiverPubKey;
81+
if (mode === MODE_ENCRYPT) {
82+
senderPubKey = key.getPublicKey();
83+
receiverPubKey = share;
84+
} else if (mode === MODE_DECRYPT) {
85+
senderPubKey = share;
86+
receiverPubKey = key.getPublicKey();
87+
} else {
88+
throw new Error('Unknown mode only ' + MODE_ENCRYPT +
89+
' and ' + MODE_DECRYPT + ' supported');
90+
}
91+
return {
92+
secret: key.computeSecret(share),
93+
context: Buffer.concat([
94+
keyLabels[keyid],
95+
lengthPrefix(receiverPubKey),
96+
lengthPrefix(senderPubKey)
97+
])
98+
};
99+
}
100+
101+
function extractSecretAndContext(params, mode) {
102+
var result = { secret: null, context: new Buffer(0) };
40103
if (params.key) {
41-
secret = base64.decode(params.key);
42-
if (secret.length !== KEY_LENGTH) {
104+
result.secret = base64.decode(params.key);
105+
if (result.secret.length !== KEY_LENGTH) {
43106
throw new Error('An explicit key must be ' + KEY_LENGTH + ' bytes');
44107
}
45108
} else if (params.dh) { // receiver/decrypt
46-
var share = base64.decode(params.dh);
47-
var key = savedKeys[params.keyid];
48-
secret = key.computeSecret(share);
109+
result = extractDH(params.keyid, params.dh, mode);
49110
} else if (params.keyid) {
50-
secret = savedKeys[params.keyid];
111+
result.secret = savedKeys[params.keyid];
51112
}
52-
if (!secret) {
113+
if (!result.secret) {
53114
throw new Error('Unable to determine key');
54115
}
55-
if (!params.salt) {
56-
throw new Error('A salt is required');
116+
if (params.authSecret) {
117+
result.secret = HKDF(base64.decode(params.authSecret), result.secret,
118+
info('auth', new Buffer(0)), SHA_256_LENGTH);
57119
}
120+
return result;
121+
}
58122

59-
var salt = base64.decode(params.salt);
60-
if (salt.length !== KEY_LENGTH) {
61-
throw new Error('The salt parameter must be ' + KEY_LENGTH + ' bytes');
62-
}
63-
var prk = HKDF_extract(salt, secret);
64-
return {
65-
key: HKDF_expand(prk, 'Content-Encoding: aesgcm128', KEY_LENGTH),
66-
nonce: HKDF_expand(prk, 'Content-Encoding: nonce', NONCE_LENGTH)
123+
function deriveKeyAndNonce(params, mode) {
124+
var salt = extractSalt(params.salt);
125+
var s = extractSecretAndContext(params, mode);
126+
var prk = HKDF_extract(salt, s.secret);
127+
var result = {
128+
key: HKDF_expand(prk, info('aesgcm128', s.context), KEY_LENGTH),
129+
nonce: HKDF_expand(prk, info('nonce', s.context), NONCE_LENGTH)
67130
};
131+
return result;
68132
}
69133

70134
function determineRecordSize(params) {
71135
var rs = parseInt(params.rs, 10);
72136
if (isNaN(rs)) {
73137
return 4096;
74138
}
75-
if (rs <= 1) {
76-
throw new Error('The rs parameter has to be greater than 1');
139+
var padSize = params.padSize || PAD_SIZE;
140+
if (rs <= padSize) {
141+
throw new Error('The rs parameter has to be greater than ' + padSize);
77142
}
78143
return rs;
79144
}
@@ -87,22 +152,23 @@ function generateNonce(base, counter) {
87152
return nonce;
88153
}
89154

90-
function decryptRecord(key, counter, buffer) {
155+
function decryptRecord(key, counter, buffer, padSize) {
91156
var nonce = generateNonce(key.nonce, counter);
92157
var gcm = crypto.createDecipheriv(AES_GCM, key.key, nonce);
93158
gcm.setAuthTag(buffer.slice(buffer.length - TAG_LENGTH));
94159
var data = gcm.update(buffer.slice(0, buffer.length - TAG_LENGTH));
95160
data = Buffer.concat([data, gcm.final()]);
96-
var pad = data.readUInt8(0);
97-
if (pad + 1 > data.length) {
161+
padSize = padSize || PAD_SIZE
162+
var pad = data.readUIntBE(0, padSize);
163+
if (pad + padSize > data.length) {
98164
throw new Error('padding exceeds block size');
99165
}
100166
var padCheck = new Buffer(pad);
101167
padCheck.fill(0);
102-
if (padCheck.compare(data.slice(1, 1 + pad)) !== 0) {
168+
if (padCheck.compare(data.slice(padSize, padSize + pad)) !== 0) {
103169
throw new Error('invalid padding');
104170
}
105-
return data.slice(1 + pad);
171+
return data.slice(padSize + pad);
106172
}
107173

108174
// TODO: this really should use the node streams stuff
@@ -117,7 +183,7 @@ function decryptRecord(key, counter, buffer) {
117183
* saveKey().
118184
*/
119185
function decrypt(buffer, params) {
120-
var key = extractKey(params);
186+
var key = deriveKeyAndNonce(params, MODE_DECRYPT);
121187
var rs = determineRecordSize(params);
122188
var start = 0;
123189
var result = new Buffer(0);
@@ -131,20 +197,22 @@ function decrypt(buffer, params) {
131197
if (end - start <= TAG_LENGTH) {
132198
throw new Error('Invalid block: too small at ' + i);
133199
}
134-
var block = decryptRecord(key, i, buffer.slice(start, end));
200+
var block = decryptRecord(key, i, buffer.slice(start, end),
201+
params.padSize);
135202
result = Buffer.concat([result, block]);
136203
start = end;
137204
}
138205
return result;
139206
}
140207

141-
function encryptRecord(key, counter, buffer, pad) {
208+
function encryptRecord(key, counter, buffer, pad, padSize) {
142209
pad = pad || 0;
143210
var nonce = generateNonce(key.nonce, counter);
144211
var gcm = crypto.createCipheriv(AES_GCM, key.key, nonce);
145-
var padding = new Buffer(pad + 1);
212+
padSize = padSize || PAD_SIZE;
213+
var padding = new Buffer(pad + padSize);
146214
padding.fill(0);
147-
padding.writeUIntBE(pad, 0, 1);
215+
padding.writeUIntBE(pad, 0, padSize);
148216
var epadding = gcm.update(padding);
149217
var ebuffer = gcm.update(buffer);
150218
gcm.final();
@@ -159,30 +227,48 @@ function encryptRecord(key, counter, buffer, pad) {
159227
* Encrypt some bytes. This uses the parameters to determine the key and block
160228
* size, which are described in the draft. Note that for encryption, the
161229
* p256-dh parameter identifies the public share of the recipient and the keyid
162-
* identifies a local ECDH key pair (created by crypto.createECDH()).
230+
* identifies a local DH key pair (created by crypto.createECDH() or
231+
* crypto.createDiffieHellman()).
163232
*/
164233
function encrypt(buffer, params) {
165-
var key = extractKey(params);
234+
var key = deriveKeyAndNonce(params, MODE_ENCRYPT);
166235
var rs = determineRecordSize(params);
167236
var start = 0;
168237
var result = new Buffer(0);
238+
var padSize = params.padSize || PAD_SIZE;
239+
var pad = isNaN(parseInt(params.pad, 10)) ? 0 : parseInt(params.pad, 10);
169240

241+
// Note the <= here ensures that we write out a padding-only block at the end
242+
// of a buffer.
170243
for (var i = 0; start <= buffer.length; ++i) {
171-
var end = Math.min(start + rs - 1, buffer.length);
172-
var block = encryptRecord(key, i, buffer.slice(start, end));
244+
// Pad so that at least one data byte is in a block.
245+
var recordPad = Math.min((1 << (padSize * 8)) - 1, // maximum padding
246+
Math.min(rs - padSize - 1, pad));
247+
pad -= recordPad;
248+
249+
var end = Math.min(start + rs - padSize - recordPad, buffer.length);
250+
var block = encryptRecord(key, i, buffer.slice(start, end),
251+
recordPad, padSize);
173252
result = Buffer.concat([result, block]);
174-
start += rs - 1;
253+
start += rs - padSize - recordPad;
254+
}
255+
if (pad) {
256+
throw new Error('Unable to pad by requested amount, ' + pad + ' remaining');
175257
}
176258
return result;
177259
}
178260

179261
/**
180262
* This function saves a key under the provided identifier. This is used to
181263
* save the keys that are used to decrypt and encrypt blobs that are identified
182-
* by a 'keyid'.
264+
* by a 'keyid'. DH or ECDH keys that are used with the 'dh' parameter need to
265+
* include a label (included in 'dhLabel') that identifies them.
183266
*/
184-
function saveKey(id, key) {
267+
function saveKey(id, key, dhLabel) {
185268
savedKeys[id] = key;
269+
if (dhLabel) {
270+
keyLabels[id] = new Buffer(dhLabel + '\0', 'ascii');
271+
}
186272
}
187273

188274
module.exports = {

nodejs/encrypt-dh.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
var base64 = require('urlsafe-base64');
4+
var crypto = require('crypto');
5+
var ece = require('./ece.js');
6+
7+
if (process.argv.length < 4) {
8+
console.warn('Usage: ' + process.argv.slice(0, 2).join(' ') +
9+
' <receiver-public> <message> [JSON args]');
10+
process.exit(2);
11+
}
12+
13+
var sender = crypto.createECDH('prime256v1');
14+
sender.generateKeys();
15+
ece.saveKey('keyid', sender, "P-256");
16+
17+
var salt = base64.encode(crypto.randomBytes(16));
18+
19+
var params = {
20+
keyid: 'keyid',
21+
dh: process.argv[2],
22+
salt: salt
23+
};
24+
25+
if (process.argv.length > 4) {
26+
var extra = JSON.parse(process.argv[4]);
27+
Object.keys(extra).forEach(function(k) {
28+
params[k] = extra[k];
29+
});
30+
}
31+
32+
console.log("Params: " + JSON.stringify(params, null, 2));
33+
var result = ece.encrypt(base64.decode(process.argv[3]), params);
34+
35+
console.log("Salt: " + salt);
36+
console.log("Public Key: " + base64.encode(sender.getPublicKey()));
37+
console.log("Encrypted Message: " + base64.encode(result));

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.2.0",
3+
"version": "0.4.1",
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",

0 commit comments

Comments
 (0)