secure-gate 0.9.0-rc.13

Secure wrappers for secrets with explicit access and mandatory zeroization — no_std-compatible, zero-overhead library with audit-friendly access patterns.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
// no_std by default; the `std` feature opts back into the standard library.
// Verified in CI by cross-building for a bare-metal target.
#![cfg_attr(not(feature = "std"), no_std)]
// Forbid unsafe code unconditionally
#![forbid(unsafe_code)]
#![warn(missing_docs)]
// `Cargo.toml` tells docs.rs to build with `--cfg docsrs`. This is what reads it:
// `doc_cfg` annotates every feature-gated item with the feature that enables it, so
// the docs people read say "Available on crate feature `encoding-hex`" instead of
// leaving the reader to infer it. Nightly-only, which is fine — docs.rs builds on
// nightly, and the only other build that sets `docsrs` is the CI step that exists to
// catch this gate being renamed. It has been renamed before: `doc_auto_cfg` was the
// spelling until 1.92 removed it and merged it into `doc_cfg`.
#![cfg_attr(docsrs, feature(doc_cfg))]

//! Secure wrappers for secrets with **explicit access** and **mandatory zeroization** — a
//! `no_std`-compatible, zero-overhead library with audit-friendly access patterns.
//!
//! Secrets are **automatically zeroized on drop** (the inner type must implement
//! [`Zeroize`](zeroize::Zeroize)). While a secret is held in [`Fixed`] or [`Dynamic`],
//! there is no `Deref` and no `AsRef`: callers reach the inner secret only via
//! [`RevealSecret`] / [`RevealSecretMut`], and `Debug` on a wrapper always prints
//! `[REDACTED]`. [`SlotWriter`], which borrows a wrapper's storage rather than holding it,
//! prints its cursor instead — position, remaining, length — and never the bytes.
//! Access has two shapes: **borrow** it, or **take** it.
//! [`with_secret()`](RevealSecret::with_secret) and
//! [`expose_secret()`](RevealSecret::expose_secret) lend you a reference while the
//! wrapper keeps ownership and keeps wiping. [`into_inner()`](RevealSecret::into_inner)
//! transfers ownership: you get the plain value and the protection ends with the call.
//!
//! Those two shapes are what the **3-tier access model** in `SECURITY.md` counts in
//! threes: `with_secret` (Tier 1) and `expose_secret` (Tier 2) are both the borrow
//! shape, differing only in how tightly the lifetime is pinned, and `into_inner`
//! (Tier 3) is the take. Two shapes, three volumes — the tiers say how loudly you
//! have to announce which one you picked, not how many protections there are.
//!
//! Extraction is a hand-off, not a third layer of protection. Encoding output is the
//! one case that keeps a wrapper — [`EncodedSecret`] **does** implement `Deref`, because
//! its job is to keep an encoded *copy* of the secret wiped until it drops. Ordinary
//! copies you make of the derefed value (`.to_string()`) are not tracked. See
//! [Where accident-prevention ends](#where-accident-prevention-ends).
//!
//! # Which type should I use?
//!
//! | Type | Allocation | Use case | Feature |
//! |------|-----------|----------|---------|
//! | [`Fixed<T>`] | Stack | Keys, nonces, tokens — compile-time-known size | Always available |
//! | [`Dynamic<T>`] | Heap | Passwords, API keys, ciphertexts — variable length | `alloc` (default) |
//!
//! Both types share the same [`RevealSecret`] / [`RevealSecretMut`] access API.
//!
//! # Quick start
//!
//! ```rust
//! use secure_gate::{Fixed, RevealSecret};
//!
//! // Wrap a 32-byte key
//! let key = Fixed::new([0x42u8; 32]);
//!
//! // Tier 1 — scoped access (preferred): secret ref cannot escape the closure
//! let first = key.with_secret(|bytes| bytes[0]);
//! assert_eq!(first, 0x42);
//!
//! // Tier 2 — direct reference (escape hatch for FFI / third-party APIs)
//! assert_eq!(key.expose_secret().len(), 32);
//!
//! // Debug is always redacted
//! assert_eq!(format!("{:?}", key), "[REDACTED]");
//! // key is zeroized when dropped
//! ```
//!
//! ```rust
//! # #[cfg(feature = "alloc")]
//! # {
//! use secure_gate::{Dynamic, RevealSecret};
//!
//! let password: Dynamic<String> = Dynamic::new(String::from("hunter2"));
//! let len = password.with_secret(|s: &String| s.len());
//! assert_eq!(len, 7);
//! # }
//! ```
//!
//! ```rust
//! use secure_gate::{fixed_newtype, RevealSecret};
//!
//! fixed_newtype!(pub Aes256Key, 32);
//!
//! let key = Aes256Key::new([0xABu8; 32]);
//! key.with_secret(|b| assert_eq!(b.len(), 32));
//! ```
//!
//! # Module structure
//!
//! ```text
//! secure_gate (lib.rs)
//! ├── Fixed<T>              ← always available, stack-allocated
//! ├── Dynamic<T>            ← requires `alloc`, heap-allocated
//! ├── traits/
//! │   ├── RevealSecret      ← immutable access (always available)
//! │   ├── RevealSecretMut   ← mutable access (always available)
//! │   ├── revealed_secrets/
//! │   │   └── EncodedSecret     ← zeroizing encoded string wrapper (alloc)
//! │   ├── ConstantTimeEq    ← ct-eq feature
//! │   ├── CloneableSecret   ← cloneable feature
//! │   ├── SerializableSecret← serde-serialize feature
//! │   ├── encoding/         ← ToHex, ToBase32, ToBase64Url, ToBech32, ToBech32m
//! │   └── decoding/         ← FromHexStr, FromBase32Str, FromBase64UrlStr, FromBech32Str, FromBech32mStr
//! ├── macros/               ← fixed_newtype!, dynamic_newtype!
//! └── error                 ← FromSliceError, HexError, Base32Error, Base64Error, Bech32Error
//! ```
//!
//! All public items are re-exported at the crate root. Use `secure_gate::Fixed`,
//! not `secure_gate::fixed::Fixed`.
//!
//! # Type taxonomy
//!
//! | Category | Types | `Deref` to secret? | Purpose |
//! |----------|-------|-------------------|---------|
//! | **Secret wrappers** | [`Fixed<T>`], [`Dynamic<T>`] | No — use [`RevealSecret`] | Hold live secrets; `Debug` → `[REDACTED]` |
//! | **Output wrapper** | [`EncodedSecret`] | Yes — yields `&str`; copies you make are yours, the buffer stays the wrapper's | Holds encoded output, wiped on drop |
//! | **Borrowed slot cursor** | [`SlotWriter`] | No — it writes, it does not read out | Appends into a slot a sized `new_with` handed out; `Debug` → cursor state only, never bytes |
//! | **Opt-in markers** | [`CloneableSecret`], [`SerializableSecret`] | — (no methods) | Implement on inner type `T` to unlock gated impls |
//!
//! `CloneableSecret` and `SerializableSecret` are implemented on the **inner type `T`**,
//! not on `Fixed<T>` or `Dynamic<T>` directly. [`EncodedSecret`] is not a secret
//! wrapper and does not interact with these markers.
//!
//! # Where accident-prevention ends
//!
//! This crate carries two separate obligations, and they end in different places.
//!
//! **Accidents must not compile.** This applies while the secret is held in
//! [`Fixed`]/[`Dynamic`], and it ends at the named extraction. `into_inner` and
//! `EncodedSecret::into_inner` are the exits that transfer ownership: you typed a
//! name, the call site is grep-able, and the wrapper is consumed. `expose_secret` is
//! a named *borrow* — the wrapper still owns the secret and still wipes it — but it
//! hands out a reference with no lifetime bound to a call site, so it is audited
//! alongside them.
//!
//! **Documented behavior must be accurate.** This never ends, and it is why the
//! taxonomy table above says `Deref: Yes` for the output wrapper.
//!
//! What [`EncodedSecret`] still guarantees: the `String` it owns is zeroized on drop,
//! and its `Debug` prints `[REDACTED]`. What it does not guarantee: copies you make
//! through `Deref` are ordinary values. `str::to_string()` and `.to_owned()` both
//! produce untracked plaintext. That is what extraction is for — the crate is not
//! trying to follow the bytes into your TLS stack. `into_inner` gives up even the first
//! guarantee, by design: it is the named end of protection.
//!
//! Two consequences worth knowing:
//!
//! - `format!("{:?}", &*encoded)` prints the encoded secret. Redaction lives on the
//!   wrapper, not on `str`; dereferencing first opts out of it.
//! - [`EncodedSecret::into_zeroizing`] returns a `Zeroizing<String>`, whose `Debug` is
//!   **not** redacted. It is a deliberate hand-off to a foreign type, not an equivalent
//!   wrapper.
//!
//! # Import paths
//!
//! ```rust
//! // ✅ Correct — always import from the crate root
//! use secure_gate::{Fixed, RevealSecret};
//!
//! // ❌ Wrong — these internal paths compile but are not the public API
//! // use secure_gate::traits::reveal_secret::RevealSecret;
//! // use secure_gate::traits::encoding::hex::ToHex;
//! ```
//!
//! # Method resolution: wrapper methods vs trait methods
//!
//! Encoding methods exist at **two levels** — both produce identical results:
//!
//! | Call style | Example | Appears in audit sweep? |
//! |-----------|---------|------------------------|
//! | **Wrapper trait impl** (ergonomic) | `key.to_hex()` (needs `use secure_gate::ToHex`) | No — grep for `to_hex` directly |
//! | **Trait via scoped access** (audit-friendly) | `key.with_secret(\|b\| b.to_hex())` | Yes — `with_secret` is grep-able |
//!
//! Both levels are impls of the **same** trait ([`ToHex`], [`ToBase32`], [`ToBase64Url`],
//! [`ToBech32`], [`ToBech32m`]): a blanket impl covers the raw bytes inside
//! `with_secret`, and per-wrapper impls on `Fixed<[u8; N]>` / `Dynamic<Vec<u8>>`
//! delegate through `with_secret` internally. One trait also means one bound:
//! `fn fingerprint<S: ToHex>(s: &S)` accepts wrappers and forwarding newtypes
//! alike.
//!
//! # Feature flags
//!
//! | Feature | Default | Description |
//! |---------|---------|-------------|
//! | `alloc` | **yes** | Heap types ([`Dynamic<T>`]), `Vec`/`String` zeroization |
//! | `std` | no | Full `std` support (implies `alloc`) |
//! | | | **Cryptographic** |
//! | `ct-eq` | no | [`ConstantTimeEq`] via `subtle` — timing-safe comparison |
//! | `rand` | no | `from_random()` / `from_rng()` — `no_std` for [`Fixed`] |
//! | `sealed` | no | `SealedFixed` / `SealedDynamic`: secrets encrypted in RAM while idle, under a locked `Prekey` (implies `std`, `rand`) |
//! | | | **Serialization** |
//! | `serde-serialize` | no | Serde `Serialize` (requires [`SerializableSecret`] marker) |
//! | `serde-deserialize` | no | Serde `Deserialize` with 1 MiB default limit |
//! | `serde` | no | Both directions |
//! | | | **Encoding** |
//! | `encoding-hex` | no | [`ToHex`] / [`FromHexStr`] via `base16ct` (constant-time) |
//! | `encoding-base32` | no | [`ToBase32`] / [`FromBase32Str`] via `base32ct` (constant-time) |
//! | `encoding-base64` | no | [`ToBase64Url`] / [`FromBase64UrlStr`] via `base64ct` (constant-time) |
//! | `encoding-bech32` | no | [`ToBech32`] / [`FromBech32Str`] (BIP-173) **and** [`ToBech32m`] / [`FromBech32mStr`] (BIP-350); `_sized::<N>` for a caller-chosen code length |
//! | `encoding` | no | All encoding features |
//! | | | **Meta** |
//! | `cloneable` | no | [`CloneableSecret`] opt-in cloning |
//! | `full` | no | Everything except `std` and `sealed` |
//!
//! # What's available without `alloc`?
//!
//! With `default-features = false`:
//! - [`Fixed<T>`], [`RevealSecret`], [`RevealSecretMut`]
//! - [`Fixed::try_from_hex`](Fixed::try_from_hex), [`Fixed::try_from_base32`](Fixed::try_from_base32),
//!   [`Fixed::try_from_base64url`](Fixed::try_from_base64url),
//!   [`Fixed::try_from_bech32`](Fixed::try_from_bech32), [`Fixed::try_from_bech32m`](Fixed::try_from_bech32m)
//!   (no-alloc stack-based decoding)
//! - [`fixed_newtype!`] (distinct nominal types — two keys of the same size that
//!   the compiler keeps apart); a plain `type` alias over [`Fixed<T>`] needs nothing
//!   at all
//! - [`FromSliceError`]
//!
//! **Not** available without `alloc`: [`Dynamic<T>`], [`EncodedSecret`],
//! encoding traits ([`ToHex`], etc.), decoding traits ([`FromHexStr`], etc.),
//! [`dynamic_newtype!`], serde support.
//!
//! # `no_std`
//!
//! `no_std` compatible (`#![no_std]` unless the `std` feature is enabled — verified in CI
//! by cross-building for `thumbv7em-none-eabihf`). [`Fixed<T>`] works without `alloc`.
//! Enable `alloc` (default) for [`Dynamic<T>`]. For pure stack / embedded builds, use
//! `default-features = false`. MSRV: **1.85** (Rust edition 2024).
//!
//! One caveat: the `rand` feature compiles without `std` or `alloc`, but
//! [`Fixed::from_random`] relies on `getrandom`, which needs a platform entropy backend.
//! On bare-metal targets you must configure one (see the `getrandom` crate's
//! "custom backend" documentation); without it, builds enabling `rand` fail at link
//! time on such targets. [`Fixed::from_rng`] with a caller-supplied RNG has no such
//! requirement.
//!
//! # Security
//!
//! This crate has **not** undergone an independent security audit. No unsafe code —
//! enforced with `#![forbid(unsafe_code)]`. Prefer scoped access ([`RevealSecret::with_secret`])
//! over direct references. Encoders return [`EncodedSecret`], which stays wiped until it
//! drops; `EncodedSecret::into_inner` is the named call that ends that. See
//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md)
//! for the full threat model.
//!
//! # Inherent Rust Limitations
//!
//! Three universal limits apply to any in-memory secret library in Rust (and in
//! C, C++, and Go) — `secure-gate` documents them honestly rather than
//! overclaiming:
//!
//! - **Stack-move residue**: moving a [`Fixed<T>`] by value leaves the prior stack
//!   slot uncleared until a later frame overwrites it. Prefer
//!   [`Fixed::new_with`](Fixed::new_with), pass by reference, or use
//!   [`Dynamic<T>`] for long-lived secrets.
//! - **Heap-reallocation residue**: `Vec` / `String` realloc frees the old buffer
//!   unzeroed. Pre-size, use `Dynamic<[u8; N]>` (boxed array — no realloc surface),
//!   or install a zero-on-dealloc global allocator such as `zeroizing-alloc` at
//!   the binary level.
//! - **Swap / core dumps**: process memory paged to disk or written on crash is
//!   outside any in-process library's reach. Use OS facilities (`mlock`,
//!   encrypted swap, disabled core dumps).
//!
//! Full discussion in
//! [SECURITY.md § Inherent Rust Limitations](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md#inherent-rust-limitations).
//!
//! See the [README](https://github.com/Slurp9187/secure-gate/blob/main/README.md) and
//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md) for full details.

