# secure-gate
`no_std`-compatible wrappers for sensitive data with explicit exposure requirements.
- `Fixed<T>` — Stack-allocated wrapper
- `Dynamic<T>` — Heap-allocated wrapper
- `FixedRng<N>` — Cryptographically secure random bytes of fixed length N
- `DynamicRng` — Heap-allocated cryptographically secure random bytes
- `CloneableArray<const N: usize>` — Cloneable fixed-size stack secret (`[u8; N]`)
- `CloneableString` — Cloneable heap-allocated text secret (`String`)
- `CloneableVec` — Cloneable heap-allocated binary secret (`Vec<u8>`)
- `HexString` — Validated lowercase hexadecimal string wrapper
- `Base64String` — Validated URL-safe base64 string wrapper (no padding)
- `Bech32String` — Validated Bech32 string wrapper (for age keys, etc.)
With the `zeroize` feature enabled, memory containing secrets is zeroed on drop, including spare capacity where applicable.
Access to secret data requires an explicit `.expose_secret()` call. There are no `Deref` implementations or other implicit access paths.
Cloning is opt-in and only available under the `zeroize` feature.
## Installation
```toml
[dependencies]
secure-gate = "0.7.0-rc.2"
```
Recommended configuration:
```toml
secure-gate = { version = "0.7.0-rc.2", features = ["full"] }
```
## Features
| `zeroize` | Memory zeroing on drop and opt-in cloning via pre-baked cloneable types |
| `rand` | Random generation (`FixedRng<N>::generate()`, `DynamicRng::generate()`) |
| `ct-eq` | Constant-time equality comparison |
| `encoding` | All encoding support (`encoding-hex`, `encoding-base64`, `encoding-bech32`) |
| `encoding-hex` | Hex encoding, `HexString`, `FixedRng` hex methods |
| `encoding-base64` | `Base64String` |
| `encoding-bech32` | `Bech32String` (age-compatible Bech32 keys) |
| `full` | All optional features |
The crate is `no_std`-compatible with `alloc`. Features are optional and add no overhead when unused.
## Security Model & Design Philosophy
`secure-gate` prioritizes **auditability** and explicitness over implicit convenience.
Every access to secret material — even inside the crate itself — goes through a method named `.expose_secret()` (or `.expose_secret_mut()`). This is deliberate:
- Makes every exposure site grep-able and obvious in code reviews
- Prevents accidental silent leaks or hidden bypasses
- Ensures consistent reasoning about secret lifetimes and memory handling
These calls are `#[inline(always)] const fn` reborrows — the optimizer elides them completely. There is **zero runtime cost**.
It's intentional "theatre" for humans and auditors, but free for the machine. Clarity of purpose wins over micro-optimizations.
## Quick Start
```rust
use secure_gate::{fixed_alias, dynamic_alias};
fixed_alias!(pub Aes256Key, 32);
dynamic_alias!(pub Password, String);
let pw: Password = "hunter2".into();
assert_eq!(pw.expose_secret(), "hunter2");
#[cfg(feature = "zeroize")]
{
use secure_gate::{CloneableArray, CloneableString, CloneableVec};
let key: CloneableArray<32> = [0u8; 32].into();
let pw: CloneableString = "hunter2".into();
let seed: CloneableVec = vec![0u8; 64].into();
let key2 = key.clone();
let pw2 = pw.clone();
let seed2 = seed.clone();
}
#[cfg(feature = "rand")]
{
use secure_gate::fixed_alias_rng;
fixed_alias_rng!(pub MasterKey, 32);
fixed_alias_rng!(pub Nonce, 24);
let key = MasterKey::generate();
let nonce = Nonce::generate();
#[cfg(feature = "encoding-hex")]
{
let hex = key.into_hex();
println!("key hex: {}", hex.expose_secret());
}
}
```
## Opt-In Cloning
Cloning is available **only** when the `zeroize` feature is enabled.
The crate provides three ready-to-use cloneable primitives (zero boilerplate):
| `CloneableArray<const N: usize>` | Stack | `[u8; N]` | Fixed-size keys/nonces |
| `CloneableString` | Heap | `String` | Passwords, tokens, API keys |
| `CloneableVec` | Heap | `Vec<u8>` | Seeds, variable-length binary |
```rust
#[cfg(feature = "zeroize")]
{
use secure_gate::{CloneableArray, CloneableString, CloneableVec};
let key: CloneableArray<32> = [0u8; 32].into();
let mut pw: CloneableString = "hunter2".into();
let seed: CloneableVec = vec![0u8; 64].into();
let key2 = key.clone(); // Safe deep clone
let pw2 = pw.clone();
let seed2 = seed.clone();
// Convenience access to inner values
pw.expose_inner_mut().push('!');
assert_eq!(pw.expose_inner(), "hunter2!");
}
```
### Semantic Aliases (Recommended)
For better readability, create type aliases:
```rust
#[cfg(feature = "zeroize")]
{
use secure_gate::{CloneableArray, CloneableString, CloneableVec};
pub type CloneablePassword = CloneableString;
pub type CloneableAes256Key = CloneableArray<32>;
pub type CloneableSeed = CloneableVec;
}
```
These are zero-cost and make intent crystal clear.
### Minimizing Stack Exposure
When reading secrets from user input (e.g., passwords), use `init_with`/`try_init_with` to reduce temporary stack exposure:
```rust
#[cfg(feature = "zeroize")]
{
use secure_gate::CloneableString;
let pw = CloneableString::init_with(|| {
// Read from terminal, network, etc.
"hunter2".to_string()
});
// Or fallible:
let pw = CloneableString::try_init_with(|| {
Ok::<String, &str>("hunter2".to_string())
}).unwrap();
}
```
The temporary is cloned to the heap and zeroized immediately.
## Randomness
```rust
#[cfg(feature = "rand")]
{
use secure_gate::fixed_alias_rng;
fixed_alias_rng!(pub JwtSigningKey, 32);
fixed_alias_rng!(pub BackupCode, 16);
let key = JwtSigningKey::generate();
let code = BackupCode::generate();
#[cfg(feature = "encoding-hex")]
{
let hex_code = code.into_hex();
println!("Backup code: {}", hex_code.expose_secret());
}
}
```
`FixedRng<N>` can only be constructed via cryptographically secure RNG.
Direct generation is also available:
```rust
#[cfg(feature = "rand")]
{
use secure_gate::{Fixed, Dynamic};
let key: Fixed<[u8; 32]> = Fixed::generate_random();
let random: Dynamic<Vec<u8>> = Dynamic::generate_random(64);
}
```
## Encoding
```rust
#[cfg(feature = "encoding-hex")]
{
use secure_gate::{encoding::hex::HexString, encoding::SecureEncodingExt};
let bytes = [0u8; 16];
let hex: String = bytes.to_hex();
let hex_upper: String = bytes.to_hex_upper();
let validated = HexString::new("deadbeef".to_string()).unwrap();
let decoded = validated.decode_secret_to_bytes();
}
#[cfg(feature = "encoding-base64")]
{
use secure_gate::encoding::base64::Base64String;
let validated = Base64String::new("SGVsbG8".to_string()).unwrap();
let decoded = validated.decode_secret_to_bytes();
}
#[cfg(feature = "encoding-bech32")]
{
use secure_gate::encoding::bech32::Bech32String;
// See the rustdoc for Bech32String for detailed usage and validation rules.
}
```
Encoding functions require explicit `.expose_secret()`. Invalid inputs to the `.new()` constructors are zeroed when the `zeroize` feature is enabled.
## Constant-Time Equality
```rust
#[cfg(feature = "ct-eq")]
{
use secure_gate::Fixed;
let a = Fixed::<[u8; 32]>::generate_random();
let b = Fixed::<[u8; 32]>::generate_random();
assert!(a.ct_eq(&a));
}
```
Available on `Fixed<[u8; N]>` and `Dynamic<T>` where `T: AsRef<[u8]>`.
## Macros
```rust
use secure_gate::{fixed_alias, dynamic_alias};
fixed_alias!(pub Aes256Key, 32);
dynamic_alias!(pub Password, String);
#[cfg(feature = "rand")]
{
use secure_gate::fixed_alias_rng;
fixed_alias_rng!(pub MasterKey, 32);
}
```
## Memory Guarantees (`zeroize` enabled)
| `Fixed<T>` | Stack | Yes | Yes | Yes (no heap) | |
| `Dynamic<T>` | Heap | Yes | Yes | No (until drop) | Use `shrink_to_fit()` |
| `FixedRng<N>` | Stack | Yes | Yes | Yes | |
| `HexString` | Heap | Yes (invalid input) | Yes | No (until drop) | Validated hex |
| `Base64String` | Heap | Yes (invalid input) | Yes | No (until drop) | Validated base64 |
| `Bech32String` | Heap | Yes (invalid input) | Yes | No (until drop) | Validated Bech32 |
## Performance
The wrappers add no runtime overhead compared to raw types in benchmarks.
## Changelog
[CHANGELOG.md](https://github.com/Slurp9187/secure-gate/blob/v070rc/CHANGELOG.md)
## License
MIT OR Apache-2.0