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

Original author: Kacper Walanus
  • Translation
Это будет история об открытом ПО, доверии и ответственности.

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


Как-то раз мне понадобилось добавить в своё приложение на 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
Инфопульс Украина
Creating Value, Delivering Excellence
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 20

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

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

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

    Кстати, основная проблема этого гема не в этом. А в том — что он не позволяет выбрать режим шифрования. Подозреваю, что если не предоставить ему IV, то он будет шифровать в режиме ECB, что никуда не годится.
      +6
      Это не я, это автор оригинальной строки. И он придерживался, в общем-то распространённого паттерна поведения: «скачать, попробовать использовать, если что-то не получается — читать документацию». Просто методы шифрования и дешифрования вроде-бы корректно сработали, ошибок не возникло, расшифровалось именно то, что шифровалось. Очень многие на этом месте решили бы, что всё ок.
        +13
        Это ж крипта. Правильнее было бы расшифровать зашифрованное другой реализацией AES.
        +10
        А, это я с переводом общался. Прошу прощения. Но в любом случае автор неправ.
          +4
          Прав автор или нет, но отсутствие проверки на корректность входных данных — это недостаток библиотеки. Если конечно это не закрытая библиотека которой пользуется только лично её автор.
          • UFO just landed and posted this here
              0
              Беда в том что зачастую слово key используется как более прикольная/заумная/краткая замена password, поэтому не каждый программист вообще подозревает о разнице, и даже если подозревает, не особо надеется, что она подразумевалась другим программистом.
                0
                Конечно, автор гема мог бы проверить, что на вход подается корректная HEX строка

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


                Вот, например, pycrypto для python. key — просто набор байтов.
                https://www.dlitz.net/software/pycrypto/api/current/Crypto.Cipher.AES-module.html


                key (byte string) — The secret key to use in the symmetric cipher. It must be 16 (AES-128), 24 (AES-192), or 32 (AES-256) bytes long

                И никаких проблем.

                +3
                Тестирование и удача — наше все.
                  0
                  Повезло, что тесты сделали, иначе всплыло бы в самый неподходящий момент.
                    +2
                    А зачем гем, если симметричное шифрование есть в stdlib?
                      0
                      Если мне память не изменяет, то эта тема уже освещалась на хабре. Линк найти не могу, в избранное не добавил.
                        +2
                        Человек все правильно сделал. Взял готовый инструмент с открытым исходным кодом, воспользовался им и даже протестировал. В результате вот эта статья.

                        Того, кто работал с криптой должно было сразу напрячь имя key которое далеко не password. Традиционно, ключ шифрования — это не пароль шифрования. И в современном мире даже не MD5 от пароля, а еще и перемешивание битов чем нибудь типа pbkdf2 на 80000 раундов. Человек мог этого не знать, но авторы криптографической библиотеки должны были сделать две вещи: четко написать о значении key в доке, не используя принцип «минимально и достаточно», как это обычно бывает когда разрабы ленятся (а вообще дока была?), и сделать защиту от дурака, а не тупо преобразовывать не-hex символы в нули.
                          –1
                          Я последнее время насмотрелся творчества програмистов, в проекте порядка гигабайта исходников. Прихожу к выводу, на уровне языка надо вводить семантику типа

                          if условие then
                          действе
                          else
                          raise exception 'вызывается принудительно на уровне языка
                          end if


                          То есть программист должен перечислить все случаи, когда процедура работает. Во всех остальных случаях должна выводиться ошибка.
                            +6
                            Поздравляю, вы на полпути к изобретению контрактов )
                              0
                              А где там эта семантика прописана в Contract Driven Development? я поверхностно глянул — не нашел.
                                0
                                а, нашел — в предусловиях

                          Only users with full accounts can post comments. Log in, please.