Python: Ignoruj ​​błąd „Nieprawidłowe dopełnienie” podczas dekodowania base64

111

Mam pewne dane zakodowane w base64, które chcę przekonwertować z powrotem na binarne, nawet jeśli występuje w nich błąd wypełnienia. Jeśli używam

base64.decodestring(b64_string)

zgłasza błąd „Nieprawidłowe wypełnienie”. Czy jest inny sposób?

AKTUALIZACJA: Dzięki za wszystkie opinie. Szczerze mówiąc, wszystkie wspomniane metody brzmiały na chybił trafił, więc zdecydowałem się wypróbować openssl. Następujące polecenie działało świetnie:

openssl enc -d -base64 -in b64string -out binary_data
FunLovinCoder
źródło
5
Czy faktycznie SPRÓBOWAŁEŚ używać base64.b64decode(strg, '-_')? To jest a priori, bez zadawania sobie trudu dostarczenia jakichkolwiek przykładowych danych, najbardziej prawdopodobnego rozwiązania Twojego problemu w Pythonie. Zaproponowane „metody” to sugestie DEBUGOWANIA, KONIECZNIE „trafienie i chybienie”, biorąc pod uwagę niedostatek dostarczonych informacji.
John Machin
2
@John Machin: Tak, WYPRÓBOWAŁEM Waszą metodę, ale nie zadziałała. Dane są poufne.
FunLovinCoder
3
Spróbujbase64.urlsafe_b64decode(s)
Daniel F
Czy mógłbyś podać wynik tego: sorted(list(set(b64_string)))proszę? Nie ujawniając niczego poufnego dla firmy, powinno to ujawnić, które znaki zostały użyte do zakodowania oryginalnych danych, które z kolei mogą dostarczyć wystarczających informacji, aby zapewnić rozwiązanie, którego nie można przegapić.
Brian Carcich
Tak, wiem, że to już rozwiązane, ale szczerze mówiąc, rozwiązanie openssl również brzmi dla mnie na chybił trafił.
Brian Carcich

Odpowiedzi:

79

Jak wspomniano w innych odpowiedziach, istnieją różne sposoby uszkodzenia danych base64.

Jednak, jak mówi Wikipedia , usunięcie dopełnienia (znaków „=” na końcu danych zakodowanych w base64) jest „bezstratne”:

Z teoretycznego punktu widzenia znak wypełniający nie jest potrzebny, ponieważ liczbę brakujących bajtów można obliczyć na podstawie liczby cyfr Base64.

Więc jeśli to naprawdę jedyna „nieprawidłowa” rzecz z danymi base64, wypełnienie można po prostu dodać z powrotem. Wymyśliłem to, aby móc analizować adresy URL „danych” w WeasyPrint, z których niektóre były base64 bez dopełnienia:

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)

Testy dla tej funkcji: weasyprint / tests / test_css.py # L68

Simon Sapin
źródło
2
Uwaga: ASCII nie Unicode, więc dla bezpieczeństwa możesz chciećstr(data)
MarkHu
4
To dobrze z jednym zastrzeżeniem. base64.decodestring jest przestarzały, użyj base64.b64_decode
ariddell
2
Aby wyjaśnić, komentarz @ariddell base64.decodestringzostał uznany za przestarzały base64.decodebytesw Py3, ale ze względu na lepszą zgodność wersji base64.b64decode.
Cas
Ponieważ base64moduł ignoruje nieprawidłowe znaki inne niż base64 w danych wejściowych, najpierw należy znormalizować dane. Usuń wszystko, co nie jest literą, cyfrą /ani +, a następnie dodaj wypełnienie.
Martijn Pieters
39

Po prostu dodaj wypełnienie zgodnie z wymaganiami. Uważajcie jednak na ostrzeżenie Michaela.

b64_string += "=" * ((4 - len(b64_string) % 4) % 4) #ugh
badp
źródło
1
Z pewnością jest coś prostszego, co odwzorowuje 0 na 0, 2 na 1 i 1 na 2.
badp
2
Dlaczego rozszerzasz do wielokrotności 3 zamiast 4?
Michał Mrozek
Wydaje się, że to właśnie sugeruje artykuł Wikipedii na temat base64.
badp
1
@bp: W kodowaniu base64 każde 24-bitowe (3 bajty) wejście binarne jest kodowane jako 4-bajtowe dane wyjściowe. output_len% 3 nie ma sensu.
John Machin
8
Samo dołączanie ===zawsze działa. Wszystkie dodatkowe =znaki są pozornie bezpiecznie odrzucane przez Pythona.
Acumenus
32

