Haben Sie mit dem Base64-Format zu tun? Dann ist diese Website genau das Richtige für Sie! Nutzen Sie unser superpraktisches Online-Tool, um Ihre Daten zu kodieren oder zu dekodieren.

Base64-Dekodierung in Swift: Ein vollständiger Leitfaden

Irgendwo in Ihrer Pipeline tragen die Daten eine Verkleidung: ein Token, das sich in einen HTTP-Header geschmuggelt hat, ein Avatar, der sich in einem JSON-Feld versteckt, eine .b64-Datei, die Sie sich schon letzte Woche vorgenommen haben, und ein E-Mail-Anhang, der als Buchstabenwand ankommt. Solche Verkleidungen in Swift auszuziehen ist einer der angenehmsten Jobs in der Sprache: ein Framework, ein Initializer und ein Regelwerk, das kurz genug ist, um auf einen Notizzettel zu passen.

Die Startseite dieser Website hat das Format selbst bereits erklärt (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. Stecken Sie sich nur zwei Fakten in die Tasche. Erstens: Base64 ist eine Art, Bytes als Text zu verkleiden, kein Schloss. Zweitens: Jede Base64-Reise in Swift läuft über genau einen Typ, Data, und der Dekodierer sitzt darauf als failable Initializer. Diese eine Tatsache prägt den Rest dieses Artikels, denn ein failable Initializer verändert, wie Sie jede Zeile danach schreiben.

Ein Typ hat den ganzen Job in der Hand

Swift streut keine Base64-Helfer über ein Dutzend Module, und Sie müssen 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 wird dasselbe Foundation mit der Open-Source-Toolchain ausgeliefert, daher verhält sich der Code unten 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 auf [] steht. 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 wunderschön direkt: Er "gibt nil zurück, wenn die Eingabe nicht als gültiges Base-64 erkannt wird". Keine Exceptions, keine geworfenen Fehler, kein Log-Spam. Nur ein stilles nil und die Verantwortung, zu entscheiden, was das für Ihren User bedeutet. Wenn Sie sich eine Sache über Base64 in Swift merken, 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 bedeutet also "gültig" für diesen Dekodierer? Es ist eine kurze Liste harter Regeln, und genau diese Liste ist der Unterschied zwischen "läuft in der Demo" und "überlebt in Produktion". Jede Zeile der folgenden Tabelle ist reales 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 werden keine Pads benötigt
==== leeres Data Padding ohne etwas dahinter ist erlaubt und dekodiert zu null Bytes
der leere String leeres Data nichts rein, nichts raus, und der Optional ist trotzdem erfolgreich
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 klares Nein
SGVs bG8h nil ein einzelnes Leerzeichen ist außerhalb des Alphabets, und der strenge Modus zeigt keine Gnade
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, also: Wenn 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 Schminke. Und die Zeile mit dem Zeilenumbruch am Ende ist der mit Abstand häufigste Grund dafür, dass eine Base64-Datei, die am Morgen noch perfekt kodiert war, am Nachmittag das Dekodieren verweigert: Irgendwo auf dem Weg wurde ein Zeilenende angehängt, und der strenge Dekodierer nimmt das persönlich.

Außerdem gibt es eine berühmte Schwachstelle, die die Tabelle nicht zeigen kann. Vergleichen Sie TQ== und TS==: Beide dekodieren zum selben Byte, M, weil die beiden niedrigsten Bits dieses letzten Zeichens verworfen werden, bevor sie überhaupt inspiziert werden. Setzen Sie stattdessen Tg== an, und Sie bekommen N, ohne Widerstand. Der Dekodierer kontrolliert die Zeichen und lässt die am Ende übrig gebliebenen Bits ziehen. Diese Nachsicht ist kein Bug, aber es bedeutet, dass zwei verschiedene Strings dieselben Daten beschreiben können, und es fängt an zu zählen, in dem Moment, in dem Ihr System Base64-Werte vergleicht, entdupliziert oder cacht (mehr dazu im Sicherheitsabschnitt).

Wenn die Eingabe verrauschter ist, als Sie denken

Base64 aus der realen Welt kommt selten als eine einzige, makellos saubere 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 dieses Rauschen zu verarbeiten, und sie ist eine gewichtige:

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 in der Dokumentation als Dekodierer beschrieben, der "unbekannte Bytes außerhalb des Base-64-Alphabets ignoriert, einschließlich Zeilenumbrüche", und für diesen Job ist es das richtige Werkzeug: Das Rauschen wird gelöscht, das Alphabet überlebt, und der Payload kommt heil raus. Aber die Option hat eine blinde Stelle, und genau die beißt Swift-Entwickler am meisten: Sie löscht jedes Zeichen außerhalb des Alphabets, einschließlich - und _ von Base64url. Sie übersetzt sie nicht zu + und /, sie wirft sie einfach weg. Je nachdem, was dieses Löschen übrig lässt, bekommen Sie ein nil (wenn die Überlebenden keine ganzen Gruppen mehr bilden) oder, schlimmer noch, eine selbstbewusste Antwort mit der falschen Byte-Anzahl. Ein 16 Zeichen langer Base64url-String, der 12 Bytes kodiert, kann aus dem nachsichtigen Dekodierer als 9 andere Bytes zurückkommen, ohne Fehler und ohne Entschuldigung.

Die Regel, die Sie einprägen sollten: .ignoreUnknownCharacters ist für Transportrauschen (Zeilenumbrüche, ein paar verirrete Leerzeichen vom Copy-Paste) gedacht, niemals für Alphabet-Unterschiede. Wenn der Payload eventuell Base64url ist, konvertieren Sie die Zeichen erst selbst, genau wie der nächste Abschnitt zeigt, und geben Sie dem Dekodierer einen sauberen Standard-String.

Das URL-Alphabet

Abschnitt 5 von RFC 4648 definiert den Cousin des Standard-Alphabets, dem Sie bisher begegnet sind: Base64url, bei dem aus + ein - wird, aus / ein _, und das =-Padding wird normalerweise weggelassen. Der Grund ist derselbe, der Ihre URLs ehrlich hält: Im Query-String wird + beim Form-Parsing als Leerzeichen gelesen, / ist ein Pfadtrennzeichen, und = trennt Schlüssel von Werten. Der RFC spricht offen über das Verhältnis der beiden: Die URL-Variante "sollte nicht als identisch mit der Base64-Kodierung betrachtet werden". JWTs, Web-Push-Nachrichten, YouTube-Video-IDs und die meisten modernen API-Identifikatoren sprechen Base64url, also rechnen Sie damit, ihn am ersten Tag zu treffen.

Auf der Dekodierungsseite hat das Rezept zwei Züge: Übersetzen Sie das Alphabet, dann füllen Sie das Padding auf, denn der strenge Dekodierer verlangt weiter 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 (nie drei) stellen die Vierergruppe wieder her, die der Dekodierer erwartet. Sie werden eine Version dieser fünfzeiligen Extension in erstaunlich vielen Swift-Codebasen finden, und aus gutem Grund. Es gibt einen Grund, warum sie in Zukunft kürzer wird: Die neuesten Apple-SDKs (26.4 und neuer, Stand dieses Artikels) haben eine native .base64URLAlphabet-Option für den Kodierer bekommen, während die passenden Dekodierungs-Optionen in open-source Foundation noch hinter einem Verfügbarkeits-Marker für eine spätere Toolchain reifen. Bis diese Ihre Mindest-Deployment-Zielversion erreicht, ist die Extension die portable Antwort, und sie funktioniert per Konstruktion in jeder Version weiter.

Erst Bytes, dann Wörter

Hier ist die Entscheidung, die der Dekodierer für Sie nicht treffen kann: Er reicht Ihnen ein Data, einen Beutel voller Bytes, die keine Ahnung haben, in welchem Zeichensatz der ursprüngliche Payload geschrieben wurde. Wenn der Payload Text war, liegt die Wahl dieses Zeichensatzes bei Ihnen, und Swift gibt Ihnen zwei Türen aus der Byte-Welt mit ganz unterschiedlichen Temperamenten.

  • String(data:encoding:) ist die strenge Tür. Sie liefert ein Optional zurück und antwortet mit nil, wenn die Bytes in der von Ihnen genannten Kodierung nicht gültig sind. Ideal für Validierung, gefährlich, wenn Sie die Antwort force-unwrappen.
  • String(decoding:as:) ist die nie-weigern-Tür. Sie liefert immer einen String zurück und ersetzt alles, was sie nicht sinnvoll deuten kann, durch das U+FFFD-Ersatzzeichen. Ideal für Logging und Vorschau, gefährlich, wenn Sie das Ergebnis speichern und es Daten nennen.
import Foundation

let bytes = Data([0xC3, 0xA5]) // die UTF-8-Schreibweise des Buchstaben a mit Ring
print(String(data: bytes, encoding: .utf8) ?? "?")      // a mit Ring, richtig gelesen
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // zwei verwirrte Buchstaben, dieselben 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 nur zurück, wenn der Vertrag schweigt und Sie lieber etwas Lesbares, aber Falsches haben wollen als Stille; reservieren Sie die nie-weigern-Tür für Debug-Ausgaben. Und ein unsichtbarer Eindringling, auf den Sie achten sollten: Wenn der Payload mit einem UTF-8 BOM beginnt (die drei Bytes EF BB BF), behält die strenge Konvertierung es bei, und Ihr String beginnt jetzt mit einem unsichtbaren U+FEFF-Zeichen, das still und leise Gleichheitsprüfungen und JSON-Roundtrips kaputt macht. Entfernen Sie es mit einer Prefix-Prüfung, wenn die Spezifikation keins verspricht.

Dateien öffnen

Der Job "Da ist 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 Datei, die nil zurückgibt, denn Tools, Mail-Clients und Editoren lieben es alle, einen Zeilenumbruch am Ende stehen 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-umgebrochen ist (Zeilenumbruch 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. Beides ist jeweils eine Zeile. Für Dateien, die einfach nur groß sind, dekodieren Sie in ausgerichteten Gruppen, statt das Ganze zu lesen: Jede Vierergruppe 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-Buffer, ein Rest-Fragment und das Ergebnis, das Sie aufbauen. Derselbe Loop verarbeitet einen Download, der als Base64 über die Leitung ankommt, eine Log-Datei, die eigentlich ein kodierter Stream ist, oder jeden Payload, der zu groß ist, um ihn in der Hand zu halten.

JWTs: Die drei Punkte lesen

Ein kompakter JSON Web Token besteht aus drei Base64url-Teilen, die durch Punkte verbunden sind, 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 übernimmt 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 reisen mit. Ein JWT ist signiert, nicht verschlüsselt: Der Header und der Payload sind öffentliche Informationen, und genau deshalb gehört niemals ein Passwort hinein (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 mit dem data:-Scheme im Text 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 den Payload abzuziehen ist nur ein Split entfernt:

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 die berühmte transparente 42-Byte-GIF, das kleinste Bild im Format, und deshalb erscheinen ihre Anfangszeichen in mehr Codebasen als fast jeder andere Base64-String im Internet. Auf Apple-Plattformen endet die Pipeline mit einer Zeile: Dasselbe Data, das Sie gerade dekodiert haben, fließt direkt in UIImage(data:) oder NSImage(data:), und deshalb ist "einen Avatar aus einer API anzeigen" ein kleines Feature 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, wo + 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")
}

Die Sicherheits-Fußnote laut und klar halten, denn sie gilt für jedes Base64, dem Sie je begegnen werden: Das ist Verpackung, kein Schutz. Basic-Auth ist nur über HTTPS akzeptabel, wo TLS die eigentliche Bewachung macht und Base64 lediglich verhindert, dass die Bytes die Header-Grammatik brechen. Dieselbe Logik erklärt Authorization: Bearer-Tokens: Das Token selbst ist ein JWT, also gilt das Dekodierungsrezept 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 nachsichtige Option existiert. Die rohen MIME-Header verraten Ihnen, welches Alphabet und welcher Umbruch vom Absender verwendet wurden (Content-Transfer-Encoding: base64), und die Lösung ist ein einziger Schalter:

import Foundation

let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
  print(String(data: data, encoding: .utf8) ?? "")
}
// This attachment survived the 76-character habit.