#[cfg(feature = "alloc")]
extern crate alloc;

// Documentation-only. Several doc comments link to `std` items this crate touches through
// gated impls (`io::Write` on `Dynamic<Vec<u8>>` and on `SlotWriter`, `io::Read` on
// `DynamicReader`). Those links are correct and resolve on docs.rs, which builds with
// `all-features`, and in CI, whose rustdoc job does the same. They do NOT resolve under
// `--features=full`, which does not enable `std`: without that feature the name `std` is
// not in scope at all, so rustdoc has nothing to point at and `-D warnings` turns each one
// into an error. That is a plain `cargo doc --features full` — something a contributor
// runs and CI does not. This brings the name into scope for doc builds only: it is
// `cfg(doc)`, set by rustdoc and nothing else, so no real build is affected, `no_std` or
// otherwise, and the links resolve in every feature combination rather than only the one
// docs.rs happens to use.
#[cfg(doc)]
extern crate std;

#[cfg(feature = "alloc")]
mod dynamic;

mod slot_writer;

/// Fixed-size secret wrapper types - always available with zero dependencies.
/// These provide fundamental secure storage abstractions for fixed-size data.
mod fixed;

/// Centralized error types - always available.
mod error;

/// Core traits for wrapper polymorphism - always available.
pub mod traits;

