Entonces, utilicé el módulo crypto
en node.js para implementar un generador de claves seguro y cifrar / descifrar las funciones. Sabiendo que el mejor amigo de la seguridad es un foco en la implementación, quiero lanzarlo aquí y asegurarme de no caer en ningún agujero.
Un poco sobre mi caso de uso: estoy creando una aplicación que se comunica a través de Internet utilizando los conectores TCP. Por el momento, el intercambio de claves y la verificación de identidad están fuera de mi alcance, generaré claves y las copiaré manualmente en cada extremo de la comunicación.
Planeo lanzar este código como un módulo node.js, exponiendo explícitamente las suposiciones y limitaciones inherentes a las elecciones que he hecho ... esto pretende ser un pony de un solo truco, hecho correctamente.
He copiado las funciones relevantes a continuación. Puede ver el código completo en enlace , que incluye varias otras funciones eliminadas, incluyendo Algunos para administrar las contraseñas de los usuarios, puedes mirar esas cosas si estás interesado, pero todo lo que pregunto aquí es las cosas que he copiado a continuación. He reescrito ligeramente estas funciones para publicar aquí para eliminar un poco de cruft opcional, entiendo que el diablo podría estar en los detalles, por eso he incluido el enlace anterior a la fuente real.
He hecho comentarios donde sea apropiado para detallar lo que estoy haciendo y por qué. Siéntase libre de corregir cualquier suposición errónea que haya establecido además de evaluar la implementación en sí misma.
generación de claves:
// gen_key() is intended for manual use on the command line.
// It's entirely synchronous and not built for high volume use
var gen_key = exports.gen_key = function(params) {
params = params || {};
// ASSERTION: I'm using AES 256, therefore the resulting
// ... key length should be 256 bits
var keylen = 256; // 256 bits
// the following are relevant to deriving keys from passwords
// ASSUMPTION: I've universally seen a salt length of 128
// ... bits recommended, so I went with it
var saltlen = 128; // 128 bits
// ASSERTION: This number is obviously dependent on the
// ... current and recent state of hardware, though for
// ... our purposes overkill is a virtue
var iterations = 1048576; // default to 2^20 iterations
// all of the relevant functions take input in bytes
var byte_length = params.byte_length ?
parseInt(params.byte_length) :
(params.bit_length ?
(parseInt(params.bit_length)/8) :
(keylen/8));
if (params.password) {
// crypto.randomBytes is a CSPRNG
var salt = params.salt?
params.salt:
crypto.randomBytes(saltlen/8);
if (params.iterations) iterations =
params.iterations;
// NOTE: The native pbkdf2 function uses sha1, which is
// ... perhaps not ideal.
var key = crypto.pbkdf2Sync(params.password, salt,
iterations, byte_length);
// NOTE: This is tuned to output a string which can
// ... be copied and saved, but allows for other types
// ... of use
return params.return_params ?
[
(params.raw?key:key.toString('base64')),
{
salt: (params.raw?salt:salt.toString('base64')),
iterations: iterations,
keylen: byte_length,
algo: 'pbkdf2',
hash: 'hmac-sha1'
}
] :
(params.raw?key:key.toString('base64'));
}
else {
// crypto.randomBytes is a CSPRNG
var key = crypto.randomBytes(byte_length);
// ASSUMPTION: A string of random bytes generated by a
// ... CSPRNG is sufficient to use as a cryptographically
// ... secure key without further processing
return params.raw?key:key.toString('base64');
}
};
cifrado simétrico:
var encipher = exports.encipher = function(payload, key, mackey, opts, cb) {
opts = opts || {};
// ASSUMPTION: I've universally seen an iv length of 128
// ... bits recommended, so I went with it
var iv_length = 128; // bit length
// this is intended for moderate volume usage, so make it asynchronous
// it's not optimized for large payload sizes at this time
// ASSUMPTION: A random IV is still preferred over a simple one even
// ... though we're using CTR (see below), which can effectively cope
// ... with a simple IV
crypto.randomBytes(iv_length/8, function(err, iv) {
if (err) cb(err);
// for now, force the cipher used and mode
opts.algorithm = 'aes-256';
// ASSUMPTION: CTR mode is preferred over CBC or another mode
// ... with padding, because it eliminates the padding-oracle
// ... attack. I use CTR over GCM because the authentication
// ... properties of GCM are not natively available in node.js
opts.mode = 'ctr';
// QUESTION: I mostly just chose this with the bigger-is-better
// ... mindset, any reason to choose something different?
opts.hmac_algo = 'sha512';
// convert our payload and keys to buffers, particularly to
// allow us to specify the encoding of our keys
if (!Buffer.isBuffer(payload)) payload = opts.payload_encoding ?
new Buffer(payload, opts.payload_encoding) :
new Buffer(payload);
if (!Buffer.isBuffer(key)) key = opts.key_encoding ?
new Buffer(key, opts.key_encoding) :
new Buffer(key);
if (!Buffer.isBuffer(mackey)) mackey = opts.mackey_encoding ?
new Buffer(mackey, opts.mackey_encoding) :
new Buffer(mackey);
var cipher = crypto.createCipheriv(opts.algorithm+'-'+opts.mode,
key, iv);
cipher.write(payload);
cipher.end();
var ciphertext = cipher.read();
// ASSERTION: the key used to generate the HMAC should be
// ... different than the key used to generate the ciphertext
// ... though it's not explicitly enforced
var hmac = crypto.createHmac(opts.hmac_algo, mackey);
// ASSERTION: The HMAC should be produced from the iv+ciphertext
hmac.write(iv);
hmac.write(ciphertext);
hmac.end();
var mac = hmac.read();
// send all of the public data needed to decrypt the message,
// the calling application can handle how they're packaged together
cb(null, mac, iv, ciphertext);
});
};
descifrado simétrico:
// This is strictly a functional reversal of the encipher method
// Only question has to do with timing
var decipher = exports.decipher = function(payload, key, mackey, mac,
iv, opts) {
opts = opts || {};
// we pass in the pieces individually, the calling application
// manages how the iv and mac are packaged together
// for now, force the cipher used and mode
opts.algorithm = 'aes-256';
opts.mode = 'ctr';
opts.hmac_algo = 'sha512';
if (!Buffer.isBuffer(payload)) payload = opts.payload_encoding ?
new Buffer(payload, opts.payload_encoding) :
new Buffer(payload);
if (!Buffer.isBuffer(key)) key = opts.key_encoding ?
new Buffer(key, opts.key_encoding) :
new Buffer(key);
if (!Buffer.isBuffer(mackey)) mackey = opts.mackey_encoding ?
new Buffer(mackey, opts.mackey_encoding) :
new Buffer(mackey);
if (!Buffer.isBuffer(iv)) iv = opts.iv_encoding ?
new Buffer(iv, opts.iv_encoding) :
new Buffer(iv);
if (!Buffer.isBuffer(mac)) mac = opts.mac_encoding ?
new Buffer(mac, opts.mac_encoding) :
new Buffer(mac);
var hmac = crypto.createHmac(opts.hmac_algo, mackey);
hmac.write(iv);
hmac.write(payload);
hmac.end();
// if we haven't authenticated, then we've got a problem
// QUESTION: Presumably the calling application needs to
// ... behave carefully to avoid enabling a timing-oracle
// ... attack?
if (hmac.read().toString(opts.mac_encoding) !==
mac.toString(opts.mac_encoding))
return new Error('Message failed to authenticate');
var decipher = crypto.createDecipheriv(opts.algorithm+'-'+opts.mode,
key, iv);
decipher.write(payload);
decipher.end();
var plaintext = decipher.read();
return plaintext; // return a raw buffer of our decrypted text
};
EDITAR - y un caso de uso:
> var onecrypt = require('./lib/onecrypt');
> var key = onecrypt.gen_key({ raw: true });
> var mackey = onecrypt.gen_key({ raw: true});
> var result;
> onecrypt.encipher('secret message YAY!', key, mackey, null, function(err, mac, iv, ciphertext) { result = [mac, iv, ciphertext]; });
-- in my use-case, I would then send the mac, iv, and ciphertext over an unencrypted TCP socket
> var plain = onecrypt.decipher(result[2], key, mackey, result[0], result[1]);
> console.log(plain.toString());
-- outputs:
'secret message YAY!'