Рассказывает 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