Три разные операции
Base64 то и дело встречается там, где люди ожидают защиты, поэтому стоит точно понимать, что он делает. Это кодирование: фиксированное публичное соответствие между байтами и текстом (см. как работает Base64). У этого соответствия нет ни ключа, ни секрета, и один и тот же ввод всегда даёт один и тот же результат.
| Кодирование (Base64, hex) | Шифрование (AES-GCM) | Хеширование (SHA-256, Argon2) | |
|---|---|---|---|
| Назначение | Записать байты текстом | Сохранить данные в тайне | Получить отпечаток или проверить данные |
| Обратимо | Да, кем угодно | Да, с ключом | Нет |
| Нужен ключ | Нет | Да | Нет (HMAC использует ключ) |
| Одинаковый ввод даёт одинаковый результат | Всегда | Нет, при случайном nonce | Да, если нет соли |
| Размер результата | Около 4/3 от ввода | Ввод плюс nonce и тег | Фиксированная длина |
Декодирование не требует вообще никаких усилий:
echo ZGVwbG95Omh1bnRlcjI= | base64 -d
# deploy:hunter2
Человеку, мельком взглянувшему на эту строку, она кажется непрозрачной. Для скрипта, сканера логов или злоумышленника это открытый текст плюс один лишний шаг.
Как данные в Base64 утекают на практике
Сценарий всегда один: значение кодируют в Base64 для передачи, кто-то принимает его за защищённое, и оно оказывается там, где его можно прочитать. Вот типичные формы этой ошибки. Они описаны в общем виде, детали каждый раз свои.
Заголовки HTTP Basic Auth
Basic-аутентификация (RFC 7617) отправляет имя пользователя и пароль в каждом запросе как Base64(user:password):
Authorization: Basic ZGVwbG95Omh1bnRlcjI=
Защищает его только TLS на соединении. Как только заголовок выходит из этого туннеля, пароль фактически становится открытым текстом. Типичные пути: обратные прокси или фреймворки, которые пишут заголовки запросов в лог, HAR-файлы из инструментов разработчика, приложенные к обращению в поддержку, вывод curl -v, вставленный в чат, и системы отслеживания ошибок, которые сохраняют запросы целиком. Декодер Basic Auth показывает, как мало нужно, чтобы такой заголовок прочитать.
Секреты Kubernetes
Kubernetes Secret хранит значения в Base64:
apiVersion: v1
kind: Secret
metadata:
name: db
data:
password: czNjcjN0
czNjcjN0 декодируется в s3cr3t. Кодирование нужно, чтобы значения могли содержать двоичные данные внутри YAML. Документация Kubernetes прямо говорит, что по умолчанию секреты хранятся в etcd незашифрованными, и прочитать их может любой, кому разрешено сделать get для Secret через API. Типичные утечки: манифесты с реальными значениями data:, закоммиченные в Git-репозиторий, вывод kubectl get secret -o yaml, вставленный в тикеты, и бэкапы etcd или состояния кластера, сохранённые без шифрования. Декодер Kubernetes Secret декодирует все значения секрета разом, и это заодно быстрый способ проверить, что на самом деле раскрывает манифест.
Учётные данные для реестров контейнеров устроены так же. Поле auth в Docker config.json или в секрете .dockerconfigjson содержит Base64(user:password); его читает декодер dockerconfigjson.
Данные внутри JWT
Подписанный JWT выглядит как запечатанный токен, но первые две его части содержат обычный JSON в Base64URL:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImVtYWlsIjoiYW5hQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIn0.<signature>
Полезная нагрузка декодируется в {"sub":"42","email":"ana@example.com","role":"admin"}. Подпись защищает токен от изменения, а конфиденциальности не даёт. Каждое браузерное расширение, прокси, лог-файл и скрипт аналитики, которые видят токен, могут прочитать его содержимое. Персональным данным, внутренним идентификаторам, которые не хочется раскрывать, и вообще всему секретному в payload JWT не место. Если без этого никак, используйте зашифрованный JWT (JWE). Как устроен токен, разобрано в статье Base64 в JWT.
«Обфусцированные» секреты в коде и конфигах
Классическая ошибка: API-ключи, сохранённые в Base64 во фронтенд-JavaScript, сборках мобильных приложений или конфигах. Обычно рассуждают так: зато ключ не лежит «открытым текстом». Это действительно спасает от наивного поиска по самому ключу, и больше ни от чего. Всё, что отправлено клиенту, этот клиент может прочитать.
В атаках та же идея работает в обратную сторону. -EncodedCommand в PowerShell принимает Base64 от текста в UTF-16LE, и его часто используют, чтобы вредоносную команду было труднее прочитать с первого взгляда. Защитники декодируют её за секунды, например декодером PowerShell EncodedCommand.
Что использовать вместо Base64
Инструмент выбирается под задачу. В каждом из случаев ниже Base64 по-прежнему нужен как текстовая обёртка вокруг байтов, которые защищены чем-то другим.
Чтобы сохранить данные в тайне: аутентифицированное шифрование
Используйте AES-256-GCM или ChaCha20-Poly1305 через хорошо поддерживаемую библиотеку. Оба режима аутентифицированные, поэтому подмена обнаруживается при расшифровке. Два правила важнее выбора алгоритма: никогда не используйте один nonce дважды с одним ключом и храните ключ не рядом с данными (в KMS, менеджере секретов или окружении, которое злоумышленник не контролирует заодно).
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
const key = randomBytes(32); // in real code, load this from a KMS or secret store
const iv = randomBytes(12); // 96-bit nonce, unique per message
const cipher = createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update("deploy:hunter2", "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
// Base64 carries the encrypted bytes as text: nonce + tag + ciphertext
const token = Buffer.concat([iv, tag, ct]).toString("base64");
const raw = Buffer.from(token, "base64");
const d = createDecipheriv("aes-256-gcm", key, raw.subarray(0, 12));
d.setAuthTag(raw.subarray(12, 28));
console.log(Buffer.concat([d.update(raw.subarray(28)), d.final()]).toString("utf8"));
// deploy:hunter2
На выходе получается строка Base64 из 56 символов, которая меняется при каждом запуске, потому что nonce случайный. Без ключа она бесполезна. Поэкспериментировать без кода можно в инструментах AES-шифрование и AES-расшифровка, они работают в браузере.
Чтобы хранить пароли: медленный хеш с солью
Пароли не нужно ни шифровать, ни кодировать. Храните хеш от функции, созданной для паролей: Argon2id, scrypt или bcrypt, с уникальной случайной солью для каждого пароля. Простой SHA-256 от пароля вычисляется слишком быстро и не выдерживает перебора. В стандартной библиотеке Python есть scrypt:
import hashlib, hmac, os
def hash_password(pw: str) -> bytes:
salt = os.urandom(16)
return salt + hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1)
def check_password(pw: str, stored: bytes) -> bool:
salt, digest = stored[:16], stored[16:]
candidate = hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1)
return hmac.compare_digest(candidate, digest)
Если результат нужно положить в текстовую колонку или JSON, кодировать его в Base64 вполне правильно.
Чтобы подтвердить целостность: подписи и HMAC
Если важно, чтобы данные никто не изменил, а читать их можно всем, используйте HMAC с общим секретом или цифровую подпись. Получаются байты, которые обычно передают в Base64 или hex. Генератор HMAC показывает результат в обеих формах.
Для учётных данных при передаче и хранении
Используйте TLS на каждом соединении, по которому идут Basic Auth или токены, предпочитайте короткоживущие токены статическим паролям, вычищайте заголовки Authorization из логов, включите шифрование секретов Kubernetes на диске и не храните реальные значения секретов в Git.
Вопросы и ответы
Безопасен ли Base64?
У Base64 нет никаких защитных свойств. Ключа нет, поэтому для декодирования не нужно ничего, кроме самой строки. Как транспортный формат для уже защищённых данных, например шифротекста или подписи, он вполне годится.
Можно ли спрятать пароль в конфиге с помощью Base64?
Нет. Любой, кто может прочитать файл, декодирует пароль командой base64 -d. Ограничьте права на файл, используйте менеджер секретов или зашифруйте значение ключом, который хранится в другом месте.
Зачем секреты Kubernetes кодируются в Base64, если не ради защиты?
Чтобы значения Secret могли содержать произвольные двоичные данные, например TLS-ключи или хранилища ключей, внутри YAML и JSON. Защиту обеспечивают RBAC, шифрование etcd на диске и аккуратное обращение с манифестами.
Нужен ли секрет, чтобы прочитать payload JWT?
Нет. Обе первые части токена содержат JSON в Base64URL, поэтому прочитать их может любой. Ключ понадобится, только чтобы проверить или создать подпись. Попробуйте в декодере JWT.
Чем кодирование отличается от шифрования и хеширования?
Кодирование может обратить кто угодно. Шифрование обращается только с ключом. Хеширование одностороннее: значение можно сверить с хешем, но получить значение из хеша нельзя.