Wygląda na to, że przed dekodowaniem wystarczy dodać dopełnienie do swoich bajtów. Istnieje wiele innych odpowiedzi na to pytanie, ale chcę zwrócić uwagę, że (przynajmniej w Pythonie 3.x) base64.b64decodeobcina dodatkowe wypełnienie, pod warunkiem, że jest wystarczająco dużo.

Więc coś takiego: b'abc='działa równie dobrze b'abc=='(jak b'abc=====').

Oznacza to, że możesz po prostu dodać maksymalną liczbę znaków wypełniających, których kiedykolwiek będziesz potrzebować - czyli trzy ( b'===') - a base64 obetnie wszystkie niepotrzebne.

Dzięki temu możesz napisać:

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

co jest prostsze niż:

base64.b64decode(s + b'=' * (-len(s) % 4))
Henry Woody
źródło
1
Okej, to nie jest zbyt "brzydkie" dzięki :) Swoją drogą, myślę, że nigdy nie potrzebujesz więcej niż 2 znaki wypełniające. Algorytm Base64 działa na grupach po 3 znaki naraz i wymaga dopełnienia tylko wtedy, gdy ostatnia grupa znaków ma tylko 1 lub 2 znaki.
Otto
@Otto dopełnienie tutaj służy do dekodowania, które działa na grupach po 4 znaki. Kodowanie Base64 działa na grupach po 3 znaki :)
Henry Woody,
ale jeśli wiesz, że podczas kodowania maksymalnie 2 zostaną dodane, które mogą zostać „utracone” później, zmuszając cię do ponownego dodania ich przed dekodowaniem, wtedy wiesz, że podczas dekodowania będziesz musiał dodać maksymalnie 2. #ChristmasTimeArgumentForTheFunOfIt
Otto
@Otto Wierzę, że masz rację. Podczas gdy ciąg zakodowany algorytmem base64 o długości, na przykład 5, wymagałby 3 znaków dopełniających, ciąg o długości 5 nie jest nawet prawidłową długością dla ciągu zakodowanego algorytmem base64. Byłbyś pojawia się błąd: binascii.Error: Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4. Dziękuję za zwrócenie uwagi!
Henry Woody
24

„Nieprawidłowe wypełnienie” może oznaczać nie tylko „brakujące wypełnienie”, ale także (wierz lub nie) „nieprawidłowe wypełnienie”.

Jeśli sugerowane metody „dodawania dopełnienia” nie działają, spróbuj usunąć niektóre bajty końcowe:

lens = len(strg)
lenx = lens - (lens % 4 if lens % 4 else 4)
try:
    result = base64.decodestring(strg[:lenx])
except etc

Aktualizacja: Wszelkie manipulacje przy dodawaniu wypełnienia lub usuwaniu prawdopodobnie złych bajtów z końca powinny być wykonywane PO usunięciu jakichkolwiek białych znaków, w przeciwnym razie obliczenia długości będą zakłócone.

Byłoby dobrze, gdybyś pokazał nam (krótką) próbkę danych, które musisz odzyskać. Edytuj swoje pytanie i skopiuj / wklej wynik print repr(sample) .

Aktualizacja 2: Możliwe, że kodowanie zostało wykonane w sposób bezpieczny dla adresów URL. W takim przypadku będziesz mógł zobaczyć znaki minus i podkreślenia w swoich danych i powinieneś być w stanie je zdekodować za pomocąbase64.b64decode(strg, '-_')

Jeśli nie widzisz znaków minus i podkreślenia w swoich danych, ale widzisz znaki plus i ukośnik, to masz inny problem i możesz potrzebować sztuczek add-padding lub remove-cruft.

Jeśli w danych nie widać żadnego znaku minus, podkreślenia, plusa i ukośnika, musisz określić dwa alternatywne znaki; będą tymi, których nie ma w [A-Za-z0-9]. Następnie musisz poeksperymentować, aby zobaczyć, w jakiej kolejności należy ich użyć w drugim argumenciebase64.b64decode()

Aktualizacja 3 : Jeśli Twoje dane są „poufne dla firmy”:
(a) powinieneś to powiedzieć z góry
(b) możemy zbadać inne sposoby zrozumienia problemu, który z dużym prawdopodobieństwem ma związek z tym, jakie znaki są używane zamiast +i /w alfabet kodowania lub inne formatowanie lub obce znaki.

