Base64-Dekodierung in Swift: Ein vollständiger Leitfaden
Irgendwo in Ihrer Pipeline laufen Ihre Daten verkleidet herum: ein Token, versteckt in einem HTTP-Header, ein Avatar, der sich in einem JSON-Feld verbirgt, eine .b64-Datei, die Sie sich vergangene Woche ansehen wollten, ein E-Mail-Anhang, der als Mauer aus Buchstaben ankam. Diese Verkleidungen in Swift ausziehen gehört zu den angenehmsten Jobs der Sprache: ein Framework, ein Initializer und ein Regelwerk, das so kurz ist, dass es auf einen Klebezettel passt.
Die Startseite dieser Seite deckt das Format selbst bereits ab (64 druckbare Zeichen, sechs Bits pro Zeichen, bis zu zwei =-Zeichen Padding in der letzten Gruppe), also erzählen wir diese Geschichte hier nicht noch einmal. Halten Sie nur zwei Fakten in der Tasche. Erstens ist base64 eine Art, Bytes als Text anzukleiden, kein Schloss. Zweitens läuft jede base64-Reise in Swift durch einen einzigen Typ, Data, und der Dekodierer lebt darauf als failable Initializer. Diese eine Tatsache prägt den Rest dieses Artikels, denn ein failable Initializer verändert, wie Sie jede Zeile, die darauf folgt, schreiben.
Ein Typ hat den ganzen Job in der Hand
Swift streut seine base64-Helfer nicht über ein Dutzend Module, und es lässt Sie auch nichts installieren. Der Dekodierer ist Data(base64Encoded:options:) in Foundation, und er gehört seit den frühen Tagen des Frameworks zur Plattform (Apple listet den Initializer ab iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 und visionOS 1.0; die Zeilenlängen-Optionen auf der Kodierungsseite reichen sogar bis iOS 7.0 zurück). Auf Linux und Windows liefert dieselbe Foundation mit der Open-Source-Toolchain mit, also verhält sich der folgende Code in einer iPhone-App, einem Server-Worker und einem Skript in Ihrem Terminal genauso.
Es gibt einen Geschwister-Initializer, Data(base64Encoded: Data, options:), für den Fall, dass Ihr base64 als rohe ASCII-Bytes ankommt statt als String. Beide nehmen ein options-Argument an, das standardmäßig [] ist. Und beide teilen einen Charakterzug, der wichtiger ist als jede Option: Sie sind failable.
import Foundation
let packed = "SGVsbG8sIFN3aWZ0IQ=="
if let data = Data(base64Encoded: packed) {
let text = String(data: data, encoding: .utf8)
print(text ?? "not text after all")
} else {
print("that was not base64")
}
// Hello, Swift!
Apples Dokumentation zum Initializer ist wunderbar direkt: Er "gibt nil zurück, wenn die Eingabe nicht als gültiges Base-64 erkannt wird". Keine Ausnahmen, keine geworfenen Fehler, keine Log-Flut. Nur ein leises nil und die Verantwortung, zu entscheiden, was das für Ihren Benutzer bedeutet. Wenn Sie eine Sache über base64 in Swift behalten sollen, dann diese: Der Dekodierer stürzt nie ab und beschwert sich nie. Er lehnt einfach ab.
Das Urteil des Dekodierers: Eine Tabelle aus Ja und Nein
Was also bedeutet "gültig" für diesen Dekodierer? Es ist eine kurze Liste harter Regeln, und genau diese Liste macht den Unterschied zwischen "funktioniert in der Demo" und "überlebt die Produktion". Jede Zeile der folgenden Tabelle ist echtes Verhalten des Initializers auf einer aktuellen Toolchain, Sie können sie also direkt in Ihre Fehlermeldungen zitieren:
| Eingabe | Urteil | Warum |
|---|---|---|
TWFu |
Man |
eine volle Vierergruppe braucht gar kein Padding |
TQ== |
M |
ein Byte plus zwei Pads, der Lehrbuchfall |
SGVsbG8h |
Hello! |
acht Zeichen sind ein Vielfaches von vier, also braucht es keine Pads |
==== |
leeres Data |
Padding ohne etwas dahinter ist legal und dekodiert zu null Bytes |
| der leere String | leeres Data |
nichts rein, nichts raus, und der Initializer hat trotzdem Erfolg |
TQ |
nil |
Länge zwei: eine Vierergruppe wurde versprochen, aber nie geliefert |
T |
nil |
ein Zeichen trägt sechs Bits, und ein Byte braucht acht |
SGVsbG8hTQ |
nil |
zehn Zeichen: die letzte Gruppe baumelt ohne ihre Pads |
TQ=== |
nil |
drei Pads: Das dritte hat nichts mehr, was es auffüllen könnte |
TQ==TQ |
nil |
Daten nach dem Padding sind ein hartes Nein |
SGVs bG8h |
nil |
ein einzelnes Leerzeichen liegt außerhalb des Alphabets, und der strenge Modus zeigt kein Mitleid |
SGVsbG8h plus ein Zeilenumbruch am Ende |
nil |
der Zeilenumbruch am Ende einer Datei, die Sie gerade gelesen haben, zählt als Rauschen |
Drei Zeilen verdienen einen zweiten Blick. Die ====-Zeile bedeutet, dass eine if let-Prüfung besteht und Ihr Code mit null Bytes weitersegelt - wenn also ein leerer Payload in Ihrer App kein gültiger Zustand ist, prüfen Sie die Anzahl direkt nach dem Dekodieren. Die Zeile mit dem leeren String ist derselbe Trick mit weniger Make-up. Und die Zeile mit dem Zeilenumbruch am Ende ist der mit Abstand häufigste Grund, warum eine base64-Datei, die am Morgen noch perfekt kodiert wurde, am Nachmittag das Dekodieren verweigert: Irgendwo dazwischen wurde ein Zeilenende eingefügt, und der strenge Dekodierer nimmt es persönlich.
Es gibt auch eine berühmte Schwachstelle, die die Tabelle nicht zeigen kann. Vergleichen Sie TQ== und TS==: Beide dekodieren zu demselben Byte, M, denn die zwei niederwertigsten Bits dieses letzten Zeichens werden verworfen, bevor sie überhaupt inspiziert werden. Zeigen Sie stattdessen Tg== darauf, und Sie erhalten N ohne Widerstand. Der Dekodierer überwacht die Zeichen und lässt die Schlussbits gewähren. Diese Nachsicht ist kein Bug, aber sie bedeutet, dass zwei verschiedene Strings dieselben Daten meinen können, und das beginnt zu zählen, sobald Ihr System base64-Werte vergleicht, entdupliziert oder zwischenspeichert (mehr dazu im Sicherheitsabschnitt).
Wenn die Eingabe mehr Rauschen hat, als Sie denken
Base64 aus der realen Welt kommt selten als eine makellose Zeile an. E-Mail-Anhänge werden bei 76 Zeichen umgebrochen, mit Wagenrücklauf und Zeilenvorschub nach jeder Zeile - eine Gewohnheit, die von der MIME-Spezifikation von 1996 geerbt wurde - und Zertifikatsdateien werden bei 64 Zeichen umgebrochen. Der Dekodierer hat genau eine Option, um mit diesem Rauschen umzugehen, und sie ist eine große:
import Foundation
let mimeBody = "SGVs\r\nbG8sIG1h\naWwgbm9pc2Uu"
if let data = Data(base64Encoded: mimeBody, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Hello, mail noise.
.ignoreUnknownCharacters ist als Dekodierer dokumentiert, der "unbekannte Bytes außerhalb von Base-64 ignoriert, einschließlich Zeilenumbruchzeichen", und für diesen Job ist es das richtige Werkzeug: Das Rauschen wird gelöscht, das Alphabet überlebt, und der Payload kommt unversehrt heraus. Aber die Option hat einen blinden Fleck, und es ist der, der Swift-Entwickler am meisten beißt: Er löscht jedes Zeichen außerhalb des Alphabets, einschließlich - und _ von base64url. Er übersetzt sie nicht in + und /, er wirft sie einfach weg. Je nachdem, was diese Löschung übrig lässt, erhalten Sie ein nil (wenn die Überlebenden keine vollen Gruppen mehr bilden) oder, noch schlimmer, eine selbstbewusste Antwort mit der falschen Byteanzahl. Ein 16 Zeichen langer base64url-String, der 12 Bytes kodiert, kann vom laxen Dekodierer als 9 andere Bytes zurückkommen, ohne Fehler und ohne Entschuldigung.
Die Regel, die Sie sich merken sollten: .ignoreUnknownCharacters ist für Transportrauschen (Zeilenumbrüche, verirrte Leerzeichen vom Kopieren) gedacht, niemals für Alphabet-Unterschiede. Wenn der Payload base64url sein könnte, wandeln Sie die Zeichen zuerst selbst um, genau wie der nächste Abschnitt es zeigt, und übergeben Sie dem Dekodierer einen sauberen Standard-String.
Das URL-Alphabet
Abschnitt 5 von RFC 4648 definiert den Cousin des Standardalphabets, das Sie bisher begleitet hat: base64url, in dem + zu - wird, / zu _ und das =-Padding meist weggelassen wird. Der Grund ist derselbe, der Ihre URLs ehrlich hält: In einem Query-String wird ein + beim Form-Parsing als Leerzeichen gelesen, ein / ist ein Pfadtrenner, und ein = trennt Schlüssel von Werten. Der RFC ist direkt in der Beziehung zwischen den beiden: Die URL-Variante "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden". JWTs, Web-Push-Nachrichten, YouTube-Video-IDs und die meisten modernen API-Kennungen sprechen base64url, also rechnen Sie damit, es gleich am ersten Tag zu treffen.
Auf der Dekodierungsseite hat das Rezept zwei Züge: das Alphabet übersetzen und dann das Padding auffüllen, denn der strenge Dekodierer will weiterhin sein Vielfaches von vier.
import Foundation
extension String {
func dataFromBase64URL() -> Data? {
var fixed = self
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
return Data(base64Encoded: fixed)
}
}
let tokenPart = "0S__zMWaTC-iVgJ-"
if let bytes = tokenPart.dataFromBase64URL() {
print(bytes.count) // 12
}
Die Modulo-Zeile ist der ganze Trick: base64url-Payloads kommen typischerweise ohne Padding an, und ein oder zwei =-Zeichen (niemals drei) stellen die Vierergruppe wieder her, die der Dekodierer erwartet. Sie werden eine Version dieser fünfzeiligen Extension in überraschend vielen Swift-Codebasen finden, und zwar aus gutem Grund. Es gibt einen Grund, warum sie in der Zukunft kürzer wird: Die neuesten Apple-SDKs (26.4 und höher, Stand jetzt) haben eine native .base64URLAlphabet-Option für den Kodierer bekommen, während die passenden Dekodierungs-Optionen noch in der Open-Source-Foundation hinter einem Verfügbarkeitsmarker für eine spätere Toolchain heranwachsen. Bis diese Ihr Mindest-Deployment-Ziel erreicht, ist die Extension die portable Antwort, und sie wird nach Konstruktion auf jeder Version weiter funktionieren.
Zuerst Bytes, dann Worte
Hier ist die Entscheidung, die der Dekodierer nicht für Sie treffen kann: Er gibt Ihnen ein Data, einen Sack Bytes ohne jede Ahnung, in welchem Zeichensatz der ursprüngliche Payload geschrieben wurde. Wenn der Payload Text war, ist es Ihre Aufgabe, diesen Zeichensatz zu wählen, und Swift gibt Ihnen zwei Türen aus der Byte-Welt mit sehr unterschiedlichen Temperamenten.
String(data:encoding:)ist die strenge Tür. Sie liefert ein Optional zurück und antwortet mitnil, wenn die Bytes in der Kodierung, die Sie genannt haben, nicht gültig sind. Ideal für Validierung, gefährlich, wenn Sie die Antwort per Force-unwrap öffnen.String(decoding:as:)ist die nie-weigernde Tür. Sie liefert immer einen String zurück und tauscht alles, was sie nicht sinnvoll deuten kann, gegen das U+FFFD-Ersatzzeichen aus. Ideal für Logging und Vorschauen, gefährlich, wenn Sie das Ergebnis speichern und es Daten nennen.
import Foundation
let bytes = Data([0xC3, 0xA5]) // die UTF-8-Schreibweise des Buchstabens a mit Ring
print(String(data: bytes, encoding: .utf8) ?? "?") // a mit Ring, richtig gelesen
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // zwei durcheinandergeratene Buchstaben, gleiche Bytes
print(String(decoding: bytes, as: UTF8.self)) // a mit Ring, und es stürzt nie ab
Das Rezept, das fast alles abdeckt: Versuchen Sie zuerst striktes UTF-8, denn das meinen moderne APIs fast immer; fallen Sie auf ISO Latin-1 zurück, nur wenn der Vertrag schweigt und Sie lieber etwas Lesbares-wenn-auch-Falsches als Stille hätten; behalten Sie die nie-weigernde Tür für Debug-Ausgaben auf. Und ein unsichtbarer Eindringling, nach dem Sie Ausschau halten sollten: Wenn der Payload mit einem UTF-8-BOM beginnt (die drei Bytes EF BB BF), behält die strenge Umwandlung ihn, und Ihr String beginnt jetzt mit einem unsichtbaren U+FEFF-Zeichen, das still und leise Gleichheitsprüfungen und JSON-Rundreisen kaputt macht. Entfernen Sie ihn mit einer Präfix-Prüfung, wenn die Spezifikation keinen verspricht.
Dateien öffnen
Der Job "es gibt eine .b64-Datei, gib mir, was sie versteckt" ist ein Lesen, ein Trimmen, ein Dekodieren und ein Schreiben. Das Trimmen ist keine Dekoration; es ist der Unterschied zwischen einer Datei, die sich öffnet, und einer, die nil zurückgibt, denn Tools, Mail-Clients und Editoren lieben es alle, am Ende einen Zeilenumbruch liegen zu lassen:
import Foundation
let inbox = URL(fileURLWithPath: "Downloads/avatar.b64")
let outbox = URL(fileURLWithPath: "Downloads/avatar.png")
let raw = try String(contentsOf: inbox, encoding: .utf8)
if let data = Data(base64Encoded:
raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
try data.write(to: outbox)
} else {
print("the file was not base64 after all")
}
Wenn die Datei MIME-verpackt ist (Zeilenumbrüche alle 76 Zeichen), haben Sie zwei saubere Auswege: Dekodieren Sie mit .ignoreUnknownCharacters und lassen Sie die Option die Zeilenenden fressen, oder entfernen Sie sie selbst mit replacingOccurrences vor einem strengen Dekodieren. Beide brauchen jeweils eine Zeile. Für Dateien, die einfach groß sind, dekodieren Sie in ausgerichteten Gruppen statt alles auf einmal zu lesen: Jede Gruppe von vier Zeichen dekodiert für sich allein, also können Sie über Lese-Grenzen hinweg nur die aktuelle Gruppe plus einen kleinen Rest mitnehmen.
import Foundation
func decodeBase64Chunks(_ stream: InputStream, into result: inout Data) throws {
let chunkSize = 65_536
var buffer = [UInt8](repeating: 0, count: chunkSize)
var leftover = ""
result = Data()
stream.open()
defer { stream.close() }
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: chunkSize)
if read < 0 { throw CocoaError(.fileReadUnknown) }
if read == 0 { break }
var text = String(decoding: buffer[0..<read], as: UTF8.self)
text = text.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
text = leftover + text
if text.count % 4 != 0 {
let whole = text.count - (text.count % 4)
leftover = String(text.suffix(text.count - whole))
text = String(text.prefix(whole))
} else {
leftover = ""
}
guard !text.isEmpty else { continue }
guard let part = Data(base64Encoded: text) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
if !leftover.isEmpty {
guard let part = Data(base64Encoded: leftover) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
}
Der Speicher bleibt flach, egal wie groß die Datei ist: ein Lese-Puffer, ein übrig gebliebenes Fragment und das Ergebnis, das Sie aufbauen. Derselbe Loop verarbeitet einen Download, der als base64 über die Leitung ankommt, eine Log-Datei, die in Wahrheit ein kodierter Stream ist, oder jeden Payload, der zu groß ist, um ihn in der Hand zu halten.
JWTs: Die drei Punkte lesen
Ein kompaktes JSON Web Token besteht aus drei base64url-Teilen, verbunden durch Punkte, und die ersten beiden dieser Teile sind nacktes JSON in einem Trenchcoat. Sie kommen ohne Padding, genau die Kombination, die der strenge Dekodierer auf den ersten Blick verwirft, also erledigt Ihr dataFromBase64URL()-Helfer aus dem URL-Abschnitt die ganze Schwerstarbeit:
import Foundation
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
func openPart(_ part: String) -> String? {
var fixed = part
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
guard let data = Data(base64Encoded: fixed) else { return nil }
return String(data: data, encoding: .utf8)
}
let pieces = token.split(separator: ".")
print(openPart(String(pieces[0])) ?? "?")
// {"alg":"HS256","typ":"JWT"}
print(openPart(String(pieces[1])) ?? "?")
// {"sub":"1234567890","name":"John Doe"}
Zwei Erinnerungen begleiten Sie. Ein JWT ist signiert, nicht verschlüsselt: Header und Payload sind öffentliche Informationen, genau deshalb gehört ein Passwort niemals in eines davon (der verschlüsselte Cousin, JWE, ist eine ganz andere Spezifikation). Und der dritte, durch Punkte getrennte Teil ist eine kryptografische Signatur, kein Dokument, also dekodieren Sie die Teile eins und zwei und lassen Sie den Rest in Ruhe.
Data URIs: Die Datei hinter dem Komma
Web-APIs lieben es, Binäres im Text mit dem data:-Schema zu verstecken: ein PNG in einem Profelfeld, eine Schriftart in einem CSS-Blob, ein QR-Code in einer Einstellungsdatei. Das Format ist data:{mime};base64,{payload}, und der Payload lässt sich mit einem einzigen Split abziehen:
import Foundation
let uri = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
let payload = uri.components(separatedBy: ",").last ?? ""
if let bytes = Data(base64Encoded: payload) {
print(String(decoding: bytes.prefix(6), as: UTF8.self)) // GIF89a
print(bytes.count) // 42
} else {
print("not a base64 data uri")
}
Das Beispiel verwendet das berühmte transparente GIF mit 42 Bytes, das kleinste Bild in dem Format, deshalb tauchen seine Anfangszeichen in mehr Codebasen auf als fast jeder andere base64-String im Internet. Auf Apple-Plattformen endet die Pipeline mit einem One-Liner: Das gleiche Data, das Sie gerade dekodiert haben, füttert direkt UIImage(data:) oder NSImage(data:), deshalb ist "einen Avatar aus einer API anzeigen" eine kleine Funktion und kein Projekt.
HTTP: Der Basic-Header und seine Freunde
Der alte Authorization: Basic-Header ist ein Benutzername und ein Passwort, verbunden durch einen Doppelpunkt, für die Reise mit Standard-base64 verpackt (nicht der URL-Dialekt: Der lebt in einem Header, in dem + und / völlig harmlos sind). Das Auspacken ist ein Split und ein Dekodieren:
import Foundation
let header = "Basic ZWRpdG9yOnMzY3JldA=="
let packed = header.replacingOccurrences(of: "Basic ", with: "")
if let creds = Data(base64Encoded: packed) {
print(String(data: creds, encoding: .utf8) ?? "") // editor:s3cret
} else {
print("malformed header")
}
Halten Sie die Sicherheits-Fußnote laut, denn sie gilt für jedes base64, das Sie je treffen werden: Das hier ist Verpacken, kein Schutz. Basic-Auth ist nur über HTTPS akzeptabel, wo TLS die eigentliche Bewachung übernimmt und base64 die Bytes nur davon abhält, die Header-Grammatik zu brechen. Dasselbe Prinzip erklärt Authorization: Bearer-Tokens: Das Token selbst ist ein JWT, also gilt das Dekodier-Rezept aus dem JWT-Abschnitt für es unverändert.
E-Mail: Die 76-Zeichen-Gewohnheit
Ein als base64 kodierter E-Mail-Anhang wird bei 76 Zeichen mit CRLF-Zeilenenden umgebrochen - genau das Rauschen, für das die laxere Option existiert. Die rohen MIME-Header verraten Ihnen, welches Alphabet und welcher Umbruch der Sender verwendet hat (Content-Transfer-Encoding: base64), und die Lösung ist ein Flag:
import Foundation
let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Dieser Anhang hat die 76-Zeichen-Gewohnheit überlebt.
Wenn Sie eine Mail-Funktion schreiben, nicht eine lesen, denken Sie daran, dass der 76-Zeichen-Umbruch Sie auch etwas kostet: Mit einem Zeilenumbruch alle 76 Zeichen landet der kodierte Text nahe bei 137 Prozent der ursprünglichen Größe, deshalb haben alte Mail-Engineer Anhanggrößen mit der Kurzformel geschätzt: "das Original mit 1,37 multiplizieren, etwa 800 Bytes Header dazurechnen". Die Zahl ist heute Folklore, aber die Arithmetik ist immer noch dieselbe.
Der doppelt verpackte Payload
Das häufigste "Meine Daten sind kaputt"-Ticket in base64-Land ist Daten, die zweimal verpackt wurden: Eine Integrations-Ebene hat sie kodiert, und eine zweite, die nie die Dokumentation gelesen hat, hat das Ergebnis kodiert. Der defensive Zug ist, einmal zu dekodieren, sich anzuschauen, was Sie bekommen haben, und wenn das Ergebnis selbst ein sauberer, base64-ähnlicher String ist (richtige Länge, richtiges Alphabet, nichts Überraschendes), es noch einmal bewusst zu dekodieren - und dann zu stoppen. Schreiben Sie keinen Loop, der dekodiert, bis er scheitert. Ein solcher Loop frisst fröhlich eine völlig intakte Datei, deren Inhalt zufällig base64-ähnlich aussieht, und danach kann niemand sagen, wo die ursprünglichen Daten anfingen.
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // sieht bereits verpackt aus
if let first = unwrapOnce(suspicious) {
let inner = String(data: first, encoding: .utf8) ?? ""
if let second = unwrapOnce(inner) {
print("it was wrapped twice:", String(data: second, encoding: .utf8) ?? "?")
}
}
// it was wrapped twice: abc
Zwei Auspackungen, zwei bewusste Entscheidungen und ein Payload, der endlich wieder einfach nur abc ist.
Das Nil etwas bedeuten lassen
Weil der Dekodierer mit nil antwortet statt zu werfen, ist der Fehlerbehandlungs-Stil Ihres base64-Codes eine Wahl, die Sie treffen, und die Wahl, über die Sie sich später freuen werden, ist ein kleiner Wrapper, der die stille Verweigerung in einen lauten, spezifischen Fehler verwandelt:
import Foundation
enum Base64Failure: Error, CustomStringConvertible {
case notBase64(Int)
var description: String {
switch self {
case .notBase64(let length):
return "input of \(length) characters is not valid base64"
}
}
}
func decodeStrict(_ text: String) throws -> Data {
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = Data(base64Encoded: cleaned) else {
throw Base64Failure.notBase64(cleaned.count)
}
return data
}
do {
let bytes = try decodeStrict("c3ludGF4IGVycm")
print(String(data: bytes, encoding: .utf8) ?? "?")
} catch {
print(error) // input of 14 characters is not valid base64
}
Der Wrapper wird auch der einzige Ort, an dem die Normalisierung wohnt: das Trimmen, eine eventuelle Alphabet-Übersetzung, ein eventuelles Padding-Auffüllen. Aufrufer erhalten eine Funktion, eine Bedeutung für Fehler, und kein ! ist in Sicht. Force-unwrap von Data(base64Encoded:)! ist der Weg, wie aus einem schlechten Payload eine abgestürzte App wird, und der Wrapper ist die billige Versicherung dagegen. Dasselbe Muster funktioniert an der Kommandozeile, wo ein Skript mit CommandLine.arguments und einem FileHandle-Schreiben "diese Datei aus der Shell dekodieren" zu einem Fünfzeilen-Werkzeug macht statt zu einem Copy-Paste-Schlenker über eine Website.
Sicherheit, gemessen in Bytes
- Es ist keine Verschlüsselung. Base64 ist eine umkehrbare, sofort lesbare Umverpackung. Wenn Ihr Threat-Modell einen Menschen mit Browser und fünf Sekunden umfasst, haben Sie null Schutz, und jeder JWT-Header beweist diesen Punkt täglich.
- Kanonicalisieren Sie vor dem Vergleich. Weil
TQ==undTS==zu denselben Bytes dekodieren, können zwei Systeme verschiedene Schreibweisen derselben Daten halten. Eine Paper von 2022, "Base64-Malleabilität in der Praxis", dokumentierte, was diese gebrochene Einzigkeitsgarantie in der Wildnis anrichtet: Log-Mismatches, Denial-of-Service-Attacken und doppelte Datenbank-Einträge. Wenn Ihre Swift-App base64-Werte zwischenspeichert, entdupliziert oder vergleicht, führen Sie an der Tür eine kanonische Dekodierung (oder eine kanonische Re-Kodierung) durch. - Limitieren Sie die Eingabe vor dem Dekodieren. Das Dekodieren von N Zeichen allokiert grob drei Viertel von N Bytes, während Sie den Eingabe-String noch in der Hand halten. Ein feindlicher Client kann 100 Megabyte des Buchstabens
Asenden und zusehen, wie Ihr Speicher klettert, bevor der Dekodierer jemals nein sagt. Prüfen Sie zuerst die Länge, günstig, und verwerfen Sie, was zu groß ist. - Passen Sie auf die laxere Option als Filter auf.
.ignoreUnknownCharacterslöscht Zeichen. Ein "Reinigungs"-Durchlauf darüber kann einen gültigen base64url-Payload zu anderen Daten machen, ohne Fehler. Es ist ein Rauschen-Filter für Zeilenumbrüche, kein Validator. - Halten Sie es aus URLs heraus, wo Sie können. Große base64-Payloads in Query-Strings oder Pfaden sprengen bequeme URL-Längen und werden von Proxys zerfleddert. Legen Sie sie stattdessen in Request-Bodies, Dateien oder Tokens.
Performance, kurz gesagt
Der Dekodierer ist ein Abtasten von Nachschlagetabellen: Jedes Zeichen wird in eine kleine Tabelle indexiert, und einige Bits werden in die Ausgabe-Bytes verschoben und oder-verknüpft. Auf einer aktuellen Toolchain ist das schnell genug für alles, was in den Speicher passt, und die Zahl, die Sie sich merken sollten, ist das Ausgabe-Verhältnis: Dekodierte Bytes sind etwa drei Viertel der Eingabelänge, also kostet Sie ein 4-Megabyte-String rund 3 Megabyte Ergebnis zusätzlich zum String, den Sie bereits in der Hand halten. Wenn Sie auf einem Pfad sind, auf dem Foundation selbst nicht erlaubt ist (ein tief eingebettetes Ziel, ein WebAssembly-Bundle), ist das Community-Paket swift-extras-base64 die bemerkenswerte Alternative: Reines Swift ohne Foundation-Abhängigkeit, ein RFC-4648-konformer Kodierer und Dekodierer mit base64url- und Padding-Optionen, und Benchmarks, die es mehrere Male schneller als Foundation zeigen. Eine frühere Implementierung desselben Pakets ist sogar in der WebSocket-Unterstützung von swift-nio eingebaut, was so nah an Produktionsqualität kommt, wie ein Nebenprojekt nur kann. Für eine gewöhnliche App oder ein Skript ist es unnötiges Gepäck; für die eingeschränkte Ecke von Swift ist es die Standardantwort.
Ein Jahrzehnt des Auspackens
Swift hat nichts davon erfunden, und es lohnt sich zu wissen, woher jedes Stück der Werkzeugkiste kommt:
- 1980er, das Ära der gleichen Maschine. Die frühesten Kodierungen dieser Familie (uuencode auf UNIX, BinHex auf dem TRS-80 und dem klassischen Mac) bewegten Dateien zwischen Maschinen, die annahmen, das andere Ende sei wie ihr eigenes. uuencode verwendete ein Alphabet aus Großbuchstaben, Ziffern und Satzzeichen, und seine Buchstaben liegen an aufeinanderfolgenden ASCII-Positionen, also bestand das Kodieren schlicht darin, 32 hinzuzuzählen, ohne jegliche Nachschlagetabelle. Dekodierer dieser Ära konnten sehr viel voraussetzen, und im Moment, in dem Daten Ökosysteme überquerten, stürzten sie um.
- 1987, das Alphabet bekommt eine Adresse. RFC 989 (Privacy-Enhanced Mail, Februar 1987) standardisierte das 64-Zeichen-Alphabet, brach Zeilen bei genau 64 Zeichen um und verwendete
=für Padding und*, um kodiert-aber-unverschlüsselte Daten zu markieren. Jeder PEM-artige Block ist ein Nachkomme dieses Dokuments. - 1996, das liberale Ära. MIME (RFC 2045) nahm das Alphabet für E-Mails, verlegte den Umbruch auf 76 Zeichen und wies konforme Dekodierer an, jedes Zeichen außerhalb des Alphabets zu ignorieren, wie die CRLF-Zeilenumbrüche. Das ist das Ära, das eine Generation darauf trainiert hat, nachsichtige Dekodierer zu erwarten, und das Ära, dessen Erwartungen Swifts strenges Standardverhalten bewusst bricht.
- 2003 bis 2006, die Regeln härten aus. RFC 3548 (2003) machte den ersten Versuch, die Familie zu vereinheitlichen; RFC 4648 (Oktober 2006) entschied es, kodifizierte die Padding-Regeln und fügte das URL-sichere Alphabet hinzu. Sein Dekodierer-Abschnitt ist der, dem Swift folgt: Zeichen außerhalb des Alphabets verwerfen, es sei denn, das Format, dem Sie dienen, sagt explizit, sie zu ignorieren, wie MIME es tut.
- 2013 bis 2014, die API wartet im Nebenraum. Apples
NSData-Klasse hatte seit Jahren base64 verpackt und ausgepackt, und die optionsbasierte API mit ihrer Dekodierungs-Option landete in iOS 7, im Jahr 2013, ein Jahr, bevor Swift existierte. Als Swift 1.0 am 9. September 2014 erschien, kam der Dekodierer mit der Sprache ein und hat seitdem dieselbe Persönlichkeit behalten: strenger Kern, ein laxer Drehknopf, failable Initializer. - 3. Dezember 2015, Linux bekommt einen Dekodierer. Swift wurde an diesem Tag open-sourced, und mit ihm überquerte das base64 von Foundation nach Linux und später nach Windows. "Base64-Dekodierung in Swift auf einer nicht-Apple-Maschine" ist knappe zehn Jahre alt: ein verspäteter Gast auf einer Party, die 1987 begann.
- 2023 bis 2026, der Rewrite. Der Foundation-Rewrite (das swift-foundation-Projekt) bewegte
Datain einen reinen Swift-Kern, und 2025 fügte ein Community-Pitch native base64url- und Padding-Weglass-Optionen hinzu. Stand jetzt liefern die neuesten SDK-Betas und die Open-Source-Toolchain die Kodierungs-Optionen, während die Dekodierungs-Optionen in der Open-Source-Toolchain noch heranwachsen, also bleiben die handgeschriebenen Extensions bis dahin die universelle Antwort.
Kleine Wunder
====ist legale Eingabe. Vier Pads und keine Daten dekodieren zu einem leerenData, dem einzigen base64-String, dessen gesamter Inhalt "hier ist nichts" ist, und Swift stimmt dem zu.- Die Zeichen-Polizei des Dekodierers kontrolliert nicht die Arbeit der Bits-Polizei:
TS==undTQ==geben Ihnen beideM, währendTg==IhnenNgibt. Gleiche Grammatik, andere Bits, keine Fragen. - Swifts
Datakann base64 dekodieren, das als Bytes ankommt statt als String, über dieData(base64Encoded: Data)-Variante, also kann ein Payload, der als ASCII über die Leitung ging, die String-Rundreise komplett überspringen. - Das Wort, das base64 ist, seit die Testvektoren geboren wurden, ist
foobar, und es packt zuZm9vYmFy. Wenn Sie je ein base64-Beispiel in der Wildnis gesehen haben, ist es ziemlich wahrscheinlich, dass foobar beteiligt war. - Das berühmte 1x1-transparente GIF ist exakt 42 Bytes groß und beginnt mit dem magischen Wort
GIF89a, deshalb tauchen seine ersten acht kodierten Zeichen in mehr Codebasen auf als fast jedes andere base64-Präfix auf der Erde. - Der moderne Open-Source-Dekodierer macht seine Prüfung auf ungültige Zeichen mit einem einzigen Vergleich: Er oder-verknüpft vier Nachschlagewerte pro Position und testet das Ergebnis gegen einen Sentinel, so dass eine Verzweigung das Schicksal einer ganzen Vierergruppe entscheidet. Die ältere Implementierung erledigte dieselbe Arbeit mit einer 128-Byte-Tabelle, in der jeder Wert ab 0x80 "kein Buchstabe" bedeutete.
- UTF-8-BOMs sind unsichtbar:
EF BB BFam Anfang eines Payloads wird zu einem U+FEFF-Zeichen, das die strenge Umwandlung überlebt und dann ein paar Zeilen Code später die Gleichheitsprüfung kaputt macht. - Swift ist 27 Jahre jünger als das Alphabet, das es dekodiert. Die Sprache erschien 2014; die 64 Buchstaben, die sie bearbeitet, wurden 1987 standardisiert und haben sich seitdem nicht verändert.
Das ist die gesamte Dekodierungs-Werkzeugkiste: ein failable Initializer mit einem kurzen Regelwerk, ein laxer Drehknopf mit einem dokumentierten blinden Fleck, ein base64url-Helfer in fünf Zeilen, eine Zeichensatz-Entscheidung, die Ihnen gehört, eine gestückelte Schleife für die großen Dateien und ein Wrapper, der dem nil etwas bedeuten lässt. Dekodieren ist der Ort, an dem base64 beißt, und Sie kennen jetzt den Namen jedes Zahns. Wenn der Job sich umdreht und Sie anfangen, Bytes für die Reise zu verpacken statt sie auszupacken, übernimmt der Zuschlag von rund 33 Prozent und die Umbruch-Optionen tauchen auf. Der verwandte Kodierungs-Artikel deckt die andere Hälfte der Rundreise vollständig ab, also schlagen Sie dort nach, wenn Sie bereit sind, in die andere Richtung zu liefern.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Swift: Ein vollständiger Leitfaden