Я пытаюсь реализовать базовые функции шифрования в моем приложении узла. Проблема заключается в том, что почти во всех найденных мной примерах и руководствах (в том числе на 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