Хеширование в Python: полное руководство с примерами

Гайд по hash(), HMAC и хешированию паролей

Хеширование в Python: полное руководство с примерами

Про хеширование часто говорят вместе с криптографией, потому что это важный инструмент для шифрования. Но кроме этого, хеширование нужно и для более простых операций, которыми программист пользуется почти в каждой программе. Как работает хеширование, как устроен этот механизм в языке Python и чем различаются его разные виды, рассказываем сегодня.

Что такое хеширование в Python

В Python хеширование нужно для нескольких вещей. Мы разберём всё и посмотрим, за что отвечает каждый и почему хеширование одно, а методы его реализации различаются.

Основные понятия и принципы хеширования

Хеширование — способ превратить любые данные в небольшое число или строку, которые называются «хеш». Можно сказать, что это «слепок» файла: он почти ничего не весит, но будет уникальным для каждого набора данных. При идеальном хешировании даже изменение одного бита информации должно давать другой хеш.

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

Хеш-функция — математический алгоритм, который принимает данные и возвращает их хеш. Внутри хеш-функции могут быть разными, потому что хеши нужны для разных целей.

Хеш — результат работы хеш-функции. Разные хеш-функции будут возвращать разное значение для одного и тоже набора данных. 

Например, из одной и той же строки Python Hash:

  • Алгоритм шифрования SHA-256 сделает такой хеш: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824. Этот хеш будет одинаковым при запуске на любой машине.
  • А встроенная в Python функция hash() даст такой хеш: 7437959953925788052. Он будет разным не только на разных компьютерах, но даже на одной системе после каждого запуска.

Процесс получения хеша односторонний. Это значит, что из хешированных данных нельзя восстановить первоначальные.

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

Но некоторые хеш-функции могут иногда выдавать одинаковые хеши при работе с разными файлами, потому что не всегда это критично.

Отличие hash() от криптографических хешей

Hash в Python — это встроенная в основной набор инструментов функция, которая нужна не для шифрования, а для быстрой работы с коллекциями — наборами элементов, структурированными разным образом. 

Благодаря таким хешам элементы в коллекциях можно искать быстро, потому что с их помощью Питон устанавливает номера ячеек в памяти. А когда у каждого элемента есть номер, не нужно перебирать весь набор, достаточно посмотреть на номер ячейки. Иногда hash() создаёт коллизии, и программа может попытаться использовать уже занятую ячейку. Но даже в этом случае работа с хешами быстрее простого перебора элементов.

Функция hash() работает быстро, но даёт разные результаты при каждом запуске программы и может создать из разных данных один и тот же хеш. Такое случается редко, но бывает. Этот метод используют для простых задач, таких как поиск элементов.

Специальные криптографические функции работают медленнее, но надёжнее — они всегда дают один и тот же результат на любой машине и при любом запуске, а вероятность совпадения хешей практически равна нулю. Криптографические хеши используются при работе с безопасностью.

Как выглядят две разные хеш-функции в коде:

# импортируем дополнительную библиотеку
import hashlib

# объявляем переменную
text = "using function for hash"

# встроенный hash(
built_in_hash = hash(text)

# SHA-256 через hashlib импортированный модуль
sha256_hash = hashlib.sha256(text.encode()).hexdigest()

# смотрим результаты
print(f"Встроенный hash(): {built_in_hash}")
print(f"SHA-256:           {sha256_hash}")

А вот какой результат они дают при запуске:

Встроенный hash(): -911619143414547343
SHA-256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

Функция hash() в Python

Эту функцию можно использовать без импортирования дополнительных модулей, просто как стандартную часть кода.

Как работает встроенная функция hash()

Функция использует простой алгоритм — для каждого типа данных разный, но всегда простой и быстрый. Например, для целого числа его хешем будет это же самое число.

Ограничения и особенности hash()

Инструмент выдаёт разные хеши при каждом запуске программы для безопасности. 

Хеши от hash Python использует для быстрого поиска элементов в их наборах — коллекциях. У нескольких элементов может быть одинаковый хеш, и тогда компьютеру нужно будет перебирать одинаковые, чтобы найти нужные данные.

Функция даёт разные результаты при каждом запуске именно потому, что защищается от большого количества одинаковых хешей. Например, злоумышленник может прислать на сервер запрос с JSON-файлом, который состоит из пар «ключ — значение». Для работы с такими файлами Питон использует хеширование даже без прямого запуска — чтобы быстро и удобно распределять информацию. Но если злоумышленник сделает так, что все значения в JSON-файле будут давать одинаковый хеш, программа начнёт перебирать ячейки памяти, чтобы положить все данные в разные ячейки.

