Как сделать простое шифрование AEAD в Node.js - PullRequest
0 голосов
/ 22 мая 2019

Я пытаюсь реализовать базовые функции шифрования в моем приложении узла. Проблема заключается в том, что почти во всех найденных мной примерах и руководствах (в том числе на Node docs ) используются неаутентифицированные шифры.

Итак, я надеюсь, что кто-то может взглянуть на то, что у меня есть, и дать мне знать, если я делаю что-то глупое или оставляю себя открытым для любых очевидных атак по побочным каналам.

Когда я запускаю этот код, все работает как положено (т. Е. Тесты, которые должны пройти, и те, которые должны быть не выполнены), за исключением одного случая.

Кажется, что я могу добавить произвольный объем данных в конец текста шифра, и сообщение все еще дешифруется без ошибок. Я думал, что аутентифицированная система шифрования должна давать сбой, когда есть какая-либо модификация текста шифра. Вы можете проверить это, изменив переменную tweakCharAt на что-то большое, например, 100. Я что-то не так сделал? Это как-то ожидаемое поведение?

const { createCipheriv, createDecipheriv, randomBytes, randomFillSync } = require('crypto');
const { promisify } = require('util');

const randomBytesPromise = promisify(randomBytes);

const algorithm = 'chacha20-poly1305';
const ivLen = 12; // IETF version of ChaCha20-Poly1305 uses a 96 bit (12 byte) nonce
const authTagLength = 16; // Poly1305 generates a 128 bit (16 byte) authentication tag

// Takes a Node Transform stream (like Cipher and Decipher) + some data and returns a Promise
const streamPromise = (stream, data) => new Promise((resolve, reject) => {

    // Create an array to hold the chunks of binary data
    const chunks = [];

    // Setup stream event handlers
    // See: https://nodejs.org/api/crypto.html#crypto_class_cipher
    stream.on('readable', () => {
        let chunk;

        // eslint-disable-next-line no-cond-assign
        while ((chunk = stream.read()) !== null) {

            chunks.push(chunk);
        }
    });

    stream.on('end', () => { resolve(Buffer.concat(chunks)); });

    stream.on('error', (error) => { reject(error); });

    // Write the given data to the stream and then close it up
    stream.write(data);
    stream.end();
});

const encrypt = (plainText, key, authenticatedData = Buffer.alloc(0)) => (

    randomBytesPromise(ivLen)
        .then((rand) => {

            const iv = rand;

            const cipher = createCipheriv(algorithm, key, iv, { authTagLength });

            cipher.setAAD(authenticatedData);

            return streamPromise(cipher, plainText)
                .then(encMessage => [iv, authenticatedData, cipher.getAuthTag(), encMessage]);
        })
        .then(parts => parts.map(buffer => buffer.toString('base64')).join(':'))
);

const decrypt = (cipherText, key) => {

    const [
        iv,
        authenticatedData,
        authTag,
        message
    ] = cipherText.split(':').map(string => Buffer.from(string, 'base64'));

    const decipher = createDecipheriv(algorithm, key, iv, { authTagLength });

    decipher.setAAD(authenticatedData);
    decipher.setAuthTag(authTag);

    return streamPromise(decipher, message);
};

const test = (message, keyBuffer, authenticatedData, testNum, tamperWithCipherText = false) => (

    encrypt(message, keyBuffer, authenticatedData)
        .then((cipherText) => {

            console.log(testNum + ' cipher text:     ', cipherText);

            let newCipherText = cipherText;

            if (tamperWithCipherText) {

                const tweakCharAt = 30; // 100;

                newCipherText = newCipherText.substring(0, tweakCharAt)
                    + '0' // '==/I5QUaGV'
                    + newCipherText.substring(tweakCharAt + 1);

                console.log(testNum + ' new cipher text: ', newCipherText);
            }

            console.log(testNum + ' AAD:             ', Buffer.from(newCipherText.split(':')[1], 'base64').toString('utf8'));

            return decrypt(newCipherText, keyBuffer);
        })
        .then((plainText) => {

            console.log(testNum + ' plain text:      ', plainText.toString('utf8') + '\n');
        })
        .catch((error) => {

            console.error(testNum, error);
        })
);

// Run some tests
const key = Buffer.allocUnsafe(32);
randomFillSync(key);
const testMessage = 'Top secret message';
const authData = Buffer.from('AAD data here', 'utf8');

// Test without authenticated data
test(testMessage, key, undefined, 1); // should work
test(testMessage, key, undefined, 2, true); // should error

// Test with authenticated data
test(testMessage, key, authData, 3); // should work
test(testMessage, key, authData, 4, true); // should error

Пример вывода

Вот некоторые результаты выполнения вышеприведенного кода в узле 12 с различными настройками для изменения текста шифра.

Изменить символ в середине (обнаружен)

1 cipher text:      /mFoBKirqH8DZ3Wv::JayX8aiYi77ApLyhI6yYfw==:uBOj318Zf9M2qaBgIkiEX2MC
1 AAD:
1 plain text:       Top secret message

2 cipher text:      QnEbbrFpYhTLPuhk::owdqDGokK9WoXE0Od5DDTQ==:QROQ3/Q1qhKz4/FSQbwFuVoG
2 new cipher text:  QnEbbrFpYhTLPuhk::owdqDGokK9Wo0E0Od5DDTQ==:QROQ3/Q1qhKz4/FSQbwFuVoG
2 AAD:
2 Error: Unsupported state or unable to authenticate data
    at Decipheriv._flush (internal/crypto/cipher.js:144:29)
    at Decipheriv.prefinish (_stream_transform.js:140:10)
    at Decipheriv.emit (events.js:200:13)
    ...
3 cipher text:      36cf2xXpsZnfFn+I:QUFEIGRhdGEgaGVyZQ==:NRRNaY9yAJ2yu7z4Nh0+Tw==:ZdY9xtFh0zcXgDcydG+zEhug
3 AAD:              AAD data here
3 plain text:       Top secret message

4 cipher text:      HWA0BWBPHe/3kFps:QUFEIGRhdGEgaGVyZQ==:w6s+CYMeU0SNoCtgbeRBsw==:2TOwGnbT2T1ZZ8qKe90BU2VJ
4 new cipher text:  HWA0BWBPHe/3kFps:QUFEIGRhdGEga0VyZQ==:w6s+CYMeU0SNoCtgbeRBsw==:2TOwGnbT2T1ZZ8qKe90BU2VJ
4 AAD:              AAD data kEre
4 Error: Unsupported state or unable to authenticate data
    at Decipheriv._flush (internal/crypto/cipher.js:144:29)
    at Decipheriv.prefinish (_stream_transform.js:140:10)
    at Decipheriv.emit (events.js:200:13)
    ...

Добавить в конец, base64 (не обнаружено)

1 cipher text:      cwtqwgX2mB5gmPh4::I2yPh0xoorrvVU+7edM1Ww==:V9qnpa25HEPw/v6zBc03tpYz
1 AAD:
1 plain text:       Top secret message

2 cipher text:      fe4spIYdiJDyIaUo::unK0AA6vI9bTDDVN/qm3Gw==:dE9bRpOi99ntKviitH4QdkGR
2 new cipher text:  fe4spIYdiJDyIaUo::unK0AA6vI9bTDDVN/qm3Gw==:dE9bRpOi99ntKviitH4QdkGRA
2 AAD:
2 plain text:       Top secret message

3 cipher text:      zzDP/FB3u4svhDoC:QUFEIGRhdGEgaGVyZQ==:GExIqrogg8Dm8ifQUhVxJg==:VH46trhHXsmcafPdImgArorb
3 AAD:              AAD data here
3 plain text:       Top secret message

4 cipher text:      TQE0QC8wUAElPYIC:QUFEIGRhdGEgaGVyZQ==:TpWkCLskrnRrQOykke+FoA==:YF3WEywHVzPNzuTg7UYKD3Ja
4 new cipher text:  TQE0QC8wUAElPYIC:QUFEIGRhdGEgaGVyZQ==:TpWkCLskrnRrQOykke+FoA==:YF3WEywHVzPNzuTg7UYKD3JaA
4 AAD:              AAD data here
4 plain text:       Top secret message

Добавить в конец, шестнадцатеричное (не обнаружено)

1 cipher text:      35ddfd0541be72780793894b::dc9ea755fbf00ee1bd9b737727b912af:5cd46cad985c2da058fa51967a675cb9bda6
1 AAD:
1 plain text:       Top secret message

2 cipher text:      0da4c80d45610cd3717e8497::0fcdea1d4ccfb9cb4347491430ccc4b3:3060d55728bc8ab2f75f80ce27ebe06f813e
2 new cipher text:  0da4c80d45610cd3717e8497::0fcdea1d4ccfb9cb4347491430ccc4b3:3060d55728bc8ab2f75f80ce27ebe06f813eA
2 AAD:
2 plain text:       Top secret message

3 cipher text:      f6ae6d9a72139b8611dd3798:41414420646174612068657265:f00d9d987f94038334603165605ce966:c6e635d8d81da0badd0ee98d64e007308853
3 AAD:              AAD data here
3 plain text:       Top secret message

4 cipher text:      a66fab00b07bc963cff3c721:41414420646174612068657265:05ed29d50a426df3b5a3616c7a467703:1b9d88bfdff01f5eacad9ce7cd9a117d6b08
4 new cipher text:  a66fab00b07bc963cff3c721:41414420646174612068657265:05ed29d50a426df3b5a3616c7a467703:1b9d88bfdff01f5eacad9ce7cd9a117d6b08A
4 AAD:              AAD data here
4 plain text:       Top secret message
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...