Проста і жахлива історія про шифрування

Це буде історія про відкрите ПЗ, довірі та відповідальності.

Задача та її розв'язок
Як-то раз мені знадобилося додати в свій додаток на 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'е всього 45 зірок і 13 форков. Але проблема в тому, що Google видає його першим результатом за запитами «aes gem» або «ruby aes gem», а ми часто віримо в те, що топові результати пошукових запитів ведуть на якісні і популярні бібліотеки. Часто програмісти взагалі не замислюються над перевіркою і написанням тестів для підключаються в проект зовнішніх бібліотек. Як ви бачите з цього прикладу — така поведінка несе в собі небезпеку.

Технічні деталі:
Gem: github.com/chicks/aes
Версія з даною помилкою: 0.5.0 / 12c3648
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.