Wenn Sie eine Mail-Funktion schreiben statt eine zu lesen, denken Sie daran, dass der 76-Zeichen-Umbruch Sie auch was kostet: Mit einem Zeilenumbruch alle 76 Zeichen landet der kodierte Text bei ungefähr 137 Prozent der Originalgröße, und deshalb schätzten alte Mail-Ingenieure Anhangsgrößen mit dem Trick "Original mal 1,37, dazu grob 800 Bytes Header". Die Zahl ist heute Folklore, aber die Rechnung ist noch immer die Rechnung.

Der doppelt verpackte Payload

Das häufigste "Meine Daten sind kaputt"-Ticket im Base64-Land betrifft Daten, die zweimal verpackt wurden: Eine Integrations-Schicht hat sie kodiert, und eine zweite Schicht, die sich nie die Dokumentation durchgelesen hat, hat das Ergebnis kodiert. Der defensive Zug ist, einmal zu dekodieren, sich anzusehen, was man bekommen hat, und wenn das Ergebnis selbst ein sauberer, base64-ähnlich aussehender String ist (richtige Länge, richtiges Alphabet, nichts Überraschendes), es ein zweites Mal bewusst zu dekodieren und dann aufzuhören. Schreiben Sie keinen Loop, der dekodiert, bis er scheitert. Ein solcher Loop frisst fröhlich eine völlig gute Datei, deren Inhalt zufällig base64-mäßig aussieht, und nachdem er gelaufen ist, kann niemand sagen, wo die originalen Daten anfingen.

