Таблица префиксов
Каждая строка проверена кодированием настоящих магических байтов формата. Стабильный префикс здесь означает часть, которая у формата никогда не меняется. Символы после него зависят от остального содержимого файла.
| Префикс Base64 | Магические байты (hex) | Формат | Чем открыть |
|---|---|---|---|
iVBORw0KGgo | 89 50 4E 47 0D 0A 1A 0A | PNG | Base64 в PNG |
/9j/ | FF D8 FF | JPEG | Base64 в JPG |
R0lGOD | 47 49 46 38 (GIF8) | GIF (R0lGODlh = GIF89a, R0lGODdh = GIF87a) | Base64 в GIF |
UklGR | 52 49 46 46 (RIFF) | WebP, WAV, AVI | Base64 в WebP, Base64 в WAV |
JVBERi0 | 25 50 44 46 2D (%PDF-) | Base64 в PDF | |
UEsDB | 50 4B 03 04 (PK..) | ZIP, DOCX, XLSX, PPTX, JAR, APK, EPUB | Base64 в ZIP, Word, Excel |
H4sI | 1F 8B 08 | gzip | Декодировать gzip из Base64 |
eJ, eN, eF, eA | 78 9C, 78 DA, 78 5E, 78 01 | zlib | Zlib и Base64 |
eyJ | 7B 22 ({") | JSON-объект, JWT | Base64 в JSON, декодер JWT |
PD94bWwg | 3C 3F 78 6D 6C 20 (<?xml ) | XML, SVG с объявлением | Base64 в XML |
PHN2Zy | 3C 73 76 67 20 (<svg ) | SVG без объявления (<svg с переводом строки даёт PHN2Zw) | Base64 в SVG |
MII | 30 82 | DER: сертификаты, ключи, CSR | декодер сертификатов |
AAAA | 00 00 00 | Блоб открытого ключа SSH или ведущие нули | отпечаток SSH-ключа |
SUQz | 49 44 33 (ID3) | MP3 с тегом ID3v2 | Base64 в MP3 |
AAABAA | 00 00 01 00 | ICO | Base64 в ICO |
d09GMg | 77 4F 46 32 00 (wOF2) | Шрифт WOFF2 с контурами TrueType (шрифты CFF начинаются с d09GMk9UVE8) | Base64 в шрифт |
TVqQ | 4D 5A 90 (MZ) | EXE или DLL для Windows (некоторые компоновщики пишут MZP, это даёт TVpQ) | Base64 в файл |
f0VMR | 7F 45 4C 46 (.ELF) | Бинарник Linux ELF | Base64 в файл |
Несколько строк требуют пояснения:
- RIFF сам по себе только контейнер. Название формата лежит в байтах с 8 по 11, а байты с 9 по 11 кодируются в символы с 13-го по 16-й:
RUJQдля WebP (EBP),QVZFдля WAV (AVE),Vkkgдля AVI (VI). Первая буква названия формата попадает в одну 3-байтную группу с размером файла, поэтому стабильного символа не даёт. - zlib не имеет единого префикса. Второй байт хранит уровень сжатия:
eJдля уровня по умолчанию,eNдля уровней с 7 по 9,eFдля 2–5,eAдля 0 и 1 (так пишетzlibв Python). - JSON начинается с
eyJ, только если первые два символа{"идут без пробела.{ "a": 1 }начинается сeyAi, отформатированный{с переводом строки даётewo, а массив[даётW. - DER-структуры начинаются с тега SEQUENCE
30, и он всегда даётM. Двухбайтная длина (30 82) даётMII, а следующий символ зависит от длины: открытый ключ RSA 2048 начинается сMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA, типичный сертификат сMIIC,MIIDилиMIIE. Короткие структуры используют однобайтную длину: закрытый ключ Ed25519 начинается сMC4C, открытый ключ P-256 сMFkw. - Открытые ключи SSH начинаются с 4-байтной длины в big-endian, поэтому блоб начинается с
AAAA. Следующие символы кодируют тип ключа:AAAAC3NzaC1lZDI1NTE5уssh-ed25519,AAAAB3NzaC1yc2Eуssh-rsa,AAAAE2VjZHNhуecdsa. Вне SSH чередаAозначает просто нулевые байты. - MP3 без тега ID3 начинается сразу с заголовка фрейма. Его биты синхронизации
FF Fxдают ведущие//, например//uQдляFF FB 90. - Base64URL заменяет
+и/, поэтому префикс JPEG превращается в_9j_. Остальные префиксы из таблицы не содержат ни одного из этих символов и в обоих алфавитах выглядят одинаково.
Почему префикс стабилен
Base64 работает с группами по 3 байта. Каждая группа из 3 байтов превращается ровно в 4 символа, и эти 4 символа зависят только от этих 3 байтов. Ничего из дальнейшей части файла назад не просачивается. Поэтому если формат всегда начинается с одних и тех же 3 байтов, его Base64 всегда начинается с одних и тех же 4 символов.
bytes: FF D8 FF
bits: 11111111 11011000 11111111
groups: 111111 111101 100011 111111
index: 63 61 35 63
chars: / 9 j /
Если длина фиксированной сигнатуры не кратна 3 байтам, последняя группа смешивает известные биты с битами из остальной части файла. Возьмём 5-байтный %PDF-. Первые 3 байта дают 4 стабильных символа (JVBE). Следующие 2 байта дают ещё 2 (Ri), а седьмой символ смешивает последние 4 бита - с первыми 2 битами цифры версии. Любая цифра начинается с битов 00, поэтому на практике этот символ всегда 0. После JVBERi0 уже видна версия: JVBERi0xLjQ (%PDF-1.4), JVBERi0xLjc (%PDF-1.7), JVBERi0yLjA (%PDF-2.0).
Сигнатура PNG длиной 8 байт даёт 10 стабильных символов. Одиннадцатый смешивает последний байт сигнатуры с первым байтом длины фрагмента IHDR, а он всегда равен 0, поэтому iVBORw0KGgo сохраняется. Поскольку за сигнатурой в любом PNG идёт этот фрагмент IHDR, у настоящих PNG общий префикс ещё длиннее: iVBORw0KGgoAAAANSUhEUg.
Когда префикс сдвигается
Префикс работает, только если магические байты стоят со смещением 0, 3, 6 и так далее. Добавьте в начало один байт, и все группы сдвинутся, а символы изменятся полностью:
| Ввод | Base64 начинается с |
|---|---|
| Сигнатура PNG | iVBORw0KGgoAAAA |
| 1 байт + сигнатура PNG | AIlQTkcNChoKAAAA |
| 2 байта + сигнатура PNG | AACJUE5HDQoaCgAA |
| 3 байта + сигнатура PNG | AAAAiVBORw0KGgoA |
С 3 лишними байтами выравнивание восстанавливается, и знакомый префикс появляется на 4 символа позже. Типичные причины сдвинутого или пропавшего префикса:
- Метка порядка байтов (BOM). Текст UTF-8 с BOM начинается с
77u/(EF BB BF). BOM занимает 3 байта, поэтому JSON после него всё равно даётeyJ:77u/eyJ…. Текст UTF-16 с BOM начинается с//4или//5(little-endian) либо с/v8(big-endian). - Текст в UTF-16. Текст в UTF-16LE ставит нулевой байт после каждого ASCII-символа, и получается много
A:Helloпревращается вSABlAGwAbABvAA==. Так выглядит-EncodedCommandв PowerShell. Используйте декодер PowerShell или Base64 в UTF-16. - Сжатие или шифрование. Сжатый gzip-ом PNG начинается с
H4sI, а у зашифрованного файла узнаваемого префикса нет вовсе. - Data URI.
data:image/png;base64,служит заголовком, Base64 начинается после запятой. Парсер data URI разделит их за вас. - Переносы строк и пробелы. Байты они не сдвигают, но поиск префикса через
startswithне сработает, если строка начинается с перевода строки или кавычки. Сначала обрежьте лишнее или прогоните строку через починку Base64.
Проверка префикса в коде
Можно сравнивать первые символы как строки, но сравнивать декодированные байты надёжнее: магические числа берутся прямо из спецификации каждого формата, и не нужно вычислять, какие символы стабильны. Декодирование первых 12 символов даёт первые 9 байтов, а этого хватает для большинства сигнатур:
import base64
SIGNATURES = {
b"\x89PNG\r\n\x1a\n": "png",
b"\xff\xd8\xff": "jpeg",
b"%PDF-": "pdf",
b"PK\x03\x04": "zip",
b"\x1f\x8b": "gzip",
}
def sniff(b64: str) -> str:
head = base64.b64decode(b64.strip()[:12])
for magic, name in SIGNATURES.items():
if head.startswith(magic):
return name
return "unknown"
print(sniff("iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB")) # png
print(sniff("JVBERi0xLjcKJcfsj6IK")) # pdf
Инструмент Base64 в файл делает ту же проверку по более длинному списку и показывает превью, если браузер умеет отображать формат. Как 6-битные группы соответствуют символам, рассказано в статье как работает Base64 и показано в таблице алфавита.
Вопросы и ответы
Что значит строка Base64, которая начинается с iVBORw0KGgo?
Это PNG-картинка. iVBORw0KGgo кодирует 8-байтную сигнатуру PNG 89 50 4E 47 0D 0A 1A 0A. Вставьте строку в Base64 в PNG, чтобы её увидеть.
Что такое /9j/ в начале строки Base64?
Это JPEG. Байты FF D8 FF, с которых начинается любой JPEG-файл, кодируются в /9j/. В Base64URL тот же префикс выглядит как _9j_.
Откуда у JWT префикс eyJ?
Токен открывается заголовком в виде JSON-объекта, а первые байты {" в Base64 дают eyJ. Полезная нагрузка тоже объект JSON, поэтому и она начинается с eyJ.
Моя строка Base64 не похожа ни на один известный префикс. Что делать?
Возможно, перед данными есть лишние байты, данные сжаты или зашифрованы, или это обычный текст. Инструмент Base64 в файл сверяет декодированные байты со множеством сигнатур, а не только с первыми символами.