Основы криптографии: шифрование и хэширование
Как превратить понятный текст в бессмыслицу для посторонних — и почему пароли хранят так, что их нельзя «расшифровать» обратно.
Шифрование — обратимое преобразование данных в нечитаемый вид по секретному ключу; хэширование — необратимое преобразование данных в короткий «отпечаток».
Зачем это нужно
Криптография защищает переписку, платежи, пароли — без неё интернет был бы небезопасен. Понимание разницы между шифрованием (обратимым) и хэшированием (необратимым) — ключ к тому, как устроены защита данных и хранение паролей. Это и важная тема информатики, и практический навык: вы научитесь шифровать текст и видеть, почему современные системы хранят не пароли, а их хэши. Весь код запускается прямо здесь.
Криптография — древнее искусство, ставшее точной наукой. Тысячелетиями люди прятали смысл сообщений: римляне сдвигали буквы, в Средневековье изобретали хитрые шифры, в мировых войнах от стойкости шифрования зависели судьбы сражений. Но настоящая революция произошла во второй половине XX века, когда криптография встала на строгую математическую основу. Сегодня она опирается не на секретность метода, а на вычислительную сложность: современные шифры устроены так, что взломать их перебором невозможно не потому, что алгоритм тайный (он как раз открыт и изучен), а потому, что перебор всех ключей занял бы у самого мощного компьютера время, превышающее возраст Вселенной. Именно эта идея — «безопасность через невозможность перебора» — отличает игрушечный шифр Цезаря, который мы взломаем за секунду, от реальных алгоритмов, защищающих ваши банковские операции. Понять эту разницу — главная цель урока, и она важнее, чем заучить конкретные алгоритмы.
Шифр Цезаря: древний и наглядный
Простейший шифр подстановки — шифр Цезаря: каждую букву сдвигают по алфавиту на фиксированное число позиций (ключ). При сдвиге на 3 «А» становится «Г». Расшифровка — сдвиг в обратную сторону. Реализуем для латиницы:
def caesar(text, shift):
result = []
for ch in text:
if ch.isalpha():
base = ord('A') if ch.isupper() else ord('a')
# сдвигаем в пределах 26 букв по кругу
result.append(chr((ord(ch) - base + shift) % 26 + base))
else:
result.append(ch) # пробелы и знаки не трогаем
return "".join(result)
msg = "Meet me at noon"
enc = caesar(msg, 3) # зашифровали сдвигом 3
dec = caesar(enc, -3) # расшифровали обратным сдвигом
print("исходный :", msg)
print("шифр :", enc)
print("дешифр :", dec)
Вывод:
исходный : Meet me at noon шифр : Phhw ph dw qrrq дешифр : Meet me at noon
Почему шифр Цезаря слаб
У шифра Цезаря всего 25 возможных ключей — их можно перебрать за секунду. Это урок о том, что стойкость шифра не должна зависеть от секретности алгоритма, только от ключа, и что пространство ключей должно быть огромным. Взломаем Цезаря полным перебором — покажем все варианты сдвига:
def caesar(text, shift):
out = []
for ch in text:
if ch.isalpha():
base = ord('A') if ch.isupper() else ord('a')
out.append(chr((ord(ch) - base + shift) % 26 + base))
else:
out.append(ch)
return "".join(out)
secret = "Khoor" # перехваченный шифртекст
print("Перебор всех ключей:")
for key in range(1, 6):
print(f" сдвиг -{key}: {caesar(secret, -key)}")
Вывод:
Перебор всех ключей: сдвиг -1: Jgnnq сдвиг -2: Ifmmp сдвиг -3: Hello сдвиг -4: Gdkkn сдвиг -5: Fcjjm
При сдвиге −3 появляется осмысленное «Hello» — ключ найден. Маленькое пространство ключей убивает любой шифр.
Симметричное шифрование через XOR
Более интересный приём — шифрование исключающим ИЛИ (XOR) с ключом. Как мы видели в разделе про двоичную арифметику, XOR обратим: применил дважды — вернул исходное. Это симметричное шифрование: один и тот же ключ и шифрует, и расшифровывает. Зашифруем строку, повторяя ключ по кругу:
def xor_cipher(text, key):
out = []
for i, ch in enumerate(text):
k = key[i % len(key)] # ключ повторяется по кругу
out.append(chr(ord(ch) ^ ord(k)))
return "".join(out)
key = "K7"
message = "secret data"
encrypted = xor_cipher(message, key)
decrypted = xor_cipher(encrypted, key) # тот же ключ возвращает текст
print("исходное :", message)
print("коды шифра:", [ord(c) for c in encrypted])
print("дешифр :", decrypted)
print("совпало :", message == decrypted)
Вывод:
исходное : secret data коды шифра: [56, 82, 40, 69, 46, 67, 107, 83, 42, 67, 42] дешифр : secret data совпало : True
Хэширование: дорога в одну сторону
Хэш-функция превращает любые данные в короткий «отпечаток» фиксированной длины. Главное её свойство — необратимость: по хэшу нельзя восстановить исходные данные. Плюс малейшее изменение входа полностью меняет хэш (лавинный эффект). Используем стандартную библиотеку hashlib с алгоритмом SHA-256:
import hashlib
def sha256(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
print("hello ->", sha256("hello"))
print("Hello ->", sha256("Hello")) # одна буква — совсем другой хэш
print("hello. ->", sha256("hello."))
print("\nдлина хэша всегда одинакова:", len(sha256("a")), "символов")
print("даже для длинного текста: ", len(sha256("a" * 10000)), "символов")
Вывод:
hello -> 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 Hello -> 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969 hello. -> 1589999b0ca6ef8814283026a9f166d51c70a910671c3d44049755f07f2eb910 длина хэша всегда одинакова: 64 символов даже для длинного текста: 64 символов
Как хранят пароли на самом деле
Теперь главное практическое применение. Сайт не хранит ваш пароль — он хранит его хэш. При входе он хэширует введённый пароль и сравнивает хэши. Так даже при утечке базы злоумышленник получит лишь хэши, из которых пароль не восстановить. Смоделируем регистрацию и вход:
import hashlib
def hash_password(pwd):
return hashlib.sha256(pwd.encode("utf-8")).hexdigest()
# «база данных»: хранятся только хэши, не пароли
users = {"anna": hash_password("Sun2024!")}
def login(name, pwd):
stored = users.get(name)
return stored is not None and stored == hash_password(pwd)
print("верный пароль :", login("anna", "Sun2024!"))
print("неверный пароль:", login("anna", "password"))
print("в базе хранится:", users["anna"][:20], "...")
Вывод:
верный пароль : True неверный пароль: False в базе хранится: 9dc08c066c5b10cc7acf ...
Попробуй сам
Проверим целостность «скачанного файла» через хэш — именно так сайты позволяют убедиться, что загрузка не повреждена и не подменена. Сравним хэши до и после «передачи».
import hashlib
def file_hash(content):
return hashlib.sha256(content.encode("utf-8")).hexdigest()
original = "важный документ версии 1.0"
published_hash = file_hash(original) # сайт публикует этот хэш
# вариант 1: файл дошёл без изменений
received_ok = "важный документ версии 1.0"
# вариант 2: файл подменили
received_bad = "важный документ версии 2.0"
print("опубликованный хэш:", published_hash[:16], "...")
print("целый файл совпал:", file_hash(received_ok) == published_hash)
print("подмена совпал:", file_hash(received_bad) == published_hash)
Вывод:
опубликованный хэш: 42e1b617e8ffabff ... целый файл совпал: True подмена совпал: False
Частые ошибки
- Путают шифрование и хэширование. Шифрование обратимо (есть расшифровка), хэширование — нет.
- Думают, что секретность алгоритма = защита. Стойкость должна зависеть только от ключа; алгоритм может быть известен всем.
- Считают слабые шифры безопасными. Шифр Цезаря и подобные взламываются перебором мгновенно.
- Хранят или сравнивают пароли в открытом виде. Сравнивают хэши; сам пароль нигде не сохраняют.
Итоги
- Шифрование обратимо: по ключу текст превращается в шифр и обратно (шифр Цезаря, XOR — симметричные).
- Стойкость шифра — в размере пространства ключей, а не в секретности алгоритма.
- Хэш-функция необратима, даёт отпечаток фиксированной длины, лавинно меняется при правке входа.
- Пароли хранят как хэши; целостность файлов проверяют сравнением хэшей.