Безопасность приложения — одна из самых главных столпов разработки. При ограниченном функционале вы сможете иметь лояльное ядро аудитории, но при глобальной уязвимости в коде, влекущие утечку персональных данных — вы потеряете вообще всех пользователей. Задача архитектора, программиста, QA в том, чтобы свести этот риск к минимуму. Но нужно помнить — абсолютной безопасности не существует. В любом случае необходимо иметь представление о том, как устроена информационная безопасность в языке Python
Что мы можем подразумевать под безопасностью в приложении? Есть три основных аспекта:
Конфиденциальность (от англ. confidence — доверие) — предотвращение утечки (разглашения) информации.
Целостность — в информатике (криптографии, теории телекоммуникаций, теории информационной безопасности) означает, что данные не были изменены при выполнении операции над ними — будь то передача, хранение или отображение.
Доступность — обеспечение доступа к информации и связанным с ней активам авторизованных пользователей по мере необходимости.
Хэширование
Хэширование — преобразование входного массива данных произвольной длины в выходную битовую строку фиксированной длины. Иначе это называется хэш-функциями ли функциями свертки, а их результаты — хэш-кодом, контрольной суммой или дайджестом сообщения (message digest). Каждый результат работы функции (хэш-код) — уникален. Чтобы создать хэш-код, он же дайджест используются алгоритмы хэширования. Самые популярные алгоритмы прямо сейчас: MD5 (относительно ненадежен), SHA-1 (относительно ненадежен), SHA-2, SHA-3 и наш ГОСТ Р 34.11-2012.
Для работы с хэшированием в питоне используются модуль hashlib для вычисления хэш-функции и модуль HMAC (читается как эйчмак) — для проверки целостности информации (это гарантия того, что данные не будут изменены в процессе передачи), модуль secrets для генерации псевдослучайных чисел и модуль getpass, чтобы создать безопасный ввод логина и пароля. Есть и более продвинутые варианты для работы с низкоуровневыми интерфейсами, криптографическими алгоритмами: симметричные шифры, дайджесты сообщений и функции вывода ключей, такие как PyCryptodome или Cryptography.
HMAC (hash-based message authentication code, код аутентификации (проверки подлинности) сообщений, использующий хэш-функции) — в криптографии один из механизмов проверки целостности информации, позволяющий гарантировать, что данные, передаваемые или хранящиеся в ненадежной среде, не были изменены посторонними лицами. Механизм HMAC использует MAC — стандарт, излагающий способ обмена данными и проверки целостности передаваемых данных с использованием секретного ключа. Два клиента, использующие HMAC, как правило, разделяют общий секретный ключ.
Обычно HMAC-аутентификация используется во внутренних системах обмена сообщениями и в межпроцессном взаимодействии. Например, если требуется написать систему, которая включает множество процессов, взаимодействующих на кластере из нескольких машин. Тогда можно использовать данный подход, чтобы гарантировать, что только разрешенные процессы могут подключаться к другим. Модуль muliprocessing внутри использует аутентификацию, основанную на HMAC, чтобы установить соединения с подпроцессами.
Простой пример хэширования пароля
Аутентификация соединения такая штука, которая может не подразумевать последующего шифрования — все сообщения передаются в открытом виде по сети. Любой, кто подключиться к вашей сети, может перехватить нужный сетевой трафик. Этого нам не нужно, поэтому мы создаем объект хэш-суммы с набором байтов h = hashlib.md5(). Добавляем в неё данные для расчета этой самой суммы h.update(b’Code’). Можно вывести хэш-сумму print(h.hexdigest()). HEX здесь прописываем т.к. используется шестнадцатеричная система счисления. Чтобы вычислить хэш используется специальная конструкция pbkdf2_hmac, в которой указаны параметры — имя алгоритма шифрования (sha256), пароль в виде строки байтов (b’pswd’, наличие соли (b’salt’) и длина расширенного ключа (100000). Плюс такого подхода в том, что недопустимо изменение даже одного символа, т.е. принцип целостности информации тут сохраняется
Код:
import hashlib, binascii # Наша хэш функция # Создание объекта хэш-суммы h = hashlib.md5() # Добавление данных для расчета суммы - можно добавлять только строку байтов h.update(b'Code') # Вывод хэш-суммы print(h.hexdigest()) #Парольный хэш dk = hashlib.pbkdf2_hmac('sha256', b'pswd', b'salt', 100000) # Вычисленное значение можно хранить в БД print(binascii.hexlify(dk))
Результат выполнения:
Пример аутентификации пользователей
Как мы будем реализовать аутентификацию пользователей в Python: сначала на клиентской стороне вычисляется значения hmac и отправляется на сервер. После запроса сервер формирует генерацию рандомного сообщения и возвращается на клиента. Затем клиент вычисляет у себя на основе секрета и рандомного сообщения значение функции hmac и отправляется серверу. Функция hmac рассчитывается также и на сервере. В завершении сервер сравнивает данные и на основе этого анализа дает заключение по аутентификации пользователя. Посмотрим как это выглядит в практической плоскости.
Серверная сторона
Функция echo_server принимает подключения клиентов (accept) и запускает функцию echo_handler — она проводит аутентификацию клиента и отсылает его же запрос обратно (эхо) с помощью функции server_authenticate. В ней мы создаем случайное рандомное сообщение и отправляем клиенту. Вычисляем HCMA-функцию и сравниваем её с результатом функции, присланным от клиента. Если они совпадают — подключаем клиента, если нет — сокет закрывается
Код:
import hmac import os # hmac.new(key, msg=None, digestmod=None) # key - byte-строка, представляющая ключ # msg - сообщение, для которого нужно вычислить хэш # digestmod - имя хэш-фукнции, которая будет применена для вычисления (sha-1, sha-256, ...) # -------------- Функция аутентификации клиента на сервере -------------------- def server_authenticate(connection, secret_key): ''' Запрос аутентификаии клиента. сonnection - сетевое соединение (сокет); secret_key - ключ шифрования, известный клиенту и серверу ''' # 1. Создаётся случайное послание и отсылается клиенту message = os.urandom(32) connection.send(message) # 2. Вычисляется HMAC-функция (локальный результат) от послания с использованием секретного ключа hash = hmac.new(secret_key, message) digest = hash.digest() # 3. Пришедший ответ от клиента сравнивается с локальным результатом HMAC response = connection.recv(len(digest)) return hmac.compare_digest(digest, response) # ---------------------- Эхо-сервер ----------------------------------------- from socket import socket, AF_INET, SOCK_STREAM secret_key = b'our_secret_key' def echo_handler(client_sock): ''' Эхо-обработка. Проводит аутентификацию клиента и отсылает его же запрос обратно (эхо). ''' if not server_authenticate(client_sock, secret_key): client_sock.close() return while True: msg = client_sock.recv(8192) if not msg: break client_sock.sendall(msg) def echo_server(address): ''' Эхо-сервер. "Слушает" указанный адрес и общается с клиентом через echo_handler. ''' sock = socket(AF_INET, SOCK_STREAM) sock.bind(address) sock.listen(5) while True: conn, addr = sock.accept() echo_handler(conn) echo_server(('', 9999))
Клиентская сторона
На клиентской части мы подключаемся к серверу (sock.connect) и отправляем сообщение аутентификации sock.send(b’Hello, my secure server!’). До того, как мы отослали серверу сообщению — проходим аутентификацию на основе секретного ключа client_authenticate(sock, secret_key). Принимаем сообщение от сервера и вычисляем hcma функцию, чтобы отправить обратно к серверу, который его сравнивает со своим результатом
Код:
import hmac import os def client_authenticate(connection, secret_key): ''' Аутентификация клиента на удаленном сервисе. Параметр connection - сетевое соединение (сокет); secret_key - ключ шифрования, известный клиенту и серверу ''' # принимаем случайное послание от сервера message = connection.recv(32) # вычисляем HMAC-функцию # <hmac.HMAC object at 0x000000BACEF3D278> # b'?\xa0\x85\x94`\xb9[\xe8\x865\x97\xb6\x06\x1e\xefj' hash = hmac.new(secret_key, message) digest = hash.digest() # отправляем ответ серверу connection.send(digest) from socket import socket, AF_INET, SOCK_STREAM secret_key = b'our_secret_key' sock = socket(AF_INET, SOCK_STREAM) sock.connect(('localhost', 9999)) # проходим аутентификацию client_authenticate(sock, secret_key) sock.send(b'Hello, my secure server!') resp = sock.recv(1024) print('Сервер ответил: ', resp.decode())
Шифрование данных
Для шифрования данных в питоне использовалась библиотека PyCrypto. Но её забросили еще в далеком 2014 году, а на сцену вышел форк (ответвление) PyCryptodome (PyCryptoDomeEx). Для шифрования данных в PyCryptodome есть поддержка нескольких алгоритмов — блочные шифры: AES, DES, 3DES, Blowfish и поточные шифры: Salsa20, ChaCha20. Устанавливается форк с помощью команды pip install pycryptodomex.
В нашем примере в переменную plaintext вносим шифруемое сообщение, преобразовав его в байты. Функция _encrypt принимает на вход тот самый текст сообщения в байтах. В переменную cipher записывается конструкция AES.new. Она создает новый шифр с использованием этого алгоритма. AES — это специальный симметричный шифр. Обратите внимание, что наша длина сообщений — 16 байт (т.к. используется AES-128). Длина ключа ASCII должна быть 16 символов (1 символ = 8 бит = 1 байт, 16 символов = 16 байт)
Код:
import os from binascii import hexlify from Cryptodome.Cipher import AES # шифруемое сообщение plaintext = b'The rain in Spain' def padding_text(text): ''' Выравнивание сообщения до длины кратной 16 байтам. В данном случае исходное сообщение дополняется пробелами. ''' pad_len = (16 - len(text) % 16) % 16 return text + b' ' * pad_len def _encrypt(plaintext, key): ''' Шифрование сообщения plaintext ключом key. Атрибут iv - вектор инициализации для алгоритма шифрования. Если не задаётся явно при создании объекта-шифра, то генерируется случайно. Его следует добавить в качестве префикса к финальному шифру, чтобы была возможность правильно расшифровать сообщение. ''' cipher = AES.new(key, AES.MODE_CBC) ciphertext = cipher.iv + cipher.encrypt(plaintext) return ciphertext def _decrypt(ciphertext, key): ''' Расшифровка шифра ciphertext ключом key Вектор инициализации берётся из исходного шифра. Его длина для большинства режимов шифрования всегда 16 байт. Расшифровываться будет оставшаяся часть шифра. ''' cipher = AES.new(key, AES.MODE_CBC, iv=ciphertext[:16]) msg = cipher.decrypt(ciphertext[16:]) return msg # Осуществим шифрование сообщения алгоритмом AES # key (строка байтов) - секретный ключ для симметричного шифрования. # Ключ должен быть длиной 16 (AES-128), 24 (AES-192) или 32 (AES-256) байта. key = b'Super Secret Key' # Длина сообщения должна быть кратна 16, поэтому выполним выравнивание. plaintext = padding_text(plaintext) # Выполним шифрование cipher = _encrypt(plaintext, key) print(hexlify(cipher)) # Выполним расшифрование msg = _decrypt(cipher, key) print(msg)
Как вам статья?