如果输入长度不能被 3 整除,为啥 base64 编码需要填充?
Posted
技术标签:
【中文标题】如果输入长度不能被 3 整除,为啥 base64 编码需要填充?【英文标题】:Why does base64 encoding require padding if the input length is not divisible by 3?如果输入长度不能被 3 整除,为什么 base64 编码需要填充? 【发布时间】:2011-05-04 02:50:47 【问题描述】:base64 编码中填充的目的是什么。以下是***的摘录:
"分配了一个额外的填充字符,可用于强制编码输出为 4 个字符的整数倍(或等效地,当未编码的二进制文本不是 3 个字节的倍数时);然后必须丢弃这些填充字符在解码但仍允许计算未编码文本的有效长度时,当其输入二进制长度不是 3 字节的倍数时(最后一个非填充字符通常被编码,因此它代表的最后一个 6 位块将在其最低有效位上补零,编码流的末尾最多可能出现两个填充字符)。”
我写了一个程序,它可以对任何字符串进行base64编码并解码任何base64编码的字符串。 padding 解决了什么问题?
【问题讨论】:
【参考方案1】:在现代没有太多好处。因此,让我们将其视为原始历史目的可能是什么的问题。
Base64 编码在 1993 年的 RFC 1421 中首次出现。该 RFC 实际上专注于加密电子邮件,而 base64 在 one small section 4.3.2.4 中进行了描述。
本 RFC 没有解释填充的目的。最接近我们提及原始目的的是这句话:
完整的编码量总是在消息结束时完成。
它不建议连接(此处为最佳答案),也不建议将易于实施作为填充的明确目的。但是,考虑到整个描述,假设这可能是为了帮助解码器以 32 位单位(“quanta”)读取输入,这并非不合理。这在今天没有任何好处,但是在 1993 年,不安全的 C 代码很可能实际上利用了这个属性。
【讨论】:
在没有填充的情况下,当第一个字符串的长度不是三的倍数时尝试连接两个字符串通常会产生一个看似有效的字符串,但第二个字符串的内容会错误解码.添加填充确保不会发生。 @supercat 如果这是目标,那么用一个“=”结束每个 base64 字符串不是更容易吗?平均长度会更短,并且仍然可以防止错误的连接。b'Zm9vYmFyZm9vYg==' b'Zm9vYmFyZm9vYmE=' b'Zm9vYmFyZm9vYmFy' b'Zm9vYmFyZm9vYmFyZg==' b'Zm9vYmFyZm9vYmFyZm8=' b'Zm9vYmFyZm9vYmFyZm9v'
的平均长度与b'Zm9vYmFyZm9vYg=' b'Zm9vYmFyZm9vYmE=' b'Zm9vYmFyZm9vYmFy=' b'Zm9vYmFyZm9vYmFyZg=' b'Zm9vYmFyZm9vYmFyZm8=' b'Zm9vYmFyZm9vYmFyZm9v='
相同【参考方案2】:
在相关说明中,这是我为您创建的任意基础转换器。享受! https://convert.zamicol.com/
什么是填充字符?
填充字符有助于满足长度要求并且没有任何意义。
填充的十进制示例: 给定所有字符串长度为 8 个字符的任意要求,数字 640 可以使用前面的 0 作为填充字符来满足此要求,因为它们没有任何意义,“00000640”。
二进制编码
字节范式:字节是事实上的标准测量单位,任何编码方案都必须与字节相关。
Base256 完全符合这种范式。 base256 中一个字节等于一个字符。
Base16,十六进制或十六进制,每个字符使用 4 位。一个字节可以代表两个base16字符。
Base64 与 base256 和 base16 不同,不能均匀地融入字节范式(base32 也不适合)。所有 base64 字符都可以用 6 位表示,不足 2 位是一个完整的字节。
我们可以将 base64 编码与字节范式表示为分数:每个字符 6 位超过每个字节 8 位。减少这个分数是 3 个字节超过 4 个字符。
这个比例,每 4 个 base64 字符占 3 个字节,是我们在编码 base64 时要遵循的规则。 Base64 编码只能保证使用 3 字节包进行测量,不像 base16 和 base256,每个字节都可以独立存在。
那么为什么鼓励使用填充,即使编码可以在没有填充字符的情况下正常工作?
如果流的长度未知,或者如果准确知道数据流何时结束会有所帮助,请使用填充。填充字符明确表示那些额外的点应该是空的,并排除任何歧义。即使长度未知,您也会知道数据流的结束位置。
作为一个反例,JOSE 等一些标准不允许使用填充字符。在这种情况下,如果缺少某些东西,加密签名将不起作用或其他非 base64 字符将丢失(如“.”)。虽然没有对长度做出假设,但也不需要填充,因为如果出现问题,它根本就不起作用。
这正是base64 RFC 所说的,
在某些情况下,在基本编码数据中使用填充 ("=") 不需要或使用。在一般情况下,当假设 无法确定传输数据的大小,需要填充 产生正确的解码数据。
[...]
base 64 中的填充步骤 [...] 如果不正确 实施,导致编码数据的非显着改变。 例如,如果输入对于 base 64 编码只有一个八位字节, 然后使用第一个符号的所有六位,但只有第一个 使用下一个符号的两位。这些填充位必须设置为 通过符合编码器归零,这在描述中进行了描述 在下面的填充上。如果这个属性不成立,就没有 碱基编码数据的规范表示,以及多个碱基 编码的字符串可以解码为相同的二进制数据。如果这 属性(以及本文档中讨论的其他内容)成立,一个规范的 编码是有保证的。
填充允许我们以不丢失位的承诺来解码 base64 编码。如果没有填充,则不再明确确认以三字节包进行测量。如果没有填充,您可能无法保证在没有额外信息的情况下准确再现原始编码,这些信息通常来自堆栈中的其他位置,例如 TCP、校验和或其他方法。
示例
这是 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
【讨论】:
-1 这是一篇关于数字系统如何工作的精彩而透彻的帖子,但它没有解释 为什么 在没有编码也能完美工作的情况下使用填充。 你读过这个问题吗?您不需要 填充即可正确解码。 我认为这个答案确实解释了这里所说的原因:“我们不能再保证在没有额外信息的情况下准确再现原始编码”。真的很简单,填充让我们知道我们收到了完整的编码。每次你有 3 个字节时,你可以放心地假设继续解码它是可以的,你不用担心,嗯……也许会再增加一个字节,可能会改变编码。 @DidierA。你怎么知道 base64 子字符串中没有 3 个字节?要解码char*
,您需要字符串的大小或空终止符。 填充是多余的。因此,OP的问题。
@Navin 如果您正在对 base64 字节进行流式解码,您不知道长度,使用 3 个字节填充,您知道每次获得 3 个字节就可以处理 4 个字符,直到您到达流的尽头。没有它,您可能需要回溯,因为下一个字节可能会导致前一个字符发生变化,因此只有在到达流的末尾时才能确保正确解码它。所以,它不是很有用,但它有一些你可能想要它的边缘情况。【参考方案3】:
您认为不需要填充的结论是正确的。总是可以从编码序列的长度中明确地确定输入的长度。
但是,在 base64 编码的字符串以这样一种方式连接单个序列的长度会丢失的情况下,填充很有用,例如,在非常简单的网络协议中可能会发生这种情况。
如果 unpadded 字符串被串联,则无法恢复原始数据,因为有关每个单独序列末尾的奇数字节数的信息会丢失。但是,如果使用填充序列,则不会产生歧义,并且可以正确解码整个序列。
编辑:插图
假设我们有一个对单词进行 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
。
为什么要使用填充?
为什么不设计协议来为每个单词加上一个整数长度的前缀呢?然后接收方可以正确解码流,不需要填充。
这是一个好主意,只要我们在开始编码之前知道我们正在编码的数据的长度。但是,如果我们不是文字,而是对来自实时摄像机的视频块进行编码呢?我们可能事先不知道每个块的长度。
如果协议使用填充,则根本不需要传输长度。数据可以在从相机传入时进行编码,每个块都以填充结束,并且接收器将能够正确解码流。
显然这是一个非常人为的例子,但也许它说明了为什么填充在某些情况下可能会有所帮助。
【讨论】:
+1 除了“因为出于某种莫名其妙的原因我们喜欢冗长和冗余”之外,唯一真正提供合理答案的答案。 这适用于明确编码的块,但预计在解码后不可分割地连接。如果你发送 U0FNSQ==QU0=,你可以重构句子,但是你会丢失组成句子的单词。我猜总比没有好。值得注意的是,GNU base64 程序会自动处理级联编码。 如果单词的长度是 3 的倍数呢?这种愚蠢的连接方式会破坏信息(单词的结尾),而不是删除填充。 Base64 连接允许编码器并行处理大块,而无需将块大小对齐为三的倍数。同样,作为一个实现细节,可能有一个编码器需要刷新一个大小不是三的倍数的内部数据缓冲区。 这个答案可能会让您认为您可以通过将“SQ==QU0=VEpN”之类的内容提供给解码器来解码。实际上,您似乎不能,例如 javascript 和 php 中的实现不支持这一点。从连接的字符串开始,您要么必须一次解码 4 个字节,要么在填充字符后拆分字符串。似乎这些实现只是忽略了填充字符,即使它们位于字符串的中间。【参考方案4】:填充以定义的方式将输出长度填充为四个字节的倍数。
【讨论】:
以上是关于如果输入长度不能被 3 整除,为啥 base64 编码需要填充?的主要内容,如果未能解决你的问题,请参考以下文章