Декодирование Base64 в Perl: полное руководство
Вам досталась строка из букв, цифр и изредка встречающихся + или /, и где-то в глубине души вы понимаете, что она совсем не то, чем выглядит. Возможно, это токен, который едет в заголовке Authorization, файл .b64, выкопанный из тикета поддержки, сертификат, облачённый в броню -----BEGIN, или глыба, спокойно лежащая в файле конфигурации. Вы открываете терминал, вводите perl, и один вопрос забирает всё остальное: как вернуть настоящие данные?
Ответ короткий и успокаивающий. Perl поставляет модуль Base64 вместе с самим языком с 2002 года, и один вызов функции, decode_base64, делает всю работу: нечего устанавливать и нечего настраивать. Пока варится кофе, небольшой повтор: Base64 переписывает каждые три байта данных четырьмя символами из алфавита в 64 символа, заполняя хвост одним-двумя знаками = так, чтобы результат всегда складывался в кратное четырём число, - именно поэтому закодированная форма обычно примерно на 33 процента больше исходной. Главная страница этого сайта объясняет формат во всех подробностях, так что это руководство тратит всё своё время на Perl-сторону: правила декодера, диалекты и реальные форматы, с которыми вы действительно столкнётесь.
Набор инструментов: пять функций, ноль установок
Каждый нужный вызов живёт в MIME::Base64, который входит в ядро Perl с версии 5.8, так что он есть на любой серьёзной установке: от той, что вшита в прошивку роутера, до той, что на сервере базы данных. Проверка - одна строка:
perl -MMIME::Base64 -e 'print $MIME::Base64::VERSION, "\n"'
# 3.16_01
Вот декодирующая сторона модуля, целиком:
| Функция | Что делает | Примечания |
|---|---|---|
decode_base64($str) |
звезда этой статьи: превращает Base64-глыбу в сырые байты | вечно молча игнорирует каждый символ вне алфавита |
MIME::Base64::decode($str) |
тот же декодер, вызванный без импорта | форма, которую вы встретите во множестве старых скриптов |
decode_base64url($str) |
декодирует URL-безопасный диалект с - и _, с заполнением или без |
добавлен в 3.11 в 2010 году; именно она читает JWT |
MIME::Base64::decoded_base64_length($str) |
говорит, насколько большими будут декодированные данные, без самого декодирования | по умолчанию не экспортируется, удобен для предварительного расчёта размера буферов |
unpack("u", $data) |
декодирует uuencoded-данные, формат, предшествовавший Base64 | встроен сам в Perl, модуль не нужен |
Карта версий этих функций, на случай если вы поддерживаете парк старых машин:
| Возможность | Доступна с |
|---|---|
decode_base64() с быстрым C-путём |
Perl 5.8 в 2002 году, когда модуль вошёл в ядро |
decoded_base64_length() |
модуль 3.10 в 2010 году |
decode_base64url() |
модуль 3.11 в 2010 году |
| Тихое декодирование, без предупреждений на подозрительный ввод | модуль 3.11 в 2010 году |
| Текущая ветка 3.16 | 2020 год, требуется Perl 5.6 или новее |
Если в вашем системном Perl по какой-то причине нет модуля, хотя его быть должно, исправление - одна из двух строк: пакет дистрибутива libmime-base64-perl на Debian и Ubuntu или cpanm MIME::Base64, чтобы взять текущий выпуск с CPAN, где модуль живёт как пакет двойного жизненного цикла со времён ядра. Для редкой машины без C-компилятора чистый Perl-аналог MIME::Base64::Perl на CPAN даёт тот же базовый интерфейс, в несколько раз медленнее, но достаточно для любой работы, кроме объёмной. Это и есть вся история зависимостей: больше ничего.
Декодер, который никогда не отказывает
Контракт длиной в одну строку. Дайте ему строку - и он вернёт декодированные байты в виде обычной Perl-строки, содержащей сырые октеты. Без объектов, без исключений, без флагов. Документация формулирует два правила, определяющих его характер, одним предложением: любой символ, не входящий в 65-символьное подмножество Base64, молча игнорируется, а любой символ, оказавшийся после символа заполнения =, никогда не декодируется. Эта вежливость - главное в этой статье, поэтому позвольте ей раз для вас поработать:
use MIME::Base64 qw(decode_base64);
print decode_base64("TWFu!"), "\n"; # Man - восклицательный знак исчезает без следа
print decode_base64("TWFu=XX"), "\n"; # Man - всё после = пропускается
print decode_base64("TQ"), "\n"; # M - без предупреждений, без комментариев
print decode_base64("T"), "\n"; # пустая строка, и тут тоже без нареканий
Последние две строки - снисходительность в её крайней форме. TQ несёт один полный байт плюс четыре лишних бита, и декодер просто сохраняет байт и выбрасывает остаток. T не несёт даже одного полного байта, поэтому результат пуст. В модуле нет ни строгого режима, ни валидатора, которые вернули бы старомодную придирчивость: с версии 3.11 в 2010 году decode_base64 даже не предупреждает об усечённом вводе, а более старые версии под -w ворчали предупреждением «преждевременный конец base64-данных». Если глыба не верна, он всё равно её декодирует, - так что контроль качества остаётся за вами.
Вот политика снисходительности в одном месте, чтобы её можно было рассмотреть целиком:
| Ввод | Результат | Почему |
|---|---|---|
"TWFu" |
Man |
чистый ввод, счастливый путь |
"TWFu!" |
Man |
восклицательный знак не входит в алфавит, поэтому он пропускается |
"TWFu=XX" |
Man |
после заполнения никогда ничего не декодируется |
"TWFuIFdvcmxkIQ==" |
Man World! |
пробельные символы в любом месте - бесплатно |
"TQ" |
M |
один полный байт умещается, лишние биты молча выбрасываются |
"T" |
пустая строка | даже одного полного байта нет, и предупреждений тоже нет |
"ab-cd_efgh" |
молча неверные байты | URL-безопасные буквы выбрасываются как шум, классическая ловушка |
Последняя строка - та, которую стоит запомнить. base64url-сегмент, отданный стандартному декодеру, не приводит к сбою: он декодируется в правдоподобно выглядящий мусор, потому что символы - и _ трактуются как чужеродный шум, а оставшиеся буквы всё ещё образуют валидные группы. Декодер - свидетель, а не охранник на входе, так что если вводу нельзя доверять, проверяйте его сами. Достаточно небольшой строгой проверки:
sub strict_base64 {
my ($blob) = @_;
$blob =~ s/[\r\n]//g; # декодер их игнорирует, и мы тоже
return 0 unless length($blob) % 4 == 0;
return $blob =~ /\A[0-9A-Za-z+\/]+(?:={1,2})?\z/ ? 1 : 0;
}
print strict_base64("TWFu"), "\n"; # 1
print strict_base64("TQ="), "\n"; # 0 - неверное число знаков заполнения
print strict_base64("ab-cd"), "\n"; # 0 - URL-безопасный алфавит
Ещё одно замечание про этот регэксп - урок, добытый дорогой ценой: если подпрограмма заканчивается голым return $x =~ /.../ и результат несостоявшегося совпадения попадает прямо в printf, Perl поднимает вводящее в заблуждение предупреждение «не хватает аргумента в printf» вместо чистого нуля. Приведите результат совпадения с помощью ? 1 : 0 перед возвратом, как делает функция выше, - и этот трюк исчезнет полностью.
Сначала байты, потом символы
Вспомните, что возвращает decode_base64: сырые байты, обычная строка, в которой не стоит флаг UTF-8. Что эти байты значат - решение только за вами, и именно на этом шаге люди спотыкаются о Unicode. Perl отслеживает, содержит строка символы или байты, и length(), substr() и большинство регулярных выражений ведут себя по-разному в зависимости от ответа. Решение - осознанно назвать свою кодировку, модулем Encode, который поставляется с каждой установкой Perl:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $raw = decode_base64("SMOrbGxvIFdvcmxkIQ==");
my $text = decode("UTF-8", $raw);
print $text, "\n"; # Hëllo World!
print length($text), " chars\n"; # 12
print length($raw), " bytes\n"; # 13
Эта пара чисел - весь урок. Глыба - 13 байтов, но только 12 символов, потому что буква с диакритикой занимает в UTF-8 два байта. Пропустите шаг с кодировкой, и байты всё равно приятно напечатаются в UTF-8-терминале, - именно поэтому ошибка остаётся невидимой, пока за них не берётся строковая функция, или пока байты не пройдут через конвейер, ожидающий символы. Если сомневаетесь, декодируйте со строгой кодировкой и позвольте исключению рассказать вам правду о байтах: передайте Encode::FB_CROAK, и decode() погибнет на невалидной последовательности, вместо того чтобы молча подставлять U+FFFD по умолчанию. Это и есть фича.
Короткий список кодировок, к которым вы действительно потянетесь:
| Кодировка | Когда использовать | Будьте внимательны |
|---|---|---|
UTF-8 |
допущение по умолчанию: API, JSON, веб-контент, современный текст | невалидные последовательности по умолчанию становятся U+FFFD; с Encode::FB_CROAK они вызывают чистую гибель, - именно этого вы и хотите |
Latin-1 |
устаревший западный текст, один байт на символ, никогда не может сломаться | он охотно изувечит UTF-8 в двойную кодировку с кракозябрами |
ASCII |
данные, в которых вы уверены: обычный 7-битный текст | любой байт выше 127 по умолчанию становится U+FFFD (с Encode::FB_CROAK - гибель) |
UTF-16 |
текст Windows, где порядок байтов решает метка порядка байтов | BOM - единственная подсказка о порядке байтов, так что держите его в байтах |
А вот и ловушка, от которой вас защищает шаг с кодировкой. Если декодированные вами байты уже UTF-8 и вы прогоните их через encode("UTF-8", ...) по пути наружу, вы не получите копию: вы получите двойное кодирование, где каждая буква с диакритикой разбухает в два символа сама по себе. Классический симптом - текст, который читался как Hëllo, а теперь читается как Hëllo, - и приёмник на другом конце провода декодирует это с полной добросовестностью. Байты на входе, байты на выходе, одна осознанно названная конвертация посередине.
base64url: алфавит для URL и токенов
Половина Base64, курсирующего по современному интернету, - это вовсе не стандартный алфавит. Символ + - это то, как браузер кодирует пробел в строке запроса, а / - разделитель пути, так что стандартные буквы в URL - катастрофа. Раздел 5 RFC 4648 определяет исправление: второй алфавит, который заменяет + и / на - и _ и, по конвенции, отбрасывает ещё и заполнение =, и переносы строк. В RFC прямо сказано, что это кодирование не следует считать идентичным base64-кодированию, и у Perl с версии 3.11 в 2010 году есть для него отдельная пара функций:
use MIME::Base64 qw(decode_base64url);
my $raw = decode_base64url("c3Vuc2V0LTQy");
print $raw, "\n"; # sunset-42
Две вещи, которые стоит знать. Во-первых, decode_base64url хорошо ладит с вводом без заполнения, - это та форма, которую вы действительно найдёте в дикой природе, так что ритуал «сначала восстановить заполнение», которого требуют другие языки, здесь не нужен; ввод с заполнением тоже работает. Во-вторых, стандартный декодер - зверь совсем другой: скормите ему base64url-сегмент, и получите молча неверные байты, потому что символы - и _ выбрасываются как шум, а остальное всё равно декодируется. Используйте правильный декодер либо нормализуйте вручную, если вас прижали к легаси-пути кода:
my $seg = "ab-cd_efgh";
$seg =~ tr{-_}{+/}; # URL-безопасные буквы, возвращены домой
$seg .= "=" x (-length($seg) % 4); # заполнение восстановлено для стандартного декодера
my $raw = decode_base64($seg);
print unpack("H*", $raw), "\n"; # 69bf9c77f79f82 - семь байтов, снова в стандартном алфавите
С base64url вы столкнётесь сразу же: в JWT, токенах, которые выдаёт каждый современный API, и в любом непрозрачном ID, живущем в URL: одиннадцатисимвольные ID видео, UUID, хранящиеся в URL-безопасном алфавите (на CPAN для этого как раз есть Data::UUID::Base64URLSafe), и ключи баз данных, которым нужно пережить адресную строку. А если у вас старый Perl, более ранний, чем эти функции в ядре, отдельный модуль MIME::Base64::URLSafe 2006 года, порт urlsafe-кодека Python, даёт urlsafe_b64encode и urlsafe_b64decode; на любой версии с 3.11 и дальше встроенные функции - выбор получше.
JWT: читаем заголовок и пелод
JSON Web Token - это структурно два куска JSON в маскировке плюс криптографическая квитанция. Компактная форма из RFC 7515 - это три base64url-сегмента, склеенные точками: защищённый заголовок, пелод и подпись. Разобрать и прочитать один - три строки:
use MIME::Base64 qw(decode_base64url);
use JSON::PP;
my $jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJob21lciJ9.uzM6l0c4...";
my ($head_b64, $claims_b64, $sig_b64) = split /\./, $jwt, 3;
my $head = decode_json(decode_base64url($head_b64));
my $claims = decode_json(decode_base64url($claims_b64));
print $claims->{sub}, " (", $head->{alg}, ")\n"; # homer (HS256)
Обратите внимание на разделение труда: decode_base64url превращает каждый сегмент в байты, а decode_json из ядерного модуля JSON::PP, который есть с Perl 5.14, превращает байты заголовка и пелода в Perl-структуры данных. Сегмент подписи тоже base64url, но это криптографическое хеш-значение, так что декодируют только первые два сегмента, а третий отдают правильной библиотеке.
Подвох тот, который все забывают: читаемое не значит валидное. Заголовок и пелод читаемы по замыслу, - значит, их может переписать любой, - а подпись - единственное доказательство. Всё, что по-настоящему важно, проверяйте, а не просто декодируйте. Модуль CPAN Crypt::JWT, построенный на CryptX, делает всю работу:
use Crypt::JWT qw(decode_jwt);
my $claims = decode_jwt(
token => $jwt,
key => $secret,
accepted_alg => "HS256",
);
При плохой подписи он падает с ошибкой, а закрепление accepted_alg закрывает дыру путаницы алгоритмов, через которую атакующий подменяет токен на более слабый вариант. Рабочий процесс «декодировать и напечатать» годится, чтобы осмотреть токен во время звонка в поддержку. Но это не аутентификация.
Файлы: из .b64 обратно к оригиналу
Файлы - это там, где культура Perl-однострочников по-настоящему блестит, и вся работа влезает в одну команду. Флаг -0777 - секретный ингредиент, потому что он читает весь файл целиком в одну строку, вместо того чтобы кормить декодер построчно:
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out
Построчная форма безопасна для одного конкретного класса файлов: тех, где каждая строка содержит кратное четырём число символов Base64, - это верно для любого правильно MIME-обёрнутого тела, поскольку 76 кратно 4. В тот самый момент, когда точки переноса становятся кривыми, - а в вручную обёрнутых файлах они почти всегда кривые, - построчное декодирование начинает производить заполнение посреди данных. Режим чтения целиком не требует таких условий, - вот почему он и есть выбор по умолчанию:
perl -MMIME::Base64 -ne 'print decode_base64($_)' < in.b64 > out
Внутри скрипта паттерн - стандартная Perl-танцовка с файлами, с одной тихой, но важной деталью: слои :raw на обоих дескрипторах, чтобы Perl никогда не пытался интерпретировать байты как платформенный текст по пути на входе и на выходе:
use MIME::Base64 qw(decode_base64);
use Digest::SHA qw(sha256_hex);
open my $in, "<:raw", $ARGV[0] or die $!;
local $/;
my $blob = <$in>;
close $in;
my $decoded = decode_base64($blob);
print sha256_hex($decoded), "\n"; # сравните с контрольной суммой отправителя
open my $out, ">:raw", $ARGV[1] or die $!;
print {$out} $decoded;
close $out;
Строка с хешем делает больше, чем хвастается. Поскольку декодер принимает почти что угодно, совпадение контрольной суммы с той, что опубликовал отправитель, - единственное доказательство, что переезд прошёл байт в байт. Для по-настоящему гигантских файлов построчный цикл - альтернатива с низкой памятью, при условии что переносы ложатся на границы по четыре символа, а MIME::Base64::decoded_base64_length скажет, насколько большим будет вывод, прежде чем вы решите вопрос с буфером.
PEM-броня: снять оболочку, сохранить DER
Файлы .pem в любом стеке безопасности - это тот же Base64, облачённый в броню: строка заголовка, строка подвала и тело, перенесённое по 64 символа, по старой конвенции Privacy Enhanced Mail. Обёртка - единственная интересная часть, потому что декодер модуля вообще не заботится о длине строк:
use MIME::Base64 qw(decode_base64);
open my $fh, "<:raw", "cert.pem" or die $!;
local $/;
my $blob = <$fh>;
close $fh;
my @body = grep { !/^-----/ && /\S/ } split /\n/, $blob;
my $der = decode_base64(join "", @body);
print length($der), " bytes of DER\n";
Строки BEGIN и END счищаются, остальное склеивается в одну строку, и все переводы строк по дороге игнорируются. Для повседневной работы с сертификатами инструменты OpenSSL уже делают это за вас; восемь строк выше - тот самый паттерн, который стоит помнить, когда сырые байты DER нужны вам самим: для хеша, отпечатка или сравнения.
Data URI: изображения, которые несут свой собственный адрес
Схема data: из RFC 2397 встраивает пелод прямо в URL: data:, необязательный медиатип, необязательный флаг ;base64, запятая и данные. Бинарные медиа, такие как изображения, используют флаг, так что пелод - это стандартный алфавит с заполнением, и обычный декодер справится с ним после небольшого разреза:
use MIME::Base64 qw(decode_base64);
my $uri = "data:image/png;base64,iVBORw0KGgo...";
$uri =~ s/^data:[^,]+,// or die "not a data URI";
my $raw = decode_base64($uri);
print unpack("H8", $raw), "\n"; # 89504e47: магические байты PNG
Проверка магических байтов - правильный ход. Если эти первые восемь шестнадцатеричных символов не 89504e47, то изображение не PNG, каким бы его ни называл медиатип, а декодер, который никогда не жалуется, как раз и позволяет такую тихую ложь.
Родственники и ископаемые: uuencode и прочие алфавиты
До того как Base64 победил, классический UNIX-способ отправить бинарник по почте - это был uuencode, и вы всё ещё встретите его в старых почтовых списках и старых инструментах. Хорошая новость: в Perl есть встроенный декодер для него, без всякого модуля, - спасибо шаблону u в pack и unpack:
my $uu = pack("u", "Hello, World!");
print $uu, "\n"; # -2&5L;&\L(%=O<FQD(0`` плюс перевод строки
my $back = unpack("u", $uu);
print $back, "\n"; # Hello, World!
Эти два вызова - точные обратные друг к другу, и это вся история, которая вам нужна, - а классическая команда uuencode из UNIX-инструментария просто оборачивает голые строки в заголовок begin и подвал end, так что пелод, который вы декодируете, - это часть между ними.
У Base64 есть и диалектные родственники, и знание того, какой декодер что ест, экономит вам сессию отладки:
| Диалект | Перенос | Где вы его встретите | Что делает decode_base64 |
|---|---|---|---|
| MIME (RFC 2045) | 76 символов | тела электронных писем | декодирует как есть: переносы строк и CRLF игнорируются |
| PEM (RFC 1421) | 64 символа | сертификаты и ключи | декодирует как есть |
| PKIX (RFC 7468) | 64 символа | текстовые структуры X.509 | декодирует как есть |
| броня OpenPGP (RFC 9580) | 76 символов плюс строка CRC24 | ключи и подписи PGP | декодирует как есть, строку контрольной суммы просто игнорирует |
| IMAP (RFC 3501) | без переноса | имена почтовых ящиков | не тот алфавит: слэш становится запятой, сначала переведите буквы |
Итог: для любого варианта стандартного алфавита, который отличается только переносом строк, одного снисходительного декодера хватает на все. Переводить символы первым делом нужно лишь тогда, когда меняется сам алфавит.
Конфигурация, базы данных и переменные окружения
Контейнерные платформы, облачные консоли и удивительно много файлов конфигурации хранят учётные данные и небольшие документы как непрозрачные Base64-строки, потому что глыба букв и цифр выглядит менее опасно, чем тот пароль, которым она является. Декодирование всегда состоит из одних и тех же двух шагов: decode_base64 плюс решение о кодировке:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $secret = decode("UTF-8", decode_base64($config->{api_key}));
Причина, по которой формат так популярен именно здесь, - та самая, о которой предупреждает RFC 4648: люди перестают замечать, что данные читаемы. Поэтому относитесь к декодированному выводу как к конфиденциальному с того момента, когда он возвращён, и держите и глыбу, и её результат подальше от файлов логов, оповещений и дампов отладки.
Та же форма встречается и в базах данных, где бинарные данные часто ездят в колонке TEXT как Base64, потому что колонка не может пообещать, что пропустит произвольные байты без изменений:
use MIME::Base64 qw(decode_base64);
my $icon = decode_base64($row->{icon_data});
open my $fh, ">:raw", "icon.png" or die $!;
print {$fh} $icon;
close $fh;
Электронная почта: MIME-части и вложения
Электронная почта - это то место, где Base64 получил своё имя, и снисходительность модуля спроектирована именно под этот трафик. MIME-часть с Content-Transfer-Encoding: base64 прибывает строками по 76 символов текста, завершаемого CRLF, и декодер съедает всю оболочку как есть, переносы строк включительно:
use MIME::Base64 qw(decode_base64);
my $part_body = "SGVsbG8sIHF1ZXJ5IQpUaGlzIE1JTUUgcGFydCB0cmF2ZWxsZWQgYXMgYmFzZTY0LCB3cmFwcGVk";
$part_body .= "\r\nIGF0IDc2IGNoYXJhY3RlcnMsIENSTEYgYmV0d2VlbiBsaW5lcy4=";
my $text = decode_base64($part_body);
print $text; # исходное двухстрочное тело сообщения
Если вы собираете или разбираете почту фреймворком, вы не делаете ничего из этого вручную: MIME::Lite сам кодирует вложение в Base64, когда вы передаёте Encoding => "base64" в attach, а Email::MIME делает то же самое автоматически. Ручная версия выше - для той почты, которая прибывает сырым текстом в лог, тикет или пересланное сообщение, - а на практике это очень много почты.
Подвохи: полный сбор, отсортированный по частоте укусов
Модуль достаточно мал, чтобы запомнить его наизусть, так что вот весь список ловушек в одном месте, отсортированный примерно по тому, как часто он кусается:
| Подвох | Что происходит | Как исправить |
|---|---|---|
| base64url-сегмент, скормленный стандартному декодеру | - и _ выбрасываются как шум, а остальное декодируется в молча неверные байты |
используйте decode_base64url либо сначала переведите алфавит и восстановите заполнение |
| Доверие тишине на повреждённом вводе | чужие символы, усечение и неверный алфавит декодируются без единого предупреждения | сначала прогоните строгую проверку, а если оригинал доступен, - подтвердите хешем |
| Не-ASCII-символы внутри глыбы | засовшаяся буква с диакритикой или вставленный Unicode-пробел молча игнорируются, сжимая результат без комментария | та же строгая проверка отклоняет всё, что вне 7-битного алфавита |
| Отношение к результату как к тексту | на байтах не стоит флаг UTF-8, так что length() считает байты, а строковые функции получают неверную картину |
до любой обработки текста прицепите decode("UTF-8", $raw) или выбранную вами кодировку |
| Двойное кодирование по пути наружу | пропуск уже UTF-8 байтов через encode("UTF-8", ...) превращает Hëllo в Hëllo |
кодируйте символы, никогда сырые байты, - и при сомнении проверяйте флаг через utf8::is_utf8() |
| Построчное декодирование файла с кривыми переносами | строки, которые не заканчиваются на границе в четыре символа, производят заполнение посреди вывода | читайте целиком через -0777 либо гарантируйте переносы по четыре символа |
| Возврат несостоявшегося совпадения регулярного выражения в числовом контексте | подпрограмма, заканчивающаяся return $x =~ /.../ и передающая результат в printf, поднимает вводящее в заблуждение предупреждение «не хватает аргумента в printf» |
приведите совпадение: return $x =~ /.../ ? 1 : 0 |
| Старый код, который ждёт старое предупреждение | скрипты до 3.11, которые опирались на ворчание «преждевременный конец base64-данных» под -w, теперь не видят ничего |
добавьте собственную строгую проверку; предупреждение ушло навсегда |
| Предположение, что декодирование - это проверка | декодер принимает почти что угодно и ничего по этому поводу не говорит | совпавшая контрольная сумма или проверенная подпись - единственное важное доказательство |
| Логирование того, что вы декодируете | формат ничего не прячет, и файл лога - ровно то место, где его найдёт следующий человек | держите декодированные секреты подальше от логов, оповещений и дампов отладки |
Хорошие привычки
Привычки, которые не дают Base64 ни разу не перехитрить ваши скрипты:
- Ожидайте байты, всегда. Пишите код, который знает, что
decode_base64возвращает сырые октеты, и явно прицепляйтеdecodeсвоей кодировки, вместо того чтобы надеяться, что терминал сделает всё правильно. - Называйте свою кодировку. По умолчанию
UTF-8, а меняйте только когда данные говорят иначе. Строгий сбойdecode("UTF-8", ..., Encode::FB_CROAK)- это фича: она говорит вам, что байты не те, что вы предполагали. - Совмещайте алфавит с источником.
decode_base64urlдля URL, токенов и ID;decode_base64для всего остального. Два алфавита не взаимозаменяемы, и декодер не скажет вам, когда вы ошибётесь в выборе. - Проверяйте до декодирования. В этом модуле нет флага строгого режима, так что небольшая проверка - ваш охранник на входе.
- По умолчанию читайте файлы целиком.
-0777илиlocal $/ = undefубирают целый класс багов точек переноса, а цена в памяти для файлов, которые вы реально декодируете, не является проблемой. - Используйте
:rawна каждом файловом дескрипторе. Бинарь на входе, бинарь на выходе. Текстовые слои - для людей, а не для байтов. - Подтверждайте хешем. Когда оригинал доступен, совпавшая контрольная сумма - единственное доказательство байт-в-байт декодирования.
- Никогда не логгируйте то, что декодируете. Формат ничего не прячет.
Краткая история, рассказанная журналом изменений
Формат старый, а отношения Perl с ним ещё старше, чем кажется. Несколько проверенных дат, по порядку:
- C-код старше Perl 5. Быстрый декодер внутри модуля происходит от кода в metamail, почтовой программе Bellcore, авторское право на которую оформлено в 1991 году, за три года до первого релиза Perl 5. Когда вы сегодня вызываете
decode_base64, работу делает кусочек девяностых. - Рождение в веб-инструментах. Модуль начался как
LWP::Base64внутри libwww-perl в середине девяностых, авторство - Мартин Костер и Йорг Райхельт, - и в апреле 1997 года он вырос в собственную CPAN-дистрибуциюMIME::Base64, версия 2.00, с записью в журнале изменений based on libwww-perl-5.08. - Эра предупреждений. С 2.03 в 1997 году усечённый ввод вместо падения вызывал предупреждение «преждевременный конец base64-данных» под
-w, а 2.11 в 1999 году поправил сборки, которые предупреждали про вполне исправные данные. Для декодеров это была более нервная декада. - В ядре с 2002 года. Perl 5.8 взял модуль в ядро дистрибутива, а синхронизация 2.13 с ядром в том же декабре принесла поддержку EBCDIC, - вот почему кодер и декодер до сих пор работают на мейнфреймах.
- URL-безопасный диалект вошёл в ядро Perl в 2010 году. Версия 3.11 добавила
decode_base64urlи её сестру, - четыре года после того, как отдельный модульMIME::Base64::URLSafeпоявился на CPAN в 2006 году, в тот самый год, когда RFC 4648 кодифицировал диалект. - Затихание. Тот же выпуск 3.11 убрал даже старое предупреждение об усечении, на случай если подозрительный ввод был намеренным, - и каждый релиз с тех пор, включая текущую ветку 3.16 с 2020 года, держит декодер вежливым и молчаливым.
Факты для любопытных, конкретно про Perl
Чтобы завершить экскурсию, вот курьёзы, которые делают эту историю хорошей:
- Декодируйте имя самого формата.
decode_base64("YmFzZTY0")возвращаетbase64. Так было с 1997 года и так будет вечно. - Декодер - вежливый призрак. За всю свою историю в 3.x он ни разу не поднял исключение на плохом вводе. Повреждено, усечено, неверный алфавит: он декодирует всё и ни на что не жалуется, - поведение, которое журнал изменений в 2010 году осознанно зацементировал.
- MIME-перенос кратен четырём не случайно. Ограничение в 76 символов - это девятнадцать трёхбайтовых групп, 57 байтов всего, помноженные на четыре символа, - поэтому построчное декодирование безопасно для любого правильно обёрнутого MIME-тела и небезопасно для всего остального.
- uuencode никогда не печатает строчную букву. Его алфавит обрывается на подчёркивании, - поэтому старые uuencoded-файлы выглядят так, будто их набирала машина с зажатой Caps Lock, и поэтому в Perl до сих пор живёт встроенный декодер для формата старше интернета.
- Perl когда-то поставляла собственную команду decode-base64. Релизы с 2.14 в 2003 году по 3.05 в 2004 году включали скрипты
encode-base64,decode-base64и их quoted-printable-близнецов; 3.06 в 2005 году перенесла их в отдельную дистрибуцию MIME-Base64-Scripts. Если вы найдёте старую установку, где эта команда лежит на PATH, теперь вы знаете, откуда она. - ID видео YouTube - это base64url в маскировке. Одиннадцатисимвольный ID в вашей адресной строке - это 64-битное число в URL-безопасном алфавите со срезанным заполнением, - так что у каждого просмотренного вами видео в URL есть Base64-строка, и
decode_base64urlумеет её читать. - Снисходительность - это стандарт, а не баг. MIME-правило «будь либеральным в том, что принимаешь» - причина, по которой этот декодер пережил три декады мусорных данных, и причина, по которой RFC 4648 предупреждает, что ту же снисходительность можно обратить в скрытый канал, если доверять недоверенному вводу.
Так что в следующий раз, когда в ваш терминал приземлится строка из букв, цифр, плюсов и слэшей, вы знаете всю историю. Один вызов функции делает работу, декодер - вежливый призрак, который никогда не откажет, у base64url есть свой декодер, кодировка - решение, которое вы принимаете осознанно, файлы приходят сырыми и уходят сырыми, а хеш - единственное важное доказательство. А если однажды вам нужно будет пройти этот путь в обратную сторону, обернуть собственные сырые данные в текстовый конверт и отправить их в мир, связанная ниже статья про кодирование Base64 в Perl разберёт тот ритуал с той же глубиной.
Последнее обновление: 2026-09-08
Связанная статья: Кодирование Base64 в Perl: полное руководство