#[cfg(feature = "sealed")]
pub mod sealed;

#[cfg(feature = "sealed")]
pub mod scoped;

/// Implementation detail of the `*_newtype!` macros — not a public API.
///
/// Generated code needs to name `Zeroize`, `serde` traits, and `alloc` types
/// without requiring the caller to depend on those crates directly, and
/// `::alloc::…` paths do not resolve in an ordinary `std` crate. Re-exporting
/// them here keeps expansions self-contained via `$crate::__private::…`.
///
/// Semver-exempt: items here may change or disappear in any release.
#[doc(hidden)]
pub mod __private {
    #[cfg(feature = "alloc")]
    pub use alloc::{boxed::Box, string::String, vec::Vec};
    #[cfg(feature = "rand")]
    pub use rand::{TryCryptoRng, TryRng};
    #[cfg(feature = "serde-deserialize")]
    pub use serde::{Deserialize, Deserializer};
    #[cfg(feature = "serde-serialize")]
    pub use serde::{Serialize, Serializer};
    pub use zeroize::{Zeroize, ZeroizeOnDrop};

    #[cfg(feature = "sealed")]
    pub use alloc::sync::Arc;

    /// A `fixed_newtype!` byte role, convertible to and from the `Fixed` it wraps without
    /// copying the bytes out by hand. Lets `sealed_fixed_newtype!` seal a role directly.
    #[cfg(feature = "sealed")]
    pub trait FixedRole: Sized {
        /// The wrapped `Fixed<[u8; N]>`.
        type Fixed: SealableFixed;
        /// Unwraps the role.
        fn into_fixed(self) -> Self::Fixed;
        /// Wraps a `Fixed` as the role.
        fn from_fixed(fixed: Self::Fixed) -> Self;
    }