import Foundation

func unwrapOnce(_ packed: String) -> Data? {
  let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
  return Data(base64Encoded: cleaned)
}

let suspicious = "WVdKag==" // sieht schon aus wie verpackt
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 Unwraps, zwei bewusste Entscheidungen, und ein Payload, der endlich wieder einfach nur abc ist.

Dem Nil eine Bedeutung geben

Weil der Dekodierer mit nil antwortet statt zu werfen, ist der Fehlerbehandlungs-Stil Ihres Base64-Codes eine Entscheidung, die Sie selbst treffen, und die Entscheidung, die Sie später dafür dankbar sein 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 zu dem einen Ort, an dem die Normalisierung lebt: das Trimmen, jede Alphabet-Übersetzung, jedes Padding-Auffüllen. Aufrufer bekommen eine Funktion, eine Bedeutung für Fehler, und kein ! in Sicht. Das Force-Unwrapping von Data(base64Encoded:)! ist der Weg, wie ein schlechter Payload zu einer abgestürzten App wird, und der Wrapper ist die günstige Versicherung dagegen. Dasselbe Muster funktioniert an der Kommandozeile, wo ein Skript mit CommandLine.arguments und einem FileHandle-Write daraus macht, "diese Datei aus der Shell zu dekodieren", ein Fünfzeilen-Utility statt eines Copy-Paste-Umwegs über eine Website.

