Простая и ужасающая история про шифрование — об Open Source, доверии и ответственности

Posted by

Рассказывает Kacper Walanus,
Senior-разработчик Ruby on Rails и тимлид в EL Passion

Задача

Я хотел написать простое приложение для шифрования и дешифрования сообщений. Алгоритм AES показался хорошим выбором, так что я начал с поиска подходящей библиотеки.

Решение

Я пишу на Ruby, так что сделал то, что сделал бы любой на моём месте — загуглил «ruby gem aes». И сразу же нашёл библиотеку под названием (сюрприз!) «aes», простейший пример использования которой выглядел вполне понятно:

require ‘aes’

message = «Super secret message»
key = «password»

encrypted = AES.encrypt(message, key) # RZhMg/RzyTXK4QKOJDhGJg==$BYAvRONIsfKjX+uYiZ8TCsW7C2Ug9fH7cfRG9mbvx9o=
decrypted = AES.decrypt(encrypted, key) # Super secret message

Если передать неверный ключ, то библиотека выдаст ошибку:

decrypted = AES.decrypt(encrypted, «Some other password») #=> aes.rb:76:in `final’: bad decrypt (OpenSSL::Cipher::CipherError)

Баг

Во время разработки я заметил одну интересную особенность. Я написал тест для проверки расшифровки сообщений с неправильным ключом. Если говорить конкретнее, я заменил один символ в ключе, с которым сообщение было зашифровано и попытался получить обратный результат, ожидая ошибку. И… мой тест провалился! Ошибка не просто не была сгенерирована, ещё и само сообщение было корректно расшифровано:

encrypted = AES.encrypt(«Super secret message», «password»)
decrypted = AES.decrypt(encrypted, «gassword») # «p» => «g»
decrypted #=> Super secret message

Хорошо, допустим я нашёл один крайне особенный случай, один на миллион. Попробуем поменять два символа в ключе:

encrypted = AES.encrypt(«Super secret message», «password»)
decrypted = AES.decrypt(encrypted, «ggssword») # «pa» => «gg»
decrypted #=> Super secret message

… и опять тот же результат!

Что же, осталась только одна вещь, которую стоило проверить — использовать совершенно другой ключ:

encrypted = AES.encrypt(«Super secret message», «password»)
decrypted = AES.decrypt(encrypted, «totally wrong password»)
decrypted #=> Super secret message

Это выглядело как серьёзная проблема безопасности, так что я решил разобраться, в чём дело.

Отладка

Проблема была вот в этой строчке библиотеки «aes»:

@cipher.key = @key.unpack(‘a2’*32).map{|x| x.hex}.pack(‘c’*32)

Для начала объясню часть про unpack. В данном случае эта функция разделяет строку на массив из 32 строк (смотрите документацию, если нужны подробности):

«password».unpack(«a2″*32)
=> [«pa», «ss», «wo», «rd», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «», «»]

Затем для каждой из этих 32 коротких строк вызывается метод #hex. А String#hex в Ruby, как известно, преобразует шестнадцатеричные строки в их десятеричное числовое представление, а в случае неудачи возвращает 0:

‘9’.hex #=> 9
‘a’.hex #=> 10
’10’.hex #=> 16
‘ff’.hex #=> 255
# zero is returned on error:
‘foobar’.hex #=> 0
‘zz’.hex #=> 0

Таким образом, любая строка не содержащая корректной шестнадцатеричной последовательности (как «ff» или «13») превратится просто в 32 нуля:

«pa».hex #=> 0
«ss».hex #=> 0
«wo».hex #=> 0
«rd».hex #=> 0
«».hex #=> 0

«password».unpack(«a2″*32).map { |x| x.hex }
#=> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
«totally wrong password».unpack(«a2″*32).map { |x| x.hex }
#=> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

И в результате расшифровать сообщение можно практически с любым ключом. Скорее всего, автор ожидал, что в качестве ключей будут использоваться корректные числовые последовательности. Описанный баг, вероятно, следствие данного наивного предположения.

Итого

Библиотека «aes» не самая большая среди прочих в Ruby, всего 45 звёзд и 13 форков. Но проблема в том, что она выдаётся на первом месте в Google при поиске «aes gem» или «ruby aes gem», а мы обычно не задаём вопросов к тому, что Google выдаёт в топе. Мы вообще очень редко проверяем внешние библиотеки, хотя, судя по всему, должны бы. Особенно, когда дело касается безопасности.

Дополнение

Хочу пояснить, что у меня не было цели обвинить в чём-либо автора библиотеки. Он написал её несколько лет назад и не мог предвидеть, что она станет выдаваться поисковиками на первых строчках в 2017 году. Я просто нашёл серьёзный баг и хочу поделиться им с другими. Это то, как я представляю общественную ответственность в движении Open Source.

Технические детали

Сама библиотека: https://github.com/chicks/aes
Версия, использованная автором: 0.5.0 / 12c3648
Пример кода

Источник: пост на blog.elpassion.com

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *