base64-ng 1.3.9

no_std-first Base64 encoding and decoding with strict APIs and a security-heavy release process
Documentation
# Migrating from the `base64` Crate

This guide targets projects using `base64` `0.22.x`.

`base64-ng` is intentionally stricter and smaller. It does not try to mirror
every compatibility setting from `base64`; it provides a strict RFC 4648 scalar
core, caller-owned buffers, optional allocation helpers, and release evidence.

The migration examples are covered by a local smoke crate:

```sh
scripts/check_migration_smoke.sh
```

The standard release gate runs this script so strict standard, URL-safe no-pad,
MIME/PEM wrapping, legacy whitespace, custom alphabets, stack buffers, secret
buffers, and stream wrapper migration examples stay in sync with the crate.

## Dependency

Before:

```toml
[dependencies]
base64 = "0.22"
```

After:

```toml
[dependencies]
base64-ng = "1.3.9"
```

For embedded or freestanding use:

```toml
[dependencies]
base64-ng = { version = "1.3.9", default-features = false }
```

### Sanitization Companion In 1.3.9

`base64-ng-sanitization` `1.3.9` migrates from `sanitization` 1.x to exact-pinned
`sanitization` `2.0.3`. The existing `decode_locked_secret_bytes` method keeps
its `LockedSecretBytesGenerateError<SanitizationDecodeError>` return type for
source compatibility. Use the additive `decode_locked_secret_bytes_fill`
method when the sanitization 2.0 `LockedSecretBytesFillError` integrity variant
must be propagated explicitly.

Mapped storage exposure is checked in sanitization 2.0. Replace
`LockedSecretBytes::with_secret` with `try_expose_secret` and
`LockedSecretVec::with_secret` with `try_with_secret`. Prefer
`LockedSanitizationCtEqExt::try_sanitization_ct_eq` or
`try_sanitization_verify` so canary corruption is returned rather than routed
through the compatibility comparison trait's explicit fail-stop behavior.

The companion feature `strict-compare` replaces sanitization's old `strict-ct`
name. `strict-ct` remains a temporary alias in `base64-ng-sanitization` for
source migration. The companion's `high-assurance` profile now includes strict
comparison and strict random canaries and is intended for supported x86_64 and
AArch64 native deployments.

For fail-closed fixed locked-storage admission, use
`decode_locked_secret_bytes_checked`. It requires memory locking, dump
exclusion, and fork exclusion before decoding plaintext directly into the
protected mapping. The built-in `decode_locked_secret_vec_checked`
implementation uses sanitization 2.0.3's protected-capacity constructor to
establish the same required controls before its decode closure runs. External
trait implementations retain a post-fill compatibility default and must
override it for the same guarantee. Existing non-checked helpers preserve their
API and require callers to inspect `protection_report()` before relying on dump
or fork exclusion.

Import `CtDecodeSanitizationProtectedExt` when callers need
`ProtectedSecretFillError` to distinguish required-control failures from
canary-integrity failures. Use
`decode_locked_secret_vec_checked_bounded::<MAX>` for untrusted runtime-size
inputs so oversized decoded capacity is rejected before protected allocation.

## Engine Mapping

| `base64` engine | `base64-ng` engine |
| --- | --- |
| `base64::engine::general_purpose::STANDARD` | `base64_ng::STANDARD` |
| `base64::engine::general_purpose::STANDARD_NO_PAD` | `base64_ng::STANDARD_NO_PAD` |
| `base64::engine::general_purpose::URL_SAFE` | `base64_ng::URL_SAFE` |
| `base64::engine::general_purpose::URL_SAFE_NO_PAD` | `base64_ng::URL_SAFE_NO_PAD` |

## Encoding

`base64`:

```rust
use base64::{Engine as _, engine::general_purpose::STANDARD};

let encoded = STANDARD.encode(b"hello");
assert_eq!(encoded, "aGVsbG8=");
```

`base64-ng` with allocation:

```rust
use base64_ng::STANDARD;

let encoded = STANDARD.encode_string(b"hello").unwrap();
assert_eq!(encoded, "aGVsbG8=");
```

`base64-ng` with caller-owned output:

```rust
use base64_ng::{STANDARD, checked_encoded_len};

let input = b"hello";
let mut output = vec![0u8; checked_encoded_len(input.len(), true).unwrap()];
let written = STANDARD.encode_slice(input, &mut output).unwrap();
output.truncate(written);

assert_eq!(output, b"aGVsbG8=");
```

### Custom Alphabet Encoding In 1.3.8

`Alphabet::ENCODE` is the authoritative encoding table for every `Engine`
surface. A hand-written `Alphabet::encode` override remains callable directly
for API compatibility, but `Engine` no longer invokes it. Applications that
previously relied on an override for custom encoding output or performance
must move that behavior into the `ENCODE` table or use their own explicit
low-level helper. Standard and URL-safe table families use crate-owned
compile-time-selected arithmetic mappers; other custom tables retain the
conservative fixed-scan mapper.

## Decoding

`base64`:

```rust
use base64::{Engine as _, engine::general_purpose::STANDARD};

let decoded = STANDARD.decode("aGVsbG8=").unwrap();
assert_eq!(decoded, b"hello");
```

`base64-ng` with allocation:

