Ho incrociato le mie t e ho punteggiato la mia lettera maiuscola? (o, implementando AES, per favore aiutatemi a dare un'occhiata al mio codice)

0

Quindi, ho usato il modulo crypto in node.js per implementare un generatore di chiavi sicuro e le funzioni di crittografia / decrittografia. Sapendo che il migliore amico della sicurezza è un riflettore sull'implementazione, voglio buttarlo qui e assicurarmi di non cadere nei buchi.

Un po 'del mio caso d'uso: sto costruendo un'app che comunica su Internet utilizzando socket tcp. Per il momento, lo scambio di chiavi e la verifica dell'identità sono al di fuori del mio scopo, genererò le chiavi e le copierò manualmente a ciascuna estremità della comunicazione.

Ho intenzione di rilasciare questo codice come un modulo node.js, esponendo esplicitamente le ipotesi e le limitazioni inerenti alle scelte che ho fatto ... questo è destinato a essere un pony un trucco, fatto bene.

Ho copiato le funzioni rilevanti di seguito. Puoi vedere il codice completo all'indirizzo link , che include diverse altre funzioni bloccate, tra cui alcuni per la gestione delle password degli utenti, puoi guardare quelle cose se sei interessato, ma tutto quello che ti chiedo qui sono le cose che ho copiato qui sotto. Ho leggermente riscritto queste funzioni per postare qui per ritagliare un piccolo cruft opzionale, capisco che il diavolo potrebbe essere nei dettagli ed è per questo che ho incluso il link sopra alla sorgente reale.

Ho fatto commenti dove appropriato per descrivere cosa sto facendo e perché. Non esitate a correggere eventuali ipotesi errate che ho esposto oltre a valutare l'implementazione stessa.

generazione di chiavi:

// 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');
    }
};

crittografia simmetrica:

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);
    });
};

decrittazione simmetrica:

// 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
};

EDIT - e un caso d'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!'
    
posta Jason 21.01.2014 - 17:37
fonte

2 risposte

3

Da un aspetto superficiale, questo sembra ragionevole o almeno la maggior parte dei potenziali difetti di implementazione risiede nella libreria crittografica javascript.

Come dici tu, non gestisce la gestione delle chiavi o l'autenticazione dell'identità, che probabilmente limita il suo uso diffuso. Ma se hai solo 2 macchine che hanno bisogno di comunicare e in grado di gestire le chiavi con un altro mezzo, questo potrebbe essere ragionevole.

Oltre a ciò, sembra basarsi principalmente sulla modalità CTR di AES con un HMAC sha-512, che sembra una scelta ragionevole.

Come potenziali miglioramenti, vorrei aggiungere un processo di handshake in cui il mittente del messaggio richiede un nonce casuale dal server (in chiaro), il ricevente genera e invia un nonce casuale e quel nonce casuale deve essere presente all'interno del corpo del messaggio (e questo viene verificato per essere uguale al nonce inviato prima che il testo cifrato decrittografato venga inviato all'applicazione). L'intenzione di questo miglioramento è di prevenire gli attacchi di riproduzione (ad esempio, se l'applicazione dice trasferire $ 100 da Alice a Bob, non si desidera che gli attacchi di riproduzione consentano a una rete di intercettazione di inviare nuovamente il messaggio 1000 volte e farlo andare all'applicazione , così Alice trasferisce $ 100.000 a Bob).

(EDIT: Inizialmente ho proposto di usare il nonce del mittente come IV, ma che presenta un attacco evidente, un intercettatore continua a intercettare e inviare lo stesso nonce. Quindi costruiscono un catalogo di messaggi che si comportano come una volta riutilizzata -pad, quali attacchi standard possono essere utilizzati per recuperare il pad (anche se non si recupera la chiave) e decrittografare il traffico).

Inoltre, cambierei (hmac.read().toString(opts.mac_encoding) !== mac.toString(opts.mac_encoding)) essere un confronto di stringhe a tempo costante, quindi il tempo di fallimento è indipendente dal numero di byte della corrispondenza MAC.

Scriverò un confronto di stringhe a tempo costante nel modo seguente:

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
}

Si noti che confronta ogni lettera delle due stringhe carattere per carattere (usando xor ^ per ogni confronto di caratteri) e restituisce solo 0 se tutte sono identiche. Non termina presto come str1 === str2 e str1 !== str2 tipicamente, se il primo carattere diverso si verifica all'inizio delle stringhe.

Sarei anche diffidente nei confronti di qualche attacco su node.js che mostra informazioni di debug e inavvertitamente perde la tua chiave.

    
risposta data 21.01.2014 - 22:33
fonte
2

In base ai commenti, il tuo protocollo è rotto. Non che io abbia effettivamente visto un problema specifico, ma hai inventato un nuovo protocollo di comunicazione sicuro. Ci sono molti libri che spiegano come costruire protocolli di comunicazione sicuri e come possono essere costruiti in modo errato. Anche gli esperti che creano protocolli sicuri commettono errori: vedi i più popolari in uso, TLS e SSL, che periodicamente devono essere corretti.

Inoltre, non hai un modello di minaccia, un modello di protocollo, un elenco di obiettivi di sicurezza, un elenco di obiettivi di sicurezza non raggiunti, ecc. Tutte queste cose sono necessarie prima che un protocollo di sicurezza possa essere valutato. Se si utilizza solo un protocollo sicuro preesistente, non è necessario crearli.

Non ho guardato il tuo codice, ma ecco un breve elenco di alcuni errori che potresti avere e che devi affrontare nel tuo modello di minaccia per avere la possibilità di essere sicuro:

  • riproduzione di messaggi
  • riordinamento dei messaggi
  • Iniezione messaggio (mid-stream)
  • revoca della chiave
  • gestione delle chiavi
  • BEAST, CRIME, BREACH
  • vettore di inizializzazione non protetto
  • vettore di inizializzazione non sicuro
  • vettore di inizializzazione riutilizzato


Utilizza TLS o SSL.

    
risposta data 21.01.2014 - 19:03
fonte

Leggi altre domande sui tag