    /// Maps `Fixed<[u8; N]>` to `SealedFixed<N>` without naming `N`.
    #[cfg(feature = "sealed")]
    pub trait SealableFixed {
        /// `SealedFixed<N>`.
        type Sealed;
    }

    #[cfg(feature = "sealed")]
    impl<const N: usize> SealableFixed for crate::Fixed<[u8; N]> {
        type Sealed = crate::SealedFixed<N>;
    }

    /// The `SealedFixed<N>` a role seals into.
    #[cfg(feature = "sealed")]
    pub type SealedOf<R> = <<R as FixedRole>::Fixed as SealableFixed>::Sealed;
}

/// Heap-allocated secret wrapper with explicit access and automatic zeroization on drop.
///
/// Variable-length secrets (passwords, API keys, ciphertexts). Inner type must implement
/// `Zeroize`. Secret bytes live on the heap only — never on the stack. Requires `alloc`.
///
/// See [`Fixed<T>`] for the stack-allocated alternative.
///
/// ```rust
/// # #[cfg(feature = "alloc")]
/// # {
/// use secure_gate::{Dynamic, RevealSecret};
///
/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
/// # }
/// ```
#[cfg(feature = "alloc")]
pub use dynamic::Dynamic;

/// A fixed-size byte secret held encrypted in RAM while idle — see the [`sealed`] module.
/// Requires the `sealed` feature.
#[cfg(feature = "sealed")]
pub use sealed::SealedFixed;

