Datei hochladen
  1. base64hub
  2. Base64 im Code
  3. C
Code

Base64 in C

Die C-Standardbibliothek hat keine Base64-Funktionen, deshalb bringen die meisten Projekte ihre eigenen mit. Hier ist eine kurze Implementierung, die wir per Fuzzing gegen das Python-Modul base64 getestet haben, dazu die Details, die meist schiefgehen.

Encoder

Je 3 Eingabebytes werden zu 4 Zeichen mit je 6 Bit. Eine letzte Gruppe aus 1 oder 2 Bytes wird mit = aufgefüllt. Die Seite So funktioniert Base64 geht die Bit-Anordnung Schritt für Schritt durch.

#include <stddef.h>
#include <stdint.h>

static const char B64[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/* Output size for n input bytes, without the terminating NUL. */
size_t b64_encoded_len(size_t n) { return 4 * ((n + 2) / 3); }

/* Writes b64_encoded_len(n) chars plus a NUL to out. */
void b64_encode(const uint8_t *in, size_t n, char *out) {
    size_t i = 0;
    for (; i + 2 < n; i += 3) {
        uint32_t v = (uint32_t)in[i] << 16 | (uint32_t)in[i + 1] << 8 | in[i + 2];
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = B64[v >> 6 & 63];
        *out++ = B64[v & 63];
    }
    if (i < n) {                         /* 1 or 2 bytes left */
        uint32_t v = (uint32_t)in[i] << 16;
        if (i + 1 < n) v |= (uint32_t)in[i + 1] << 8;
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = (i + 1 < n) ? B64[v >> 6 & 63] : '=';
        *out++ = '=';
    }
    *out = '\0';
}

Die Schleifenbedingung i + 2 < n statt i < n - 2 ist wichtig: Bei size_t läuft n - 2 über und wird zu einer riesigen Zahl, wenn n 0 oder 1 ist. Die Casts nach uint32_t verhindern, dass in[i] << 16 auf einem vorzeichenbehafteten int ausgeführt wird.

Strikter Decoder

Der Decoder lehnt alles ab, was kein kanonisches Base64 mit Padding ist: falsche Länge, Zeichen außerhalb des Alphabets, Padding in der Mitte und Padding-Bits ungleich null.

static int b64_val(unsigned char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

/* Returns the number of bytes written, or -1 on invalid input.
   out needs room for 3 * (len / 4) bytes. */
long b64_decode(const char *in, size_t len, uint8_t *out) {
    if (len % 4 != 0) return -1;
    size_t o = 0;
    for (size_t i = 0; i < len; i += 4) {
        int a = b64_val(in[i]), b = b64_val(in[i + 1]);
        int c = b64_val(in[i + 2]), d = b64_val(in[i + 3]);
        int last = (i + 4 == len);
        if (a < 0 || b < 0) return -1;
        if (c < 0) {                     /* must be "xx==" at the end */
            if (!last || in[i + 2] != '=' || in[i + 3] != '=' || (b & 15)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
        } else if (d < 0) {              /* must be "xxx=" at the end */
            if (!last || in[i + 3] != '=' || (c & 3)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
        } else {
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
            out[o++] = (uint8_t)(c << 6 | d);
        }
    }
    return (long)o;
}

b64_val nimmt ein unsigned char, damit Bytes über 127 auf Plattformen mit vorzeichenbehaftetem char nicht zu negativen Indizes werden. Eine Lookup-Tabelle mit 256 Einträgen ist schneller; die if-Kette lässt sich leichter mit bloßem Auge prüfen.

Verwendung und Ergebnisse

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "héllo wörld";          /* source file saved as UTF-8 */
    char enc[64];
    b64_encode((const uint8_t *)s, strlen(s), enc);
    printf("%s\n", enc);                     /* aMOpbGxvIHfDtnJsZA== */

    uint8_t dec[64];
    long n = b64_decode(enc, strlen(enc), dec);
    printf("%ld %.*s\n", n, (int)n, dec);    /* 13 héllo wörld */
    return 0;
}

C kennt keine Textkodierung: Die Bytes im String-Literal sind genau das, was in der Quelldatei steht. Mit einer UTF-8-Quelldatei und dem Standard-Ausführungszeichensatz von GCC erhalten Sie UTF-8.

Was b64_decode für einige typische Eingaben zurückgibt:

EingabeRückgabeGrund
""0leer ist gültig
aGk=2hi
aGk-1Länge kein Vielfaches von 4
aGk=!!-1Länge und Zeichen
aGl=-1Padding-Bits ungleich null
PDw_Pz8-Pg==-1URL-sichere Zeichen
a===-1zu viel Padding

Wir haben mit gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined kompiliert, 2.000 zufällige Puffer per Roundtrip geprüft und die Entscheidung „akzeptiert/abgelehnt“ für 20.000 zufällige Strings mit Pythons b64decode(validate=True) plus einer Kontrolle durch erneutes Kodieren verglichen. Keine Abweichungen, keine Meldungen der Sanitizer. Ihre eigenen Strings können Sie mit dem Base64-Validator testen, der dieselben Regeln anwendet.

Eine Datei oder stdin in Blöcken kodieren

Lesen Sie Blöcke, deren Größe ein Vielfaches von 3 ist, dann ist die aneinandergehängte Ausgabe identisch mit der Kodierung der ganzen Datei auf einmal:

int main(void) {
    uint8_t in[3 * 1024];                /* multiple of 3: no padding mid-stream */
    char out[4 * 1024 + 1];
    size_t n;
    while ((n = fread(in, 1, sizeof in, stdin)) > 0) {
        b64_encode(in, n, out);
        fputs(out, stdout);
    }
    return ferror(stdin) ? 1 : 0;
}

Bei einer zufälligen Datei mit 1.000.003 Bytes lieferte ./b64 < big.bin dieselbe Ausgabe wie base64 -w 0 big.bin. fread liest weiter, bis der Puffer voll ist oder die Eingabe endet, kurze Lesevorgänge aus einer Pipe fügen also kein Padding ein. Unter Windows rufen Sie zuerst _setmode(_fileno(stdin), _O_BINARY) auf, sonst beschädigt die CRLF-Umwandlung binäre Eingaben.

URL-sichere Ausgabe

Wandeln Sie nach dem Kodieren um: zwei Zeichen tauschen und das Padding entfernen.

void b64_to_url(char *s) {
    char *w = s;
    for (char *r = s; *r; r++) {
        if (*r == '=') break;
        *w++ = (*r == '+') ? '-' : (*r == '/') ? '_' : *r;
    }
    *w = '\0';
}
/* bytes {0xfb, 0xff}: "+/8=" -> "-_8" */

Zum Dekodieren bilden Sie in b64_val - auf 62 und _ auf 63 ab und akzeptieren eine letzte Gruppe aus 2 oder 3 Zeichen ohne =. Siehe Base64 vs. Base64URL und Padding erklärt.

Typische Bugs in selbst geschriebenem Base64

  • Puffer um eins zu klein. Das NUL-Zeichen am Ende vergessen oder die Ausgabe mit n * 4 / 3 berechnet, was zu klein ist, sobald n kein Vielfaches von 3 ist. Der Größenrechner und die Anleitung zum Größenzuwachs liefern genaue Zahlen.
  • Lookups mit vorzeichenbehaftetem char. table[c] mit negativem c liest außerhalb des Arrays.
  • strlen auf Binärdaten. Dekodierte Ausgabe kann 0x00 enthalten; verwenden Sie die zurückgegebene Länge, nicht strlen.
  • Trigraphen. In den strikten Modi von C99/C11 enthält ein Test-String wie "<<???>>" den Trigraphen ??>, den der Compiler in } verwandelt. GCC warnt mit -Wtrigraphs. Schreiben Sie ihn als "<<?\?\?>>" oder verwenden Sie ein Byte-Array. C23 hat Trigraphen abgeschafft.

Die C++-Fassung dieses Codes mit std::string und std::vector steht unter Base64 in C++.

Häufige Fragen

Wie groß muss der Ausgabepuffer sein?

Beim Kodieren 4 * ((n + 2) / 3) Zeichen plus ein Byte für das abschließende NUL. Beim Dekodieren reichen immer 3 * (len / 4) Bytes; die tatsächliche Länge ist wegen des Paddings bis zu zwei Bytes kürzer.

Gibt es in OpenSSL oder glibc eine Base64-Funktion, die ich stattdessen nutzen kann?

OpenSSL hat EVP_EncodeBlock und EVP_DecodeBlock, und seine BIO-Schicht kann Base64 streamen. glibc hat keine öffentliche Base64-API. Linkt Ihr Projekt eine dieser Bibliotheken ohnehin, ist das vernünftig; sonst ist der Code unten einfacher als eine neue Abhängigkeit.

Warum akzeptiert mein Decoder Strings wie aGl=?

aGl= und aGk= ergeben beide hi; der erste hat Padding-Bits ungleich null. Tolerante Decoder ignorieren diese Bits. Der Decoder auf dieser Seite lehnt sie ab, indem er in der letzten Gruppe (c & 3) und (b & 15) prüft.

Weiterlesen