Base64 Encoding & Decoding in C
C has no standard library function for Base64. You either implement it yourself using a lookup table or link against OpenSSL. Here is a complete, correct implementation plus the OpenSSL alternative.
No stdlib support — your options
Unlike Python (base64), Java (java.util.Base64), or JavaScript (btoa/atob), the C standard library ships with no Base64 routines at all. None of stdio.h, stdlib.h, string.h, or any other ISO C header expose an encoder or decoder. If you need Base64 in C, you must bring it yourself.
There are three practical paths, and the right one depends on what your project already links against:
- Roll your own lookup table. Base64 is a simple, deterministic transform. A correct, portable encoder and decoder fit in roughly 50 lines each with no dependencies. This is the standard choice for embedded targets, single-file CLI tools, and any code that must stay dependency-free.
- Use OpenSSL. If your binary already links
libcrypto(for TLS, hashing, or signatures), reuse its well-testedEVP_EncodeBlock/EVP_DecodeBlockor the streamingBIO_f_base64filter rather than maintaining your own. - Drop in a small library such as libb64 or aklomp/base64 (the latter offers SIMD-accelerated paths). Useful when you want vetted code but not OpenSSL's footprint.
This article walks through a complete hand-written implementation first — because understanding the transform makes every library API obvious — and then shows the OpenSSL alternative for production use.
Encoder: the lookup table approach
Base64 reads the input three bytes (24 bits) at a time and re-groups those 24 bits into four 6-bit values. Each 6-bit value (0–63) indexes a 64-character alphabet to produce one output character. When the input length is not a multiple of three, the final group is padded with = characters so the output length is always a multiple of four.
The encoder is built around a single 64-byte alphabet table. We process full 3-byte chunks in the main loop, then handle the 1- or 2-byte remainder as a special case with padding.
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static const char b64_enc_table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
/* Encodes `len` bytes from `in` into a freshly malloc'd, null-terminated
* Base64 string. Caller must free() the result. Returns NULL on alloc fail. */
char *base64_encode(const uint8_t *in, size_t len) {
size_t out_len = 4 * ((len + 2) / 3); /* ceil(len/3) * 4 */
char *out = malloc(out_len + 1); /* +1 for '\0' */
if (!out) return NULL;
size_t i, j;
for (i = 0, j = 0; i + 2 < len; i += 3) {
uint32_t n = ((uint32_t)in[i] << 16)
| ((uint32_t)in[i + 1] << 8)
| (uint32_t)in[i + 2];
out[j++] = b64_enc_table[(n >> 18) & 0x3F];
out[j++] = b64_enc_table[(n >> 12) & 0x3F];
out[j++] = b64_enc_table[(n >> 6) & 0x3F];
out[j++] = b64_enc_table[ n & 0x3F];
}
/* Handle the trailing 1 or 2 bytes. */
size_t rem = len - i;
if (rem == 1) {
uint32_t n = (uint32_t)in[i] << 16;
out[j++] = b64_enc_table[(n >> 18) & 0x3F];
out[j++] = b64_enc_table[(n >> 12) & 0x3F];
out[j++] = '=';
out[j++] = '=';
} else if (rem == 2) {
uint32_t n = ((uint32_t)in[i] << 16)
| ((uint32_t)in[i + 1] << 8);
out[j++] = b64_enc_table[(n >> 18) & 0x3F];
out[j++] = b64_enc_table[(n >> 12) & 0x3F];
out[j++] = b64_enc_table[(n >> 6) & 0x3F];
out[j++] = '=';
}
out[j] = '\0';
return out;
}
Note the use of uint8_t for the input. This matters: if you read input as plain char on a platform where char is signed, bytes with the high bit set become negative and the shift/mask arithmetic produces garbage. Always treat binary data as unsigned bytes.
The expression (len + 2) / 3 is the standard integer-arithmetic trick for computing ceil(len / 3) without floating point. Multiplying that by 4 gives the exact encoded length for any input, including the padding characters.
Decoder: reverse lookup table
Decoding inverts the process: read four input characters, map each back to its 6-bit value, and reassemble the 24-bit group into three output bytes. Instead of scanning the 64-character alphabet for every input character (slow), we build a 256-entry reverse table that maps any byte directly to its 6-bit value, or to a sentinel for invalid characters.
The decoder below validates input length, ignores nothing silently, and correctly reduces the output count when it sees padding. It returns the decoded byte count through an out-parameter.
#include <stdint.h>
#include <stdlib.h>
/* -1 = invalid, -2 = padding ('='). Index by unsigned byte value. */
static const int8_t b64_dec_table[256] = {
['A']= 0,['B']= 1,['C']= 2,['D']= 3,['E']= 4,['F']= 5,['G']= 6,['H']= 7,
['I']= 8,['J']= 9,['K']=10,['L']=11,['M']=12,['N']=13,['O']=14,['P']=15,
['Q']=16,['R']=17,['S']=18,['T']=19,['U']=20,['V']=21,['W']=22,['X']=23,
['Y']=24,['Z']=25,['a']=26,['b']=27,['c']=28,['d']=29,['e']=30,['f']=31,
['g']=32,['h']=33,['i']=34,['j']=35,['k']=36,['l']=37,['m']=38,['n']=39,
['o']=40,['p']=41,['q']=42,['r']=43,['s']=44,['t']=45,['u']=46,['v']=47,
['w']=48,['x']=49,['y']=50,['z']=51,['0']=52,['1']=53,['2']=54,['3']=55,
['4']=56,['5']=57,['6']=58,['7']=59,['8']=60,['9']=61,['+']=62,['/']=63,
['=']=-2,
/* every other entry is implicitly 0 — fix that below at runtime,
* or initialise to -1 explicitly. See note. */
};
/* Decodes a null-terminated Base64 string. On success returns a malloc'd
* buffer and writes the byte count to *out_len. Returns NULL on error. */
uint8_t *base64_decode(const char *in, size_t in_len, size_t *out_len) {
if (in_len % 4 != 0) return NULL; /* must be a multiple of 4 */
if (in_len == 0) { *out_len = 0; return malloc(1); }
size_t pad = 0;
if (in[in_len - 1] == '=') pad++;
if (in[in_len - 2] == '=') pad++;
size_t cap = (in_len / 4) * 3 - pad;
uint8_t *out = malloc(cap ? cap : 1);
if (!out) return NULL;
size_t i, j = 0;
for (i = 0; i < in_len; i += 4) {
int8_t a = b64_dec_table[(uint8_t)in[i]];
int8_t b = b64_dec_table[(uint8_t)in[i + 1]];
int8_t c = b64_dec_table[(uint8_t)in[i + 2]];
int8_t d = b64_dec_table[(uint8_t)in[i + 3]];
if (a == -1 || b == -1) { free(out); return NULL; }
uint32_t n = ((uint32_t)a << 18) | ((uint32_t)b << 12);
out[j++] = (n >> 16) & 0xFF;
if (c != -2) { /* not padding */
n |= (uint32_t)c << 6;
out[j++] = (n >> 8) & 0xFF;
if (d != -2) {
n |= (uint32_t)d;
out[j++] = n & 0xFF;
}
}
}
*out_len = j;
return out;
}
The designated-initializer table above leaves all unspecified entries at 0 — which collides with the valid value for 'A'. To make invalid-character detection work, you must initialise the whole array to -1 first (e.g. fill it at startup with a loop, or generate the table so every non-alphabet byte is -1). Skipping this means malformed input is silently accepted. Decoder validation is the most common place real-world Base64 code goes wrong.
A robust approach is to populate the table at runtime once: set all 256 entries to -1, then walk b64_enc_table assigning table[(uint8_t)b64_enc_table[k]] = k, and finally set table['='] to the padding sentinel. That guarantees correctness without hand-typing 256 entries.
URL-safe variant
Standard Base64 uses + and / as the last two alphabet characters. Both are problematic in URLs and filenames: + is interpreted as a space in query strings and / is a path separator. RFC 4648 §5 defines a URL-safe alphabet that swaps them for - (minus) and _ (underscore). The transform is otherwise identical.
To support both variants without duplicating the entire encoder, add a flag and swap only the two affected characters on output:
static const char b64_url_table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789-_";
/* Pass `url_safe = 1` to emit the RFC 4648 URL-safe alphabet. */
char *base64_encode_ex(const uint8_t *in, size_t len, int url_safe) {
const char *table = url_safe ? b64_url_table : b64_enc_table;
/* ...identical loop, but index `table` instead of `b64_enc_table`... */
/* (omit '=' padding entirely if you want unpadded URL-safe output) */
}
For decoding, the cleanest fix is to normalise input before the lookup: map - to + and _ to / as you read each character, then use the same reverse table. Alternatively, add ['-']=62 and ['_']=63 entries to a decode table that already contains the standard symbols — accepting both alphabets at once.
URL-safe Base64 is frequently emitted without padding (JWTs are the canonical example). If you strip the = characters, your decoder must accept input whose length is not a multiple of four. Compute the original group size from in_len % 4 (a remainder of 2 means one trailing byte, 3 means two) instead of rejecting it. See URL-Safe Base64 for the full details.
Using OpenSSL BIO/EVP API
If your project already links libcrypto, prefer OpenSSL's implementation over a hand-rolled one. There are two interfaces. The simplest is the one-shot EVP_EncodeBlock/EVP_DecodeBlock pair, which operates on full buffers in memory:
#include <openssl/evp.h>
#include <stdlib.h>
#include <string.h>
char *openssl_b64_encode(const unsigned char *in, int len) {
int out_len = 4 * ((len + 2) / 3);
char *out = malloc(out_len + 1);
if (!out) return NULL;
/* EVP_EncodeBlock writes the data plus a trailing '\0' and
* returns the number of characters written (excluding the '\0'). */
EVP_EncodeBlock((unsigned char *)out, in, len);
return out;
}
For larger or streamed data, the BIO filter chain is idiomatic. You push a BIO_f_base64 filter onto a memory or file BIO and write through it. One important detail: BIO_f_base64 inserts a newline every 64 characters by default, which most modern consumers do not want — set BIO_FLAGS_BASE64_NO_NL to disable it.
#include <openssl/bio.h>
#include <openssl/evp.h>
char *bio_b64_encode(const unsigned char *in, int len) {
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); /* no line breaks */
BIO *mem = BIO_new(BIO_s_mem());
b64 = BIO_push(b64, mem); /* base64 -> mem */
BIO_write(b64, in, len);
BIO_flush(b64);
BUF_MEM *bptr;
BIO_get_mem_ptr(b64, &bptr);
char *out = malloc(bptr->length + 1);
memcpy(out, bptr->data, bptr->length);
out[bptr->length] = '\0';
BIO_free_all(b64); /* frees whole chain */
return out;
}
EVP_DecodeBlock does not tell you how many padding bytes were present — it always returns a multiple of three. If the input ended in one or two = characters, you must subtract the corresponding number of bytes from the returned length yourself, or you will keep one or two trailing zero bytes that were never part of the original data.
For a deeper dive into the BIO chain, error handling, and the streaming decode path, see the dedicated OpenSSL Base64 article.
Memory management and buffer sizing
Because C gives you no automatic strings, the single most important correctness concern is computing output sizes exactly and freeing what you allocate. Get the size formula wrong by one byte and you get either a buffer overflow or a truncated result.
- Encoding size:
encoded_len = 4 * ((input_len + 2) / 3), plus1for the null terminator if you treat the result as a C string. This is exact, not an upper bound. - Decoding size (upper bound):
decoded_max = (encoded_len / 4) * 3. The actual decoded length is this minus the number of trailing=padding characters (0, 1, or 2).
You have two allocation strategies. Either the function allocates and returns a buffer (the approach used above — caller owns it and must free() it), or the caller passes in a buffer plus its capacity and the function reports how many bytes it wrote. The caller-supplied-buffer style avoids heap allocation entirely, which matters on embedded systems:
/* Returns the number of bytes written, or -1 if `out_cap` is too small.
* Computes the required size up front so it never overflows the buffer. */
int base64_encode_buf(const uint8_t *in, size_t len,
char *out, size_t out_cap) {
size_t need = 4 * ((len + 2) / 3) + 1; /* incl. terminator */
if (out_cap < need) return -1;
/* ...encode directly into `out`... */
return (int)(need - 1);
}
Whatever convention you pick, document ownership explicitly in the header comment. The most common Base64 memory bug in C is the caller forgetting to free() a returned heap buffer, or freeing a caller-supplied stack buffer. Pick one convention per function and never mix them.
Compile and test
A quick round-trip test is the fastest way to confirm your encoder and decoder agree. The canonical RFC 4648 test vector "Man" should encode to "TWFu", and "Ma" to "TWE=". Wire up a small main:
#include <stdio.h>
#include <string.h>
int main(void) {
const char *msg = "Hello, base64.dev!";
char *enc = base64_encode((const uint8_t *)msg, strlen(msg));
printf("encoded: %s\n", enc);
size_t dec_len;
uint8_t *dec = base64_decode(enc, strlen(enc), &dec_len);
printf("decoded: %.*s\n", (int)dec_len, dec);
/* round-trip must match the original exactly */
int ok = dec_len == strlen(msg) && memcmp(dec, msg, dec_len) == 0;
printf("round-trip: %s\n", ok ? "OK" : "FAIL");
free(enc);
free(dec);
return ok ? 0 : 1;
}
Compile the standalone version with warnings cranked up so you catch signedness and conversion bugs early:
cc -std=c11 -Wall -Wextra -Wconversion -o b64test base64.c main.c ./b64test
If you used the OpenSSL functions, add the link flags:
cc -std=c11 -Wall -Wextra -o b64test base64_openssl.c main.c -lcrypto ./b64test
Run your build under sanitizers during testing: cc -fsanitize=address,undefined .... AddressSanitizer catches the off-by-one buffer overruns that are so easy to introduce in size calculations, and UBSan flags the signed-overflow and shift mistakes that hide in the bit-twiddling. Both are essentially free to enable and pay for themselves the first time they fire.
Once your round-trip passes for the empty string, 1-byte, 2-byte, and 3-byte inputs (the four padding cases), the implementation is almost certainly correct for everything else.
FAQ
Is there a base64 function in C standard library?
No. The C standard library (stdio.h, stdlib.h, string.h, etc.) does not include Base64 encoding. You must either write your own implementation using a lookup table or use a third-party library such as OpenSSL (EVP_EncodeBlock) or libb64.
How do I calculate the output buffer size for Base64 in C?
Encoded output is ceil(input_len / 3) * 4 bytes, plus 1 for the null terminator. In C: size_t encoded_len = ((input_len + 2) / 3) * 4 + 1;. For decoding: decoded_len is at most ceil(encoded_len / 4) * 3.
Should I use OpenSSL or a custom implementation for Base64 in C?
For production code that already links against OpenSSL, use EVP_EncodeBlock/EVP_DecodeBlock — they are well-tested and maintained. For embedded systems, CLI tools, or projects without OpenSSL, a 50-line lookup-table implementation is common and portable.
Try Base64 encoding and decoding instantly
Paste any string or file — base64.dev auto-detects and converts it instantly.
Open base64.dev →