Почему кодировка base64 требует заполнения, если длина ввода не делится на 3?

103

Какова цель заполнения в кодировке base64. Ниже приводится выдержка из Википедии:

"Выделяется дополнительный символ заполнения, который может использоваться для преобразования закодированного вывода в целое число, кратное 4 символам (или, что эквивалентно, когда незашифрованный двоичный текст не кратен 3 байтам); эти символы заполнения должны быть затем отброшены при декодировании, но по-прежнему позволяют вычислять эффективную длину незакодированного текста, когда его входная двоичная длина не будет кратна 3 байтам (последний символ, не являющийся заполнителем, обычно кодируется так, что последний 6-битовый блок, который он представляет, будет равен нулю -добавлен младшие значащие биты, в конце закодированного потока может находиться не более двух символов заполнения) ".

Я написал программу, которая могла кодировать в base64 любую строку и декодировать любую строку в кодировке base64. Какую проблему решает обивка?

Ананд Патель
источник

Ответы:

215

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

Однако заполнение полезно в ситуациях, когда строки в кодировке base64 объединяются таким образом, что длины отдельных последовательностей теряются, как это может случиться, например, в очень простом сетевом протоколе.

Если незаполненные строки объединены, восстановить исходные данные невозможно, поскольку информация о количестве нечетных байтов в конце каждой отдельной последовательности теряется. Однако, если используются дополненные последовательности, нет двусмысленности, и последовательность в целом может быть декодирована правильно.

Изменить: иллюстрация

Предположим, у нас есть программа, которая кодирует слова base64, объединяет их и отправляет по сети. Он кодирует «I», «AM» и «TJM», складывает результаты вместе без заполнения и передает их.

  • Iкодируется в SQ( SQ==с заполнением)
  • AMкодируется в QU0( QU0=с заполнением)
  • TJMкодируется в VEpN( VEpNс заполнением)

Значит переданные данные есть SQQU0VEpN. Приемник base64-декодирует это I\x04\x14\xd1Q)вместо предполагаемого IAMTJM. Результат - нонсенс, потому что отправитель уничтожил информацию о том, где заканчивается каждое слово в закодированной последовательности. Если бы отправитель отправил SQ==QU0=VEpNвместо этого, получатель мог бы декодировать это как три отдельные последовательности base64, которые были бы объединены, чтобы дать IAMTJM.

Зачем нужна прокладка?

Почему бы просто не разработать протокол так, чтобы каждое слово было префиксом целочисленной длины? Тогда получатель сможет правильно декодировать поток и не потребуется заполнение.

Это отличная идея, если мы знаем длину кодируемых данных до того, как начнем их кодировать. Но что, если вместо слов мы кодируем фрагменты видео с прямой камеры? Мы можем не знать заранее длину каждого фрагмента.

Если бы протокол использовал заполнение, вообще не было бы необходимости передавать длину. Данные могут быть закодированы по мере их поступления от камеры, каждый фрагмент заканчивается заполнением, и получатель сможет правильно декодировать поток.

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

TJM
источник
24
+1 Единственный ответ, который действительно дает разумный ответ, кроме «потому что мы любим многословие и избыточность по какой-то необъяснимой причине».
Недействительно
1
Это работает нормально для фрагментов, которые кодируются отчетливо, но, как ожидается, будут неделимо объединены после декодирования. Если вы отправите U0FNSQ == QU0 =, вы сможете восстановить предложение, но потеряете слова, из которых оно состоит. Думаю, лучше, чем ничего. Примечательно, что программа GNU base64 автоматически обрабатывает сцепленные кодировки.
Марсело Кантос,
2
Что, если длина слов кратна 3? Этот тупой способ конкатенации уничтожает информацию (окончания слов), а не удаление отступов.
GreenScape
2
Конкатенация Base64 позволяет кодировщикам обрабатывать большие фрагменты параллельно без необходимости выравнивания размеров фрагментов до кратного трем. Точно так же в качестве детали реализации может быть кодировщик, которому необходимо очистить внутренний буфер данных размером, не кратным трем.
Andre D
2
Этот ответ может заставить вас подумать, что вы можете декодировать что-то вроде «SQ == QU0 = VEpN», просто передав его в декодер. На самом деле кажется, что вы не можете, например, реализации в javascript и php не поддерживают это. Начиная с конкатенированной строки, вам нужно либо декодировать 4 байта за раз, либо разбить строку после символов заполнения. Похоже, что эти реализации просто игнорируют символы заполнения, даже если они находятся в середине строки.
Роман
39

