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.6.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.
- Validation-only APIs for strict and legacy profiles when callers need to reject malformed input without materializing decoded bytes.
- Line-wrapped encoding for MIME/PEM-style output and caller-selected wrapping policies.
- Strict line-wrapped validation and decoding profiles for MIME/PEM-style input.
- Custom alphabet validation helpers for user-defined 64-byte alphabets.
- Named dependency-free profiles for MIME, PEM, bcrypt-style, and
crypt(3)-style Base64. - Stack-backed encoded output buffers for short values without
alloc. - Redacted secret owned buffers for sensitive encoded or decoded bytes when
allocis enabled. - Separate
ctscalar validation and decode module for sensitive payloads that avoids secret-indexed lookup tables during Base64 symbol mapping. 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 behind admission evidence:
- Admitted AVX2, AVX-512, and ARM NEON fast paths.
- Async streaming wrappers only after the
tokiofeature passes the dependency and cancellation-safety admission bar in docs/ASYNC.md. - Expanded Kani proof harnesses.
- Broader benchmark evidence against the established
base64crate.
Trust Dashboard
| Area | Status |
|---|---|
| License | MIT OR Apache-2.0 |
| MSRV | Rust 1.95.0 |
| Runtime dependencies | Zero external crates |
| Unsafe policy | Scalar encode/decode remains safe Rust; audited unsafe is limited to volatile wiping and SIMD prototypes |
| Active backend | Scalar only |
| Strict decoding | Default, canonical, no whitespace |
| Legacy compatibility | Explicit opt-in APIs |
| Constant-time posture | Constant-time-oriented scalar validation/decode; no formal cryptographic guarantee |
| Cleanup posture | Best-effort initialized-byte cleanup and redacted secret wrappers |
| Release evidence | fmt, clippy, tests, docs, deny, audit, license, SBOM, reproducibility |
Full adoption details live in docs/TRUST.md. Security-control and CWE mapping lives in docs/SECURITY_CONTROLS.md.
Install
[]
= "0.6.0"
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 | Reserved for future async streaming wrappers; currently inert and dependency-free. |
kani |
no | Reserved for verifier harnesses; normal builds do not require Kani. |
fuzzing |
no | Reserved for verifier and fuzz harness integration; published crate stays dependency-free. |
Disable defaults for embedded or freestanding use:
[]
= { = "0.6.0", = 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!;
For sensitive payloads, encode_slice_clear_tail and
encode_in_place_clear_tail clear unused bytes after the encoded prefix and
clear the caller-owned output buffer on encode error.
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!;
Validation Without Decoding
Use validation-only APIs when a protocol needs to sanitize input before storing, routing, or accounting for it:
use ;
assert!;
assert!;
STANDARD.validate_result.unwrap;
assert!;
assert!;
For line-wrapped or spaced legacy inputs, use the explicit legacy profile:
use STANDARD;
assert!;
assert!;
Line-Wrapped Encoding
Use LineWrap when a protocol needs MIME/PEM-style line lengths:
use ;
let wrap = new;
let mut output = ;
let written = STANDARD
.encode_slice_wrapped
.unwrap;
assert_eq!;
Built-in policies include LineWrap::MIME, LineWrap::PEM, and
LineWrap::PEM_CRLF. Wrapping inserts line endings between encoded lines and
does not append a trailing line ending after the final line.
Named profiles carry the wrapping policy for common protocols:
use ;
assert_eq!;
assert_eq!;
let mut encoded = ;
let written = MIME.encode_slice.unwrap;
assert_eq!;
assert!;
The same policy can be used for strict wrapped decoding. Unlike legacy whitespace decoding, this accepts only the configured line ending and requires every non-final line to have the configured encoded length:
use ;
let wrap = new;
let mut output = ;
let written = STANDARD
.decode_slice_wrapped
.unwrap;
assert_eq!;
Custom Alphabets
User-defined alphabets can be validated before use:
use ;
;
validate_alphabet.unwrap;
assert_eq!;
Built-in non-RFC alphabets are available for explicit interoperability:
use ;
let mut bcrypt = ;
let written = BCRYPT.encode_slice.unwrap;
assert_eq!;
let mut crypt = ;
let written = CRYPT.encode_slice.unwrap;
assert_eq!;
The bcrypt and crypt(3) profiles provide alphabets and no-padding behavior
only. They do not parse or verify complete password-hash strings.
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.
For sensitive payloads, use decode_slice_clear_tail or
decode_in_place_clear_tail to clear unused bytes after the decoded prefix. On
decode error these variants clear the caller-owned output buffer before
returning the error. The legacy whitespace profile also provides
decode_slice_legacy_clear_tail and decode_in_place_legacy_clear_tail.
The ct module provides the same clear-tail decode variants for callers using
the constant-time-oriented scalar decoder.
For short values, encode_buffer returns a stack-backed EncodedBuffer
without requiring the alloc feature:
use ;
let encoded = STANDARD..unwrap;
assert_eq!;
let bcrypt = BCRYPT..unwrap;
assert_eq!;
EncodedBuffer exposes bytes only through as_bytes and as_str, redacts the
payload from Debug, and clears its backing array when dropped as best-effort
data-retention reduction.
When an owned heap buffer is acceptable but accidental logging is not, use
encode_secret and decode_secret:
use STANDARD;
let encoded = STANDARD.encode_secret.unwrap;
assert_eq!;
assert_eq!;
let decoded = STANDARD.decode_secret.unwrap;
assert_eq!;
assert_eq!;
SecretBuffer clears initialized bytes when dropped, but it does not claim
formal zeroization and cannot clean historical copies outside the wrapper or
allocator spare capacity.
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.- Scalar encode/decode remains safe Rust.
- One audited unsafe helper in
src/lib.rsperforms volatile best-effort wiping so cleanup writes are not optimized away. - Future unsafe SIMD remains isolated under
src/simd.rs. - Local checks verify that
allow(unsafe_code)is confined to the volatile wipe helper and SIMD boundary, every unsafe function is inventoried, and every unsafe block has a nearbySAFETY:explanation. Architecture intrinsics, CPU feature detection, and target-feature gates are checked against the same boundary. - docs/UNSAFE.md inventories every current unsafe site and its safety invariants.
- docs/ASYNC.md defines the admission bar for any future
async/Tokio API while the
tokiofeature remains inert. - docs/DEPENDENCIES.md defines the dependency admission bar for any future external crate.
runtime::backend_report()exposes the active backend, detected candidate, SIMD feature status, and scalar-only security posture for audit logging.runtime::require_backend_policy()lets deployments assert scalar execution, disabled SIMD features, or no detected SIMD candidate.BackendPolicy::HighAssuranceScalarOnlycombines the scalar/no-SIMD deployment checks into one assertion.- Runtime backend, posture, and policy enums expose stable string identifiers for CI artifacts, audit logs, and deployment evidence.
- Runtime backend reports and policy failures use stable key/value display output for log ingestion.
- 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. A separate
ctmodule provides a constant-time-oriented scalar validation and decode path for callers that need a narrower timing target. Its malformed-input errors are intentionally non-localized, clear-tail variants clear caller-owned buffers on error, and it is not documented as a formally verified cryptographic constant-time API. - Clear-tail encode/decode variants are available for callers that want best-effort cleanup of unused caller-owned buffers without adding a runtime dependency.
- Streaming wrappers clear internal pending and queued byte buffers on drop and as buffered bytes are consumed, as best-effort retention reduction.
- Legacy compatibility must be opt-in.
- Release gates include formatting, clippy, tests, Miri when installed, docs, dependency policy, audit, license review, isolated fuzz/perf dependency checks, 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 the unsafe hardware
acceleration gate, see docs/SIMD.md.
For the trust dashboard and CWE/security-control mapping, see
docs/TRUST.md and
docs/SECURITY_CONTROLS.md.
For panic-free public API policy, see
docs/PANIC_POLICY.md.
For constant-time-oriented decode verification requirements, see
docs/CONSTANT_TIME.md.
For dependency admission rules, see docs/DEPENDENCIES.md.
For adoption guidance from the established base64 crate, see
docs/MIGRATION.md.
For performance evidence guidance, see docs/BENCHMARKS.md.
For fuzz target and corpus policy, see docs/FUZZING.md.
Local Checks
Run the standard gate:
Check the zero-external-crate policy directly:
Check reserved feature placeholders directly:
Run the release gate:
Install cross-compilation targets used by the local and CI target checks:
Required security tools:
Optional deep tools:
Verify optional tool installation:
Compile fuzz targets without running a campaign:
Validate the committed fuzz corpus policy directly:
Compile and audit the isolated performance harness:
Run the scalar comparison benchmark:
Run a target with cargo-fuzz:
Miri is installed as a nightly Rust component, not as a Cargo package:
Kani may need a one-time setup after installation:
On openSUSE Tumbleweed, install rustup first if it is not already present:
The local release gate runs Miri automatically when rustup run nightly cargo miri is available. scripts/check_miri.sh covers no-default-features scalar
APIs and all-features alloc/stream APIs. 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.