Автор статьи: Рустем Галиев (IBM Senior DevOps Engineer & Integration Architect)

Привет, Хабр!

Сегодня продолжим про безопасную архитектуру.

В современном цифровом мире безопасность данных становится все более актуальной задачей. С ростом числа кибератак и утечек информации, защита конфиденциальных данных пользователей является приоритетом для разработчиков программного обеспечения. Одним из ключевых методов обеспечения безопасности является хеширование и добавление соли (salting) к паролям и другим критически важным данным.

Хеширование представляет собой процесс преобразования исходных данных в уникальный зашифрованный код фиксированной длины. Этот метод широко используется для защиты паролей, так как даже при утечке хешированных данных восстановить оригинальный пароль крайне сложно. Однако, хеширование само по себе не всегда достаточно для полной безопасности. Злоумышленники могут применять атаки типа радужных таблиц (rainbow tables) для нахождения исходных значений по их хешам.

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

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

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

Поскольку любой может создать цифровой сертификат, сам сертификат бесполезен, если он не подтвержден доверенной третьей стороной. Эти доверенные стороны известны как центры сертификации (Certificate Authorities, CAs), и по умолчанию все мы доверяем ряду CAs, которые выступают своего рода стражами Интернета.

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



Для защиты конфиденциальности в Интернете вам понадобятся SSL/TLS и HTTPS – или Secure Socket Layer/Transport Layer Security и Hypertext Transfer Protocol Secure. Веб-трафик по умолчанию не зашифрован и открыт для перехвата и записи.

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



Еще одна форма защиты конфиденциальности — использование хеширования. Учтите, что хеширование не является шифрованием.

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

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



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



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

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

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



Для предотвращения таких атак можно использовать технику, известную как «соление» (salting). Соль — это случайное значение, которое добавляется к паролю перед хешированием. Это делает каждый хеш уникальным, даже если пользователи используют одинаковые пароли. Таким образом, таблицы поиска становятся бесполезными, так как для каждой соли нужно будет создавать отдельную таблицу.

Мы можем преодолеть эту проблему, сочетая хеширование с другим важным понятием, известным как «соль» (salt). Добавление соли к хешу означает добавление случайных данных к сообщению, чтобы оно не всегда давало одинаковый результат. Это защищает от атак, в которых злоумышленники пытаются угадать общие пароли для создания коллизий.

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



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

Давайте рассмотрим пример реализации на python

Мы будем использовать библиотеку hashlib для хеширования и os для генерации случайной соли.

Генерация соли:

salt = os.urandom(16)

Мы используем os.urandom(16) для генерации случайной соли размером 16 байт (128 бит). Это обеспечит уникальность соли для каждого пароля.

Хеширование пароля:

hash_object = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)

Мы используем функцию pbkdf2_hmac из библиотеки hashlib для хеширования пароля.

Параметры функции:
'sha256' — алгоритм хеширования.
password.encode() — пароль, преобразованный в байты.
salt — сгенерированная соль.
100000 — количество итераций хеширования (чем больше, тем безопаснее, но медленнее).

Возврат соли и хеша:

return salt, hash_object

Мы возвращаем пару (соль, хеш), которые затем могут быть сохранены в базе данных.

Проверка пароля:

hash_object = hashlib.pbkdf2_hmac('sha256', provided_password.encode(), stored_salt, 100000)
return hash_object == stored_password


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

Пример использования

Хеширование и вывод соли и хеша:

password = 'my_secure_password'
salt, hashed_password = hash_password(password)
print(f"Salt: {salt.hex()}")
print(f"Hashed Password: {hashed_password.hex()}")


Мы хешируем пароль и выводим соль и хеш в шестнадцатеричном формате.

Проверка правильного и неправильного пароля:

is_valid = verify_password(hashed_password, salt, 'my_secure_password')
print(f"Password is valid: {is_valid}")
is_valid_wrong = verify_password(hashed_password, salt, 'wrong_password')
print(f"Password is valid: {is_valid_wrong}")


Мы проверяем правильный и неправильный пароль, и выводим результаты проверки.

В итоге у нас получилось:

import hashlib
import os

def hash_password(password):
    # Генерируем случайную соль
    salt = os.urandom(16)  # 16 байт (128 бит) - стандартный размер соли
    # Конкатенируем пароль и соль и хешируем результат
    hash_object = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
    # Возвращаем соль и хеш в виде пары (salt, hash)
    return salt, hash_object

def verify_password(stored_password, stored_salt, provided_password):
    # Хешируем предоставленный пароль с той же солью
    hash_object = hashlib.pbkdf2_hmac('sha256', provided_password.encode(), stored_salt, 100000)
    # Сравниваем хеши
    return hash_object == stored_password

# Пример использования:
password = 'my_secure_password'
salt, hashed_password = hash_password(password)

print(f"Salt: {salt.hex()}")
print(f"Hashed Password: {hashed_password.hex()}")

# Проверка пароля
is_valid = verify_password(hashed_password, salt, 'my_secure_password')
print(f"Password is valid: {is_valid}")

is_valid_wrong = verify_password(hashed_password, salt, 'wrong_password')
print(f"Password is valid: {is_valid_wrong}")


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

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


  1. David_Osipov
    19.06.2024 14:13
    +2

    Согласно OWASP, PPBKDF2-SHA256 должно итерировать минимум 600 000. А лучше используйте везде Argon2, если вам не требуется играть по правилам FIPS.