```rust
use base64_ng::STANDARD;

let decoded = STANDARD.decode_vec(b"aGVsbG8=").unwrap();
assert_eq!(decoded, b"hello");
```

`base64-ng` with caller-owned output:

```rust
use base64_ng::{STANDARD, decoded_capacity};

let input = b"aGVsbG8=";
let mut output = vec![0u8; decoded_capacity(input.len())];
let written = STANDARD.decode_slice(input, &mut output).unwrap();
output.truncate(written);

assert_eq!(output, b"hello");
```

## Strictness Differences

`base64-ng` rejects ambiguous input by default:

- whitespace is not ignored
- mixed standard and URL-safe alphabets are rejected
- padding in the payload body is rejected
- trailing bytes after terminal padding are rejected
- non-canonical trailing bits are rejected
- padded engines require canonical padding

If the old project depends on line-wrapped or spaced Base64, use the explicit
legacy whitespace APIs:

```rust
use base64_ng::STANDARD;

let decoded = STANDARD.decode_vec_legacy(b" aG\r\nVs\tbG8= ").unwrap();
assert_eq!(decoded, b"hello");
```

The legacy profile only ignores ASCII space, tab, carriage return, and line
feed. It still rejects mixed alphabets, malformed padding, trailing payload
after padding, and non-canonical trailing bits. If the old project accepts
broader non-canonical input, normalize or reject that input before calling
`base64-ng`.

## Length And Memory Handling

`base64-ng` exposes recoverable length helpers:

```rust
use base64_ng::{
    LineEnding, LineWrap, checked_encoded_len, checked_wrapped_encoded_len, decoded_capacity,
};

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_capacity(8), 6);
```

Use `checked_encoded_len` for untrusted length metadata before allocating.
Use `decode_slice` or `decode_in_place` when a caller-owned memory limit is
required.

## Streaming

Enable the `stream` feature for `std::io` wrappers:

```toml
[dependencies]
base64-ng = { version = "1.3.9", features = ["stream"] }
```

```rust
use std::io::Write;
use base64_ng::{STANDARD, stream::Encoder};

let mut encoder = Encoder::new(Vec::new(), STANDARD);
assert_eq!(encoder.engine(), STANDARD);
assert!(encoder.is_padded());

encoder.write_all(b"he").unwrap();
assert!(encoder.has_pending_input());

encoder.write_all(b"llo").unwrap();
assert!(encoder.has_pending_input());

encoder.try_finish().unwrap();
assert!(encoder.is_finalized());

let encoded = encoder.finish().unwrap();

assert_eq!(encoded, b"aGVsbG8=");
```

Writer adapters expose `try_finish()` when a caller wants to finalize pending
Base64 input and flush the wrapped writer without immediately consuming the
adapter. After successful finalization, later non-empty writes return
`InvalidInput`. Writer adapters buffer encoded or decoded output internally
before draining it into the wrapped writer, so failed wrapped writes can be
retried by calling `flush()` or `try_finish()` again without re-encoding or
re-decoding accepted input. Direct `write()` calls may report partial progress;
use `write_all()` when the whole input slice must be consumed. Stream adapters
also expose non-sensitive state helpers such as
`engine()`, `is_padded()`, `pending_len()`, `has_pending_input()`,
`pending_input_needed_len()`, `buffered_output_len()`,
`buffered_output_capacity()`, `buffered_output_remaining_capacity()`, and
`has_finished_input()`, and decoder-side `has_terminal_padding()` for framed
protocols and audit logging. Use `can_into_inner()` and `try_into_inner()` when
recovering the wrapped reader or writer should be refused if it would discard
pending input or buffered output. Decoder writer and reader adapters also
expose `is_failed()` and fail closed after malformed Base64 input; unchecked
`into_inner()` remains available for explicit recovery of the wrapped object
after a decode error.

The core crate's `tokio` feature is reserved, inert, and dependency-free. For
Tokio applications, use the optional `base64-ng-tokio` companion crate instead:

```toml
[dependencies]
base64-ng = "1.3.9"
base64-ng-tokio = "1.3.9"
tokio = { version = "1.53.1", features = ["io-util"] }
```

```rust
use base64_ng::STANDARD;
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(&STANDARD, &mut input, &mut output, 1024).await?;
assert_eq!(output, b"aGVsbG8=");

let mut reader = EncoderReader::new(&b"hello"[..], STANDARD);
let mut streamed = Vec::new();
reader.read_to_end(&mut streamed).await?;
assert_eq!(streamed, b"aGVsbG8=");

let mut writer = EncoderWriter::new(Vec::new(), STANDARD);
writer.write_all(b"hello").await?;
writer.shutdown().await?;
let encoded = writer.into_inner()?;
assert_eq!(encoded, b"aGVsbG8=");
# Ok(())
# }
```

## Security Notes

The scalar encode/decode core has no external crate dependencies and remains
safe Rust. The only scalar-side unsafe code is the audited volatile wipe helpers
used by clear-tail and secret-buffer cleanup APIs; architecture-specific unsafe
code remains limited to the dedicated SIMD boundary.
Release gates include tests, clippy, docs, dependency policy, RustSec audit,
license review, SBOM generation, reproducible package/build checks, and Miri
when installed.

`base64-ng` currently hardens obvious timing pitfalls in scalar encode/decode,
but it does not claim a formally verified cryptographic constant-time API.