Sicherheit, gemessen in Bytes

  • Das ist keine Verschlüsselung. Base64 ist ein umkehrbares, sofort lesbares Umpacken. Wenn Ihr Threat-Modell einen Menschen mit Browser und fünf Sekunden enthält, haben Sie null Schutz, und jeder JWT-Header beweist diesen Punkt täglich.
  • Kanonicalisieren Sie, bevor Sie vergleichen. Weil TQ== und TS== zu denselben Bytes dekodieren, können zwei Systeme verschiedene Schreibweisen derselben Daten halten. Eine Paper aus 2022, "Base64 Malleability in Practice", dokumentierte, was diese gebrochene Eindeutigkeitsgarantie in der Wildnis anrichtet: Log-Mismatches, Denial-of-Service-Angriffe und doppelte Datenbank-Einträge. Wenn Ihre Swift-App Base64-Werte cacht, entdupliziert oder vergleicht, führen Sie an der Tür genau einen kanonischen Decode durch (oder eine kanonische Re-Kodierung).
  • Limitieren Sie die Eingabe vor dem Dekodieren. Das Dekodieren von N Zeichen reserviert ungefähr drei Viertel von N Bytes, während Sie noch den Eingabe-String in der Hand halten. Ein feindlicher Client kann 100 Megabyte des Buchstabens A senden und zusehen, wie Ihr Speicher klettert, bevor der Dekodierer überhaupt ein Nein sagt. Prüfen Sie zuerst die Länge, billig und schnell, und verwerfen Sie, was zu groß ist.
  • Hüten Sie sich vor der nachsichtigen Option als Filter. .ignoreUnknownCharacters löscht Zeichen. Ein "Sanitizing"-Durchlauf damit kann einen gültigen Base64url-Payload zu anderen Daten machen, ohne Fehler. Sie 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 komfortable URL-Längen und werden von Proxys zerfetzt. Legen Sie sie stattdessen in Anfrage-Body, Dateien oder Tokens.