Кстати, вот базовый конвертер для произвольного базового преобразования, который я создал для вас. Наслаждайтесь! https://convert.zamicol.com/

Что такое символы заполнения?

Символы заполнения помогают удовлетворить требования к длине и не имеют смысла.

Десятичный пример заполнения: Учитывая произвольное требование, чтобы все строки имели длину 8 символов, число 640 может удовлетворить это требование, используя предшествующие 0 в качестве символов заполнения, поскольку они не имеют значения, «00000640».

Двоичное кодирование

Парадигма байтов: байт - это де-факто стандартная единица измерения, и любая схема кодирования должна относиться к байтам.

Base256 как раз вписывается в эту парадигму. Один байт равен одному символу в base256.

Base16 , шестнадцатеричный или шестнадцатеричный, использует 4 бита для каждого символа. Один байт может представлять два символа base16.

Base64 не вписывается равномерно в парадигму байтов (как и base32), в отличие от base256 и base16. Все символы base64 могут быть представлены 6 битами, на 2 бита меньше полного байта.

Мы можем представить кодировку base64 по сравнению с байтовой парадигмой в виде дроби: 6 бит на символ по 8 бит на байт . Уменьшенная эта дробь составляет 3 байта на 4 символа.

Это соотношение, 3 байта на каждые 4 символа base64, является правилом, которому мы хотим следовать при кодировании base64. Кодирование Base64 может обещать только измерения с 3-байтовыми пакетами , в отличие от base16 и base256, где каждый байт может стоять сам по себе.

Так почему же рекомендуется использовать заполнение, даже если кодирование может работать нормально без символов заполнения?

Если длина потока неизвестна или может быть полезно знать, когда именно заканчивается поток данных, используйте заполнение. Символы заполнения явно сообщают, что эти лишние места должны быть пустыми, и исключают любую двусмысленность. Даже если длина неизвестна с заполнением, вы будете знать, где заканчивается ваш поток данных.

Например, некоторые стандарты, такие как JOSE , не позволяют использовать символы заполнения. В этом случае, если чего-то не хватает, криптографическая подпись не будет работать или будут отсутствовать другие символы, отличные от base64 (например, "."). Хотя предположений о длине не делается, заполнение не требуется, потому что, если что-то не так, оно просто не сработает.

И это именно то, что говорится в RFC base64 :

В некоторых случаях использование заполнения ("=") в данных с базовым кодированием не требуется или не используется. В общем случае, когда невозможно сделать предположения о размере транспортируемых данных, требуется заполнение для получения правильных декодированных данных.

[...]

Шаг заполнения в базе 64 [...], если он реализован неправильно, приведет к незначительным изменениям закодированных данных. Например, если на входе только один октет для кодирования base 64, то используются все шесть битов первого символа, но используются только первые два бита следующего символа. Эти биты заполнения ДОЛЖНЫ быть установлены в ноль соответствующими кодировщиками, что описано в описаниях заполнения ниже. Если это свойство не выполняется, каноническое представление данных с базовым кодированием отсутствует, и несколько строк с базовым кодированием могут быть декодированы в одни и те же двоичные данные. Если это свойство (и другие, обсуждаемые в этом документе) соблюдены, каноническая кодировка гарантирована.

Заполнение позволяет нам декодировать кодировку base64 без потери битов. Без заполнения больше нет явного подтверждения измерения в трехбайтовых пакетах. Без заполнения вы не сможете гарантировать точное воспроизведение исходной кодировки без дополнительной информации, обычно из другого места в вашем стеке, например TCP, контрольных сумм или других методов.

Примеры