Такой перебор сильно замедляет работу, поэтому при каждом запуске программы Питон добавляет к исходным данным ещё случайные — числа, байты, строки. Эта случайная информация называется «соль». Тогда количество коллизий будет минимально.

Примеры использования hash() для разных типов данных

Вот несколько примеров работы встроенной функции с разными значениями.

Целые числа int при хешировании возвращают это же самое число:

# объявляем переменную и присваиваем ей значение
value = 42
# выводим на экран хеш переменной,
# полученный через встроенную функцию
print(hash(value))

Запускаем и получаем то же самое значение, что мы присвоили переменной:

42

Числа с плавающей точкой типа float можно представить как дроби. Хеш такого числа рассчитывается как смесь хешей двух целых чисел: числителя и знаменателя. В самой программе этого не видно:

# объявляем переменную и присваиваем ей значение
value = 3.14
# выводим на экран хеш переменной,
# полученный через встроенную функцию
print(hash(value))

Хеш выглядит сложнее по сравнению с целым числом:

322818021289917443

При этом если число типа int и число float равны, например 11 и 11.0, то и хеш у них будет одинаковый и в обоих случаях он будет целым числом.

Строки считаются немного сложнее. Для хеша данных типа string функция берёт unicode-код каждого символа, смешивает эти коды через сложение и умножение с учётом позиций символов, добавляет «соль» и получает результат как целое число.

# объявляем переменную и присваиваем ей значение
value = "Привет, КОД!"
# выводим на экран хеш переменной,
# полученный через встроенную функцию
print(hash(value))

Получаем примерно такой хеш:

6040374651277641747

Модуль hashlib для хеширования

Этот модуль импортируется в программу верхней строкой кода:

# импортируем дополнительную библиотеку
import hashlib

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

Основные алгоритмы хеширования

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

Как выглядит работа разных алгоритмов в коде:

# импортируем модуль hashlib для хеширования
import hashlib

# создаём исходную строку, которую будем хешировать
message = "Returns Module Objects"

# переводим строку в байты, потому что hashlib работает с байтами
message_bytes = message.encode("utf-8")

# создаём хеш с помощью алгоритма MD5
md5_hash = hashlib.md5(message_bytes).hexdigest()

# создаём хеш с помощью алгоритма SHA-1
sha1_hash = hashlib.sha1(message_bytes).hexdigest()

# создаём хеш с помощью алгоритма SHA-256
sha256_hash = hashlib.sha256(message_bytes).hexdigest()

# выводим результаты
print("Оригинальное сообщение:", message)
print("MD5-хеш:     ", md5_hash)
print("SHA-1-хеш:   ", sha1_hash)
print("SHA-256-хеш:", sha256_hash)

А так будут выглядеть хеши в разных алгоритмах:

Оригинальное сообщение: Случайная строка
MD5-хеш: f9c707e5620dcb8838ab17d22f51c9b2
SHA-1-хеш: 1f3230f657625af04dd0f71d837381b79f866102
SHA-256-хеш: c69f97964f1cfd33bdc926869f0ac7207823b653dc0641a8040e939e2c943efe

Все результаты получились разной длины, но важно помнить, что длинный хеш не обязательно будет самым надёжным. Главный показатель безопасности в том, как именно работают математические операции и сколько нужно времени, чтобы взломать такой алгоритм.

Метод hexdigest() и его применение

После получения хеша через методы библиотеки hashlib результат нельзя просто вывести на экран командой print(). Мы получим объект хеша, который будет выглядеть примерно так:

Метод hexdigest() нужен, чтобы получить читаемый результат, который проще и удобнее использовать. Это понятный набор символов:

f9c707e5620dcb8838ab17d22f51c9b2

Практические примеры использования hashlib

На многих сайтах, где выкладывают файлы, например программы и прошивки, рядом с кнопкой «Скачать» есть строка вроде:

SHA256: e99a18c428cb38d5f260853678922e03abd83345b7a5f6f9096bb599b5cdb927

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

Другой пример — передача документов, подписанных электронной подписью. После подписи отправитель создаёт хеш файла и пересылает его вместе с оригиналом документов. Если получатель после этого создаёт хеш полученных файлов у себя и при сравнении выясняет, что эти хеши различаются, он понимает, что файл был изменён уже после подписания.

HMAC в Python

