Secure, no_std-first Base64 for Rust.
Strict RFC 4648 decoding, caller-owned buffers, zero runtime dependencies, and release-gated security evidence.
base64-ng is a no_std-first Base64 crate focused on correctness, strict decoding, caller-owned buffers, and a security-heavy release process. Its 2.0 family combines a safe scalar foundation with separately admitted hardware acceleration and optional integration and protocol companions.
Strict RFC 4648 behavior remains the default. Forgiving, wrapped, legacy, and protocol-specific behavior is explicitly named. Streaming is available through the core stream feature and the Tokio companion, fuzz and formal-verification harnesses are isolated from published packages, and SIMD execution is limited to backends with the documented admission evidence.
Zero external runtime or development dependencies in Cargo.toml. Optional
ecosystem dependencies remain isolated in companion packages.
The current STANDARD, STANDARD_NO_PAD, URL_SAFE, and
URL_SAFE_NO_PAD engines implement strict RFC 4648 Base64 behavior.
STANDARD and URL_SAFE require and emit canonical padding;
the explicitly named _NO_PAD engines reject padding and emit none. Strict
decoders reject whitespace, mixed alphabets, impossible lengths, malformed or
forbidden padding, trailing data after padding, and non-canonical unused
trailing bits. Legacy whitespace and MIME/PEM-style line handling are available
only through separately named opt-in APIs.
The test suite includes the RFC 4648 Section 10 vectors. The exact RFC Editor
text, checksum, reviewed errata, and normative requirements mapping are locked
under rfc/ and verified
offline according to the
RFC source policy.
The authoritative 2.0 commit plan and supporting
governance decision define the numbered,
pentest-gated path from the signed 1.3.9 baseline to the single final 2.0.0
release.
The 2.0 API migration ledger and
package topology freeze public renames,
removals, capability edges, and companion boundaries before implementation.
This source tree is the 2.0.0 package-family candidate. The latest
published release remains 1.3.9 until final external review, the
exact-candidate evidence campaign, required green CI, and the signed
v2.0.0 tag are complete.
The candidate includes the complete 2.0 API, synchronized companion crates, and the supported npm Wasm loader. See the release freeze, release notes, and migration guide for the frozen scope and adoption path.
Use the following Git dependency only when testing the exact candidate before the signed release:
base64-ng = { git = "https://github.com/valkyoth/base64-ng", rev = "<reviewed-commit>" }"Project validated" means the named repository tests and evidence gates pass
on the stated environments. It does not mean independent verification,
certification, formal proof, or a portable performance guarantee. A backend is
eligible for safe automatic dispatch only when its row says admitted;
included candidates with incomplete hardware evidence remain unreachable from
normal public dispatch.
| Surface | Implementation | Project execution evidence | Safe automatic dispatch | Independent verification |
|---|---|---|---|---|
| Portable scalar encode and strict decode | Complete | Native x86-64, Apple/AWS AArch64; QEMU s390x, PowerPC64, and RISC-V | admitted |
Not independently verified |
| x86 SSSE3/SSE4.1 and AVX2 encode/decode | Complete | Native x86-64 differential, direct-kernel, assembly, and benchmark gates | admitted |
Not independently verified |
| x86 AVX-512 VBMI encode/decode | Complete | Native AMD AVX-512 VBMI; second Intel performance corroboration is queued for 2.0.1, so no portable throughput claim is made | admitted with conservative exact-host thresholds; strict decode remains exact/static only |
Not independently verified |
| little-endian AArch64 NEON encode/decode | Complete | Prior native Apple M2 and AWS Neoverse-N1 correctness evidence plus assembly and direct-kernel gates; exact-candidate retained performance bundles are required before release | admitted only after the strict release gate accepts those retained bundles |
Not independently verified |
wasm simd128 encode/decode |
Complete | Node/V8, Wasmtime, Chromium/V8, Firefox/SpiderMonkey, and Safari/WebKit package/runtime gates | admitted for the documented SIMD artifact |
Not independently verified |
| RISC-V RVV 1.0 encode/decode | Complete candidate | QEMU VLEN 128/256 plus generated assembly; no accepted native RVV report | not admitted; published execution remains scalar |
Not independently verified |
| AArch64 SVE encode/decode | Complete candidate | QEMU vector lengths 128/256/512 plus generated assembly; no accepted native SVE report | not admitted; public execution remains NEON or scalar |
Not independently verified |
| Constant-time-oriented secret encode/decode | Complete scalar bounded path | Fixed-work tests, Kani, assembly review, and dudect-style project evidence | Separate scalar path; never ordinary SIMD dispatch | No formal or independent constant-time verification |
| Big-endian acceleration | No backend implemented | Complete scalar suites under s390x and PowerPC64 QEMU only | not admitted; scalar only |
No native hardware verification |
The detailed evidence and non-claims are maintained in the Trust Dashboard, SIMD policy, RISC-V review, and SVE review. This table is updated whenever a backend implementation, execution environment, or admission decision changes.
Before 2.0.0, only release assurance remains: final external review, the
exact-candidate evidence campaign, green required CI and CodeQL, the report-only
Commit 55 seal, and the authorized signed tag. No additional runtime feature is
planned for the 2.0 release candidate.
RVV and SVE remain non-dispatchable candidates pending native hardware evidence. Big-endian execution remains scalar. Secret operations remain on the separate scalar fixed-work path. Project tests, Kani harnesses, timing evidence, and QEMU runs are scoped evidence, not certification or whole-crate formal proof.
| Area | Status |
|---|---|
| License | MIT OR Apache-2.0 |
| MSRV | Rust 1.90.0 |
| Active release toolchain | Rust 1.97.1 |
| Runtime dependencies | Zero external crates |
| Unsafe policy | Scalar encode/decode remains safe Rust; audited unsafe is limited to volatile wiping, CT comparison/barrier helpers, and the reviewed SIMD boundary |
| Active backend | Scalar by default; std x86/x86_64 encode selects SSSE3/SSE4.1, AVX2, or AVX-512 VBMI by length, strict decode selects SSSE3/SSE4.1 or AVX2, plus little-endian std aarch64 NEON and wasm simd128 under their admitted profiles; AVX-512 strict decode is exact/static only |
| Strict RFC 4648 decoding | Default, canonical, no whitespace |
| Legacy compatibility | Explicit opt-in APIs |
| Constant-time posture | Constant-time-oriented scalar validation/decode plus bounded 2.0 secret frames with private staging and isolated timing evidence; no formal cryptographic guarantee |
| Cleanup posture | Best-effort initialized-byte cleanup and redacted secret wrappers |
| Kani | 43 normal and 19 release-host advanced harnesses with explicit resource limits on Rust 1.90.0 and cargo-kani 0.67.0; 6 high-cost integrated/wrapped harnesses remain exploratory, and this is not a whole-crate formal-verification claim |
| 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.
The minimum supported Rust version is Rust 1.90.0. New deployments should
prefer the latest tested stable Rust; the 2.0 candidate is released with Rust
1.97.1 while retaining a separate MSRV gate.
The active release toolchain is Rust 1.97.1. MSRV remains Rust 1.90.0 and
is checked separately in CI so the project can build and test with the latest
stable compiler without dropping older supported users.
Compatibility evidence for the 2.0.0 workspace candidate:
| Rust | Local Evidence |
|---|---|
1.90.0 |
✓ MSRV compatibility check |
1.91.0 - 1.97.0 |
✓ cargo check --all-features |
1.97.1 |
✓ active release toolchain and cargo check --all-features |
[dependencies]
base64-ng = "2.0.0"For ordinary public data, the shortest API uses strict RFC 4648 Standard Base64 with canonical padding:
let encoded = base64_ng::encode(b"hello").unwrap();
assert_eq!(encoded, "aGVsbG8=");
let decoded = base64_ng::decode(encoded.as_bytes()).unwrap();
assert_eq!(decoded, b"hello");These convenience functions use normal strict decoding with detailed errors.
They are not the secret-bearing, fixed-work path. For keys, tokens, passwords,
or other secrets, start with the
bounded secret decoder and the secrets
feature. For Serde fields, use
base64-ng-serde instead of writing
a custom serializer.
Choose the narrowest API matching the surrounding contract:
| Need | Start with |
|---|---|
| Ordinary owned Standard Base64 | base64_ng::encode and base64_ng::decode |
| Explicit alphabet or padding policy | A STRICT_* preset with encode_to_string and decode_to_vec |
| Transactional caller-owned buffers | encode_into and decode_into |
| Heapless incremental processing | encoder() and decoder() |
| Secret-bearing data | secret::{SecretArrayFrame, SecretVecFrame} |
| Serde fields | base64-ng-serde |
For example, an explicit strict preset makes the alphabet and padding policy visible at the call site:
use base64_ng::STRICT_STANDARD_PADDED;
let encoded = STRICT_STANDARD_PADDED.encode_to_string(b"hello").unwrap();
assert_eq!(encoded, "aGVsbG8=");
let decoded = STRICT_STANDARD_PADDED.decode_to_vec(encoded.as_bytes()).unwrap();
assert_eq!(decoded, b"hello");When validated encoded text must carry its policy through storage or
transport, use the ordinary Base64String<S> owner:
use base64_ng::{Base64String, STRICT_STANDARD_PADDED};
let stored = Base64String::encode(STRICT_STANDARD_PADDED, b"hello").unwrap();
assert_eq!(stored.as_str(), "aGVsbG8=");
assert_eq!(stored.decode().unwrap(), b"hello");Base64String is printable and non-wiping. It is not a secret container.
base64_ng::prelude provides a focused import set for ordinary 2.0 code; it
deliberately omits secret, compatibility, protocol, and historical APIs.
The historical STANDARD family remains as reviewed compatibility API.
Forgiving web decode, legacy whitespace, line wrapping, and protocol-specific
transforms require separately named opt-in APIs.
The root crate documents and tests each 2.0 capability independently:
| Capability | Primary guide |
|---|---|
| Validated alphabets, sealed codecs, strict presets, and custom policy builders | Codec specifications |
| Error, progress, lifecycle, atomicity, and rollback contracts | Operation contracts |
Transactional caller-owned and allocating one-shot operations, including policy-carrying Base64String storage |
Transactional one-shot |
| Heapless incremental encode and strict padded/unpadded decode | Encoder, padded decoder, finalization |
| Const transforms and fixed-capacity ordinary buffers | Const and bounded buffers |
| Ordinary and staged secret-adjacent in-place transforms | In-place operations |
| Allocation-free formatting, rollback-safe append, and encoded chunk iteration | Formatting, append, and chunks |
| Validated line wrapping and accurately scoped body profiles | Line wrapping, profiles |
| Exact WHATWG forgiving decode and explicitly scoped compatibility policies | Web forgiving Base64, profiles |
| Bounded secret owners, fixed-work encode/decode, and explicit exposure | Secret storage, decode, encode |
| Protected allocations, assurance tokens, operation reports, and teardown | Assurance and protected memory, reporting |
| Runtime backend health, checked execution, quarantine, and dispatch reporting | Backend health, dispatch matrix |
| Synchronous I/O and Tokio async I/O | Synchronous I/O, async overview |
| MIME, PEM, OpenPGP, IMAP, multibase, and password-record protocols | Protocol registry, companion crates |
| Wasm package and runtime loading | Wasm runtime review, loader package |
| Serde, bytes, derive, subtle, sanitization, and Tokio integrations | Companion crates and each package-local README |
The migration guide contains compiled examples for the canonical one-shot, incremental, in-place, compatibility, secret, streaming, and companion boundaries. Each companion crate also carries a package-local README and runnable examples for its complete public scope.
The crate is dual-licensed:
license = "MIT OR Apache-2.0"| 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 | Admitted std runtime-dispatched or compile-time-proven no_std encode and strict-decode acceleration for Standard and URL-safe alphabets, with KAT/quarantine and scalar fallback. |
stream |
no | std::io streaming wrappers. |
secrets |
no | Dependency-free 2.0 secret storage, explicit exposure/declassification, bounded constant-time-oriented transforms, and generation-bound assurance/protected-memory APIs. |
checked-backend |
no | Enables simd plus bounded scalar/SIMD comparison, permanent process quarantine on mismatch, and one scalar retry without exposing suspect chunks. |
allow-wasm32-best-effort-wipe |
no | Explicitly allow wasm32 secrets builds with compiler-fence-only cleanup; ordinary codecs do not need it. |
allow-compiler-fence-only-wipe |
no | Explicitly allow secrets builds on unsupported native architectures with compiler-fence-only cleanup after platform review. |
tokio |
no | Reserved placeholder in the core crate; currently inert and dependency-free. Use base64-ng-tokio for the admitted async helper and streaming adapter surface. |
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. |
High-assurance deployments can build with
RUSTFLAGS="--cfg base64_ng_require_high_assurance". This custom cfg is not a
Cargo feature, so normal --all-features evidence and docs.rs builds remain
usable. It establishes build eligibility only. Assured 2.0 operations also
require a runtime assurance token and an allocation-specific protected owner.
The simd feature may coexist for ordinary APIs; assured secret operations
remain scalar. Use BackendPolicy::HighAssuranceScalarOnly when the entire
process must reject ordinary SIMD.
The core base64-ng crate keeps its zero-runtime-dependency policy. Optional
ecosystem integrations live as separate crates so applications can opt into
their own approved dependency set without changing the base package.
The 2.0.0 family syncs all companion crates to the same version so docs.rs
and crates.io examples resolve consistently across the workspace.
| Crate | Purpose |
|---|---|
base64-ng |
Stable zero-runtime-dependency facade crate and primary user entry point. |
base64-ng-sanitization |
Optional sanitization integration with native Choice comparison helpers and opt-in locked secret decode helpers. |
base64-ng-derive |
Dependency-free Base64Secret derive with sealed codec, staged decode, exact length, and opt-in exposure policy. |
base64-ng-serde |
Optional serde wrappers for projects that already admit serde. |
base64-ng-bytes |
Optional bytes helpers for Bytes, Buf, and BufMut users. |
base64-ng-subtle |
Sealed subtle::ConstantTimeEq integration for final 2.0 secret owners and token/MAC comparison boundaries. |
base64-ng-tokio |
Optional Tokio read-all/write-all helpers and async reader/writer streaming adapters. |
base64-ng-imap |
Bounded legacy RFC 3501 Section 5.1.3 modified-Base64 payload transforms over already-converted UTF-16BE bytes; not a complete mailbox codec. |
base64-ng-mime |
Bounded RFC 2045 Section 6.8 Base64 content-transfer body encoding and decoding; not a MIME message or header parser. |
base64-ng-multibase |
Bounded strict support for the four registered Base64-family multibase prefixes; not a complete open-world multibase registry. |
base64-ng-password |
Bounded Passlib PBKDF2 and SHA-crypt field/record transforms with exact checksum permutations; never hashes or verifies passwords. |
base64-ng-openpgp |
Bounded complete RFC 9580 ordinary ASCII armor parser and generator with explicit CRC-24 policy and opt-in secret payload release. |
base64-ng-pem |
Bounded complete RFC 7468 textual encoding parser and generator with labels, boundaries, multiple blocks, and opt-in secret payload release. |
base64-ng-wasm-loader |
Supported byte-only JavaScript/npm loader with separately selected scalar and simd128 artifacts. |
Subcrates are documented so crate pages are readable, but they belong to the
main base64-ng crate family and are not intended as independent protocol
products. Package versions and crates.io links are tracked in
Crate Version Matrix so releases can publish
only the crates that changed instead of republishing the whole ecosystem.
The 2.0 JavaScript companion selects an artifact with an embedded SIMD probe before instantiation and reports the selected posture:
npm install base64-ng-wasm-loaderimport { Codecs, createBase64Ng } from "base64-ng-wasm-loader";
const base64 = await createBase64Ng();
const input = new TextEncoder().encode("hello");
const encoded = base64.encode(input, Codecs.URL_SAFE_NO_PAD);
const decoded = base64.decode(encoded, Codecs.URL_SAFE_NO_PAD);
base64.dispose();It accepts bytes only, snapshots input, commits *Into destinations only after
success, rejects shared/resizable/detached/overlapping storage, and exposes no
secret API or wasm-memory views. See
packages/base64-ng-wasm-loader/README.md.
base64-ng-sanitization provides extension helpers for
base64_ng::ct::CtEngine that decode directly into
sanitization::SecretBytes<N> in no_std, with SecretVec helpers behind its
own alloc feature. The 2.0.0 companion uses exact-pinned
sanitization =2.0.3 and exposes sanitization::ct::Choice comparison
helpers through SanitizationCtEqExt. Locked containers additionally expose
fallible integrity-checked comparison through LockedSanitizationCtEqExt.
Heap-backed convenience decode has a 1 MiB default ceiling, reports reservation
failure, and offers const-generic bounded variants for protocol limits.
Stack-backed fixed and staged helpers reject capacities above 1,024 bytes at
compile time.
Built-in checked fixed-size and dynamic decode establish required memory-lock,
dump, and fork controls before plaintext materialization. The 2.0 trait has no
post-construction compatibility defaults: external implementations must define
every locked checked/fill method explicitly. The protected extension API
preserves protection versus canary-integrity failures and offers a bounded
dynamic helper that rejects oversized decoded capacity before mapping
allocation.
The 2.0 companion additionally implements
SanitizationProtectedDecodeExt for Base64<S>. Its fixed and bounded dynamic
methods establish separate protected staging and destination mappings before
the fixed-work SecretFrame sees input. The single-allocation no-copy route is
Base64::decode_assured; only that core provider path carries the quarantine,
generation, and fallible-teardown claims from the 2.0 assurance model. See
docs/2.0_SANITIZATION_PROTECTED_FILL.md.
Enable the companion's
high-assurance feature to
decode directly into sanitization::LockedSecretBytes or
sanitization::LockedSecretVec on supported x86_64 or AArch64 native targets,
using
sanitization memory locking plus strict random-canary and assembly-comparison
checks:
[dependencies]
base64-ng = { version = "2.0.0", default-features = false }
base64-ng-sanitization = { version = "2.0.0", default-features = false }use base64_ng::ct;
use base64_ng_sanitization::{CtDecodeSanitizationExt, SanitizationCtEqExt};
let secret = ct::STANDARD
.decode_secret_bytes::<5>(b"aGVsbG8=")
.unwrap();
assert!(secret.sanitization_verify(
b"hello",
"example compares public expected bytes"
));For attacker-controlled dynamic input, enable alloc and make the public
decoded-output ceiling explicit:
use base64_ng::ct;
use base64_ng_sanitization::CtDecodeSanitizationBoundedExt;
let secret = ct::STANDARD
.decode_secret_vec_bounded::<4096>(b"aGVsbG8=")
.unwrap();
secret.with_secret(|bytes| assert_eq!(bytes, b"hello"));[dependencies]
base64-ng-sanitization = { version = "2.0.0", features = ["high-assurance"] }use base64_ng::ct;
use base64_ng_sanitization::{CtDecodeSanitizationExt, LockedSanitizationCtEqExt};
let locked = ct::STANDARD
.decode_locked_secret_bytes_checked::<5>(b"aGVsbG8=")
.unwrap();
locked
.try_expose_secret(|bytes| assert_eq!(bytes, b"hello"))
.unwrap();
assert!(locked
.try_sanitization_verify(
b"hello",
"example authentication decision is public"
)
.unwrap());high-assurance selects compiled hardening controls. Fixed-size checked decode
and the built-in dynamic checked decode require memory-lock, dump, and fork
controls before plaintext materialization. Inspect protection_report() before
relying on non-checked compatibility helpers. Import
CtDecodeSanitizationProtectedExt when incident handling must distinguish
protection setup from canary corruption, or when dynamic output needs a
compile-time decoded-capacity limit.
base64-ng-derive provides a dependency-free Base64Secret derive for tuple
newtypes around the final fixed-size 2.0 secret owner:
[dependencies]
base64-ng = { version = "2.0.0", default-features = false, features = ["secrets"] }
base64-ng-derive = "2.0.0"use base64_ng::secret::SecretInput;
use base64_ng_derive::Base64Secret;
#[derive(Base64Secret)]
#[base64_ng(
alphabet = "standard",
padding = "padded",
exact_length = 5,
exposure = "read"
)]
struct ApiKey(base64_ng::secret::SecretArray<5>);
let input = SecretInput::new(b"aGVsbG8=");
let key = ApiKey::decode_base64(&input).unwrap();
assert_eq!(key.expose_secret().as_bytes(), b"hello");
assert_eq!(
key.encode_base64().unwrap().expose_secret().as_bytes(),
b"aGVsbG8="
);The derive requires all codec, padding, exact-length, and exposure choices at
the declaration. It generates no ordinary string parsing, implicit slice
conversion, cloning, or equality traits. See
2.0_DERIVE_HARDENING.md.
base64-ng-serde provides explicit serialization wrappers without admitting
serde into the core package. Its 2.0 adapters use validated codec
specifications, borrowed encoded input where the format permits it, and
explicit string-versus-byte-string serializer behavior:
[dependencies]
base64-ng-serde = { version = "2.0.0", features = ["secrets"] }
serde = { version = "1.0.229", features = ["derive"] }#[derive(serde::Serialize, serde::Deserialize)]
struct Message {
#[serde(with = "base64_ng_serde::standard")]
payload: Vec<u8>,
}Field-level modules are available for standard, standard_no_pad,
url_safe, url_safe_no_pad, mime, and pem. Matching bounded::*
modules decode into fixed-capacity DecodedArray<CAP> values. Allocating
compatibility adapters enforce a 1 MiB decoded default; every ordinary field
module also offers deserialize_with_limit::<D, MAX>, while stack-backed
adapters cap CAP at 4096 bytes. Derived encoded-input ceilings run before
full validation. The optional secret::* modules decode strict Standard and
URL-safe fields through fixed-work frames into wiping SecretArray<CAP>
storage. General Serde format parsing and any parser-owned allocation remain
outside these boundaries.
base64-ng-bytes provides fragment-preserving Bytes, Buf, and BufMut
helpers over the sealed 2.0 codec. Owned results are transactional, while
stateful arbitrary-BufMut adapters report exact committed prefixes:
[dependencies]
base64-ng = "2.0.0"
base64-ng-bytes = "2.0.0"
bytes = "1.12.1"use base64_ng::STRICT_STANDARD_PADDED;
use base64_ng_bytes::Base64BytesExt;
use bytes::Bytes;
let encoded = STRICT_STANDARD_PADDED
.encode_buf(Bytes::from_static(b"hello"))
.unwrap();
assert_eq!(&encoded[..], b"aGVsbG8=");base64-ng-subtle provides a sealed subtle::ConstantTimeEq integration for
final 2.0 secret owners and views in projects that already admit subtle:
[dependencies]
base64-ng = { version = "2.0.0", features = ["secrets"] }
base64-ng-subtle = "2.0.0"use base64_ng::{
STRICT_STANDARD_PADDED,
secret::{SecretArrayFrame, SecretInput},
};
use base64_ng_subtle::SubtleSecretEq;
let mut frame = SecretArrayFrame::<5>::new(&STRICT_STANDARD_PADDED).unwrap();
frame.update(&SecretInput::new(b"aGVsbG8=")).unwrap();
let decoded = frame.finish().unwrap();
assert!(bool::from(decoded.subtle_ct_eq_public_len(b"hello")));Length mismatch is public and returns Choice::from(0) immediately. The
companion intentionally provides no boolean convenience method.
base64-ng-tokio provides read-all async helpers and fixed-buffer
AsyncRead/AsyncWrite adapters over the shared 2.0 incremental core. Prefer
EncoderReader::new_exact or DecoderReader::new_exact for a framed source
whose adjacent bytes must remain unread. The transactional *_limited
read-all helpers may consume one overflow lookahead byte and wipe their private
allocations on return, error, and cancellation. Writer adapters retain accepted
input across backpressure; call shutdown to finalize tails before checked
inner recovery. Read-all collection, incremental transformation, and output
delivery consume Tokio cooperative budget between bounded chunks so an
always-ready custom I/O object cannot indefinitely monopolize a runtime worker.
Unlimited helpers still require a trusted finite source:
[dependencies]
base64-ng = "2.0.0"
base64-ng-tokio = "2.0.0"
tokio = { version = "1.53.1", features = ["io-util"] }use base64_ng::STRICT_STANDARD_PADDED;
use base64_ng_tokio::{encode_reader_to_writer_limited, EncoderReader, EncoderWriter};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
# async fn example() -> std::io::Result<()> {
let mut input = &b"hello"[..];
let mut output = Vec::new();
encode_reader_to_writer_limited(
&STRICT_STANDARD_PADDED,
&mut input,
&mut output,
1024,
).await?;
assert_eq!(output, b"aGVsbG8=");
let mut reader = EncoderReader::new_exact(&b"helloNEXT"[..], &STRICT_STANDARD_PADDED, 5);
let mut streamed = Vec::new();
reader.read_to_end(&mut streamed).await?;
assert_eq!(streamed, b"aGVsbG8=");
let mut writer = EncoderWriter::new(Vec::new(), &STRICT_STANDARD_PADDED);
writer.write_all(b"hello").await?;
writer.shutdown().await?;
assert_eq!(writer.into_inner(), b"aGVsbG8=");
# Ok(())
# }Disable defaults for embedded or freestanding use:
[dependencies]
base64-ng = { version = "2.0.0", default-features = false }Enable admitted encode acceleration on supported std targets with the
simd feature. The public encode APIs do not change; runtime dispatch selects
an admitted backend only when the CPU and input shape match the admission
scope, otherwise scalar encode is used. The same feature enables admitted
strict decode acceleration for Standard and URL-safe alphabets after
whole-input scalar validation:
[dependencies]
base64-ng = { version = "2.0.0", features = ["simd"] }use base64_ng::{runtime, STANDARD};
let encoded = STANDARD.encode_string(b"hello").unwrap();
assert_eq!(encoded, "aGVsbG8=");
let decoded = STANDARD.decode_vec(encoded.as_bytes()).unwrap();
assert_eq!(decoded, b"hello");
let report = runtime::backend_report();
println!("encode: {}", report.encode_backend.backend);
println!("strict decode: {}", report.strict_decode_backend.backend);
println!("secret decode: {}", report.secret_decode_backend.backend);
println!("Wasm artifact: {}", report.wasm_artifact_posture.as_str());
println!("Wasm runtime: {}", report.wasm_runtime_posture.as_str());use base64_ng::STRICT_STANDARD_PADDED;
let input = b"hello";
let mut encoded = [0u8; 8];
let written = STRICT_STANDARD_PADDED
.encode_into(input, &mut encoded)
.unwrap();
assert_eq!(&encoded[..written], b"aGVsbG8=");
let mut decoded = [0u8; 5];
let written = STRICT_STANDARD_PADDED
.decode_into(&encoded, &mut decoded)
.unwrap();
assert_eq!(&decoded[..written], input);In-place encoding:
use base64_ng::STRICT_STANDARD_PADDED;
let mut buffer = [0u8; 8];
buffer[..5].copy_from_slice(b"hello");
let encoded_len = STRICT_STANDARD_PADDED
.encode_in_place(&mut buffer, 5)
.unwrap();
assert_eq!(&buffer[..encoded_len], b"aGVsbG8=");Canonical encode_into and decode_into are transactional: every returned
error leaves the complete destination unchanged. Incremental and in-place APIs
instead report exact committed progress; choose the contract that matches the
surrounding protocol.
Heapless incremental encoding:
use base64_ng::{Status, STRICT_STANDARD_PADDED};
let mut state = STRICT_STANDARD_PADDED.encoder();
let mut output = [0u8; 8];
let step = state.update(b"hello", &mut output).unwrap();
let mut written = step.progress().output_produced();
let final_step = state.finish(&mut output[written..]).unwrap();
written += final_step.progress().output_produced();
assert_eq!(final_step.status(), Status::Complete);
assert_eq!(&output[..written], b"aGVsbG8=");Heapless incremental strict decoding uses the same progress contract:
use base64_ng::{Status, STRICT_STANDARD_PADDED};
let mut state = STRICT_STANDARD_PADDED.decoder();
let mut output = [0u8; 5];
let step = state.update(b"aGVsbG8=", &mut output).unwrap();
let mut written = step.progress().output_produced();
let final_step = state.finish(&mut output[written..]).unwrap();
written += final_step.progress().output_produced();
assert_eq!(final_step.status(), Status::Complete);
assert_eq!(&output[..written], b"hello");Allocation-free formatting, append rollback, and chunk iteration:
use base64_ng::STRICT_STANDARD_PADDED;
let display = STRICT_STANDARD_PADDED.display(b"hello").unwrap();
assert_eq!(format!("{display}"), "aGVsbG8=");
let mut appended = String::from("prefix:");
STRICT_STANDARD_PADDED
.encode_append(b"hello", &mut appended)
.unwrap();
assert_eq!(appended, "prefix:aGVsbG8=");
let chunks = STRICT_STANDARD_PADDED
.encoded_chunks(b"hello")
.unwrap()
.map(|chunk| chunk.as_bytes().to_vec())
.collect::<Vec<_>>();
assert_eq!(chunks, [b"aGVs".as_slice(), b"bG8="].map(<[u8]>::to_vec));Compile-time encoding:
use base64_ng::{STRICT_STANDARD_PADDED, STRICT_URL_SAFE_UNPADDED};
const HELLO: [u8; 8] = match STRICT_STANDARD_PADDED.encode_array(b"hello") {
Ok(output) => output,
Err(_) => panic!("reviewed const encode failed"),
};
const URL_BYTES: [u8; 3] = match STRICT_URL_SAFE_UNPADDED.encode_array(b"\xfb\xff") {
Ok(output) => output,
Err(_) => panic!("reviewed const encode failed"),
};
assert_eq!(&HELLO, b"aGVsbG8=");
assert_eq!(&URL_BYTES, b"-_8");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 returns ConstTransformError and can fail during
const evaluation when matched as above.
Use encode_array for fixed-size static values, not for runtime data whose
size is controlled by an attacker.
Compile-time strict decoding:
use base64_ng::{STRICT_STANDARD_PADDED, STRICT_URL_SAFE_UNPADDED};
const HELLO: [u8; 5] = match STRICT_STANDARD_PADDED.decode_array(b"aGVsbG8=") {
Ok(output) => output,
Err(_) => panic!("reviewed const decode failed"),
};
const URL_BYTES: [u8; 2] = match STRICT_URL_SAFE_UNPADDED.decode_array(b"-_8") {
Ok(output) => output,
Err(_) => panic!("reviewed const decode failed"),
};
assert_eq!(&HELLO, b"hello");
assert_eq!(&URL_BYTES, b"\xfb\xff");decode_array is strict and returns Result for malformed input, padding
errors, and undersized output arrays. It is useful for fixed static Base64
literals and does not replace the ct APIs for secret-bearing decode.
For runtime values with compile-time capacity ceilings, use ordinary bounded arrays:
use base64_ng::STRICT_STANDARD_PADDED;
let encoded = STRICT_STANDARD_PADDED
.encode_bounded::<8>(b"hello")
.unwrap();
let decoded = STRICT_STANDARD_PADDED
.decode_bounded::<5>(encoded.as_bytes())
.unwrap();
assert_eq!(decoded.as_bytes(), b"hello");For untrusted length metadata, use checked length calculation:
use base64_ng::{
LineEnding, LineWrap, checked_encoded_len, checked_wrapped_encoded_len, decoded_len,
};
assert_eq!(checked_encoded_len(5, true), Some(8));
assert_eq!(
checked_wrapped_encoded_len(5, true, LineWrap::new(4, LineEnding::Lf)),
Some(9)
);
assert_eq!(decoded_len(b"aGVsbG8=", true).unwrap(), 5);Use validation-only APIs when a protocol needs to sanitize input before storing, routing, or accounting for it:
use base64_ng::{STRICT_STANDARD_PADDED, STRICT_URL_SAFE_UNPADDED};
STRICT_STANDARD_PADDED.validate(b"aGVsbG8=").unwrap();
assert!(STRICT_STANDARD_PADDED.validate(b"aGVsbG8").is_err());
STRICT_URL_SAFE_UNPADDED.validate(b"-_8").unwrap();
assert!(STRICT_URL_SAFE_UNPADDED.validate(b"+/8").is_err());For line-wrapped or spaced legacy inputs, use the explicit legacy profile:
use base64_ng::{STRICT_STANDARD_PADDED, legacy};
let mut decoded = [0u8; 5];
let written = legacy::ASCII_WHITESPACE
.decode_into(
&STRICT_STANDARD_PADDED,
b" aG\r\nVs\tbG8= ",
&mut decoded,
)
.unwrap();
assert_eq!(&decoded[..written], b"hello");Exact WHATWG forgiving decode is a separate ordinary policy:
use base64_ng::{STRICT_STANDARD_PADDED, web};
assert_eq!(web::FORGIVING.decode_to_vec(" Z h = = ").unwrap(), b"f");
assert!(STRICT_STANDARD_PADDED.decode_to_vec(b" Z h = = ").is_err());Forgiving and legacy policies are intentionally unavailable to secret frames.
Use LineWrap when a protocol needs MIME/PEM-style line lengths:
use base64_ng::{LineEnding, LineWrap, STANDARD};
let wrap = LineWrap::new(4, LineEnding::Lf);
let mut output = [0u8; 9];
let written = STANDARD
.encode_slice_wrapped(b"hello", &mut output, wrap)
.unwrap();
assert_eq!(&output[..written], b"aGVs\nbG8=");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. LineEnding
exposes name(), Display, as_str(), as_bytes(), and byte_len() for
allocation-free policy inspection. name() and Display return printable
identifiers such as LF and CRLF; as_str() returns the literal line-ending
bytes. LineWrap exposes line_len(), line_ending(), and is_valid() for
const-friendly policy checks and implements Display as line_len:name, for
example 76:CRLF. LineWrap::new rejects zero line lengths; use
LineWrap::checked_new when wrapping policy comes from configuration.
Named profiles carry the wrapping policy for common protocols:
use base64_ng::{LineEnding, MIME, PEM};
assert_eq!(MIME.line_wrap().unwrap().line_len, 76);
assert_eq!(MIME.line_len(), Some(76));
assert_eq!(MIME.line_ending(), Some(LineEnding::CrLf));
assert_eq!(MIME.to_string(), "padded=true wrap=76:CRLF");
assert_eq!(PEM.line_wrap().unwrap().line_len, 64);
assert_eq!(PEM.line_len(), Some(64));
let mut encoded = [0u8; 82];
let written = MIME.encode_slice(&[0x5a; 58], &mut encoded).unwrap();
assert_eq!(&encoded[76..78], b"\r\n");
assert!(MIME.validate(&encoded[..written]));An engine can also be promoted explicitly to an unwrapped profile when a common configuration path expects profile values, or to the matching constant-time-oriented decoder when sensitive decode policy is required:
use base64_ng::STANDARD;
let profile = STANDARD.profile();
let ct_decoder = STANDARD.ct_decoder();
assert!(profile.is_padded());
assert!(!profile.is_wrapped());
assert_eq!(ct_decoder.decoded_len(b"aGVsbG8=").unwrap(), 5);The 2.0 secret surface keeps classified encoding in a separate bounded scalar state and wiping owner:
use base64_ng::{STRICT_STANDARD_PADDED, secret::SecretInput};
let input = SecretInput::new(b"secret");
let encoded = STRICT_STANDARD_PADDED
.encode_secret_array::<8>(&input)
.unwrap();
assert_eq!(encoded.expose_secret().as_bytes(), b"c2VjcmV0");Built-in secret alphabets use arithmetic mapping; validated custom alphabets
use a fixed 64-entry scan. Encoded output stays redacted and wiping until the
caller explicitly exposes or declassifies it. See
docs/2.0_SECRET_ENCODING.md.
Allocation-specific assured operations require both a token and one protected owner; an ordinary mutable slice cannot substitute for that owner:
use base64_ng::{
STRICT_STANDARD_PADDED,
assurance::{AssuranceContext, BestEffortProvider, ProtectedSecret, ProviderLimits},
secret::SecretInput,
};
let context = AssuranceContext::new();
let token = context.best_effort_token();
let provider = BestEffortProvider::<1>::new(ProviderLimits {
max_identities: 1,
max_logical_bytes: 8,
max_effective_pages: 2,
max_registry_entries: 1,
max_retry_attempts: 1,
max_maintenance_work: 1,
page_size: 8,
})?;
let allocation = ProtectedSecret::try_new(&provider, &token, 8)?;
let encoded = STRICT_STANDARD_PADDED.encode_assured(
&token,
allocation,
&SecretInput::new(b"secret"),
)?;
assert_eq!(encoded.expose_secret().as_bytes(), b"c2VjcmV0");
let operation = encoded.operation_report(&token)?;
assert_eq!(operation.snapshot().operation, "secret-encode");
assert_eq!(operation.snapshot().wipe, "wipe-not-completed");
let cleanup = encoded.try_close()?;
assert_eq!(cleanup.snapshot().lifecycle, "closed");
# Ok::<(), Box<dyn std::error::Error>>(())The dependency-free default provider is finite, volatile, and best-effort. It
does not lock pages or provide persistent teardown recovery. Reviewed deployed
providers and attested tokens use an explicit unsafe extension boundary. See
docs/2.0_ASSURANCE_AND_PROTECTED_MEMORY.md.
Per-operation backend, token, allocation, and teardown reporting is documented
in docs/2.0_OPERATION_REPORTING.md.
When wrapping policy comes from configuration, prefer checked construction.
Use Engine::checked_profile_with_wrap when the profile should use the same
engine and only the wrapping policy is dynamic:
use base64_ng::{LineEnding, LineWrap, STANDARD};
let wrap = LineWrap::checked_new(76, LineEnding::CrLf).unwrap();
let profile = STANDARD.checked_profile_with_wrap(wrap).unwrap();
assert!(profile.is_valid());
assert!(profile.is_wrapped());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 base64_ng::{LineEnding, LineWrap, STANDARD};
let wrap = LineWrap::new(4, LineEnding::Lf);
let mut output = [0u8; 5];
let written = STANDARD
.decode_slice_wrapped(b"aGVs\nbG8=", &mut output, wrap)
.unwrap();
assert_eq!(&output[..written], b"hello");
let encoded = STANDARD.encode_wrapped_buffer::<9>(b"hello", wrap).unwrap();
assert_eq!(encoded.as_bytes(), b"aGVs\nbG8=");
let decoded = STANDARD
.decode_wrapped_buffer::<5>(encoded.as_bytes(), wrap)
.unwrap();
assert_eq!(decoded.as_bytes(), b"hello");New 2.0 code validates and owns custom alphabets before constructing a sealed runtime codec:
use base64_ng::CodecBuilder;
let codec = CodecBuilder::from_table(
*b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
)
.unwrap()
.build()
.unwrap();
let encoded = codec.encode_to_string(b"hello").unwrap();
assert_eq!(codec.decode_to_vec(encoded.as_bytes()).unwrap(), b"hello");ValidatedAlphabet and CodecBuilder reject duplicate, forbidden, padded, or
wrong-length tables before a codec exists. For custom tables, secret operations
use a deliberately conservative fixed 64-entry scan for every emitted or
decoded symbol. Benchmark this tradeoff before using custom alphabets for
untrusted high-volume traffic.
The historical define_alphabet! and Alphabet trait remain available only as
reviewed 1.x compatibility surfaces. New code should use the validated 2.0
value so policy construction and ownership are explicit.
Built-in non-RFC alphabets are available for explicit interoperability:
use base64_ng::{BCRYPT, CRYPT};
let mut bcrypt = [0u8; 4];
let written = BCRYPT.encode_slice(&[0xff, 0xff, 0xff], &mut bcrypt).unwrap();
assert_eq!(&bcrypt[..written], b"9999");
let mut crypt = [0u8; 4];
let written = CRYPT.encode_slice(&[0xff, 0xff, 0xff], &mut crypt).unwrap();
assert_eq!(&crypt[..written], b"zzzz");The bcrypt and crypt(3) profiles provide alphabets and no-padding behavior
only. They do not parse or verify complete password-hash strings.
Strict decoding rejects whitespace. If an existing protocol allows line-wrapped or spaced Base64, use the explicit legacy APIs:
use base64_ng::STANDARD;
let mut output = [0u8; 5];
let written = STANDARD
.decode_slice_legacy(b" aG\r\nVs\tbG8= ", &mut output)
.unwrap();
assert_eq!(&output[..written], b"hello");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.
For untrusted payloads, size buffers before decoding or encoding. The checked helpers let callers reject impossible or oversized metadata before allocating:
use base64_ng::{STANDARD, checked_encoded_len, decoded_capacity};
let input = b"hello";
let encoded_len = checked_encoded_len(input.len(), true).unwrap();
assert_eq!(encoded_len, 8);
let mut encoded = vec![0u8; encoded_len];
let written = STANDARD.encode_slice(input, &mut encoded).unwrap();
encoded.truncate(written);
let max_decoded = decoded_capacity(encoded.len());
let mut decoded = vec![0u8; max_decoded];
let written = STANDARD.decode_slice(&encoded, &mut decoded).unwrap();
decoded.truncate(written);
assert_eq!(decoded, input);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, decode_in_place_legacy_clear_tail, and
decode_buffer_legacy. Strict line-wrapped profiles provide
decode_in_place_wrapped, decode_in_place_wrapped_clear_tail, and the same
in-place behavior through Profile::decode_in_place. The ct module provides
the same clear-tail decode variants for callers using the constant-time-oriented
scalar decoder, ct::CtEngine::decoded_len for sizing caller-owned buffers
under the same opaque malformed-input policy, plus
ct::CtEngine::decode_buffer for stack-backed no-alloc decoded output.
For constant-time-oriented in-place decode, prefer
ct::CtEngine::decode_in_place_clear_tail. The non-clear-tail CT in-place API
was removed before the 1.0 stable boundary because failed in-place decode can
partially destroy the encoded input and retain decoded plaintext in the same
buffer. If the encoded token must be logged or retried after failure, keep a
separate copy before any in-place decode.
The default strict decoders are not constant-time decoders: they preserve exact
error indexes and may return early for malformed input, padding, length, or
output-size errors. Use base64_ng::ct for secret-bearing payloads where decode
timing posture matters more than localized error diagnostics.
Do not use STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD,
MIME, PEM, BCRYPT, or CRYPT as token-comparison or key-material decode
APIs when the encoded bytes or rejection reason are sensitive. Use
ct::STANDARD, ct::URL_SAFE_NO_PAD, or STANDARD.ct_decoder() instead and
perform any final token comparison with a constant-time-oriented comparison
appropriate for the protocol.
For reusable secret output buffers, use ct::CtEngine::decode_slice_clear_tail
or ct::CtEngine::decode_buffer. The non-clear-tail CT slice API was removed
before the 1.0 stable boundary because it can leave real decoded plaintext
from valid leading quanta in output when later malformed input is rejected
after the fixed-shape decode pass.
For shared-memory, HSM-adjacent, sandboxed, or other multi-principal threat
models where even transient writes to caller-owned output are unacceptable, use
ct::CtEngine::decode_slice_staged_clear_tail with a private staging buffer.
This staged API should be the default for enclave-adjacent code, shared memory,
or any service where another principal could observe the public output buffer
during the decode call. decode_slice_clear_tail wipes on error before it
returns, but the CT loop still writes decoded bytes before the final error gate.
For short values, encode_buffer returns a stack-backed EncodedBuffer
and decode_buffer returns a stack-backed DecodedBuffer without requiring
the alloc feature:
use base64_ng::{BCRYPT, MIME, STANDARD};
let encoded = STANDARD.encode_buffer::<8>(b"hello").unwrap();
assert_eq!(encoded.as_str(), "aGVsbG8=");
assert_eq!(encoded.as_utf8().unwrap(), "aGVsbG8=");
assert_eq!(encoded.to_string(), "aGVsbG8=");
let decoded = STANDARD.decode_buffer::<5>(encoded.as_bytes()).unwrap();
assert_eq!(decoded.as_bytes(), b"hello");
let bcrypt = BCRYPT.encode_buffer::<4>(&[0xff, 0xff, 0xff]).unwrap();
assert_eq!(bcrypt.as_bytes(), b"9999");
let wrapped = MIME.encode_buffer::<82>(&[0x5a; 58]).unwrap();
let decoded = MIME.decode_buffer::<58>(wrapped.as_bytes()).unwrap();
assert_eq!(decoded.as_bytes(), &[0x5a; 58]);EncodedBuffer exposes bytes only through as_bytes, fallible as_utf8, and
as_str, and implements Display for allocation-free formatting of encoded
Base64 text. That Display implementation emits the full Base64 payload; do
not use EncodedBuffer for encoded secrets that may reach logs or error
messages.
DecodedBuffer exposes bytes through as_bytes and provides a fallible
as_utf8 view for decoded text. Both expose is_full() and
remaining_capacity() for no-alloc sizing checks, redact the payload from
Debug, clear their backing arrays when dropped as best-effort data-retention
reduction, and provide explicit equal-length comparison through
constant_time_eq_public_len. They intentionally do not
implement PartialEq/==: the helper is a dependency-free best-effort
comparison, not a formal cryptographic token/MAC comparison primitive. Length
mismatch returns immediately and must be treated as public protocol
information. Applications that require a formally audited comparison should
admit that dependency at the application boundary, for example by comparing
exposed bytes with subtle. Do not use these helpers as the sole MAC,
bearer-token, password-hash, or authentication-secret comparison primitive in
high-assurance systems.
into_exposed_array is the explicit no-alloc ownership escape hatch for both
stack-backed buffers. It returns ExposedEncodedArray or
ExposedDecodedArray, keeping redacted formatting and best-effort drop-time
cleanup after ownership transfer. If a bare array is unavoidable, call
into_exposed_unprotected_array_caller_must_zeroize; cleanup then becomes the
caller responsibility.
Stack-backed buffers clear their backing arrays when dropped, but they cannot
clear historical stack-frame copies made by the compiler, caller code, panic
machinery, or operating system crash capture. For highly sensitive payloads,
prefer the clear-tail APIs as soon as the value is no longer needed, keep
secret lifetimes short, and combine crate-level cleanup with process policies
for locked memory, encrypted or disabled swap and hibernation, core dumps,
crash reporting, and allocator isolation for secret regions.
Cloning EncodedBuffer or DecodedBuffer creates a second live copy; avoid
cloning secret material unless the duplicate lifetime is explicitly accounted
for.
On wasm32, the wipe barrier uses only a compiler fence; the wasm runtime JIT
may still optimize or retain cleared bytes outside the crate's control.
Ordinary public-data builds require no cleanup opt-in. Enable secrets when
requesting the 2.0 secret capability; on WASM that capability also requires
allow-wasm32-best-effort-wipe. Secret builds on unsupported native targets
similarly require allow-compiler-fence-only-wipe after platform review.
Neither acknowledgement is a high-assurance upgrade.
When an owned heap buffer is acceptable but accidental logging is not, use
encode_secret and decode_secret:
use base64_ng::STANDARD;
let encoded = STANDARD.encode_secret(b"hello").unwrap();
assert_eq!(encoded.expose_secret(), b"aGVsbG8=");
assert_eq!(format!("{encoded:?}"), r#"SecretBuffer { bytes: "<redacted>", len: 8 }"#);
let decoded = STANDARD.decode_secret(encoded.expose_secret()).unwrap();
assert_eq!(decoded.expose_secret(), b"hello");
assert!(decoded.constant_time_eq_public_len(b"hello"));
assert_eq!(format!("{decoded}"), "<redacted>");
let wrapped = STANDARD
.encode_wrapped_secret(b"hello", base64_ng::LineWrap::PEM)
.unwrap();
let unwrapped = STANDARD
.decode_wrapped_secret(wrapped.expose_secret(), base64_ng::LineWrap::PEM)
.unwrap();
assert_eq!(unwrapped.expose_secret(), b"hello");
let legacy = STANDARD
.decode_secret_legacy(b" aG\r\nVs\tbG8= ")
.unwrap();
assert_eq!(legacy.expose_secret(), b"hello");
let decoded = base64_ng::SecretBuffer::try_from("aGVsbG8=").unwrap();
assert_eq!(decoded.expose_secret(), b"hello");SecretBuffer conversion traits use the normal strict STANDARD decoder.
They provide redacted owned storage and best-effort cleanup, not
constant-time-oriented decoding. Use base64_ng::ct or the
base64-ng-derive/base64-ng-sanitization companions for secret-bearing
protocol inputs where malformed-input timing matters.
For malformed-input timing-sensitive payloads, prefer the ct owned secret
helper:
use base64_ng::ct;
let decoded = ct::STANDARD.decode_secret(b"aGVsbG8=").unwrap();
assert!(decoded.constant_time_eq_public_len(b"hello"));For shared-memory, enclave-adjacent, HSM-style, or multi-principal deployments where even transient writes into the final heap allocation are unacceptable, use stack-staged owned decode:
use base64_ng::ct;
let decoded = ct::STANDARD
.decode_secret_staged::<5>(b"aGVsbG8=")
.unwrap();
assert!(decoded.constant_time_eq_public_len(b"hello"));SecretBuffer clears vector spare capacity when a vector is wrapped, and clears
initialized bytes plus spare capacity when dropped. It does not claim formal
zeroization and cannot clean historical copies outside the wrapper or make
guarantees about allocator behavior. SecretBuffer intentionally does not
implement PartialEq/==; use the explicit
constant_time_eq_public_len helper only when its best-effort, public-length
security contract is sufficient. Length mismatch returns immediately and must
be treated as public protocol information. Applications that require a
formally audited comparison should admit that dependency at the application
boundary, for example by comparing exposed bytes with subtle.
SecretBuffer does not lock memory; high-assurance deployments should pair it
with OS memory-locking, encrypted or disabled swap, crash-dump suppression, and
allocator isolation where those controls are required.
On wasm32, the same compiler-fence-only wipe-barrier caveat applies to owned
secret buffers. This 1.x compatibility type still performs best-effort cleanup
in ordinary builds, but only an explicit secrets build requests the 2.0
fail-closed secret policy and its allow-wasm32-best-effort-wipe
acknowledgement.
expose_secret_utf8 provides an explicit borrowed text view when the secret
bytes are valid UTF-8.
into_exposed_vec consumes the wrapper and returns an ExposedSecretVec, which
keeps redacted formatting and best-effort drop-time cleanup. If a raw Vec<u8>
is unavoidable, call
into_exposed_unprotected_vec_caller_must_zeroize; that method name is
intentionally loud because cleanup becomes the caller's responsibility.
try_into_exposed_string provides an explicit escape hatch for UTF-8 text and
returns an ExposedSecretString, which keeps redacted formatting and
best-effort drop-time cleanup. If a raw String is unavoidable, call
into_exposed_unprotected_string_caller_must_zeroize; cleanup then becomes the
caller responsibility. Invalid UTF-8 returns the redacted wrapper unchanged.
SecretBuffer also implements From<Vec<u8>> and From<String> for callers
that already own sensitive bytes or text and want to move them into the
redacted wrapper without copying initialized bytes. With alloc enabled,
stack-backed EncodedBuffer and DecodedBuffer values can also be consumed
into SecretBuffer; the stack backing array is cleared when the consumed
buffer drops at the end of the conversion.
TryFrom<&str>, TryFrom<&[u8]>, and TryFrom<&[u8; N]> for
EncodedBuffer<CAP> encode raw input bytes with strict standard padded Base64.
The same byte and text conversions for DecodedBuffer<CAP> and SecretBuffer
decode strict standard padded Base64.
DecodedBuffer<CAP> and SecretBuffer also implement FromStr with the same
strict standard padded decode policy. Use explicit engine or profile methods
for URL-safe, no-padding, MIME/PEM, bcrypt-style, or custom alphabets.
With the default alloc feature, vector and string helpers are available:
use base64_ng::STANDARD;
let encoded = STANDARD.encode_vec(b"hello").unwrap();
assert_eq!(encoded, b"aGVsbG8=");
let encoded_string = STANDARD.encode_string(b"hello").unwrap();
assert_eq!(encoded_string, "aGVsbG8=");
let infallible = STANDARD.encode_string_infallible(b"hello");
assert_eq!(infallible, "aGVsbG8=");
let decoded = STANDARD.decode_vec(&encoded).unwrap();
assert_eq!(decoded, b"hello");The infallible encode helpers are for ordinary trusted byte buffers where
failure would indicate an internal length/allocation invariant break rather
than invalid input. Use the fallible helpers when input length comes from
untrusted metadata, allocation pressure must be reported, or the caller needs a
recoverable error. On 32-bit targets, very large inputs can overflow the
encoded length calculation, so services should keep externally sized buffers on
the fallible encode_* APIs.
With the stream feature, std::io encoders are available:
use std::io::{Read, Write};
use base64_ng::STANDARD;
let mut encoder = STANDARD.encoder_writer(Vec::new());
encoder.write_all(b"he").unwrap();
encoder.write_all(b"llo").unwrap();
assert!(encoder.has_pending_input());
encoder.try_finish().unwrap();
assert_eq!(encoder.get_ref(), b"aGVsbG8=");
let encoded = encoder.finish().unwrap();
assert_eq!(encoded, b"aGVsbG8=");
let mut reader = STANDARD.encoder_reader(&b"hello"[..]);
let mut encoded = String::new();
reader.read_to_string(&mut encoded).unwrap();
assert_eq!(encoded, "aGVsbG8=");
let mut decoder = STANDARD.decoder_writer(Vec::new());
decoder.write_all(b"aGVs").unwrap();
decoder.write_all(b"bG8=").unwrap();
assert!(decoder.has_terminal_padding());
let decoded = decoder.finish().unwrap();
assert_eq!(decoded, b"hello");
let mut reader = STANDARD.decoder_reader(&b"aGVsbG8="[..]);
let mut decoded = Vec::new();
reader.read_to_end(&mut decoded).unwrap();
assert_eq!(decoded, b"hello");
assert!(reader.has_terminal_padding());
assert!(reader.is_finished());The explicit adapter constructors remain available when the engine should be passed separately:
use base64_ng::{STANDARD, stream::Encoder};
let encoder = Encoder::new(Vec::new(), STANDARD);
assert_eq!(encoder.engine(), STANDARD);The stream adapters expose engine() and is_padded() for policy inspection,
plus pending_len() and has_pending_input() for partial Base64 quantum
visibility, plus pending_input_needed_len() for the number of bytes needed to
complete the partial quantum. Reader adapters also expose
buffered_output_len(), buffered_output_capacity(),
buffered_output_remaining_capacity(), and has_buffered_output() for bytes
already decoded or encoded but not yet returned to the caller. Decoders
additionally expose has_terminal_padding() so framed protocols can tell when
a padded payload has ended and leave adjacent bytes for the next protocol
layer. Reader adapters also expose is_finished() once EOF or terminal padding
has been reached and all buffered output has been drained, and
has_finished_input() when the wrapped reader has reached EOF or terminal
padding but buffered output may still remain. Writer adapters expose
try_finish() to finalize pending input and flush the wrapped writer without
consuming the adapter, plus is_finalized() for explicit state inspection;
after successful finalization, later writes are rejected. Writer adapters also
expose buffered_output_len(), buffered_output_capacity(),
buffered_output_remaining_capacity(), and has_buffered_output() for encoded
or decoded bytes accepted by the adapter but not yet drained into the wrapped
writer. If a wrapped writer fails, retrying flush() or try_finish() drains
the buffered output without re-encoding or re-decoding the accepted input. All
stream adapters also expose can_into_inner() and try_into_inner() as
checked recovery paths that refuse to return the wrapped reader or writer while
doing so would discard pending input or buffered output. Their Debug output
reports adapter state without formatting the wrapped reader or writer,
including recovery readiness, pending quantum state, and fixed output queue
capacity. As with other std::io::Write implementations, direct write()
calls may accept only part of the provided input while buffering encoded or
decoded output; use write_all() when the whole input slice must be consumed.
Decoder writer and reader adapters fail closed after malformed Base64 input;
is_failed() exposes that state, while unchecked into_inner() remains
available for explicit recovery of the wrapped object.
URL-safe, no-padding encoding:
use base64_ng::URL_SAFE_NO_PAD;
let mut encoded = [0u8; 7];
let written = URL_SAFE_NO_PAD.encode_slice(b"hello", &mut encoded).unwrap();
assert_eq!(&encoded[..written], b"aGVsbG8");base64-ng treats Base64 as infrastructure code. Fast paths are never allowed to outrun evidence.
Security commitments:
- Stable Rust first. MSRV remains Rust
1.90.0; the active release toolchain is Rust1.97.1. New deployments should prefer the latest tested stable Rust, currently Rust1.97.1. no_stdcore by default.- Scalar encode/decode remains safe Rust.
- Audited unsafe helpers in
src/cleanup.rsperform volatile best-effort wiping plus architecture-gated inline assembly and hardware store-ordering fences where stable Rust supports them, so cleanup writes resist common dead-store elimination and are ordered before the cleanup boundary on supported native architectures. Constant-time comparison, byte accumulation, CT scan, and CT result-gate hardening remain audited insrc/ct/. - Unsafe SIMD remains isolated under
src/simd/; admitted AVX-512 VBMI, AVX2, SSSE3/SSE4.1, NEON, and narrow wasmsimd128encode and strict decode paths are gated by their documented runtime profiles, and all non-admitted backends and API surfaces remain prototype-only or scalar. - Every ordinary accelerated operation/backend pair must pass a direct
known-answer test before first use. Runtime health reports expose its
generation and
never-run,testing,healthy, orquarantinedstate; malformed input cannot quarantine a backend.checked-backendadds bounded redundant scalar comparison and scalar retry without exposing a suspect chunk. - Local checks verify that
allow(unsafe_code)is confined to the volatile wipe helpers 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 async/Tokio
APIs. The optional companion crate now admits read-all/write-all helpers and
manual
AsyncRead/AsyncWritestreaming adapters; the coretokiofeature remains reserved and inert. - docs/2.0_SYNCHRONOUS_IO.md defines exact prefix commitment, retry, framing, third-party writer failure, and bounded secret-frame rules for the synchronous adapters rebuilt on the 2.0 incremental core.
- docs/DEPENDENCIES.md defines the dependency admission bar for any future external crate.
runtime::backend_report()exposes the active admitted backend, detected candidate, candidate detection mode, SIMD feature status, security posture, and a conservative unsafe-boundary posture flag for audit logging. In the 2.0, non-scalar active values describe admitted encode dispatch, and strict decode dispatch is exposed separately throughBackendReport::active_decode_backend(). The unsafe-boundary flag is true only when the reservedsimdfeature is disabled; SIMD-enabled builds must rely on the release evidence scripts for boundary validation. Onno_std, acceleration requires complete compile-time target-feature evidence plus the atomic health latch; otherwise execution is scalar. Unsafe deployment attestation is represented by a thread-bound, generation-boundStaticBackendTokenand does not bypass KAT or quarantine. Itsencode_standardandencode_url_safemethods execute the rewritten SSSE3/SSE4.1, AVX2, or AVX-512 hot path when that exact token remains healthy. Itsdecode_standardanddecode_url_safemethods execute direct SSSE3/SSE4.1, AVX2, or AVX-512 strict decode. Automatic x86 strict decode remains on SSSE3/SSE4.1 or AVX2 because retained AVX-512 measurements missed the frozen performance margin; exact static-token calls may still use AVX-512 from one 64-byte encoded block. Other token backends and invalidated generations use scalar execution. The complete frozen policy is indocs/2.0_DISPATCH_AND_PERFORMANCE_MATRIX.md.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 and rejects CT gate postures that are ordering-only, compiler-fence-only, or hardware-barrier-unattested. AArch64 deployments that have platform evidence for CSDB may compile with--cfg base64_ng_aarch64_csdb_attested; that cfg is an operator attestation, not an automatic CPU probe. It reportshardware-speculation-barrier-build-assertedso logs distinguish a deployment assertion from a native target guarantee, and it is intentionally not a Cargo feature so--all-featurescannot enable it by accident.- 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.
Engine,ct::CtEngine,LineEnding,LineWrap, andProfileimplement printableDisplayoutput for policy logging without payload materialization.- CI runs platform tests on Linux, Windows, pinned macOS ARM images, pinned
Intel macOS, and
macos-latestso the GitHub-hosted macOS migration remains visible without hiding compatibility regressions behind the moving label. - 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 that scans the selected alphabet for every symbol so custom alphabets do not fall back to standard ASCII assumptions. 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. Input length, padding length, decoded length, and final success/failure are public; callers that need protocol-level success/failure timing resistance should continue with fixed-shape dummy downstream work after decode failure. - 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.
- Kani harnesses stay in-tree and release-gated. The current
no-default-features harness set verifies cleanly with the Rust
1.90.0Kani toolchain andcargo-kani 0.67.0; this is scoped bounded evidence, not a whole-crate formal-verification claim.
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. The exact 2.0 pre-gate,
post-gate, target, timing, and generated-code evidence boundary is in
docs/2.0_TIMING_AND_CODEGEN.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.
Run the standard gate:
scripts/checks.shThe standard gate includes isolated dudect, fuzz, and performance harness compile/dependency checks. It does not run fuzz campaigns or benchmarks.
Check the zero-external-crate policy directly:
scripts/validate-dependencies.shCheck release-facing documentation versions directly:
scripts/validate-doc-versions.shCheck reserved feature placeholders directly:
scripts/check_reserved_features.shCheck the wasm fail-closed cleanup policy directly:
scripts/check_wasm_wipe_policy.shRun the release gate:
scripts/stable_release_gate.shInstall cross-compilation targets used by the local and CI target checks:
rustup target add aarch64-unknown-linux-gnu x86_64-unknown-freebsd wasm32-unknown-unknown thumbv7em-none-eabihfRun the dependency-free no-alloc portability smoke crate across the same installed target list:
scripts/check_no_alloc_smoke.shRun the macOS host verification on an Apple Silicon or Intel Mac:
scripts/check_macos.shOn an M2 MacBook Pro this runs the real host tests on
aarch64-apple-darwin, then compile-checks both aarch64-apple-darwin and
x86_64-apple-darwin.
Run the AArch64 Linux host verification on an ARM Linux machine, such as an Amazon Graviton instance:
scripts/check_aarch64_linux.shThis runs the host tests, all-feature tests, clippy, direct NEON encode/decode
evidence, backend evidence, SIMD feature-bundle checks, and SIMD admission
validators on the real AArch64 host. To include the retained NEON performance
campaign, set BASE64_NG_RUN_COMMIT29_PERF=1.
Required security tools:
CI and local release scripts use scripts/ci_install_rust.sh; that script uses
rust-toolchain.toml as the single source of truth for the active release
toolchain. MSRV remains Rust 1.90.0 and is checked separately.
cargo install --locked cargo-audit --version 0.22.2
cargo install --locked cargo-license --version 0.7.0
cargo install --locked cargo-deny --version 0.20.2
cargo install --locked cargo-sbom --version 0.10.0Optional deep tools:
cargo install --locked cargo-nextest --version 0.9.140
cargo install --locked cargo-fuzz --version 0.13.2
cargo install --locked kani-verifier --version 0.67.0Verify optional tool installation:
cargo nextest --version
cargo fuzz --version
cargo kani --versionCompile and audit fuzz targets directly while iterating on fuzz harnesses:
scripts/check_fuzz.shValidate the committed fuzz corpus policy directly:
scripts/check_fuzz_corpus.shCompile and audit the isolated performance harness directly:
scripts/check_perf.shRun the complete reproducible campaign. The perf crate measures production auto dispatch, scalar, and every exact backend available on the host:
BASE64_NG_RUN_PERF=1 scripts/check_perf.shThe campaign runs correctness before and after measurement, captures two raw
sample sets, validates reproducibility, and records exact-pinned
base64 0.23.0 and base64ct 1.8.3 comparisons only for matching canonical
slice semantics. See docs/BENCHMARKS.md.
Run a target with cargo-fuzz:
cargo +nightly fuzz run decode
cargo +nightly fuzz run in_place
cargo +nightly fuzz run stream_chunks
cargo +nightly fuzz run differentialMiri is installed as a nightly Rust component, not as a Cargo package:
rustup toolchain install nightly --component miri
cargo +nightly miri setup
scripts/check_miri.shKani may need a one-time setup after installation:
cargo kani setupOn openSUSE Tumbleweed, install rustup first if it is not already present:
sudo zypper install rustupThe 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.
- 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.
- Constant-time claims require empirical timing evidence, generated-code review, and explicit documented exclusions.
- Compatibility modes must be visible in the type/API surface.
- Release evidence belongs in the repository and CI, not in memory.
See CONTRIBUTING.md for contribution rules and docs/RELEASE.md for the maintainer release checklist.
