如果输入长度不能被3整除,为什么Base64编码需要填充?

191
在base64编码中,填充的目的是什么?以下是维基百科的摘录:“分配了一个额外的填充字符,它可以用来将编码输出强制转换为4个字符的整数倍(或者当未编码的二进制文本不是3个字节的倍数时等效); 在解码时必须丢弃这些填充字符,但仍然允许计算未编码文本的有效长度,当其输入二进制长度不是3个字节的倍数时(通常对最后一个非填充字符进行编码,以便其表示的最后6位块在其最低有效位上进行零填充,在编码流的结尾最多可能出现两个填充字符)。 我编写了一个程序,可以对任何字符串进行base64编码并解码任何base64编码的字符串。 填充解决了什么问题?
5个回答

359

你的结论是正确的,无需填充。始终可以通过编码序列的长度明确地确定输入的长度。

但是,在某些情况下,填充是有用的,例如在拼接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

填充的意义何在?

为什么不只是设计协议以在每个单词之前加上整数长度?然后接收方可以正确解码流,并且不需要填充。

这是个很好的想法,只要我们在开始编码之前知道要编码的数据长度。但如果我们不是编码单词,而是来自实时摄像头的视频块,我们可能事先不知道每个块的长度。

如果协议使用填充,就根本不需要传输长度。数据可以随着从相机输入而编码,每个块以填充结束,并且接收器将能够正确解码流。

显然,这是一个非常牵强的例子,但也许说明了为什么在某些情况下填充可能是有帮助的。


58
唯一提供合理答案的回答是“因为我们出于某种无法解释的原因喜欢冗长和冗余”。 - Invalid
2
这对于编码不同但在解码后预期不可分割地连接的块来说是可以正常工作的。如果您发送U0FNSQ==QU0=,您可以重构句子,但会丢失组成句子的单词。我想这总比没有好。值得注意的是,GNU base64程序自动处理连接的编码。 - Marcelo Cantos
6
如果单词长度是3的倍数会怎样?这种愚蠢的拼接方式会破坏信息(单词结尾),而不是去除填充。 - GreenScape
3
Base64串联允许编码器在不必将块大小调整为三的倍数的情况下并行处理大块数据。同样地,作为一项实现细节,可能存在一个需要刷新内部数据缓冲区且大小不是三的倍数的编码器。 - Andre D
9
这个回答可能会让你认为你可以通过将类似于"SQ==QU0=VEpN"的字符串直接输入解码器来解码它。但事实上,似乎不行,例如JavaScript和PHP中的实现就不支持这样做。如果从连接的字符串开始,你需要每次解码4个字节,或在填充字符后拆分字符串。似乎那些实现忽略填充字符,即使它们在字符串中间也是如此。 - Roman
显示剩余5条评论

68

另外,这是我为您创建的任意进制转换器。享受吧!https://convert.zamicol.com/

填充字符是什么?

填充字符有助于满足长度要求,不具备其他含义。

填充的十进制示例: 假设任何字符串都需要满足8个字符的任意要求,则数字640可以使用前导0作为填充字符来满足该要求,因为它们没有实际意义,"00000640"。

二进制编码

字节范式:在编码中,字节是事实上的标准测量单位,任何方案都必须与字节相关。

Base256 完全符合字节范式。一个字节等于 base256 中的一个字符。

Base16,十六进制或者简称 hex,每个字符使用 4 位二进制数表示。一个字节可以表示两个 base16 字符。

Base64 不像 base256 和 base16 那样能够完全适应字节范式(base32 也不能)。所有的 base64 字符可以用 6 位二进制数表示,比一个完整字节短 2 位。

我们可以将base64编码与字节范式表示为一个分数:每个字符6位,每个字节8位。简化后这个分数是3个字节对应4个字符。
这个比率,即每4个base64字符对应3个字节,是我们在编码时要遵循的规则。Base64编码只能保证按照3字节一组进行测量,而不像base16和base256那样每个字节都可以单独使用。
那么为什么即使没有填充字符编码也可以正常工作,但还是鼓励使用填充字符呢?
如果流的长度未知或者知道数据流何时结束可能会有帮助,请使用填充字符。填充字符明确地表示这些额外的位置应该为空,并消除了任何歧义。即使长度未知,通过填充字符,你也可以知道数据流的结束位置。
作为反例,一些标准(如JOSE)不允许填充字符。在这种情况下,如果缺少某些内容,则加密签名将无法工作,或者其他非base64字符将丢失(例如“.”)。虽然不会做出长度的假设,但不需要填充,因为如果有问题,它就根本无法工作。
而这正是base64 RFC所说的。
在某些情况下,base编码数据中使用填充符("=")是不需要或不被使用的。一般情况下,当无法对传输数据的大小做出假设时,需要填充符才能产生正确的解码数据。
在base64编码中,填充步骤如果实现不当,就会导致编码数据发生非重要的变化。例如,如果输入只有一个八位组用于base64编码,则第一个符号的所有六个比特将被使用,但下一个符号仅使用前两个比特。这些填充比特必须由符合规范的编码器设置为零,这在下面的填充描述中有所说明。如果这个属性不成立,那么base编码数据就没有规范表示,并且多个base编码字符串可以解码为相同的二进制数据。如果这个属性(以及本文档中讨论的其他属性)成立,则可以保证规范编码。
填充使我们能够解码base64编码并承诺不会丢失任何比特。没有填充,就不再有明确的三字节包量度的确认。没有填充,您可能无法保证原始编码的精确复制,除非从堆栈中的其他位置(如TCP、校验和或其他方法)获取附加信息。
作为替代基于桶的转换方案(如base64),基数转换没有任意的桶大小,并且对于从左到右的读者是左填充的。通常使用迭代除以基数转换方法进行基数转换。