Jedną z takich możliwości byłoby zbadanie, jakie niestandardowe znaki znajdują się w Twoich danych, np

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
John Machin
źródło
Dane pochodzą ze standardowego zestawu znaków base64. Jestem prawie pewien, że problem polega na tym, że brakuje 1 lub więcej znaków - stąd błąd wypełnienia. Jeśli nie ma solidnego rozwiązania w Pythonie, pójdę z moim rozwiązaniem wywoływania openssl.
FunLovinCoder
1
„Rozwiązanie”, które po cichu ignoruje błędy, prawie nie zasługuje na określenie „solidne”. Jak wspomniałem wcześniej, różne sugestie Pythona były metodami DEBUGOWANIA, aby dowiedzieć się, na czym polega problem, przygotowując do rozwiązania ZASADNEGO ... czy nie jesteś tym zainteresowany?
John Machin
7
Moim wymaganiem NIE jest rozwiązanie problemu, dlaczego base64 jest uszkodzony - pochodzi ze źródła, nad którym nie mam kontroli. Moim wymaganiem jest podanie informacji o otrzymanych danych, nawet jeśli są uszkodzone. Jednym ze sposobów jest pobranie danych binarnych z uszkodzonego base64, abym mógł zebrać informacje z bazowego ASN.1. strumień. Zadałem pierwotne pytanie, ponieważ chciałem uzyskać odpowiedź na to pytanie, a nie odpowiedź na inne pytanie - na przykład jak debugować uszkodzony base64.
FunLovinCoder
Po prostu znormalizuj ciąg, usuń wszystko, co nie jest znakiem Base64. Gdziekolwiek, nie tylko na początku czy na końcu.
Martijn Pieters
24

Posługiwać się

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

Zasługa komentarza gdzieś tutaj.

>>> 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'

>>> 
warvariuc
źródło
4
Ma na
myśli
22

Jeśli wystąpi błąd wypełnienia, prawdopodobnie oznacza to, że ciąg jest uszkodzony; Ciągi zakodowane w base64 powinny mieć wielokrotność czterech długości. Możesz spróbować =samodzielnie dodać znak wypełniający ( ), aby ciąg był wielokrotnością czterech, ale powinien już mieć to, chyba że coś jest nie tak

Michał Mrozek
źródło
Bazowe dane binarne to ASN.1. Nawet z korupcją chcę wrócić do pliku binarnego, ponieważ nadal mogę uzyskać przydatne informacje ze strumienia ASN.1.
FunLovinCoder
nieprawda, jeśli chcesz zdekodować jwt do kontroli bezpieczeństwa, będziesz go potrzebować
DAG
4

Sprawdź dokumentację źródła danych, które próbujesz zdekodować. Czy to możliwe, że zamierzałeś użyć base64.urlsafe_b64decode(s)zamiast base64.b64decode(s)? To jeden z powodów, dla których mogłeś zobaczyć ten komunikat o błędzie.

Dekoduj ciągi przy użyciu alfabetu bezpiecznego dla adresów URL, który zastępuje - zamiast + i _ zamiast / w standardowym alfabecie Base64.

Dzieje się tak na przykład w przypadku różnych interfejsów API Google, takich jak Google Identity Toolkit i ładunki Gmaila.

Daniel F.
źródło
1
To wcale nie odpowiada na pytanie. Dodatkowo urlsafe_b64decodewymaga również wypełnienia.
rdb
Cóż, był problem, który miałem przed udzieleniem odpowiedzi na to pytanie, który był związany z Google Identity Toolkit. Otrzymałem nieprawidłowy błąd wypełnienia (wydaje mi się, że był na serwerze), nawet jeśli wypełnienie wydawało się prawidłowe. Okazało się, że musiałem skorzystać base64.urlsafe_b64decode.
Daniel F
Zgadzam się, że to nie odpowiada na pytanie, rdb, ale to było dokładnie to, co chciałem usłyszeć. Przeformułowałem odpowiedź na nieco ładniejszy ton, mam nadzieję, że to zadziała dla Ciebie, Danielu.
Henrik Heimbuerger
Całkiem w porządku. Nie zauważyłem, że zabrzmiało to trochę niemiło, pomyślałem tylko, że najszybszym rozwiązaniem będzie rozwiązanie problemu iz tego powodu powinno być pierwszą rzeczą, którą należy wypróbować. Dziękuję za zmianę, mile widziane.
Daniel F
Ta odpowiedź rozwiązała mój problem z dekodowaniem tokena dostępu Google pochodzącego z tokena JWT. Wszystkie inne próby zakończyły się „nieprawidłowym wypełnieniem”.
John Hanley
2

