¿He cruzado mis t y punteado mis j minúsculas? (o, implementando AES, por favor ayude a poner un vistazo a mi código)

0

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!'
    
pregunta Jason 21.01.2014 - 17:37
fuente

2 respuestas

3

Desde una perspectiva superficial, esto parece razonable o al menos la mayoría de las fallas de implementación potenciales residirían en la biblioteca criptográfica javascript.

Como usted dice, no maneja la administración de claves o la autenticación de identidad, lo que probablemente limita su uso generalizado. Pero si solo tiene 2 máquinas que necesitan comunicarse y puede administrar las claves por otros medios, esto puede ser razonable.

Aparte de eso, parece depender principalmente del modo AES CTR con un sha-512 HMAC, lo que parece una opción razonable.

Como posibles mejoras, agregaría un proceso de intercambio en el que el remitente del mensaje solicita un nonce aleatorio del servidor (en texto plano), el receptor genera y envía el nonce aleatorio, y ese nonce aleatorio debe estar presente dentro del cuerpo del mensaje (y esto se verifica que sea el mismo que el nonce enviado antes de que el texto cifrado descifrado se envíe a la aplicación). La intención de esta mejora es evitar los ataques de repetición (por ejemplo, si la aplicación dice que transfiera $ 100 de Alice a Bob, no desea que los ataques de repetición permitan que un intruso de la red reenvíe ese mensaje 1000 veces y haga que vaya a la aplicación). , entonces Alice transfiere $ 100,000 a Bob).

(EDITAR: Originalmente propuse usar el nonce del remitente como el IV, pero eso presenta un ataque obvio; un interceptor no deseado sigue interceptando y enviando el mismo nonce. Luego crean un catálogo de mensajes que actúan como una reutilización de una sola vez -pad, que ataques estándar se pueden usar para recuperar el pad (incluso si no recuperar la clave) y descifrar el tráfico).

También, cambiaría (hmac.read().toString(opts.mac_encoding) !== mac.toString(opts.mac_encoding)) Para ser una comparación de cadena de tiempo constante, el tiempo de falla es independiente de cuántos bytes de MAC coinciden.

Escribiría una comparación de cadena de tiempo constante de la siguiente manera:

function constant_time_str_cmp(str1, str2) {
    if(str1.length !== str2.length) { return false; }
    var result = 0;
    for(var i = 0; i < str1.length; i++) {
       result |= (str1.charCodeAt(i) ^ str2.charCodeAt(i));
    }
    return result === 0
}

Tenga en cuenta que esto compara cada letra de las dos cadenas carácter por carácter (usando xor ^ para cada comparación de caracteres) y solo devuelve 0 si todas son idénticas. No termina temprano como suelen hacer str1 === str2 y str1 !== str2 , si el primer carácter diferente aparece temprano en las cadenas.

También desconfiaría de algún ataque en node.js que muestre información de depuración y pierda inadvertidamente su clave.

    
respondido por el dr jimbob 21.01.2014 - 22:33
fuente
2

Según los comentarios, tu protocolo está roto. No es que haya visto un problema específico, pero usted ha inventado un nuevo protocolo de comunicaciones seguras. Hay muchos libros que explican cómo construir protocolos de comunicaciones seguros y cómo pueden construirse mal. Incluso los expertos que crean protocolos seguros cometen errores: vea los más populares en uso, TLS y SSL, que periódicamente deben solucionarse.

Además, no tiene un modelo de amenaza, un modelo de protocolo, una lista de objetivos de seguridad, una lista de objetivos de seguridad no alcanzados, etc. Todas estas cosas son necesarias antes de poder evaluar un protocolo de seguridad. Si solo usa un protocolo seguro preexistente, no necesita crearlos.

No he mirado su código, pero aquí hay una breve lista de algunos errores que puede tener y que debe abordar en su modelo de amenaza para tener alguna posibilidad de estar seguro:

  • reproducción de mensajes
  • reordenación de mensajes
  • inyección de mensajes (flujo intermedio)
  • revocación de claves
  • gestión de claves
  • BESTIA, CRIMEN, INCUMPLIMIENTO
  • vector de inicialización sin protección
  • vector de inicialización inseguro
  • vector de inicialización reutilizado



Utilice TLS o SSL.

    
respondido por el atk 21.01.2014 - 19:03
fuente

Lea otras preguntas en las etiquetas