[Перевод] Простая и ужасающая история о шифровании

Это будет история об открытом ПО, доверии и ответственности.

Задача и её решение


Как-то раз мне понадобилось добавить в своё приложение на Ruby симметричное шифрование. Алгоритм AES показался мне хорошим выбором и я решил найти библиотеку шифрования с поддержкой этого алгоритма. Поскольку я писал на Ruby, то сделал то же самое, что сделал бы на моём месте практически каждый программист на Ruby — пошел в Google и написал запрос «ruby gem aes». Конечно же, Google первой строкой предложил мне gem, называющийся (вот неожиданность!) — «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

Если вы при расшифровке использовали неверный пароль, gem выбрасывал ошибку:

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

Ну, отлично. Что же могло пойти не так?

Баг


После подключения gem’a я задейсвовал его функциональность в новой фиче и, просто на всякий случай, написал пару тестов для него — для расшифровки с правильным паролем и для ошибки расшифровки с неверным паролем. Во втором тесте я просто заменил первую букву пароля при расшифровке. Я рассчитывал получить ошибку расшифровки, что являлось бы в данном случае корректно пройденным тестом. И… мой тест провалился! Я не только не получил ошибку декодирования, я даже получил верно расшифрованные данные неверным паролем!
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

Это уже выглядело кричащей дырой в безопасности, так что я решил разобраться, что же здесь происходит.

Отладка


Проблема возникала из-за следующей строки в коде gem’a:
@cipher.key = @key.unpack('a2'*32).map{|x| x.hex}.pack('c'*32)

Прежде всего давайте объясним, что делает unpack. В данном случае она разделяет входную строку на массив из 32-ух строк (см. документацию):

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

Далее, для каждой из полученных строк вызывается метод #hex. String#hex в Ruby конвертирует строки, содержащие hex-числа в целые числа (а если конвертация не удаётся, то в число 0).

'9'.hex   #=> 9
'a'.hex   #=> 10
'10'.hex  #=> 16
'ff'.hex  #=> 255
# 0 в случае ошибки конвертации:
'foobar'.hex  #=> 0
'zz'.hex      #=> 0

Таким образом, любая строка, не содержащая в себе корректное hex-число, будет трансформирована в массив из 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]

То есть мы практически всегда можем расшифровать любое зашифрованное сообщение любым паролем. Я думаю, автор подразумевал, что входным параметром функции шифрования всегда будет hex-число (и в этом случае gem сработал бы надёжно). Однако интерфейс gem’а не предполагает никаких ошибок при шифровании с обычной строкой, что приводит к ложному ощущению надёжности шифрования при его полном фактическом отсутствии.

Выводы


aes — не очень распространённый gem. На момент написания статьи у него на GitHub’е вcего 45 звёзд и 13 форков. Но проблема в том, что Google выдаёт его первым результатом по запросам «aes gem» или «ruby aes gem», а мы часто верим в то, что топовые результаты поисковых запросов ведут на качественные и популярные библиотеки. Часто программисты вообще не задумываются над проверкой и написанием тестов для подключаемых в проект внешних библиотек. Как вы видите из этого примера — такое поведение несёт в себе опасность.

Технические детали:
Gem: github.com/chicks/aes
Версия с данной ошибкой: 0.5.0 / 12c3648

Комментарии (4)

  • 13 января 2017 в 17:35 (комментарий был изменён)

    0

    Хм, в примере же явно написано «key», а не «password». И в документации написано «key». Почему вы решили что там должен быть пароль?

    Конечно, автор гема мог бы проверить, что на вход подается корректная HEX строка.

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

    Кстати, основная проблема этого гема не в этом. А в том — что он не позволяет выбрать режим шифрования. Подозреваю, что если не предоставить ему IV, то он будет шифровать в режиме ECB, что никуда не годится.

    • 13 января 2017 в 17:48

      +1

      Это не я, это автор оригинальной строки. И он придерживался, в общем-то распространённого паттерна поведения: «скачать, попробовать использовать, если что-то не получается — читать документацию». Просто методы шифрования и дешифрования вроде-бы корректно сработали, ошибок не возникло, расшифровалось именно то, что шифровалось. Очень многие на этом месте решили бы, что всё ок.
      • 13 января 2017 в 17:50

        0

        Это ж крипта. Правильнее было бы расшифровать зашифрованное другой реализацией AES.
    • 13 января 2017 в 17:49

      0

      А, это я с переводом общался. Прошу прощения. Но в любом случае автор неправ.

© Habrahabr.ru