Comparing version 0.5.2 to 0.6.0
357
ece.js
'use strict'; | ||
/* | ||
* Encrypted content coding | ||
* | ||
* === Note about versions === | ||
* | ||
* This code supports multiple versions of the draft. This is selected using | ||
* the |version| parameter. | ||
* | ||
* aes128gcm: The most recent version, the salt, record size and key identifier | ||
* are included in a header that is part of the encrypted content coding. | ||
* | ||
* aesgcm: The version that is widely deployed with WebPush (as of 2016-11). | ||
* This version is selected by default, unless you specify a |padSize| of 1. | ||
* | ||
* aesgcm128: This version is old and will be removed in an upcoming release. | ||
* This version is selected by providing a |padSize| parameter of 1. | ||
*/ | ||
@@ -6,6 +23,8 @@ var crypto = require('crypto'); | ||
var savedKeys = {}; | ||
var keyLabels = {}; | ||
var saved = { | ||
keymap: {}, | ||
keylabels: {} | ||
}; | ||
var AES_GCM = 'aes-128-gcm'; | ||
var PAD_SIZE = 2; | ||
var PAD_SIZE = { 'aes128gcm': 2, 'aesgcm': 2, 'aesgcm128': 1 }; | ||
var TAG_LENGTH = 16; | ||
@@ -22,7 +41,16 @@ var KEY_LENGTH = 16; | ||
console.warn(m + ' [' + k.length + ']: ' + base64.encode(k)); | ||
return k; | ||
}; | ||
} else { | ||
keylog = function() {}; | ||
keylog = function(m, k) { return k; }; | ||
} | ||
/* Optionally base64 decode something. */ | ||
function decode(b) { | ||
if (typeof b === 'string') { | ||
return base64.decode(b); | ||
} | ||
return b; | ||
} | ||
function HMAC_hash(key, input) { | ||
@@ -36,6 +64,10 @@ var hmac = crypto.createHmac('sha256', key); | ||
function HKDF_extract(salt, ikm) { | ||
return HMAC_hash(salt, ikm); | ||
keylog('salt', salt); | ||
keylog('ikm', ikm); | ||
return keylog('extract', HMAC_hash(salt, ikm)); | ||
} | ||
function HKDF_expand(prk, info, l) { | ||
keylog('prk', prk); | ||
keylog('info', info); | ||
var output = new Buffer(0); | ||
@@ -52,3 +84,3 @@ var T = new Buffer(0); | ||
return output.slice(0, l); | ||
return keylog('expand', output.slice(0, l)); | ||
} | ||
@@ -69,15 +101,4 @@ | ||
function extractSalt(salt) { | ||
if (!salt) { | ||
throw new Error('A salt is required'); | ||
} | ||
salt = base64.decode(salt); | ||
if (salt.length !== KEY_LENGTH) { | ||
throw new Error('The salt parameter must be ' + KEY_LENGTH + ' bytes'); | ||
} | ||
return salt; | ||
} | ||
function lengthPrefix(buffer) { | ||
var b = Buffer.concat([ new Buffer(2), buffer ]); | ||
var b = Buffer.concat([new Buffer(2), buffer]); | ||
b.writeUIntBE(buffer.length, 0, 2); | ||
@@ -87,17 +108,19 @@ return b; | ||
function extractDH(keyid, dh, mode) { | ||
if (!savedKeys[keyid]) { | ||
throw new Error('No known DH key for ' + keyid); | ||
function extractDH(header, mode) { | ||
var key = header.privateKey; | ||
if (!key) { | ||
if (!header.keymap || !header.keyid || !header.keymap[header.keyid]) { | ||
throw new Error('No known DH key for ' + header.keyid); | ||
} | ||
key = header.keymap[header.keyid]; | ||
} | ||
if (!keyLabels[keyid]) { | ||
throw new Error('No known DH key label for ' + keyid); | ||
if (!header.keylabels[header.keyid]) { | ||
throw new Error('No known DH key label for ' + header.keyid); | ||
} | ||
var share = base64.decode(dh); | ||
var key = savedKeys[keyid]; | ||
var senderPubKey, receiverPubKey; | ||
if (mode === MODE_ENCRYPT) { | ||
senderPubKey = key.getPublicKey(); | ||
receiverPubKey = share; | ||
receiverPubKey = header.dh; | ||
} else if (mode === MODE_DECRYPT) { | ||
senderPubKey = share; | ||
senderPubKey = header.dh; | ||
receiverPubKey = key.getPublicKey(); | ||
@@ -109,7 +132,8 @@ } else { | ||
return { | ||
secret: key.computeSecret(share), | ||
secret: key.computeSecret(header.dh), | ||
context: Buffer.concat([ | ||
keyLabels[keyid], | ||
lengthPrefix(receiverPubKey), | ||
lengthPrefix(senderPubKey) | ||
decode(header.keylabels[header.keyid]), | ||
Buffer.from([0]), | ||
lengthPrefix(receiverPubKey), // user agent | ||
lengthPrefix(senderPubKey) // application server | ||
]) | ||
@@ -119,13 +143,13 @@ }; | ||
function extractSecretAndContext(params, mode) { | ||
function extractSecretAndContext(header, mode) { | ||
var result = { secret: null, context: new Buffer(0) }; | ||
if (params.key) { | ||
result.secret = base64.decode(params.key); | ||
if (header.key) { | ||
result.secret = header.key; | ||
if (result.secret.length !== KEY_LENGTH) { | ||
throw new Error('An explicit key must be ' + KEY_LENGTH + ' bytes'); | ||
} | ||
} else if (params.dh) { // receiver/decrypt | ||
result = extractDH(params.keyid, params.dh, mode); | ||
} else if (params.keyid) { | ||
result.secret = savedKeys[params.keyid]; | ||
} else if (header.dh) { // receiver/decrypt | ||
result = extractDH(header, mode); | ||
} else if (typeof header.keyid !== undefined) { | ||
result.secret = header.keymap[header.keyid]; | ||
} | ||
@@ -137,4 +161,4 @@ if (!result.secret) { | ||
keylog('context', result.context); | ||
if (params.authSecret) { | ||
result.secret = HKDF(base64.decode(params.authSecret), result.secret, | ||
if (header.authSecret) { | ||
result.secret = HKDF(header.authSecret, result.secret, | ||
info('auth', new Buffer(0)), SHA_256_LENGTH); | ||
@@ -146,18 +170,80 @@ keylog('authsecret', result.secret); | ||
function deriveKeyAndNonce(params, mode) { | ||
var padSize = params.padSize || PAD_SIZE; | ||
var salt = extractSalt(params.salt); | ||
var s = extractSecretAndContext(params, mode); | ||
var prk = HKDF_extract(salt, s.secret); | ||
function webpushSecret(header, mode) { | ||
if (!header.authSecret) { | ||
throw new Error('No authentication secret for webpush'); | ||
} | ||
keylog('authsecret', header.authSecret); | ||
var remotePubKey, senderPubKey, receiverPubKey; | ||
if (mode === MODE_ENCRYPT) { | ||
senderPubKey = header.privateKey.getPublicKey(); | ||
remotePubKey = receiverPubKey = header.dh; | ||
} else if (mode === MODE_DECRYPT) { | ||
remotePubKey = senderPubKey = header.keyid; | ||
receiverPubKey = header.privateKey.getPublicKey(); | ||
} else { | ||
throw new Error('Unknown mode only ' + MODE_ENCRYPT + | ||
' and ' + MODE_DECRYPT + ' supported'); | ||
} | ||
keylog('remote pubkey', remotePubKey); | ||
keylog('sender pubkey', senderPubKey); | ||
keylog('receiver pubkey', receiverPubKey); | ||
return keylog('secret dh', | ||
HKDF(header.authSecret, | ||
header.privateKey.computeSecret(remotePubKey), | ||
Buffer.concat([ | ||
Buffer.from('WebPush: info\0'), | ||
receiverPubKey, | ||
senderPubKey | ||
]), | ||
SHA_256_LENGTH)); | ||
} | ||
function extractSecret(header, mode) { | ||
if (header.key) { | ||
if (header.key.length !== KEY_LENGTH) { | ||
throw new Error('An explicit key must be ' + KEY_LENGTH + ' bytes'); | ||
} | ||
return keylog('secret key', header.key); | ||
} | ||
if (!header.privateKey) { | ||
// Lookup based on keyid | ||
var key = header.keymap && header.keymap[header.keyid]; | ||
if (!key) { | ||
throw new Error('No saved key (keyid: "' + header.keyid + '")'); | ||
} | ||
return key; | ||
} | ||
return webpushSecret(header, mode); | ||
} | ||
function deriveKeyAndNonce(header, mode) { | ||
if (!header.salt) { | ||
throw new Error('must include a salt parameter for ' + header.version); | ||
} | ||
var keyInfo; | ||
var nonceInfo; | ||
if (padSize === 1) { | ||
var secret; | ||
if (header.version === 'aesgcm128') { | ||
// really old | ||
keyInfo = 'Content-Encoding: aesgcm128'; | ||
nonceInfo = 'Content-Encoding: nonce'; | ||
} else if (padSize === 2) { | ||
secret = extractSecretAndContext(header, mode).secret; | ||
} else if (header.version === 'aesgcm') { | ||
// old | ||
var s = extractSecretAndContext(header, mode); | ||
keyInfo = info('aesgcm', s.context); | ||
nonceInfo = info('nonce', s.context); | ||
secret = s.secret; | ||
} else if (header.version === 'aes128gcm') { | ||
// latest | ||
keyInfo = Buffer.from('Content-Encoding: aes128gcm\0'); | ||
nonceInfo = Buffer.from('Content-Encoding: nonce\0'); | ||
secret = extractSecret(header, mode); | ||
} else { | ||
throw new Error('Unable to set context for padSize ' + params.padSize); | ||
throw new Error('Unable to set context for mode ' + params.version); | ||
} | ||
var prk = HKDF_extract(header.salt, secret); | ||
var result = { | ||
@@ -172,12 +258,45 @@ key: HKDF_expand(prk, keyInfo, KEY_LENGTH), | ||
function determineRecordSize(params) { | ||
var rs = parseInt(params.rs, 10); | ||
if (isNaN(rs)) { | ||
return 4096; | ||
/* Parse command-line arguments. */ | ||
function parseParams(params) { | ||
var header = {}; | ||
if (params.version) { | ||
header.version = params.version; | ||
} else { | ||
header.version = (params.padSize === 1) ? 'aesgcm128' : 'aesgcm'; | ||
} | ||
var padSize = params.padSize || PAD_SIZE; | ||
if (rs <= padSize) { | ||
throw new Error('The rs parameter has to be greater than ' + padSize); | ||
header.rs = parseInt(params.rs, 10); | ||
if (isNaN(header.rs)) { | ||
header.rs = 4096; | ||
} | ||
return rs; | ||
if (header.rs <= PAD_SIZE[header.version]) { | ||
throw new Error('The rs parameter has to be greater than ' + | ||
PAD_SIZE[header.version]); | ||
} | ||
if (params.salt) { | ||
header.salt = decode(params.salt); | ||
if (header.salt.length !== KEY_LENGTH) { | ||
throw new Error('The salt parameter must be ' + KEY_LENGTH + ' bytes'); | ||
} | ||
} | ||
header.keyid = params.keyid; | ||
if (params.key) { | ||
header.key = decode(params.key); | ||
} else { | ||
header.privateKey = params.privateKey; | ||
if (!header.privateKey) { | ||
header.keymap = params.keymap || saved.keymap; | ||
} | ||
if (header.version !== 'aes128gcm') { | ||
header.keylabels = params.keylabels || saved.keylabels; | ||
} | ||
if (params.dh) { | ||
header.dh = decode(params.dh); | ||
} | ||
} | ||
if (params.authSecret) { | ||
header.authSecret = decode(params.authSecret); | ||
} | ||
return header; | ||
} | ||
@@ -195,3 +314,13 @@ | ||
function decryptRecord(key, counter, buffer, padSize) { | ||
/* Used when decrypting aes128gcm to populate the header values. Modifies the | ||
* header values in place and returns the size of the header. */ | ||
function readHeader(buffer, header) { | ||
var idsz = buffer.readUIntBE(20, 1); | ||
header.salt = buffer.slice(0, KEY_LENGTH); | ||
header.rs = buffer.readUIntBE(KEY_LENGTH, 4); | ||
header.keyid = buffer.slice(21, 21 + idsz); | ||
return 21 + idsz; | ||
} | ||
function decryptRecord(key, counter, buffer, header) { | ||
keylog('decrypt', buffer); | ||
@@ -204,3 +333,3 @@ var nonce = generateNonce(key.nonce, counter); | ||
keylog('decrypted', data); | ||
padSize = padSize || PAD_SIZE | ||
var padSize = PAD_SIZE[header.version]; | ||
var pad = data.readUIntBE(0, padSize); | ||
@@ -210,2 +339,3 @@ if (pad + padSize > data.length) { | ||
} | ||
keylog('padding', data.slice(0, padSize + pad)); | ||
var padCheck = new Buffer(pad); | ||
@@ -224,11 +354,27 @@ padCheck.fill(0); | ||
* size, which are described in the draft. Binary values are base64url encoded. | ||
* For an explicit key that key is used. For a keyid on its own, the value of | ||
* the key is a buffer that is stored with saveKey(). For ECDH, the p256-dh | ||
* parameter identifies the public share of the recipient and the keyid is | ||
* anECDH key pair (created by crypto.createECDH()) that is stored using | ||
* saveKey(). | ||
* | ||
* |params.version| contains the version of encoding to use: aes128gcm is the latest, | ||
* but aesgcm and aesgcm128 are also accepted (though the latter two might | ||
* disappear in a future release). If omitted, assume aesgcm, unless | ||
* |params.padSize| is set to 1, which means aesgcm128. | ||
* | ||
* If |params.key| is specified, that value is used as the key. | ||
* | ||
* If |params.keyid| is specified without |params.dh|, the keyid value is used | ||
* to lookup the |params.keymap| for a buffer containing the key. | ||
* | ||
* For version aesgcm and aesgcm128, |params.dh| includes the public key of the sender. The ECDH key | ||
* pair used to decrypt is looked up using |params.keymap[params.keyid]|. | ||
* | ||
* Version aes128gcm is stricter. The |params.privateKey| includes the private | ||
* key of the receiver. The keyid is extracted from the header and used as the | ||
* ECDH public key of the sender. | ||
*/ | ||
function decrypt(buffer, params) { | ||
var key = deriveKeyAndNonce(params, MODE_DECRYPT); | ||
var rs = determineRecordSize(params); | ||
var header = parseParams(params); | ||
if (header.version === 'aes128gcm') { | ||
var headerLength = readHeader(buffer, header); | ||
buffer = buffer.slice(headerLength); | ||
} | ||
var key = deriveKeyAndNonce(header, MODE_DECRYPT); | ||
var start = 0; | ||
@@ -238,3 +384,3 @@ var result = new Buffer(0); | ||
for (var i = 0; start < buffer.length; ++i) { | ||
var end = start + rs + TAG_LENGTH; | ||
var end = start + header.rs + TAG_LENGTH; | ||
if (end === buffer.length) { | ||
@@ -248,3 +394,3 @@ throw new Error('Truncated payload'); | ||
var block = decryptRecord(key, i, buffer.slice(start, end), | ||
params.padSize); | ||
header); | ||
result = Buffer.concat([result, block]); | ||
@@ -261,6 +407,6 @@ start = end; | ||
var gcm = crypto.createCipheriv(AES_GCM, key.key, nonce); | ||
padSize = padSize || PAD_SIZE; | ||
var padding = new Buffer(pad + padSize); | ||
padding.fill(0); | ||
padding.writeUIntBE(pad, 0, padSize); | ||
keylog('padding', padding); | ||
var epadding = gcm.update(padding); | ||
@@ -273,13 +419,36 @@ var ebuffer = gcm.update(buffer); | ||
} | ||
var encrypted = Buffer.concat([epadding, ebuffer, tag]); | ||
keylog('encrypted', encrypted); | ||
return encrypted; | ||
return keylog('encrypted', Buffer.concat([epadding, ebuffer, tag])); | ||
} | ||
function writeHeader(header) { | ||
var ints = new Buffer(5); | ||
var keyid = Buffer.from(header.keyid || []); | ||
if (keyid.length > 255) { | ||
throw new Error('keyid is too large'); | ||
} | ||
ints.writeUIntBE(header.rs, 0, 4); | ||
ints.writeUIntBE(keyid.length, 4, 1); | ||
return Buffer.concat([header.salt, ints, keyid]); | ||
} | ||
/** | ||
* Encrypt some bytes. This uses the parameters to determine the key and block | ||
* size, which are described in the draft. Note that for encryption, the | ||
* p256-dh parameter identifies the public share of the recipient and the keyid | ||
* identifies a local DH key pair (created by crypto.createECDH() or | ||
* crypto.createDiffieHellman()). | ||
* size, which are described in the draft. | ||
* | ||
* |params.version| contains the version of encoding to use: aes128gcm is the latest, | ||
* but aesgcm and aesgcm128 are also accepted (though the latter two might | ||
* disappear in a future release). If omitted, assume aesgcm, unless | ||
* |params.padSize| is set to 1, which means aesgcm128. | ||
* | ||
* If |params.key| is specified, that value is used as the key. | ||
* | ||
* If |params.keyid| is specified without |params.dh|, the keyid value is used | ||
* to lookup the |params.keymap| for a buffer containing the key. | ||
* | ||
* For Diffie-Hellman (WebPush), |params.dh| includes the public key of the | ||
* receiver. |params.privateKey| is used to establish a shared secret. For | ||
* versions aesgcm and aesgcm128, if a private key is not provided, the ECDH key | ||
* pair used to encrypt is looked up using |params.keymap[params.keyid]|, and | ||
* |params.keymap| defaults to the values saved with saveKey(). Key pairs can | ||
* be created using |crypto.createECDH()|. | ||
*/ | ||
@@ -290,7 +459,22 @@ function encrypt(buffer, params) { | ||
} | ||
var key = deriveKeyAndNonce(params, MODE_ENCRYPT); | ||
var rs = determineRecordSize(params); | ||
var header = parseParams(params); | ||
if (!header.salt) { | ||
header.salt = crypto.randomBytes(KEY_LENGTH); | ||
} | ||
var result; | ||
if (header.version === 'aes128gcm') { | ||
// Save the DH public key in the header. | ||
if (header.privateKey && !header.keyid) { | ||
header.keyid = header.privateKey.getPublicKey(); | ||
} | ||
result = writeHeader(header); | ||
} else { | ||
// No header on other versions | ||
result = new Buffer(0); | ||
} | ||
var key = deriveKeyAndNonce(header, MODE_ENCRYPT); | ||
var start = 0; | ||
var result = new Buffer(0); | ||
var padSize = params.padSize || PAD_SIZE; | ||
var padSize = PAD_SIZE[header.version]; | ||
var pad = isNaN(parseInt(params.pad, 10)) ? 0 : parseInt(params.pad, 10); | ||
@@ -303,10 +487,10 @@ | ||
var recordPad = Math.min((1 << (padSize * 8)) - 1, // maximum padding | ||
Math.min(rs - padSize - 1, pad)); | ||
Math.min(header.rs - padSize - 1, pad)); | ||
pad -= recordPad; | ||
var end = Math.min(start + rs - padSize - recordPad, buffer.length); | ||
var end = Math.min(start + header.rs - padSize - recordPad, buffer.length); | ||
var block = encryptRecord(key, i, buffer.slice(start, end), | ||
recordPad, padSize); | ||
result = Buffer.concat([result, block]); | ||
start += rs - padSize - recordPad; | ||
start += header.rs - padSize - recordPad; | ||
} | ||
@@ -320,11 +504,8 @@ if (pad) { | ||
/** | ||
* This function saves a key under the provided identifier. This is used to | ||
* save the keys that are used to decrypt and encrypt blobs that are identified | ||
* by a 'keyid'. DH or ECDH keys that are used with the 'dh' parameter need to | ||
* include a label (included in 'dhLabel') that identifies them. | ||
* Deprecated. Use the keymap and keylabels arguments to encrypt()/decrypt(). | ||
*/ | ||
function saveKey(id, key, dhLabel) { | ||
savedKeys[id] = key; | ||
saved.keymap[id] = key; | ||
if (dhLabel) { | ||
keyLabels[id] = new Buffer(dhLabel + '\0', 'ascii'); | ||
saved.keylabels[id] = dhLabel; | ||
} | ||
@@ -331,0 +512,0 @@ } |
{ | ||
"name": "http_ece", | ||
"version": "0.5.2", | ||
"version": "0.6.0", | ||
"description": "Encrypted Content-Encoding for HTTP", | ||
@@ -11,2 +11,6 @@ "homepage": "https://github.com/martinthomson/encrypted-content-encoding", | ||
}, | ||
"contributors": [{ | ||
"name": "Marco Castelluccio", | ||
"email": "mcastelluccio@mozilla.com" | ||
}], | ||
"repository": { | ||
@@ -13,0 +17,0 @@ "type": "git", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
20992
6
497