Загрузить файл
  1. base64hub
  2. Статьи о Base64
  3. Дополнение (=)
Разбор

Дополнение в Base64: зачем нужны знаки =

Знак = в конце Base64 обозначает неполный последний блок. Одни форматы его отбрасывают, другие декодеры без него не работают, а вернуть его можно одной строкой кода.

Зачем Base64 дополнение

Base64 превращает 3 байта в 4 символа (весь алгоритм разобран в статье как работает Base64). Если длина ввода не кратна 3, последний блок получается неполным:

Байтов в последнем блокеСимволов данныхДополнениеПример
34нетMan → TWFu
23=Ma → TWE=
12==M → TQ==

= сохраняет длину результата кратной 4. Это облегчало жизнь декодерам эпохи MIME: они читали ввод фиксированными группами по 4 символа и знали, где кончаются данные, не отслеживая общую длину.

Строго говоря, эта информация избыточна. Последняя группа из 3 символов может означать только 2 байта, а группа из 2 символов только 1 байт, так что декодеру, который считает символы, = вообще не нужен. Поэтому Base64 без дополнения возможен, и поэтому многие форматы им пользуются.

Есть один случай, когда дополнение несёт настоящую информацию: склейка. Если соединить две строки с дополнением, = в середине показывает, где кончается первая. Правда, декодеры справляются с этим плохо. На TQ==TQ== (два закодированных M) стандартный декодер Python вернул b'M\x04\xd0', Buffer в Node вернул только M, а atob выбросил исключение. Лучше декодировать каждый кусок отдельно.

Когда дополнение необязательно

RFC 4648 §3.2 требует (MUST), чтобы кодеры добавляли дополнение, если использующая Base64 спецификация явно не говорит иного. Некоторые говорят:

  • JWT и JOSE (RFC 7515) используют Base64URL без дополнения. Заголовок JWT вроде eyJhbGciOiJIUzI1NiJ9 не содержит =, даже если длина не кратна 4. См. Base64 в JWT.
  • URL и имена файлов часто содержат Base64URL без дополнения, потому что = в строке запроса имеет своё значение и его пришлось бы экранировать как %3D. Полностью разница описана на странице Base64 и Base64URL.
  • WebAuthn и OAuth PKCE (code_challenge из RFC 7636) тоже используют Base64URL без дополнения.

Стандартный Base64 в почте, PEM-файлах, data URI и большинстве JSON API идёт с дополнением. Если сомневаетесь, выдавайте стандартный Base64 с дополнением, а Base64URL без него, и делайте свой декодер терпимым к обоим вариантам.

Декодеры, которым нужно дополнение

Поведение различается сильнее, чем принято думать. Вот результаты декодирования TWE (Ma без дополнения) стандартным декодером каждой библиотеки:

Язык / инструментВызовРезультат для TWE
Python 3.13base64.b64decodebinascii.Error: Incorrect padding
Python 3.13base64.urlsafe_b64decodeтоже Incorrect padding
.NETConvert.FromBase64StringFormatException
Gobase64.StdEncodingошибка; для ввода без дополнения нужен RawStdEncoding
Gobase64.RawStdEncodingMa, но отвергает TWE=
RubyBase64.strict_decode64ArgumentError: invalid base64
RubyBase64.decode64, urlsafe_decode64Ma
Java 21Base64.getDecoder().decodeMa
Node.jsBuffer.from(s, 'base64'), atobMa
PHP 8.4base64_decode (и в строгом режиме)Ma
GNU coreutils 9.7base64 -dMa
OpenSSL 3.5openssl base64 -dпустой вывод, код выхода 0

Строка про OpenSSL заслуживает внимания. Он ничего не вывел и всё равно сообщил об успехе, так что скрипт, который пропускает Base64 без дополнения через openssl base64 -d, молча создаёт пустой файл. В тех же тестах он ничего не вернул и для одной строки без завершающего перевода строки; этот случай исправил флаг -A.

Go строже всех в обратную сторону: RawStdEncoding отказывается от строки, в которой дополнение есть. Если вы принимаете ввод из разных источников, убирайте = и используйте raw-вариант либо добавляйте дополнение и используйте стандартный.

Примеры для конкретных языков есть на страницах с кодом, например Base64 в Python и Base64 в Go.

Как вернуть пропавшее дополнение

Правило зависит только от длины строки после удаления пробельных символов:

len % 4Что дописатьЧто это значит
0ничегоблоки полные
2==в последнем блоке 1 байт
3=в последнем блоке 2 байта
1исправить нельзястрока обрезана или в ней лишний символ

Остаток 1 никогда не бывает корректным. Один символ Base64 несёт 6 бит, а этого мало для байта, так что никакое дополнение не сделает строку декодируемой. Если вы это видите, ищите пропущенный символ, лишний символ или посчитанный перевод строки.

В большинстве языков исправление укладывается в одно выражение:

import base64
s = "eyJzdWIiOiIxMjM0In0"            # 19 chars, 19 % 4 == 3
padded = s + "=" * (-len(s) % 4)     # adds one "="
print(base64.urlsafe_b64decode(padded))
# b'{"sub":"1234"}'
const s = "eyJzdWIiOiIxMjM0In0";
const padded = s + "=".repeat((4 - (s.length % 4)) % 4);
// "eyJzdWIiOiIxMjM0In0="

Валидатор Base64 показывает длину строки и состояние дополнения, а починка Base64 за один шаг добавляет недостающие =, убирает пробелы и кавычки и переводит URL-safe символы.

Неканонические кодировки

У дополнения есть менее заметный родственник: неиспользуемые биты в последнем символе данных. При кодировании Ma его 16 бит заполняют две с половиной 6-битные группы. Третий символ несёт последние 4 бита a плюс 2 бита-заполнителя, и RFC 4648 §3.5 требует, чтобы кодер обнулял эти биты. Так получается E (000100).

Декодер биты-заполнители игнорирует, поэтому TWF= (F здесь равен 000101, биты-заполнители 01) декодируется в тот же Ma. В наших тестах Python, Node, Java и PHP приняли TWF= и вернули Ma. Точно так же TR== декодируется в M, как и TQ==.

Такие строки, как TWF=, называют неканоническими. Они важны в нескольких местах:

  • Подписи и хеши над текстом Base64. Две разные строки декодируются в одни и те же байты, поэтому сравнение строк Base64 для проверки равенства данных может дать неверный ответ. Сравнивайте декодированные байты.
  • Дедупликация и ключи кеша. Та же проблема: нормализуйте, декодируя и кодируя заново.
  • Строгие парсеры. RFC 4648 разрешает декодерам отвергать ненулевые биты-заполнители. StdEncoding.Strict() в Go и GNU base64 -d отвергают TWF=.

Похожая странность бывает с лишним дополнением. В TWE== на один = больше нужного. Python и Buffer в Node его приняли, а atob выбросил InvalidCharacterError.

Дополнение при кодировании частями

Если кодировать большой файл в Base64 кусками, каждый кусок, кроме последнего, должен быть кратен 3 байтам. Иначе каждый кусок получит своё дополнение, и склеенный результат будет неверным. Кодирование hello world!! кусками по 4 байта дало aGVsbA==byB3bw==cmxkIQ==IQ==, а кусками по 3 байта получилось aGVsbG8gd29ybGQhIQ==, ровно как при кодировании всего сразу. Для переноса строк в MIME удобнее всего брать куски по 57 байт на строку, потому что 57 байт превращаются ровно в 76 символов.

Исправить дополнение автоматически

Вставьте строку с пропавшим или испорченным дополнением и получите корректный Base64.

Исправить Base64

Вопросы и ответы

Можно ли просто удалить знаки = из Base64?

Данные не пострадают: длина строки и так говорит декодеру, сколько байтов в последнем блоке. Декодируется ли результат, зависит от получателя: Base64URL в JWT ожидает строку без дополнения, а b64decode в Python и Convert.FromBase64String в .NET такую строку отвергают.

Почему в конце моей строки Base64 нет =?

Либо длина ввода была кратна 3 байтам и дополнение не понадобилось, либо источник использует Base64 без дополнения, что обычно для Base64URL в токенах и URL.

Что дописать к строке, если Python пишет Incorrect padding?

Перед декодированием допишите '=' * (-len(s) % 4). Если строка URL-safe, используйте ещё и base64.urlsafe_b64decode. Если длина по модулю 4 равна 1, дополнение не поможет: строка обрезана или в ней лишний символ.

Может ли строка Base64 заканчиваться тремя знаками =?

Нет. В последнем блоке остаётся 1 или 2 байта, и они дают == или =. Три = означали бы блок без данных, а такого не бывает.

Читайте также