Механизм проверки подлинности отправителя данных. Сейчас объясним подробнее, как он работает.

Что такое HMAC и для чего он нужен

В этом процессе обычно участвуют два участника обмена информацией и сервер, через который они обмениваются сообщениями. Что ещё есть в этом процессе:

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

HMAC-функция одного из участников вычисляет хеш от сообщения, смешанного с секретным ключом. После этого хеш и сообщение отправляются на сервер, где сообщение смешивается с копией секретного ключа и снова вычисляется хеш. После этого хеши сравниваются. Если они совпадают, отправитель считается доверенным и его сообщение можно переслать второму участнику.

Можно сказать, что HMAC — это уникальная подпись сообщения.

Пример реализации HMAC в Python

Создать HMAC просто — нужны ключ, сообщение и алгоритм хеширования. В результате получится безопасная подпись.

Ключ и сообщение нужно создать в байтовом виде, потому что хеш-функции работают только с такими данными. Для этого нужно просто поставить букву b перед строкой:

# импортируем дополнительные библиотеки
import hmac
import hashlib

# создаём ключ и данные в байтовом виде
key = b"mysecret"
data = b"hmac python"

# создаём HMAC
signature = hmac.new(key, data, hashlib.sha1).hexdigest()

# выводим на экран подпись
print("Подпись HMAC:", signature)

Подпись HMAC выглядит так же, как обычный хеш:

Подпись HMAC: f222de61ca790444303ad759c651221785558c8d

Безопасность и применение HMAC

HMAC безопаснее простого хеша, потому что использует секретный ключ. Без ключа злоумышленник не сможет воспроизвести подпись, даже если знает алгоритм.

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

Хеширование паролей в Python

Хешировать пароли нужно обязательно, если у вас есть сервис, к которому получают доступ пользователи. Чтобы сделать базу паролей защищённой в случае взлома, в ней не хранят сами пароли — только их хеши. Тогда при попытке авторизации пользователь вводит пароль, а система считает хеш от этого пароля и проверяет, совпадает ли он с хешем в базе. А поскольку хеши, полученные надёжным алгоритмом, уникальны, то так можно проверить совпадение паролей.

Механизмов хеширования в Питоне несколько, но для хранения паролей можно использовать только некоторые. Дальше рассказываем почему.

Почему нельзя использовать обычные хеши для паролей

Обычные хеш-функции работают быстро, но это удобно не только обычным разработчикам, но и хакерам. Если злоумышленник украдёт базу данных, через алгоритм SHA-256 он сможет вычислять миллионы хешей самых популярных паролей в секунду и сравнивать их с теми, что находятся в украденной им базе. 

Хеши, полученные таким образом, всегда одинаковые, поэтому при совпадении хешей злоумышленник автоматически узнает и пароль.

Модуль bcrypt для безопасного хранения паролей

Для страховки от кражи базы данных делают хитро: используют медленные алгоритмы, а к каждому паролю перед вычислением хеша добавляют соль. Теперь подобрать пароль очень сложно, потому что к нему добавляются случайные данные.

Один из самый популярных модулей называется bcrypt. Перед использованием его нужно установить через терминал, зайдя в корневую папку проекта и выполнив команду pip install bcrypt.

Пример реализации хеширования паролей

Пример работы упрощённой программы с модулем bcrypt:

# импортируем предварительно установленный модуль
import bcrypt

# создаём оригинальный пароль
password = b"secret is used"

# создаём соль методом bcrypt.gensalt и хешируем пароль
hashed = bcrypt.hashpw(password, bcrypt.gensalt())

# смотрим, как выглядит хеш
print("Хеш:", hashed)

# сравниваем введённый пароль с сохранённым хешем
input_password = b"secret is used"
if bcrypt.checkpw(input_password, hashed):
   print("Пароль верный!")
else:
   print("Пароль неверный!")

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

Проверяем программу:

Хеш: b’$2b$12$YfULzNVql2lqLLvI3kKjN.9/YEBx.YCZ0vLlyUDN1b0ITH69vKa16′
Пароль верный!

Хакер тоже может посмотреть на хеш и достать оттуда соль. Но теперь он не сможет быстро сравнивать хеши самых популярных паролей с базой данных, потому что для каждой соли придётся создавать свою таблицу. А если у каждой записи своя соль, а алгоритм вычисления работает медленно, то подобрать пароли по хешам становится практически невозможно.

Бонус для читателей

Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая. 

Вам может быть интересно
hard