Dodanie wypełnienia jest raczej ... kłopotliwe. Oto funkcja, którą napisałem z pomocą komentarzy w tym wątku, a także strony wiki dla base64 (jest to zaskakująco pomocne) 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)
Bryan Lott
źródło
2

Możesz po prostu użyć, base64.urlsafe_b64decode(data)jeśli próbujesz zdekodować obraz internetowy. Automatycznie zajmie się wyściółką.

WINOROŚLI
źródło
to naprawdę pomaga!
Księżyc
1

Istnieją dwa sposoby poprawienia opisanych tutaj danych wejściowych lub, dokładniej i zgodnie z OP, aby metoda b64decode modułu Pythona base64 mogła przetwarzać dane wejściowe na coś bez wywoływania niezauważonego wyjątku:

  1. Dołącz == na końcu danych wejściowych i wywołaj base64.b64decode (...)
  2. Jeśli to powoduje wyjątek, to

    ja. Złap to przez try / z wyjątkiem,

    ii. (R?) Usuń dowolne = znaki z danych wejściowych (uwaga: może to nie być konieczne),

    iii. Dołącz A == do danych wejściowych (będą działać od A == do P ==),

    iv. Wywołaj base64.b64decode (...) z tymi A == - dołączonymi danymi wejściowymi

Wynik z punktu 1. lub punktu 2. powyżej da pożądany rezultat.

Ostrzeżenia

Nie gwarantuje to, że zdekodowany wynik będzie taki, jaki był pierwotnie zakodowany, ale (czasami?) Zapewni OP wystarczający do pracy z:

Nawet z korupcją chcę wrócić do pliku binarnego, ponieważ nadal mogę uzyskać przydatne informacje ze strumienia ASN.1 ”).

Zobacz co wiemy i Założenia poniżej.

TL; DR

Z kilku szybkich testów base64.b64decode (...)

  1. wygląda na to, że ignoruje znaki inne niż [A-Za-z0-9 + /]; co obejmuje ignorowanie = s, chyba że są to ostatnie znaki w przeanalizowanej grupie czterech, w którym to przypadku = s przerywa dekodowanie (a = b = c = d = daje taki sam wynik jak abc = i a = = b == c == daje taki sam wynik jak ab ==).

  2. Okazuje się również, że wszystkie dołączane znaki są ignorowane po punkcie, w którym base64.b64decode (...) kończy dekodowanie np. Od an = jako czwarty w grupie.

Jak zauważono w kilku komentarzach powyżej, na końcu danych wejściowych wymagane jest albo zero, albo jeden lub dwa = s wypełnienia, gdy wartość [liczba przeanalizowanych znaków do tego punktu modulo 4] wynosi 0 lub 3, lub 2, odpowiednio. Tak więc, począwszy od pozycji 3. i 4. powyżej, dołączenie dwóch lub więcej = s do danych wejściowych, poprawi wszelkie problemy [Nieprawidłowe wypełnienie] w tych przypadkach.

JEDNAK, dekodowanie nie może obsłużyć przypadku, w którym [całkowita liczba przeanalizowanych znaków modulo 4] wynosi 1, ponieważ wymaga co najmniej dwóch zakodowanych znaków, aby reprezentować pierwszy zdekodowany bajt w grupie trzech zdekodowanych bajtów. W un uszkodzony zakodowane dane wejściowe, to [N modulo 4] = 1 przypadek nie dzieje, ale jak PO stwierdził, że znaki mogą być niedostępne, może się zdarzyć tutaj. Dlatego samo dołączanie = s nie zawsze będzie działać i dlaczego dołączanie A == będzie działać, gdy dołączanie == nie. NB Użycie [A] jest prawie dowolne: dodaje tylko wyczyszczone (zerowe) bity do zdekodowanych, które mogą być poprawne lub nie, ale wtedy przedmiotem tutaj nie jest poprawność, ale uzupełnienie przez base64.b64decode (...) bez wyjątków .

To, co wiemy z PO, a zwłaszcza z kolejnych komentarzy, to

  • Podejrzewa się, że w danych wejściowych zakodowanych algorytmem Base64 brakuje danych (znaków)
  • Kodowanie Base64 wykorzystuje standardowe 64 wartości-miejsc oraz dopełnienie: AZ; az; 0-9; +; /; = jest dopełnieniem. Potwierdza to lub przynajmniej sugeruje fakt, że openssl enc ...działa.