/// A `Vec<u8>` or `String` secret held encrypted in RAM while idle — see the [`sealed`]
/// module. Requires the `sealed` feature.
#[cfg(feature = "sealed")]
pub use sealed::SealedDynamic;

/// The locked, wipeable pre-key every sealing key is derived from — see the [`sealed`]
/// module. Requires the `sealed` feature.
#[cfg(feature = "sealed")]
pub use sealed::Prekey;

/// Why a [`Prekey`]'s pages are not locked in RAM. Requires the `sealed` feature.
#[cfg(feature = "sealed")]
pub use sealed::LockError;

/// Cursor-like reader over [`Dynamic<Vec<u8>>`] — see [`Dynamic::as_reader`].
#[cfg(feature = "std")]
pub use dynamic::DynamicReader;

/// Append-shaped writing into the fixed slot handed to a sized `new_with`.
///
/// The sized constructors hand out a slot whose length is already decided, which is what
/// makes growth — and therefore the reallocation hazard — inexpressible. `SlotWriter`
/// restores the append shape on top of that, so concatenating a wire format does not mean
/// writing offsets by hand. [`push_slice`](SlotWriter::push_slice) panics on overrun and
/// leaves the slot untouched; the `std`-gated [`Write`](std::io::Write) path cannot do that
/// and stay inside `Write`'s contract, so it truncates and reports the refusal on a later
/// call. `Debug` shows the cursor — position, remaining, length — and never the bytes.
pub use slot_writer::SlotWriter;

#[cfg(all(feature = "alloc", feature = "serde-deserialize"))]
/// Default maximum byte length for `Dynamic<Vec<u8>>` / `Dynamic<String>` deserialization (1 MiB).
///
/// The standard `serde::Deserialize` impl for both types rejects payloads exceeding this value.
/// Pass a custom ceiling to [`Dynamic::deserialize_with_limit`] when a different limit is needed.
///
/// **Important:** this limit is enforced *after* the upstream deserializer has fully
/// materialized the payload. It is a **result-length acceptance bound**, not a
/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
/// transport or parser layer upstream.
pub use dynamic::MAX_DESERIALIZE_BYTES;

/// Stack-allocated secret wrapper with explicit access and automatic zeroization on drop.
///
/// Fixed-size secrets (keys, nonces, tokens). Inner type must implement `Zeroize`.
/// Always available — works without `alloc`. Prefer [`new_with`](Fixed::new_with) over
/// [`new`](Fixed::new) when minimizing stack residue matters.
///
/// See [`Dynamic<T>`] for the heap-allocated alternative.
///
/// ```rust
/// use secure_gate::{Fixed, RevealSecret};
///
/// let key = Fixed::new([0xABu8; 32]);
/// key.with_secret(|b| assert_eq!(b[0], 0xAB));
/// assert_eq!(format!("{:?}", key), "[REDACTED]");
/// ```
pub use fixed::Fixed;

/// Marker trait that opts a secret type into cloning. No methods — gates the `Clone`
/// impl on [`Fixed`] and [`Dynamic`]. Each clone is independently zeroized on drop,
/// but increases the in-memory exposure surface. Requires `cloneable` feature.
///
/// Implement this on your inner type `T`; `Fixed<T>` and `Dynamic<T>` then gain the
/// gated `Clone` impl automatically. **This marker is deliberately not implemented by
/// default** on `Fixed<T>` or `Dynamic<T>` — cloning is an opt-in risk that must be
/// explicitly enabled. Without the `cloneable` feature this type does not exist at all.
///
/// **Coherence note:** Rust's orphan rule prevents downstream crates from implementing
/// this marker for foreign types such as `String`, `Vec<u8>`, or `[u8; N]` — so
/// `Dynamic<String>` and `Fixed<[u8; N]>` themselves can never become `Clone`. To opt
/// a secret into cloning, define a newtype inner type in your crate (deriving
/// `Zeroize` and `Clone`) and implement the marker on it. This is intentional: it keeps
/// every cloneable secret type visible in *your* code. See the trait-level docs for a
/// worked example.
///
/// See also [`SerializableSecret`] (the other opt-in marker trait).
#[cfg(feature = "cloneable")]
pub use traits::CloneableSecret;

