base64-ng
base64-ng is a no_std-first Base64 crate focused on correctness, strict decoding, caller-owned buffers, and a security-heavy release process. The long-term goal is to provide modern hardware acceleration without making unsafe SIMD the foundation of trust.
The crate starts conservative: a small scalar implementation, strict RFC 4648 behavior, and a test/release system modeled after hardened Rust service projects. Streaming is available behind an explicit feature, fuzz harnesses are isolated from the published crate, and future SIMD and Kani work remain gated until they have evidence.
Current Status
The current public release is 0.2.0.
Implemented now:
no_stdcore with optionalallocandstdfeatures.- Zero external runtime or development dependencies in
Cargo.toml. - Standard and URL-safe alphabets.
- Padded and unpadded encoding into caller-provided output buffers.
- Stable compile-time encoding into caller-sized arrays.
- Strict decoding into caller-provided output buffers.
- In-place encoding when the caller provides enough spare capacity.
- Optional
allocvector and string helpers. - In-place decode API built on the same strict scalar decoder.
- Explicit legacy decode APIs that ignore ASCII transport whitespace while keeping alphabet and padding validation strict.
std::iostreaming encoders and decoders behind thestreamfeature.- Focused unit and integration tests.
- Isolated
cargo-fuzzharnesses for decode, in-place decode, and stream chunk-boundary behavior. - Local check scripts, release gate, dependency policy, audit config, CI, SBOM script, and reproducible build check.
Planned:
- Constant-time-focused scalar decoder mode.
- AVX2, AVX-512, and ARM NEON fast paths.
- Async streaming wrappers.
- Kani proof harnesses.
- Criterion benchmarks against the established
base64crate.
Install
[]
= "0.2"
The crate is dual-licensed:
= "MIT OR Apache-2.0"
Features
| Feature | Default | Purpose |
|---|---|---|
alloc |
yes | Vec and encoded String convenience APIs. |
std |
yes | std::error::Error support and feature base for I/O. |
simd |
no | Future hardware acceleration. |
stream |
no | std::io streaming wrappers. |
tokio |
no | Future async streaming wrappers. |
kani |
no | Future verifier harnesses. |
fuzzing |
no | Reserved for verifier and fuzz harness integration; published crate stays dependency-free. |
Disable defaults for embedded or freestanding use:
[]
= { = "0.2", = false }
Example
use ;
let input = b"hello";
const ENCODED_CAPACITY: usize = match checked_encoded_len ;
let mut encoded = ;
let written = STANDARD.encode_slice.unwrap;
assert_eq!;
let mut decoded = ;
let written = STANDARD.decode_slice.unwrap;
assert_eq!;
In-place encoding:
use STANDARD;
let mut buffer = ;
buffer.copy_from_slice;
let encoded = STANDARD.encode_in_place.unwrap;
assert_eq!;
Compile-time encoding:
use ;
const HELLO: = STANDARD.encode_array;
const URL_BYTES: = URL_SAFE_NO_PAD.encode_array;
assert_eq!;
assert_eq!;
Stable Rust cannot yet express the encoded length as the return array length
directly, so encode_array uses the destination array type supplied by the
caller. A wrong output length fails during const evaluation.
For untrusted length metadata, use checked length calculation:
use ;
assert_eq!;
assert_eq!;
Legacy Whitespace Decoding
Strict decoding rejects whitespace. If an existing protocol allows line-wrapped or spaced Base64, use the explicit legacy APIs:
use STANDARD;
let mut output = ;
let written = STANDARD
.decode_slice_legacy
.unwrap;
assert_eq!;
Legacy decoding only ignores ASCII space, tab, carriage return, and line feed. Alphabet selection, padding placement, trailing data after padding, and non-canonical trailing bits remain strict.
Bounded Memory Use
For untrusted payloads, size buffers before decoding or encoding. The checked helpers let callers reject impossible or oversized metadata before allocating:
use ;
let input = b"hello";
let encoded_len = checked_encoded_len.unwrap;
assert_eq!;
let mut encoded = vec!;
let written = STANDARD.encode_slice.unwrap;
encoded.truncate;
let max_decoded = decoded_capacity;
let mut decoded = vec!;
let written = STANDARD.decode_slice.unwrap;
decoded.truncate;
assert_eq!;
decode_vec validates the complete input before allocating decoded output.
Use decode_slice or decode_in_place when the caller needs hard memory
limits and owns the output buffer.
With the default alloc feature, vector and string helpers are available:
use STANDARD;
let encoded = STANDARD.encode_vec.unwrap;
assert_eq!;
let encoded_string = STANDARD.encode_string.unwrap;
assert_eq!;
let decoded = STANDARD.decode_vec.unwrap;
assert_eq!;
With the stream feature, std::io encoders are available:
use ;
use ;
let mut encoder = new;
encoder.write_all.unwrap;
encoder.write_all.unwrap;
let encoded = encoder.finish.unwrap;
assert_eq!;
let mut reader = new;
let mut encoded = Stringnew;
reader.read_to_string.unwrap;
assert_eq!;
let mut decoder = new;
decoder.write_all.unwrap;
decoder.write_all.unwrap;
let decoded = decoder.finish.unwrap;
assert_eq!;
let mut reader = new;
let mut decoded = Vecnew;
reader.read_to_end.unwrap;
assert_eq!;
URL-safe, no-padding encoding:
use URL_SAFE_NO_PAD;
let mut encoded = ;
let written = URL_SAFE_NO_PAD.encode_slice.unwrap;
assert_eq!;
Security Model
base64-ng treats Base64 as infrastructure code. Fast paths are never allowed to outrun evidence.
Security commitments:
- Stable Rust first. Current toolchain pin: Rust
1.95.0. no_stdcore by default.- No unsafe code in scalar code.
- Future unsafe SIMD isolated under
src/simd/. - Strict decoding rejects malformed padding and trailing data.
- Runtime scalar APIs are expected to return
ResultorOptionfor malformed input and size errors instead of panicking. - Public encoded-length overflow is recoverable through
ResultorOption; untrusted length metadata should never require a panic. - Scalar encode avoids input-derived alphabet table indexes, and scalar decode uses branch-minimized arithmetic. These paths are hardened against obvious timing pitfalls, but they are not documented as formally verified cryptographic constant-time APIs.
- Legacy compatibility must be opt-in.
- Release gates include formatting, clippy, tests, Miri when installed, docs, dependency policy, audit, license review, SBOM, and reproducible build checks.
- Future Kani proofs target in-place decoding bounds and scalar decoder invariants.
See docs/PLAN.md, SECURITY.md,
docs/RELEASE_EVIDENCE.md, and
docs/CONSTANT_TIME.md.
For adoption guidance from the established base64 crate, see
docs/MIGRATION.md.
Local Checks
Run the standard gate:
Check the zero-external-crate policy directly:
Run the release gate:
Required security tools:
Optional deep tools:
Compile fuzz targets without running a campaign:
Run a target with cargo-fuzz:
Miri is installed as a nightly Rust component, not as a Cargo package:
On openSUSE Tumbleweed, install rustup first if it is not already present:
The local release gate runs Miri automatically when cargo +nightly miri is
available. The large deterministic sweep tests are ignored only under Miri
because they are already covered by the normal release gate and are too slow for
an interpreter.
Project Principles
- Keep external crates to the absolute minimum. The current crate dependency graph is only
base64-ng. - Correctness first, speed second, unsafe last.
- The scalar implementation is the reference behavior.
- SIMD must prove equivalence to scalar behavior across fuzzed and deterministic inputs.
- Compatibility modes must be visible in the type/API surface.
- Release evidence belongs in the repository and CI, not in memory.
Contributing And Releases
See CONTRIBUTING.md for contribution rules and docs/RELEASE.md for the maintainer release checklist.