Python: 在 base64 解码时忽略 'Incorrect padding' 错误

195

我有一些经过Base64编码的数据,即使其中存在填充错误,我也想将其转换回二进制。如果我使用

base64.decodestring(b64_string)

它引发了一个“填充不正确”的错误。还有其他方法吗?

更新:感谢所有的反馈。老实说,提到的所有方法听起来都有点靠运气,所以我决定尝试openssl。以下命令非常有效:

openssl enc -d -base64 -in b64string -out binary_data

8
你是否尝试过使用base64.b64decode(strg, '-_')?在你没有提供任何示例数据的情况下,这是解决你问题最有可能的Python方法。之前提出的"方法"只是调试建议,鉴于提供的信息很少,它们必然是"靠瞎猜"的。 - John Machin
4
@John Machin: 是的,我确实尝试了你的方法,但没有成功。这些数据是公司机密。 - FunLovinCoder
7
尝试运行 base64.urlsafe_b64decode(s)。该函数用于解码使用URL安全字符集编码的Base64字符串。 - Daniel F
1
请问您可以提供此代码的输出结果吗?sorted(list(set(b64_string)))。通过此方式,我们可以知道用于编码原始数据的字符集,进而提供一个更加准确的解决方案,同时不会泄露任何公司机密信息。 - Brian Carcich
1
是的,我知道这个问题已经解决了,但说实话,openssl 的解决方案对我来说也有些靠运气。 - Brian Carcich
显示剩余2条评论
22个回答

153

看起来你只需要在解码之前给字节添加填充即可。 这个问题有很多其他答案,但是我想指出的是(至少在Python 3.x中),base64.b64decode会截断任何额外的填充,只要首先提供足够的填充。

因此,像b'abc='这样的东西与b'abc=='(以及b'abc=====')一样有效。

这意味着您只需添加可能需要的最大填充字符即可,这是两个(b'=='),而base64将截断不必要的填充。

这让你可以写成:

base64.b64decode(s + b'==')

比起以下这个更简单:

base64.b64decode(s + b'=' * (-len(s) % 4))

请注意,如果字符串s已经有了一些填充(例如b"aGVsbG8="),则该方法仅在设置了validate关键字参数为False时才有效(这是默认值)。如果validateTrue,则如果总填充长度超过两个字符,将导致引发binascii.Error异常。

文档中可以看到:

如果validateFalse(默认值),则在填充检查之前会丢弃既不属于普通 Base-64 字母表,也不属于另一种字母表的字符。 如果validateTrue,输入中的这些非字母表字符会导致一个binascii.Error异常。

然而,如果validateFalse(或留空以使用默认值),您可以盲目地添加两个填充字符而没有任何问题。感谢 eel ghEEz 在评论中指出这一点。


2
好的,这不太“丑”,谢谢 :) 顺便说一下,我认为你永远不需要超过2个填充字符。Base64算法每次处理3个字符组,并且只有在最后一组字符长度为1或2时才需要填充。 - Otto
2
@Otto 这里的填充是为了解码,它按4个字符为一组进行工作。而 Base64 编码 是按3个字符为一组进行工作的 :) - Henry Woody
1
但是,如果您知道在编码过程中最多只会添加2个字符,这些字符可能会在后来“丢失”,迫使您在解码之前重新添加它们,那么您就知道在解码过程中最多只需要添加2个字符。#圣诞节的争论趣味 - Otto
2
@Otto,我相信你是对的。虽然长度为5的base64编码字符串需要3个填充字符,但长度为5的字符串甚至不是一个有效的base64编码字符串长度。你会得到这个错误:binascii.Error: Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4。感谢你指出这一点! - Henry Woody
显示剩余7条评论

103

就像其他回答中所说,base64数据可能会以各种方式损坏。

然而,正如维基百科所述,移除填充(在base64编码数据结尾处的'='字符)是“无损的”:

从理论上讲,填充字符是不必要的,因为可以通过Base64数字的数量计算出缺失的字节数。

因此,如果这确实是您的base64数据唯一的“问题”,则可以将填充字符添加回去。我设计了这个方法来解析WeasyPrint中的“data”URL,其中一些是没有填充的base64数据:

import base64
import re