示例

这里是RFC 4648的示例表单 (https://www.rfc-editor.org/rfc/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

25
这是一篇关于数字系统如何工作的详细好文,但它没有解释为什么在编码时不使用填充也可以完美运作。 - Matti Virkkunen
4
你有没有看清楚问题?为了正确解码,你不需要填充。 - Navin
3
我认为这个答案确实解释了原因,如下所述:“没有额外的信息,我们不能再保证原始编码的精确复制。”其实很简单,填充让我们知道我们已经收到完整的编码。每当你有3个字节时,你可以放心地假设它是安全的进行解码,你不用担心,嗯...可能还会有一个字节出现,可能会改变编码。 - Didier A.
1
@DidierA。你怎么知道base64子串中没有3个字节?要解码char*,你需要字符串的大小或空终止符。填充是多余的。因此,OP的问题。 - Navin
7
如果你正在流式解码base64字节,那么你不知道长度,有了3个字节的填充,你就知道每次获得3个字节时可以处理4个字符,直到达到流的末尾。如果没有这个填充,你可能需要回溯,因为下一个字节可能会导致前一个字符发生改变,因此只有在到达流的末尾时才能确保正确解码。所以,它并不是非常有用,但在一些特殊情况下可能会用到。 - Didier A.
显示剩余2条评论

12
在现代,Base64编码没有太多好处。因此,让我们把它看作一个关于其原始历史目的的问题。 Base64编码首次出现在1993年可追溯到RFC 1421中。实际上,这个RFC专注于加密电子邮件,而base64在第4.3.2.4小节中被描述。 该RFC并没有解释填充的目的。我们所说的最接近原始目的的提及是这句话:

完整的编码量总是在消息结束时完成。

它并没有暗示连接(顶部答案),也没有将填充的易于实现作为显式目的。然而,考虑到整个描述,可以合理地假设这可能是为了帮助解码器以32位单位("quanta")读取输入。尽管当今已经没有好处了,在1993年,不安全的C代码很可能实际上利用了这个特性。

2
在没有填充的情况下,当第一个字符串的长度不是三的倍数时,尝试连接两个字符串通常会产生一个看似有效的字符串,但第二个字符串的内容会被错误地解码。添加填充可以确保这种情况不会发生。 - supercat
2
如果这是目标的话,那么每个base64字符串都以单个“=”结尾不是更容易吗?平均长度会更短,而且仍然可以防止错误的串联。 - Roman Starkov
4
这些字符串的平均长度相同:b'Zm9vYmFyZm9vYg==' b'Zm9vYmFyZm9vYmE=' b'Zm9vYmFyZm9vYmFy' b'Zm9vYmFyZm9vYmFyZg==' b'Zm9vYmFyZm9vYmFyZm8=' b'Zm9vYmFyZm9vYmFyZm9v'b'Zm9vYmFyZm9vYg=' b'Zm9vYmFyZm9vYmE=' b'Zm9vYmFyZm9vYmFy=' b'Zm9vYmFyZm9vYmFyZg=' b'Zm9vYmFyZm9vYmFyZm8=' b'Zm9vYmFyZm9vYmFyZm9v=' - Scott

5
使用填充后,base64字符串的长度总是4的倍数(如果不是,则该字符串肯定已经损坏),因此,代码可以轻松地处理在循环中一次处理4个字符(始终将4个输入字符转换为三个或更少的输出字节)。因此,填充使得校验更容易进行(length % 4 != 0 ==> 错误,因为有填充时不可能不是4的倍数),并且使得处理更加简单和高效。
我知道人们会想:即使没有填充,我也可以在循环中处理所有的4字节块,然后只需添加对最后1至3字节的特殊处理即可。这只需要几行额外的代码,速度差异太小而无法测量。这可能是正确的,但您考虑的是C(或更高级语言)以及具有大量RAM存储器和强大CPU的情况。如果您需要使用简单的DSP在硬件中解码base64,具有非常有限的处理能力、没有RAM存储器,并且必须使用非常有限的微型汇编编写代码怎么办?如果您根本不能使用代码,而每件事都必须仅用晶体管堆叠(硬连线的硬件实现)完成呢?有了填充,与没有填充相比,这就变得更加简单了。

0

填充以定义的方式将输出长度填充为四个字节的倍数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接