Чому кодування base64 вимагає заповнення, якщо довжина вводу не ділиться на 3?


101

Яка мета заповнення в кодуванні base64. Далі наведено витяг із Вікіпедії:

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

Я написав програму, яка могла б base64 кодувати будь-який рядок і декодувати будь-який рядок, закодований base64. Яку проблему вирішує прокладка?

Відповіді:


210

Ваш висновок про непотрібність набивання правильний. Завжди можна однозначно визначити довжину вводу з довжини закодованої послідовності.

Однак заповнення корисно в ситуаціях, коли кодовані рядки 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.

Чому дошкуляти підбиванням?

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

Це чудова ідея, якщо ми знаємо довжину даних, які кодуємо, перш ніж розпочати їх кодування. Але що, якби замість слів ми кодували фрагменти відео з живої камери? Ми можемо не знати довжину кожного шматка заздалегідь.

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

Очевидно, що це дуже надуманий приклад, але, можливо, він ілюструє, чому прокладки можуть бути корисними в деяких ситуаціях.


22
+1 Єдина відповідь, яка насправді дає розумну відповідь, крім того, "тому що ми любимо багатослівність і надмірність з якоїсь незрозумілої причини".
Недійсний

1
Це працює добре для фрагментів, які кодуються чітко, але, як очікується, будуть нерозривно об'єднані після декодування. Якщо ви надіслали U0FNSQ == QU0 =, ви можете відновити речення, але ви втратите слова, що складають речення. Краще, ніж нічого, я думаю. Примітно, що програма GNU base64 автоматично обробляє об'єднані кодування.
Marcelo Cantos

2
Що робити, якщо довжина слів кратна 3? Цей безглуздий спосіб об’єднання знищує інформацію (закінчення слів), а не видаляє заповнення.
GreenScape

2
Конкатенація Base64 дозволяє кодерам обробляти великі фрагменти паралельно без тягаря вирівнювання розмірів фрагментів до кратного трьом. Так само, як деталь реалізації, там може бути кодер, який повинен очистити внутрішній буфер даних розміром, який не кратний трьом.
Andre D

1
Ця відповідь може змусити вас подумати, що ви можете декодувати щось на зразок "SQ == QU0 = VEpN", просто передавши це декодеру. Насправді здається, що ви не можете, наприклад, реалізації в javascript та php цього не підтримують. Починаючи з об'єднаного рядка, вам потрібно або декодувати 4 байти за раз, або розділити рядок після заповнення символів. Здається, ці реалізації просто ігнорують символи заповнення, навіть коли вони знаходяться в середині рядка.
Роман

38

У відповідній примітці, ось базовий перетворювач для довільного базового перетворення, який я створив для вас. Насолоджуйтесь! 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 , не дозволяють заповнення символів. У цьому випадку, якщо чогось не вистачає, криптографічний підпис не працюватиме або будуть відсутні інші символи, що не є базовими64 (наприклад, "."). Незважаючи на те, що припущення щодо довжини не зроблені, відступ не потрібен, оскільки якщо щось не так, це просто не спрацює.

І це саме те, що каже base64 RFC,

За деяких обставин використання заповнення ("=") у кодованих базовими даними даних не вимагається і не використовується. У загальному випадку, коли неможливо зробити припущення щодо розміру даних, що транспортуються, для отримання правильних декодованих даних потрібно заповнення.

[...]

Крок заповнення в базі 64 [...] при неправильній реалізації призведе до незначущих змін закодованих даних. Наприклад, якщо введенням є лише один октет для кодування базового 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 немає ще 3 байтів? Для декодування a char*вам потрібен розмір рядка або нульовий термінатор. Набивання зайве. Отже, питання ОП.
Navin

4
@Navin Якщо ви декодуєте потік base64 байт, ви не знаєте довжини, з 3 байтовими відступами ви знаєте, що кожного разу, коли ви отримуєте 3 байти, ви можете обробляти 4 символи, поки не дійдете до кінця потоку. Без нього вам може знадобитися повернутися назад, оскільки наступний байт може призвести до зміни попереднього символу, отже, ви зможете бути впевнені, що правильно його розшифрували, лише досягнувши кінця потоку. Отже, це не дуже корисно, але у ньому є кілька крайових випадків, коли вам це може знадобитися.
Didier A.

1

Сучасна користь від нього не дуже велика. Тож давайте розглянемо це як питання про те, якою могла бути первісна історична мета.

Кодування Base64 вперше з’являється у RFC 1421 від 1993 року. Цей RFC фактично зосереджений на шифруванні електронної пошти, а base64 описаний в одному невеликому розділі 4.3.2.4 .

Цей RFC не пояснює мету заповнення. Найближче до згадки про початкову мету - це речення:

Повний квант кодування завжди заповнюється в кінці повідомлення.

Це не пропонує конкатенацію (тут найкраща відповідь), а також простоту реалізації як явну мету для заповнення. Однак, беручи до уваги весь опис, не безпідставно вважати, що це, можливо, було призначене для того, щоб допомогти декодеру прочитати вхідні дані в 32-розрядних одиницях ( "кванти" ). Сьогодні це не приносить користі, однак у 1993 році небезпечний код С дуже ймовірно насправді скористався цією властивістю.


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='
Скотта
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.