Вот пример формы RFC 4648 ( http://tools.ietf.org/html/rfc4648#section-8 )

Каждый символ внутри функции «BASE64» использует один байт (base256). Затем мы переводим это в base64.

BASE64("")       = ""           (No bytes used. 0%3=0.)
BASE64("f")      = "Zg=="       (One byte used. 1%3=1.)
BASE64("fo")     = "Zm8="       (Two bytes. 2%3=2.)
BASE64("foo")    = "Zm9v"       (Three bytes. 3%3=0.)
BASE64("foob")   = "Zm9vYg=="   (Four bytes. 4%3=1.)
BASE64("fooba")  = "Zm9vYmE="   (Five bytes. 5%3=2.)
BASE64("foobar") = "Zm9vYmFy"   (Six bytes. 6%3=0.)

Вот кодировщик, с которым вы можете поиграть: http://www.motobit.com/util/base64-decoder-encoder.asp

Замикол
источник
16
-1 Это хороший и подробный пост о том, как работают системы счисления, но он не объясняет, почему используется заполнение, когда кодировка отлично работала бы без него.
Матти Вирккунен
2
Вы хоть вопрос читали? Для правильного декодирования отступы не нужны .
Navin
3
Я думаю, что этот ответ на самом деле объяснил причину, указанную здесь: «мы больше не можем гарантировать точное воспроизведение исходной кодировки без дополнительной информации». На самом деле все просто, отступы сообщают нам, что мы получили полную кодировку. Каждый раз, когда у вас есть 3 байта, вы можете с уверенностью предположить, что можно продолжить и декодировать его, не беспокойтесь об этом, гул ... может быть, придет еще один байт, возможно, изменит кодировку.
Дидье А.
@DidierA. Как узнать, что в подстроке base64 нет еще трех байтов? Чтобы декодировать a char*, вам нужен либо размер строки, либо нулевой терминатор. Заполнение избыточно. Следовательно, вопрос ОП.
Navin
4
@Navin Если вы выполняете потоковое декодирование байтов base64, вы не знаете длину, с заполнением 3 байта, вы знаете, что каждый раз, когда у вас есть 3 байта, вы можете обрабатывать 4 символа, пока не дойдете до конца потока. Без него вам может потребоваться возврат назад, потому что следующий байт может привести к изменению предыдущего символа, поэтому вы можете быть уверены, что декодировали его правильно, только когда вы достигли конца потока. Итак, это не очень полезно, но у него есть несколько крайних случаев, когда вы можете это сделать.
Didier A.
2

В наши дни от этого мало пользы. Итак, давайте посмотрим на это как на вопрос, какова могла быть первоначальная историческая цель.

Кодирование Base64 впервые появляется в RFC 1421 от 1993 года. Этот RFC фактически ориентирован на шифрование электронной почты, а base64 описан в одном небольшом разделе 4.3.2.4 .

Этот RFC не объясняет цель заполнения. Ближе всего к упоминанию первоначальной цели у нас есть это предложение:

Полный такт кодирования всегда завершается в конце сообщения.

Он не предполагает конкатенации (верхний ответ здесь) или простоты реализации в качестве явной цели для заполнения. Однако, рассматривая полное описание, есть основания предположить, что это могло быть предназначено для того, чтобы помочь декодеру считывать ввод в 32-битных единицах ( «квантах» ). Сегодня это бесполезно, однако в 1993 году небезопасный код C, скорее всего, действительно воспользовался бы этим свойством.

Роман Старков
источник
1
При отсутствии заполнения попытка объединить две строки, когда длина первой строки не кратна трем, часто приводит к кажущейся допустимой строке, но содержимое второй строки декодируется неправильно. Добавление заполнения гарантирует, что этого не произойдет.
supercat
1
@supercat Если бы это было целью, не было бы проще заканчивать каждую строку base64 одним знаком "="? Средняя длина будет меньше, и это все равно предотвратит ошибочные конкатенации.
Роман Старков
2
Средняя длина b'Zm9vYmFyZm9vYg==' b'Zm9vYmFyZm9vYmE=' b'Zm9vYmFyZm9vYmFy' b'Zm9vYmFyZm9vYmFyZg==' b'Zm9vYmFyZm9vYmFyZm8=' b'Zm9vYmFyZm9vYmFyZm9v' такая же, как у b'Zm9vYmFyZm9vYg=' b'Zm9vYmFyZm9vYmE=' b'Zm9vYmFyZm9vYmFy=' b'Zm9vYmFyZm9vYmFyZg=' b'Zm9vYmFyZm9vYmFyZm8=' b'Zm9vYmFyZm9vYmFyZm9v='
Скотт