/// Constant-time equality for secrets — prevents timing side-channel attacks.
///
/// Provides [`ct_eq()`](ConstantTimeEq::ct_eq) via the `subtle` crate. `==` is
/// **deliberately not implemented** on [`Fixed`] / [`Dynamic`] — always use `ct_eq`.
/// Requires `ct-eq` feature.
///
/// ```rust
/// # #[cfg(feature = "ct-eq")]
/// # {
/// use secure_gate::{Fixed, ConstantTimeEq};
///
/// let a = Fixed::new([1u8; 32]);
/// let b = Fixed::new([1u8; 32]);
/// assert!(a.ct_eq(&b));
/// # }
/// ```
#[cfg(feature = "ct-eq")]
pub use traits::ConstantTimeEq;

/// Explicit immutable access to secret contents (3-tier access model).
///
/// - **Tier 1** (preferred): [`with_secret()`](RevealSecret::with_secret) — scoped closure,
///   borrow cannot escape.
/// - **Tier 2** (escape hatch): [`expose_secret()`](RevealSecret::expose_secret) — direct
///   `&T` reference for FFI / third-party APIs.
/// - **Tier 3** (consumption): [`into_inner()`](RevealSecret::into_inner) — transfers
///   ownership of the plain value; protection ends with the call.
///
/// Length metadata lives in the separate [`SecretLen`] trait. See
/// [`RevealSecretMut`] for the mutable counterpart.
// Kept as a separate `use` from `SecretLen` on purpose: rustdoc 1.70 (the 0.8
// line's MSRV toolchain) ICEs resolving intra-doc links on a grouped
// `pub use a::{B, C};` re-export, and the two lines share this file.
pub use traits::RevealSecret;

/// Length metadata for secrets whose inner type has a meaningful length.
///
/// [`len()`](SecretLen::len) / [`byte_len()`](SecretLen::byte_len) /
/// [`is_empty()`](SecretLen::is_empty) do not expose contents, but length itself
/// can be sensitive for variable-length secrets — see the trait's Security
/// section. Implemented for `Fixed<[T; N]>`, `Dynamic<String>`, and
/// `Dynamic<Vec<T>>`; deliberately not for custom inner types.
pub use traits::SecretLen;

/// Explicit mutable access to secret contents.
///
/// Extends [`RevealSecret`]. Prefer [`with_secret_mut()`](RevealSecretMut::with_secret_mut)
/// (Tier 1) over [`expose_secret_mut()`](RevealSecretMut::expose_secret_mut) (Tier 2).
/// Implemented by [`Fixed`], [`Dynamic`], and the generated newtypes. [`EncodedSecret`]
/// implements neither this nor [`RevealSecret`] — it is an output wrapper, not a secret one.
pub use traits::RevealSecretMut;

/// Placeholder values left behind by [`RevealSecret::into_inner`] (Tier 3 access).
///
/// When `into_inner` moves the real secret out of a wrapper, it must leave *something*
/// behind for the wrapper's `Drop` impl to zeroize. `SentinelValue::sentinel_value()`
/// produces that inert placeholder (an all-default array, empty `String`, or empty
/// `Vec`). Implemented for `[T; N]` (any `N`, `T: Default`), `String`, and `Vec<T>`.
///
/// Implement this for your own inner types to make `into_inner` available on wrappers
/// around them. A sentinel must never contain secret material.
///
/// Leaving it unimplemented is a deliberate position, not a gap: `into_inner` is then
/// uncallable for that inner type, so a secret with no safe inert placeholder simply
/// cannot be extracted by ownership transfer. `with_secret` and `expose_secret` still
/// work. That is the safe default rather than a missing feature.
pub use traits::SentinelValue;

/// Asserts that an inner type owns no heap allocation.
///
/// Required by [`Fixed::new`], which is what makes `SECURITY.md`'s claim that `Fixed<T>`
/// "has no realloc surface" true rather than aspirational. Before this bound existed,
/// `Fixed<Vec<u8>>`, `Fixed<String>` and `fixed_newtype!(pub Name, generic Vec<u8>)` all
/// compiled, and measured, they abandoned an unwiped buffer holding the whole secret on
/// any capacity change — the same weakness `Dynamic` documents, minus `Dynamic`'s
/// safe-growth `std::io::Write` path.
///
/// Implemented for the primitives, `[T; N]` for any `N`, tuples up to four elements, and
/// `Option<T>`. Nothing heap-owning is implemented: `Vec<T>`, `String` and `Box<[T]>` are
/// all absent, because the residue does not need a capacity change — replacing the whole
/// value abandons the allocation too, measured at 1024 of 1024 bytes through a
/// `Fixed<Box<[u8]>>`. For a heap-backed secret use [`Dynamic`], which documents the
/// residue and offers the one safe growth path.
///
/// A custom inner type writes `impl FixedStorage for MyKey {}`. Like [`CloneableSecret`],
/// the compiler checks that you made the claim, not that it is true — see the trait docs
/// for the test to apply and for why an assertion is the right trade here.
pub use traits::FixedStorage;