Performance, kurz gesagt

Der Dekodierer ist ein Lookup-Table-Durchlauf: Jedes Zeichen wird in einer kleinen Tabelle nachgeschlagen, und einige Bits werden verschoben und mit Oder verknüpft zu Ausgabe-Bytes. Auf einer aktuellen Toolchain ist das für alles, was in den Speicher passt, schnell genug, und die Zahl, die Sie sich merken sollten, ist das Ausgabe-Verhältnis: Dekodierte Bytes sind etwa drei Viertel der Eingabe-Länge, also kostet Sie ein 4-Megabyte-String ungefähr 3 Megabyte Ergebnis obendrauf, zum String, den Sie schon halten. Wenn Sie auf einem Weg sind, wo Foundation selbst nicht erlaubt ist (ein tief eingebettetes Target, ein WebAssembly-Bundle), ist das Community-Package swift-extras-base64 die bemerkenswerte Alternative: reines Swift ohne Foundation-Abhängigkeit, ein RFC-4648-konformer Encoder und Decoder mit Base64url- und Padding-Optionen, und Benchmarks, die es ein Vielfaches schneller als Foundation machen. Eine frühere Implementation desselben Pakets ist sogar in swift-nios WebSocket-Unterstützung enthalten, was für ein Side-Project ziemlich nah an Produktionsqualität kommt. 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 Standard-Antwort.

Ein Jahrzehnt des Auspackens

Swift hat nichts davon erfunden, und es lohnt sich zu wissen, woher jedes Stück der Werkzeugkiste stammt:

  • 1980er, die Ä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 ihre. uuencode verwendete ein Alphabet aus Großbuchstaben, Ziffern und Interpunktion, und seine Buchstaben liegen auf aufeinanderfolgenden ASCII-Positionen, also war Kodieren eine Frage des Addierens von 32, ohne jegliche Lookup-Tabelle. Dekodierer dieser Ära konnten viel voraussetzen, und in dem Moment, als Daten Ökosysteme überquerten, kippte es 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 kodiertes, aber unverschlüsseltes Daten zu markieren. Jeder PEM-artige Block ist ein Nachkomme dieses Dokuments.
  • 1996, die liberale Ära. MIME (RFC 2045) nahm das Alphabet für E-Mails, verschob den Umbruch auf 76 Zeichen und sagte konformen Dekodierern, jedes Zeichen außerhalb des Alphabets zu ignorieren, wie die CRLF-Zeilenumbrüche. Das ist die Ära, die eine Generation trainierte, nachsichtige Dekodierer zu erwarten, und die Ära, deren Erwartungen Swifts strenger Standard bewusst bricht.
  • 2003 bis 2006, die Regeln härten aus. RFC 3548 (2003) versuchte als Erster, die Familie zu vereinigen; RFC 4648 (Oktober 2006) regelte es, kodifizierte die Padding-Regeln und fügte das URL-sichere Alphabet hinzu. Sein Dekodierer-Abschnitt ist der, den Swift befolgt: Lehn Sie Zeichen außerhalb des Alphabets ab, außer das Format, das Sie bedienen, sagt ausdrücklich, sie zu ignorieren, wie MIME es tut.
  • 2013 bis 2014, die API wartet im Nebenraum. Apples NSData-Klasse hat seit Jahren Base64 gepackt und ausgepackt, und die optionenbasierte 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 herein und hat seitdem dieselbe Persönlichkeit behalten: strenger Kern, ein nachsichtiger Regler, failable Initializer.
  • 3. Dezember 2015, Linux bekommt einen Dekodierer. Swift wurde an diesem Tag open-sourced, und mit ihm wanderte Foundations Base64 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) verlegte Data in einen reinen Swift-Kern, und 2025 fügte ein Community-Pitch native Optionen für Base64url und für das Weglassen des Paddings hinzu. Stand dieses Artikels liefern die neuesten SDK-Betas und die Open-Source-Toolchain die Kodierungs-Optionen aus, während die Dekodierungs-Optionen in der Open-Source-Toolchain noch reifen, also bleiben die handgeschriebenen Extensions bis dahin die universelle Antwort.