def decode_base64(data, altchars=b'+/'):
    """Decode base64, padding being optional.

    :param data: Base64 data as an ASCII byte string
    :returns: The decoded byte string.

    """
    data = re.sub(rb'[^a-zA-Z0-9%s]+' % altchars, b'', data)  # normalize
    missing_padding = len(data) % 4
    if missing_padding:
        data += b'='* (4 - missing_padding)
    return base64.b64decode(data, altchars)

该函数的测试内容:weasyprint/tests/test_css.py#L68


3
注意:ASCII而非Unicode,为了安全起见,您可能需要使用str(data) - MarkHu
5
这很好,只有一个小问题。base64.decodestring已被弃用,请改用base64.b64_decode。 - ariddell
2
为了澄清@ariddell的评论,base64.decodestring在Py3中已被弃用,但为了版本兼容性最好使用base64.b64decode - Cas
因为base64模块会忽略输入中无效的非base64字符,所以您首先需要规范化数据。删除任何不是字母、数字、/+的内容,然后再添加填充。 - Martijn Pieters
“added back”它一开始就没有丢失。 - Benjamin Atkin
拥有4n+1个标准化(未填充)编码输入字母已经是无效编码的迹象,因为base64仅编码为4n4n+24n+3个未填充字母。 - eel ghEEz

53

只需根据需要添加填充。然而请注意迈克尔的警告。

b64_string += "=" * ((4 - len(b64_string) % 4) % 4) #ugh

2
肯定有更简单的方法将0映射到0,2映射到1,1映射到2。 - badp
2
你为什么要扩展到3的倍数而不是4的倍数? - Michael Mrozek
1
在base64编码中,每24位(3字节)二进制输入被编码为4字节输出。output_len%3没有意义。 - John Machin
18
只需添加 === 即可。Python 似乎可以安全地忽略多余的 = 符号。 - Asclepius
1
感谢您提供的一行代码!但是为什么要使用内部模数呢?((4 - len(b64_string)) % 4)似乎对于所有值,包括len=0这样的边缘情况,都返回相同的结果。 - Luc
显示剩余2条评论

30
"不正确的填充"不仅可以指缺少填充,而且(信不信由你)还可以指填充不正确。如果建议的"添加填充"方法不起作用,请尝试删除一些末尾字节:
lens = len(strg)
lenx = lens - (lens % 4 if lens % 4 else 4)
try:
    result = base64.decodestring(strg[:lenx])
except etc

更新:在移除任何空格之后才进行添加填充或删除可能的末尾无效字节的操作,否则长度计算可能会出错。

如果您能展示一段(简短)需要恢复的数据样本,将是一个好主意。编辑您的问题并复制/粘贴以下结果:print repr(sample)

更新2:可能已经以url安全的方式进行了编码。如果是这种情况,则您将能够在数据中看到减号和下划线字符,并且您应该能够通过使用 base64.b64decode(strg,'-_') 来解码它。

如果您的数据中没有减号和下划线字符,但可以看到加号和斜杠字符,则您可能有其他问题,并且可能需要添加填充或删除无用字符。

如果您的数据中既没有减号、下划线、加号也没有斜杠字符,则需要确定两个备选字符;它们将是不在[A-Za-z0-9]中的字符。然后,您需要自行实验以确定它们在base64.b64decode()的第二个参数中需要使用的顺序。

更新3:如果您的数据是“公司保密”:
(a) 您应该事先说明
(b) 我们可以探索其他理解问题的途径,这很可能与编码字母表中替代+/字符有关,或者其他格式或无关字符。

其中一条路径是检查您的数据中有哪些非“标准”字符,例如:

from collections import defaultdict
d = defaultdict(int)
import string
s = set(string.ascii_letters + string.digits)
for c in your_data:
   if c not in s:
      d[c] += 1
print d

数据由标准的base64字符集组成。我相信问题是因为缺少一个或多个字符,因此出现了填充错误。除非在Python中有一个强大的解决方案,否则我将使用我的解决方案调用openssl。 - FunLovinCoder
1
一个“解决方案”如果默默地忽略错误,那么很难称之为“健壮”。正如我之前提到的,各种Python建议都是调试方法,用于找出问题所在,为了一个有原则的解决方案……你不感兴趣吗? - John Machin
7
我的要求并不是解决为什么base64数据会损坏的问题——它来自于我无法控制的源头。我的要求是即使数据损坏,也要提供有关接收到的数据的信息。一种方法是从损坏的base64中获取二进制数据,以便我可以从底层的ASN.1流中获取信息。我提出了原始问题,是因为我想要得到那个问题的答案,而不是另一个问题的答案——比如如何调试损坏的base64。 - FunLovinCoder
只需对字符串进行规范化,删除任何不是Base64字符的内容。不仅限于开头或结尾。 - Martijn Pieters
这是在本页所有答案中唯一对我有效的答案。 - christegho