/// Encoded string **output wrapper** for zeroizing encoded output.
///
/// This is an **output wrapper** — it exists *only* to keep encoded data zeroized until
/// it drops. It is **not** a secret wrapper like [`Fixed`]/[`Dynamic`] and does not
/// accept [`CloneableSecret`] or [`SerializableSecret`] markers.
///
/// Returned by every encoding method (`to_hex`, `to_hex_upper`, `to_base32`,
/// `to_base64url`, `try_to_bech32`, `try_to_bech32m`, and the `_sized::<N>` forms of
/// the last two — only bech32 and bech32m take a code length).
/// Wraps `Zeroizing<String>` with `Debug` → `[REDACTED]`. Its one accessor is
/// `Deref<Target = str>`; there are deliberately no `AsRef` impls, because `Deref`
/// already opens that door. Deliberately **no** `Display` either: `{}` on this type is
/// a compile error, so `Debug` redaction cannot mislead a caller into logging the
/// encoded secret. Write it out with `&*encoded`.
///
/// Use [`into_inner()`](EncodedSecret::into_inner) to extract a plain `String`
/// (ends zeroization) or [`into_zeroizing()`](EncodedSecret::into_zeroizing) to keep
/// the wiping. `into_zeroizing` is a partial downgrade: zeroize-on-drop survives, the
/// redacted `Debug` does not, because `zeroize::Zeroizing` derives its own.
///
/// Requires `alloc` feature.
#[cfg(feature = "alloc")]
pub use traits::EncodedSecret;

/// Marker trait that opts a secret type into Serde serialization. No methods — gates the
/// `Serialize` impl on [`Fixed`] and [`Dynamic`]. Serialization exposes the full secret;
/// audit every impl. Requires `serde-serialize` feature.
///
/// **`Deserialize` does NOT require this marker** — it is gated separately by the
/// `serde-deserialize` feature with its own impls on the wrapper types directly. This
/// marker controls `Serialize` only.
///
/// Implement this on your inner type `T`; `Fixed<T>` and `Dynamic<T>` then gain the
/// gated `Serialize` impl automatically. **This marker is deliberately not implemented
/// by default** on `Fixed<T>` or `Dynamic<T>` — serialization is an opt-in risk that
/// must be explicitly enabled. Without the `serde-serialize` feature this type does not
/// exist at all.
///
/// **Coherence note:** Rust's orphan rule prevents downstream crates from implementing
/// this marker for foreign types such as `String`, `Vec<u8>`, or `[u8; N]` — so
/// `Fixed<[u8; N]>` and `Dynamic<String>` themselves can never become `Serialize`. To
/// opt a secret into serialization, define a newtype inner type in your crate (deriving
/// `Zeroize` and `Serialize`) and implement the marker on it. This is intentional: it
/// keeps every serializable secret type visible in *your* code. See the trait-level
/// docs for a worked example.
///
/// See also [`CloneableSecret`] (the other opt-in marker trait).
#[cfg(feature = "serde-serialize")]
pub use traits::SerializableSecret;

// Newtype macros (`dynamic_newtype!` needs `alloc`)
mod macros;

/// Decodes Base32 strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
/// Requires `encoding-base32` + `alloc`. See [`ToBase32`] for the encoding counterpart.
#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
pub use traits::FromBase32Str;

/// Decodes Base64url strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
/// Requires `encoding-base64` + `alloc`. See [`ToBase64Url`] for the encoding counterpart.
#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
pub use traits::FromBase64UrlStr;

/// Decodes Bech32 (BIP-173) strings to `Vec<u8>` with HRP validation.
/// Requires `encoding-bech32` + `alloc`. See [`ToBech32`] for the encoding counterpart.
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
pub use traits::FromBech32Str;

/// Decodes Bech32m (BIP-350) strings to `Vec<u8>` with HRP validation.
/// Requires `encoding-bech32` + `alloc`. See [`ToBech32m`] for the encoding counterpart.
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
pub use traits::FromBech32mStr;

/// Decodes hex strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
/// Requires `encoding-hex` + `alloc`. See [`ToHex`] for the encoding counterpart.
#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
pub use traits::FromHexStr;

/// Encodes byte data as Base32 strings (RFC 4648 §6, uppercase, no padding).
/// Blanket impl for `AsRef<[u8]> + `[`EncodableBytes`]. Returns [`EncodedSecret`].
/// Requires `encoding-base32` + `alloc`.
/// See [`FromBase32Str`] for the decoding counterpart.
#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
pub use traits::ToBase32;

/// Encodes byte data as Base64url strings (RFC 4648, URL-safe, no padding).
/// Blanket impl for `AsRef<[u8]> + `[`EncodableBytes`]. Returns [`EncodedSecret`].
/// Requires `encoding-base64` + `alloc`.
/// See [`FromBase64UrlStr`] for the decoding counterpart.
#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
pub use traits::ToBase64Url;