Kleine Wunder

  • ==== ist legale Eingabe. Vier Pads und keine Daten dekodieren zu einem leeren Data, dem einzigen Base64-String, dessen gesamter Inhalt "hier ist nichts" ist, und Swift stimmt ihm zu.
  • Die Zeichenpolizei des Dekodierers kontrolliert nicht die Arbeit der Bit-Polizei: TS== und TQ== reichen Ihnen beide M, während Tg== Ihnen N reicht. Gleiche Grammatik, andere Bits, keine Fragen gestellt.
  • Swifts Data kann Base64 dekodieren, das als Bytes ankommt statt als String, über die Data(base64Encoded: Data)-Variante, also kann ein Payload, der als ASCII über die Leitung ging, den String-Roundtrip komplett überspringen.
  • Das Wort, das seit der Geburt der Testvektoren base64 ist, ist foobar, und es packt zu Zm9vYmFy. Wenn Sie schon einmal ein Base64-Beispiel in der Wildnis gesehen haben, ist es gut möglich, dass foobar beteiligt war.
  • Die berühmte 1x1-transparente GIF ist genau 42 Bytes groß und beginnt mit dem Magic-Word GIF89a, und deshalb erscheinen ihre ersten acht kodierten Zeichen in mehr Codebasen als fast jedes andere Base64-Präfix auf der Erde.
  • Der moderne Open-Source-Dekodierer macht seine ungültige-Zeichen-Prüfung mit einem einzigen Vergleich: Er verknüpft vier pro Position nachgeschlagene Werte mit Oder und testet das Ergebnis gegen einen Sentinel, so dass ein einziges Branch das Schicksal einer ganzen Vierergruppe entscheidet. Die ältere Implementation machte dieselbe Arbeit mit einer 128-Byte-Tabelle, in der jeder Wert ab 0x80 "kein Buchstabe" bedeutete.
  • UTF-8-BOMs sind unsichtbar: EF BB BF am Anfang eines Payloads wird zu einem U+FEFF-Zeichen, das die strenge Konvertierung überlebt und dann ein paar Zeilen Code später Gleichheitsprüfungen kaputt macht.
  • Swift ist 27 Jahre jünger als das Alphabet, das es dekodiert. Die Sprache erschien 2014; die 64 Buchstaben, die es verarbeitet, wurden 1987 standardisiert und haben sich seither nicht geändert.

Das ist die ganze Dekodierungs-Werkzeugkiste: ein failable Initializer mit einem kurzen Regelwerk, ein nachsichtiger Regler mit einem dokumentierten blinden Fleck, ein Fünfzeilen-Base64url-Helfer, eine Zeichensatz-Entscheidung, die Ihnen zusteht, ein Chunked-Loop für die großen Dateien und ein Wrapper, der dem Nil eine Bedeutung gibt. Dekodieren ist der Ort, an dem Base64 beißt, und jetzt kennen Sie die Namen jedes Zahns. Wenn der Job sich umdreht und Sie anfangen, Bytes für die Reise zu packen, statt sie auszupacken, übernimmt der Aufschlag von ungefähr 33 Prozent, und die Umbruch-Optionen erscheinen. Der verwandte Kodierungs-Artikel deckt die andere Hälfte dieses Roundtrips vollständig ab, also schalten Sie dorthin um, wenn Sie bereit sind, in die andere Richtung zu versenden.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Kodierung in Swift: Ein vollständiger Leitfaden