30

使用

string += '=' * (-len(string) % 4)  # restore stripped '='s

这里的评论是原作者发表的。

>>> import base64

>>> enc = base64.b64encode('1')

>>> enc
>>> 'MQ=='

>>> base64.b64decode(enc)
>>> '1'

>>> enc = enc.rstrip('=')

>>> enc
>>> 'MQ'

>>> base64.b64decode(enc)
...
TypeError: Incorrect padding

>>> base64.b64decode(enc + '=' * (-len(enc) % 4))
>>> '1'

>>> 

4
他的意思是这条评论:http://stackoverflow.com/questions/2941995/python-ignore-incorrect-padding-error-when-base64-decoding#comment12174484_2942039 - jackyalcine

24
如果出现填充错误,则很可能意味着您的字符串已损坏;base64编码字符串应具有四的倍数长度。您可以尝试自行添加填充字符(=),使字符串成为四的倍数,但是除非有问题,否则它应该已经具有这个长度。

底层二进制数据是ASN.1。即使存在损坏,我仍然希望返回二进制数据,因为我仍然可以从ASN.1流中获取一些有用的信息。 - FunLovinCoder
1
如果您想解码JWT进行安全检查,则需要它。 - DAG

14

不正确的填充错误是由于有时元数据也存在于编码的字符串中而导致的。如果您的字符串看起来像:'data:image/png;base64,...base 64 stuff....',那么您需要在解码之前删除第一部分。

例如,如果您有图像Base64编码字符串,则可以尝试以下代码片段:

from PIL import Image
from io import BytesIO
from base64 import b64decode
imagestr = 'data:image/png;base64,...base 64 stuff....'
im = Image.open(BytesIO(b64decode(imagestr.split(',')[1])))
im.save("image.png")

7
如果您想解码网络图像,可以简单地使用base64.urlsafe_b64decode(data)。 它会自动处理填充。

这不是我的经验。至少在3.11.3版本中不是这样。 - Travis Griggs

4

请查看你尝试解码的数据源文件的文档。你是否想使用base64.urlsafe_b64decode(s)而非base64.b64decode(s)?这可能是你遇到此错误消息的原因之一。

使用URL安全字符表解码字符串s,其中将+替换为-,将/替换为_,以代替标准Base64字符表中的符号。

例如,这是各种Google API(如Google身份验证工具包和Gmail负载)使用的情况。


2
这根本没有回答问题。另外,urlsafe_b64decode也需要填充。 - rdb
1
在回答这个问题之前,我遇到了一个与Google身份工具包相关的问题。我一直收到不正确的填充错误(我认为是在服务器上),即使填充看起来是正确的。结果发现我必须使用base64.urlsafe_b64decode - Daniel F
我同意,rdb,这并没有回答问题,但这正是我需要听到的。我重新表达了答案,语气更加友好,希望这对你有用,Daniel。 - Henrik Heimbuerger
非常好。我没有注意到它听起来有点不友善,我只是认为如果能解决问题,那么这应该是最快的修复方法,因此应该是首先尝试的事情。感谢您的更改,非常欢迎。 - Daniel F

3

增加填充有些繁琐。这是我写的函数,结合了本线程中的评论和Base64维基页面(它非常有帮助)https://en.wikipedia.org/wiki/Base64#Padding

import logging
import base64
def base64_decode(s):
    """Add missing padding to string and return the decoded base64 string."""
    log = logging.getLogger()
    s = str(s).strip()
    try:
        return base64.b64decode(s)
    except TypeError:
        padding = len(s) % 4
        if padding == 1:
            log.error("Invalid base64 string: {}".format(s))
            return ''
        elif padding == 2:
            s += b'=='
        elif padding == 3:
            s += b'='
        return base64.b64decode(s)

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