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. 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.
RFC 4648 Conformance
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.
Current Status
This source tree defines the synchronized 2.0.0 package family. The
implementation and package topology are frozen. Publication is authorized only
from the signed v2.0.0 tag after external review, required CI, the complete
release gate, and source-bound hardware evidence pass.
The 2.0 family includes the complete 2.0 API, synchronized companion crates, the supported npm Wasm loader, and exact-profile RVV dispatch for the reviewed Linux/SpacemiT X60 identity. See the release freeze, release notes, and migration guide for the frozen scope and adoption path.
Reviewers testing an untagged candidate should pin the exact reviewed Git revision:
= { = "https://github.com/valkyoth/base64-ng", = "<reviewed-commit>" }
Backend Verification Status
"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, and RISC-V; 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 | Native Apple Silicon and AWS Neoverse-N1 correctness, direct-kernel, assembly, and retained 15-sample performance bundles accepted through the source-equivalence gate | admitted |
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 exact-profile backend | QEMU VLEN 128/256 fallback/direct evidence plus native Banana Pi BPI-F3 SpacemiT X60 VLEN 256 correctness, signal/thread, ABI, cleanup, and performance evidence; the release gate requires an exact integrated-source bundle | admitted only for the exact Linux/X60 identity from 384 raw encode bytes and 1024 encoded strict-decode bytes; all other RISC-V stays 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.
General 2.0 implementation is complete, including exact-profile native RVV admission. Release assurance requires external review, exact-source native RVV and NEON evidence, green required CI and CodeQL, candidate-local package evidence, the report-only Commit 55 seal, and the authorized signed tag.
RVV dispatch is limited to the measured Linux/SpacemiT X60 identity; other RISC-V profiles remain scalar. SVE remains non-dispatchable 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, native runs, and QEMU runs are scoped evidence, not certification or whole-crate formal proof.
Trust Dashboard
| 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, wasm simd128, and exact Linux/SpacemiT X60 RVV 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.
Rust Version Support
The minimum supported Rust version is Rust 1.90.0. New deployments should
prefer the latest tested stable Rust; the 2.0 family is built and release-gated
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:
| 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 |
Install
[]
= "2.0.0"
For ordinary public data, the shortest API uses strict RFC 4648 Standard Base64 with canonical padding:
let encoded = encode.unwrap;
assert_eq!;
let decoded = decode.unwrap;
assert_eq!;
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 STRICT_STANDARD_PADDED;
let encoded = STRICT_STANDARD_PADDED.encode_to_string.unwrap;
assert_eq!;
let decoded = STRICT_STANDARD_PADDED.decode_to_vec.unwrap;
assert_eq!;
When validated encoded text must carry its policy through storage or
transport, use the ordinary Base64String<S> owner:
use ;
let stored = encode.unwrap;
assert_eq!;
assert_eq!;
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.
2.0 API Guide
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:
= "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 | 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.
Companion Crates
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. |
@valkyoth/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:
import from "@valkyoth/base64-ng-wasm-loader";
const base64 = await ;
const input = .;
const encoded = base64.;
const decoded = base64.;
base64.;
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:
[]
= { = "2.0.0", = false }
= { = "2.0.0", = false }
use ct;
use ;
let secret = STANDARD
.
.unwrap;
assert!;
For attacker-controlled dynamic input, enable alloc and make the public
decoded-output ceiling explicit:
use ct;
use CtDecodeSanitizationBoundedExt;
let secret = STANDARD
.
.unwrap;
secret.with_secret;
[]
= { = "2.0.0", = ["high-assurance"] }
use ct;
use ;
let locked = STANDARD
.
.unwrap;
locked
.try_expose_secret
.unwrap;
assert!;
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:
[]
= { = "2.0.0", = false, = ["secrets"] }
= "2.0.0"
use SecretInput;
use Base64Secret;
;
let input = new;
let key = decode_base64.unwrap;
assert_eq!;
assert_eq!;
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:
[]
= { = "2.0.0", = ["secrets"] }
= { = "1.0.229", = ["derive"] }
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:
[]
= "2.0.0"
= "2.0.0"
= "1.12.1"
use STRICT_STANDARD_PADDED;
use Base64BytesExt;
use Bytes;
let encoded = STRICT_STANDARD_PADDED
.encode_buf
.unwrap;
assert_eq!;
base64-ng-subtle provides a sealed subtle::ConstantTimeEq integration for
final 2.0 secret owners and views in projects that already admit subtle:
[]
= { = "2.0.0", = ["secrets"] }
= "2.0.0"
use ;
use SubtleSecretEq;
let mut frame = new.unwrap;
frame.update.unwrap;
let decoded = frame.finish.unwrap;
assert!;
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:
[]
= "2.0.0"
= "2.0.0"
= { = "1.53.1", = ["io-util"] }
use STRICT_STANDARD_PADDED;
use ;
use ;
# async
Disable defaults for embedded or freestanding use:
[]
= { = "2.0.0", = 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:
[]
= { = "2.0.0", = ["simd"] }
use ;
let encoded = STANDARD.encode_string.unwrap;
assert_eq!;
let decoded = STANDARD.decode_vec.unwrap;
assert_eq!;
let report = backend_report;
println!;
println!;
println!;
println!;
println!;
Canonical 2.0 Examples
use STRICT_STANDARD_PADDED;
let input = b"hello";
let mut encoded = ;
let written = STRICT_STANDARD_PADDED
.encode_into
.unwrap;
assert_eq!;
let mut decoded = ;
let written = STRICT_STANDARD_PADDED
.decode_into
.unwrap;
assert_eq!;
In-place encoding:
use STRICT_STANDARD_PADDED;
let mut buffer = ;
buffer.copy_from_slice;
let encoded_len = STRICT_STANDARD_PADDED
.encode_in_place
.unwrap;
assert_eq!;
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 ;
let mut state = STRICT_STANDARD_PADDED.encoder;
let mut output = ;
let step = state.update.unwrap;
let mut written = step.progress.output_produced;
let final_step = state.finish.unwrap;
written += final_step.progress.output_produced;
assert_eq!;
assert_eq!;
Heapless incremental strict decoding uses the same progress contract:
use ;
let mut state = STRICT_STANDARD_PADDED.decoder;
let mut output = ;
let step = state.update.unwrap;
let mut written = step.progress.output_produced;
let final_step = state.finish.unwrap;
written += final_step.progress.output_produced;
assert_eq!;
assert_eq!;
Allocation-free formatting, append rollback, and chunk iteration:
use STRICT_STANDARD_PADDED;
let display = STRICT_STANDARD_PADDED.display.unwrap;
assert_eq!;
let mut appended = Stringfrom;
STRICT_STANDARD_PADDED
.encode_append
.unwrap;
assert_eq!;
let chunks = STRICT_STANDARD_PADDED
.encoded_chunks
.unwrap
.map
.;
assert_eq!;
Compile-time encoding:
use ;
const HELLO: = match STRICT_STANDARD_PADDED.encode_array ;
const URL_BYTES: = match STRICT_URL_SAFE_UNPADDED.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 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 ;
const HELLO: = match STRICT_STANDARD_PADDED.decode_array ;
const URL_BYTES: = match STRICT_URL_SAFE_UNPADDED.decode_array ;
assert_eq!;
assert_eq!;
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 STRICT_STANDARD_PADDED;
let encoded = STRICT_STANDARD_PADDED
.
.unwrap;
let decoded = STRICT_STANDARD_PADDED
.
.unwrap;
assert_eq!;
For untrusted length metadata, use checked length calculation:
use ;
assert_eq!;
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 ;
STRICT_STANDARD_PADDED.validate.unwrap;
assert!;
STRICT_URL_SAFE_UNPADDED.validate.unwrap;
assert!;
For line-wrapped or spaced legacy inputs, use the explicit legacy profile:
use ;
let mut decoded = ;
let written = ASCII_WHITESPACE
.decode_into
.unwrap;
assert_eq!;
Exact WHATWG forgiving decode is a separate ordinary policy:
use ;
assert_eq!;
assert!;
Forgiving and legacy policies are intentionally unavailable to secret frames.
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. 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 ;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
let mut encoded = ;
let written = MIME.encode_slice.unwrap;
assert_eq!;
assert!;
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 STANDARD;
let profile = STANDARD.profile;
let ct_decoder = STANDARD.ct_decoder;
assert!;
assert!;
assert_eq!;
The 2.0 secret surface keeps classified encoding in a separate bounded scalar state and wiping owner:
use ;
let input = new;
let encoded = STRICT_STANDARD_PADDED
.
.unwrap;
assert_eq!;
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 ;
let context = new;
let token = context.best_effort_token;
let provider = new?;
let allocation = try_new?;
let encoded = STRICT_STANDARD_PADDED.encode_assured?;
assert_eq!;
let operation = encoded.operation_report?;
assert_eq!;
assert_eq!;
let cleanup = encoded.try_close?;
assert_eq!;
# Ok::
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 ;
let wrap = checked_new.unwrap;
let profile = STANDARD.checked_profile_with_wrap.unwrap;
assert!;
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!;
let encoded = STANDARD..unwrap;
assert_eq!;
let decoded = STANDARD
.
.unwrap;
assert_eq!;
Custom Alphabets
New 2.0 code validates and owns custom alphabets before constructing a sealed runtime codec:
use CodecBuilder;
let codec = from_table
.unwrap
.build
.unwrap;
let encoded = codec.encode_to_string.unwrap;
assert_eq!;
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 ;
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, 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 ;
let encoded = STANDARD..unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let decoded = STANDARD..unwrap;
assert_eq!;
let bcrypt = BCRYPT..unwrap;
assert_eq!;
let wrapped = MIME..unwrap;
let decoded = MIME..unwrap;
assert_eq!;
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 STANDARD;
let encoded = STANDARD.encode_secret.unwrap;
assert_eq!;
assert_eq!;
let decoded = STANDARD.decode_secret.unwrap;
assert_eq!;
assert!;
assert_eq!;
let wrapped = STANDARD
.encode_wrapped_secret
.unwrap;
let unwrapped = STANDARD
.decode_wrapped_secret
.unwrap;
assert_eq!;
let legacy = STANDARD
.decode_secret_legacy
.unwrap;
assert_eq!;
let decoded = try_from.unwrap;
assert_eq!;
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 ct;
let decoded = STANDARD.decode_secret.unwrap;
assert!;
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 ct;
let decoded = STANDARD
.
.unwrap;
assert!;
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 STANDARD;
let encoded = STANDARD.encode_vec.unwrap;
assert_eq!;
let encoded_string = STANDARD.encode_string.unwrap;
assert_eq!;
let infallible = STANDARD.encode_string_infallible;
assert_eq!;
let decoded = STANDARD.decode_vec.unwrap;
assert_eq!;
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 ;
use STANDARD;
let mut encoder = STANDARD.encoder_writer;
encoder.write_all.unwrap;
encoder.write_all.unwrap;
assert!;
encoder.try_finish.unwrap;
assert_eq!;
let encoded = encoder.finish.unwrap;
assert_eq!;
let mut reader = STANDARD.encoder_reader;
let mut encoded = Stringnew;
reader.read_to_string.unwrap;
assert_eq!;
let mut decoder = STANDARD.decoder_writer;
decoder.write_all.unwrap;
decoder.write_all.unwrap;
assert!;
let decoded = decoder.finish.unwrap;
assert_eq!;
let mut reader = STANDARD.decoder_reader;
let mut decoded = Vecnew;
reader.read_to_end.unwrap;
assert_eq!;
assert!;
assert!;
The explicit adapter constructors remain available when the engine should be passed separately:
use ;
let encoder = new;
assert_eq!;
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 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. 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_std-capable core; the default convenience feature set enablesallocandstd, anddefault-features = falsekeeps the core freestanding.- 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 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.
Local Checks
Run the standard gate:
The 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:
Check release-facing documentation versions directly:
Check reserved feature placeholders directly:
Check the wasm fail-closed cleanup policy directly:
Run the release gate:
Install cross-compilation targets used by the local and CI target checks:
Run the dependency-free no-alloc portability smoke crate across the same installed target list:
Run the macOS host verification on an Apple Silicon or Intel Mac:
On 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:
This 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.
Optional deep tools:
Verify optional tool installation:
Compile and audit fuzz targets directly while iterating on fuzz harnesses:
Validate the committed fuzz corpus policy directly:
Manage resumable one-hour release campaigns across local and SSH workers:
The ignored SQLite session pins one clean commit, tracks all 18 fuzz targets plus the native RISC-V admission campaign, and retrieves and validates remote evidence before final aggregation. See docs/FUZZING.md for the operator workflow and trust boundary.
Compile and audit the isolated performance harness directly:
Run 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
The 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:
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 the core dependency graph empty and isolate optional ecosystem dependencies in companion crates.
- 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.
Contributing And Releases
See CONTRIBUTING.md for contribution rules and docs/RELEASE.md for the maintainer release checklist.