Założenia

  • Dane wejściowe zawierają tylko 7-bitowe dane ASCII
  • Jedynym rodzajem uszkodzenia jest brak zakodowanych danych wejściowych
  • OP nie dba o zdekodowane dane wyjściowe w dowolnym momencie po tym, co odpowiada brakującym zakodowanym danym wejściowym

Github

Oto opakowanie umożliwiające wdrożenie tego rozwiązania:

https://github.com/drbitboy/missing_b64

Brian Carcich
źródło
1

Niepoprawny błąd dopełniania jest spowodowany czasami metadane są również obecne w zakodowanym ciągu Jeśli twój ciąg wygląda mniej więcej tak: 'data: image / png; base64, ... base 64 stuff ....', musisz usunąć pierwszy część przed odkodowaniem.

Powiedz, że masz ciąg znaków zakodowany w formacie base64, a następnie wypróbuj poniższy fragment.

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")
sam
źródło
0

Po prostu dodaj dodatkowe znaki, takie jak „=” lub inne, i ustaw je jako wielokrotność 4, zanim spróbujesz zdekodować docelową wartość ciągu. Coś jak;

if len(value) % 4 != 0: #check if multiple of 4
    while len(value) % 4 != 0:
        value = value + "="
    req_str = base64.b64decode(value)
else:
    req_str = base64.b64decode(value)
Syed Mauze Rehan
źródło
0

W przypadku, gdy ten błąd pochodzi z serwera internetowego: spróbuj zakodować adres URL swojej wartości postu. Wysyłałem POST przez "curl" i odkryłem, że nie koduję url mojej wartości base64, więc znaki takie jak "+" nie były zmieniane, więc logika dekodowania adresu URL serwera WWW automatycznie wykonywała dekodowanie url i + zamieniła na spacje.

„+” to prawidłowy znak base64 i być może jedyny znak, który zostaje zniekształcony przez nieoczekiwane dekodowanie adresu URL.

Curtis Yallop
źródło
0

W moim przypadku napotkałem ten błąd podczas analizowania wiadomości e-mail. Mam załącznik jako ciąg base64 i wyodrębniam go przez re.search. Ostatecznie na końcu pojawił się dziwny dodatkowy podciąg.

dHJhaWxlcgo8PCAvU2l6ZSAxNSAvUm9vdCAxIDAgUiAvSW5mbyAyIDAgUgovSUQgWyhcMDAyXDMz
MHtPcFwyNTZbezU/VzheXDM0MXFcMzExKShcMDAyXDMzMHtPcFwyNTZbezU/VzheXDM0MXFcMzEx
KV0KPj4Kc3RhcnR4cmVmCjY3MDEKJSVFT0YK

--_=ic0008m4wtZ4TqBFd+sXC8--

Kiedy usunąłem --_=ic0008m4wtZ4TqBFd+sXC8--i usunąłem ciąg, parsowanie zostało naprawione.

Dlatego radzę upewnić się, że dekodujesz poprawny ciąg base64.

Daniil Mashkin
źródło
0

Powinieneś użyć

base64.b64decode(b64_string, ' /')

Domyślnie ołtarze to '+/'.

Quoc
źródło
1
To nie działa w Pythonie 3.7. assert len ​​(altchars) == 2, repr (altchars)
Dat TT
0

Napotkałem również ten problem i nic nie działało. W końcu udało mi się znaleźć rozwiązanie, które działa na mnie. Spakowałem zawartość w base64 i stało się to 1 na milion rekordów ...

To jest wersja rozwiązania zaproponowana przez Simona Sapina.

W przypadku braku dopełnienia 3, usuwam ostatnie 3 znaki.

Zamiast „0gA1RD5L / 9AUGtH9MzAwAAA ==”

Otrzymujemy „0gA1RD5L / 9AUGtH9MzAwAA”

        missing_padding = len(data) % 4
        if missing_padding == 3:
            data = data[0:-3]
        elif missing_padding != 0:
            print ("Missing padding : " + str(missing_padding))
            data += '=' * (4 - missing_padding)
        data_decoded = base64.b64decode(data)   

Zgodnie z tą odpowiedzią Trailing As w base64 powodem jest zero. Ale nadal nie mam pojęcia, dlaczego koder to psuje ...

Mitzi
źródło