/// Encodes byte data as Bech32 (BIP-173) strings. Blanket impl for
/// `AsRef<[u8]> + `[`EncodableBytes`]; returns `Result<`[`EncodedSecret`]`, _>`.
/// The plain methods use [`BECH32_CODE_LENGTH`]; `try_to_bech32_sized::<N>` takes the
/// code length as a parameter. Requires `encoding-bech32` + `alloc`.
/// See [`FromBech32Str`] for the decoding counterpart.
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
pub use traits::ToBech32;

/// Encodes byte data as Bech32m (BIP-350) strings. Blanket impl for
/// `AsRef<[u8]> + `[`EncodableBytes`]; returns `Result<`[`EncodedSecret`]`, _>`.
/// The plain methods use [`BECH32_CODE_LENGTH`]; `try_to_bech32m_sized::<N>` takes the
/// code length as a parameter. Requires `encoding-bech32` + `alloc`.
/// See [`FromBech32mStr`] for the decoding counterpart.
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
pub use traits::ToBech32m;

/// Opt-in marker for byte-shaped encoding inputs. Every `To*` trait is blanket
/// implemented for `AsRef<[u8]> + EncodableBytes`; the second bound keeps string-shaped
/// types out, so an already-encoded value cannot be silently encoded again. Implement it
/// for your own byte newtype to make it encodable.
#[cfg(feature = "encoding-bech32")]
pub use traits::Case;

#[cfg(any(
    feature = "encoding-hex",
    feature = "encoding-base32",
    feature = "encoding-base64",
    feature = "encoding-bech32",
))]
pub use traits::EncodableBytes;

/// Bech32 (BIP-173) checksum with a caller-chosen code length. `N` caps the length of
/// the whole encoded string and never enters the checksum. Above [`BECH32_CODE_LENGTH`]
/// the BCH error-detection guarantee no longer holds - see the type docs.
#[cfg(feature = "encoding-bech32")]
pub use traits::{Bech32Sized, Bech32Standard};

/// Bech32m (BIP-350) checksum with a caller-chosen code length - the bech32m twin of
/// [`Bech32Sized`], with the same rules and the same guarantee boundary.
#[cfg(feature = "encoding-bech32")]
pub use traits::{Bech32mSized, Bech32mStandard};

/// The bech32 BCH code length (1023) used by every non-`_sized` method, and the helper
/// that sizes an `N` from an HRP length and a payload byte count.
#[cfg(feature = "encoding-bech32")]
pub use traits::{BECH32_CODE_LENGTH, bech32_code_length};

/// Encodes byte data as hexadecimal strings (constant-time via `base16ct`).
/// Blanket impl for `AsRef<[u8]> + `[`EncodableBytes`]. Provides `to_hex()` and
/// `to_hex_upper()`, both returning [`EncodedSecret`].
/// Requires `encoding-hex` + `alloc`.
/// See [`FromHexStr`] for the decoding counterpart.
#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
pub use traits::ToHex;

/// Errors from Bech32 (BIP-173) and Bech32m (BIP-350) decoding.
/// Variant shapes are identical in debug and release builds; no secret material
/// (payload bytes, HRP strings) is ever carried in an error.
#[cfg(feature = "encoding-bech32")]
pub use error::Bech32Error;

/// Errors from Base32 (RFC 4648 §6, uppercase, unpadded) decoding. Variant shapes are
/// identical in debug and release builds; only numeric length metadata is carried.
#[cfg(feature = "encoding-base32")]
pub use error::Base32Error;

/// Errors from Base64url decoding. Variant shapes are identical in debug and
/// release builds; only numeric length metadata is carried.
#[cfg(feature = "encoding-base64")]
pub use error::Base64Error;

/// Errors from hex decoding. Variant shapes are identical in debug and
/// release builds; only numeric length metadata is carried.
#[cfg(feature = "encoding-hex")]
pub use error::HexError;

/// Error returned when a byte slice cannot be converted to `Fixed<[u8; N]>` due to
/// length mismatch. Produced by `Fixed::try_from(&[u8])`.
pub use error::FromSliceError;

// The README's examples are compiled and run by `cargo test --doc`, so they cannot
// silently rot. `cfg(doctest)` is set only while rustdoc *collects doctests* — never
// while it *builds documentation* — so this item never reaches docs.rs or `cargo doc`
// output, and the README is not duplicated onto the crate page.
//
// Gated on `full` as well: the README documents the fully-featured crate, so its
// examples use `alloc` and the encoding traits unconditionally. Without this gate the
// `--no-default-features` doctest job in CI would fail on imports the README does not
// feature-gate. CI's `--features full` and `--all-features` entries do run them.
//
// It must live at the end of the file: an item here would otherwise sit ahead of the
// `//!` crate docs above, which is not allowed.
#[cfg(all(doctest, feature = "full"))]
#[doc = include_str!("../README.md")]
pub struct ReadmeDoctests;