lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 40c40f1e592f7b999cc67be5d76e9a163bc97c42
parent 16ded0a690655fedd6da0e8a427785c6e76098d3
Author: triesap <tyson@radroots.org>
Date:   Sat,  1 Aug 2026 12:29:26 +0000

secrets: document package API

- publish the complete package and security charter
- include README content as crate-level documentation
- add an explicit memory-provider envelope example
- enforce documentation and baseline boundaries

Diffstat:
Mcrates/secrets/Cargo.toml | 5+++++
Mcrates/secrets/README.md | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Acrates/secrets/examples/explicit_memory_provider.rs | 32++++++++++++++++++++++++++++++++
Mcrates/secrets/src/lib.rs | 6++++--
Mcrates/secrets/tests/package_boundary.rs | 33+++++++++++++++++++++++++++++++++
5 files changed, 230 insertions(+), 6 deletions(-)

diff --git a/crates/secrets/Cargo.toml b/crates/secrets/Cargo.toml @@ -38,5 +38,10 @@ futures-executor = { workspace = true } hex = { workspace = true } serde_json = { workspace = true, features = ["std"] } +[[example]] +name = "explicit_memory_provider" +path = "examples/explicit_memory_provider.rs" +required-features = ["memory"] + [lints] workspace = true diff --git a/crates/secrets/README.md b/crates/secrets/README.md @@ -1,7 +1,159 @@ # radroots_secrets -Secret material and protected-storage abstractions for Radroots. +`radroots_secrets` defines the bounded secret-provider, key-wrapping, and +authenticated-envelope contracts for Radroots. Hosts choose and configure a +provider explicitly; this crate does not own application authorization, +credential UI, account policy, runtime execution, or global provider state. -This package root is established for the Release V1 refactor. Secret types, -providers, and wrapping policy are migrated in the subsequent secrets -checkpoints. +The crate is pre-release and publication remains disabled. Its Cargo version +is frozen at `0.1.0-alpha` until the coordinated release contract explicitly +changes it. + +## Canonical surface + +| Module | Responsibility | +| --- | --- | +| `id` | Validated, redacted identifiers, backend kinds, key versions, and single-owner references. | +| `provider` | Provider capabilities, exact selection policy, and the executor-neutral provider SPI. | +| `wrapping` | Bounded zeroizing plaintext, wrapped values, requests, and the key-wrapping SPI. | +| `envelope` | Versioned XChaCha20-Poly1305 envelope encoding with authenticated provider metadata. | +| `memory` | Opt-in, process-local adapter for development and deterministic tests. | +| `file` | Opt-in encrypted file adapter with secure path, permission, and durable-write rules. | +| `keyring` | Opt-in operating-system credential-store adapter with lazy native access. | +| `error` | Normalized errors that do not expose provider-native messages or secret values. | + +The curated root exports only `EncryptedEnvelope`, `Error`, `SecretId`, +`SecretRef`, `SecretProvider`, and `KeyWrapping`. Supporting request, policy, +adapter, and value types remain in their owning modules so security boundaries +stay explicit. The reviewed Rust surface is recorded in the +[public API baseline](../../docs/api/radroots_secrets.txt). + +## Explicit provider and envelope flow + +The memory adapter is empty when constructed. This example provisions an +explicit key, supplies a separate explicit data key and nonce, seals one value, +serializes the envelope, and opens it again: + +```rust +use futures_executor::block_on; +use radroots_secrets::EncryptedEnvelope; +use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; +use radroots_secrets::id::{BackendKind, KeyVersion}; +use radroots_secrets::memory::MemoryProvider; +use radroots_secrets::wrapping::SecretMaterial; +use radroots_secrets::{SecretId, SecretRef}; + +let reference = SecretRef::new( + SecretId::parse("example-profile-key")?, + BackendKind::Memory, + KeyVersion::new(1)?, +); +let provider = MemoryProvider::new(); +provider.provision( + &reference, + SecretMaterial::from_slice(&[0x41; 32])?, +)?; + +let plaintext = SecretMaterial::from_slice(b"private profile value")?; +let data_key = SecretMaterial::from_slice(&[0x41; 32])?; +let request = SealRequest::new( + reference, + &plaintext, + SealMaterial::new(data_key, Nonce::new([0x24; 24])), +); +let encoded = block_on(EncryptedEnvelope::seal(&provider, request))?.encode()?; +let decoded = EncryptedEnvelope::decode(&encoded)?; +let opened = block_on(decoded.open(&provider))?; + +opened.expose_secret(|bytes| assert_eq!(bytes, b"private profile value")); +# Ok::<(), Box<dyn std::error::Error>>(()) +``` + +A runnable version is available at +[`examples/explicit_memory_provider.rs`](examples/explicit_memory_provider.rs). +Its fixed key and nonce exist only to make the example deterministic. Production +hosts must supply cryptographically strong key material and a unique nonce for +every encryption under the same data key. + +## Features and supported targets + +| Feature | Default | Effect | +| --- | --- | --- | +| `std` | yes | Enables standard-library integration; it performs no I/O by itself. | +| `serde` | yes | Serializes validated identifiers and encoded envelopes, never plaintext material or capability references. | +| `memory` | no | Enables the explicit process-local adapter; requires `std`. | +| `file` | no | Enables encrypted file persistence; requires `std`. | +| `keyring` | no | Enables lazy operating-system credential-store access; requires `std`. | + +The core contracts compile without the standard library. `file` and `keyring` +are native host adapters; `memory` is available on standard-library targets. +No feature installs a global provider, starts a runtime, generates key +material, selects a fallback, opens storage, or performs a credential prompt. + +## Security and serialization contract + +`SecretMaterial` is bounded, single-owner, zeroizing plaintext. It implements +neither `Clone` nor serialization and exposes bytes only inside an explicit +closure. `SecretRef`, seal requests, and seal material are likewise not +cloneable or serializable. Secret identifiers are serialized only where a +documented wire or storage format requires them; their ordinary diagnostics +remain redacted. + +`EncryptedEnvelope` authenticates its format version, cipher, key source, +backend, key version, secret identifier, nonce, wrapped data key, and +ciphertext length. Decode validates all lengths and enum values before provider +access. Envelope serialization is a persistence contract; Rust layout and +debug output are not. Unknown versions, ciphers, key sources, malformed +lengths, backend mismatches, and authentication failures fail closed. + +Provider-native error strings are normalized before crossing the public +boundary. Callers must still avoid logging plaintext, serialized identifiers, +encoded envelopes, or provider configuration. + +## Side effects, cancellation, and commit points + +Constructing `MemoryProvider` and `KeyringProvider`, querying capabilities, +validating identifiers, selecting a provider, and encoding or decoding an +already-built envelope do not access secret storage. `KeyringProvider` creates +native credential entries lazily when an explicit operation begins. + +Memory provisioning commits when the in-process map accepts the value. File +provisioning commits when the no-clobber entry is durably renamed and its +directory is synchronized. File rotation commits the new version before +removing the old version and can resume that boundary. Keyring provisioning +commits when the native store accepts the credential. Removal is idempotent; +rotation requires a higher version of the same identifier. + +The SPIs return executor-neutral futures and do not create cancellation tokens +or background work. Dropping a future before its provider commit point cancels +only work the provider has not committed. After a durable or native commit, +the host must inspect or retry the exact operation; it must not assume that +dropping the future rolled the operation back. The built-in adapters perform no +implicit retry or fallback. + +## Intended consumers + +Direct consumers are storage adapters, SDK signing hosts, Myc custody hosts, +and other first-party runtimes that own provider selection and authorization. +Applications should normally consume these contracts through `radroots_sdk`. +External provider implementations may implement `SecretProvider` and +`KeyWrapping` without depending on a built-in adapter. + +This package must not acquire actor authorization, signing policy, account +management, application database ownership, UI prompts, executor ownership, +global sessions, network transport, or provider fallback. Those responsibilities +remain with their dedicated packages and hosts. + +## Package charter + +The authoritative responsibility, dependency, feature, module, root-export, +and forbidden-scope contract is the +[Radroots crates Release V1 specification](../../docs/specs/radroots_crates_release_v1.md). +The baseline generation procedure and pinned toolchain are documented in +[`docs/api/README.md`](../../docs/api/README.md). + +## Copyright + +Except as otherwise noted, all files in the `radroots_secrets` distribution +are copyright (c) 2025 Tyson Lupul. See `LICENSE` for usage, redistribution, +and warranty terms. diff --git a/crates/secrets/examples/explicit_memory_provider.rs b/crates/secrets/examples/explicit_memory_provider.rs @@ -0,0 +1,32 @@ +use futures_executor::block_on; +use radroots_secrets::EncryptedEnvelope; +use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; +use radroots_secrets::id::{BackendKind, KeyVersion}; +use radroots_secrets::memory::MemoryProvider; +use radroots_secrets::wrapping::SecretMaterial; +use radroots_secrets::{SecretId, SecretRef}; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let reference = SecretRef::new( + SecretId::parse("example-profile-key")?, + BackendKind::Memory, + KeyVersion::new(1)?, + ); + let provider = MemoryProvider::new(); + provider.provision(&reference, SecretMaterial::from_slice(&[0x41; 32])?)?; + + let plaintext = SecretMaterial::from_slice(b"private profile value")?; + let data_key = SecretMaterial::from_slice(&[0x41; 32])?; + let request = SealRequest::new( + reference, + &plaintext, + SealMaterial::new(data_key, Nonce::new([0x24; 24])), + ); + let encoded = block_on(EncryptedEnvelope::seal(&provider, request))?.encode()?; + let decoded = EncryptedEnvelope::decode(&encoded)?; + let opened = block_on(decoded.open(&provider))?; + + opened.expose_secret(|bytes| assert_eq!(bytes, b"private profile value")); + println!("opened one explicitly provisioned in-memory envelope"); + Ok(()) +} diff --git a/crates/secrets/src/lib.rs b/crates/secrets/src/lib.rs @@ -1,6 +1,8 @@ -//! Secret material and protected-storage abstractions for Radroots. - +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] +#![deny(rustdoc::broken_intra_doc_links)] +#![doc = include_str!("../README.md")] extern crate alloc; diff --git a/crates/secrets/tests/package_boundary.rs b/crates/secrets/tests/package_boundary.rs @@ -4,6 +4,8 @@ use std::path::{Path, PathBuf}; const MANIFEST: &str = include_str!("../Cargo.toml"); const ROOT: &str = include_str!("../src/lib.rs"); +const README: &str = include_str!("../README.md"); +const EXAMPLE: &str = include_str!("../examples/explicit_memory_provider.rs"); #[test] fn manifest_has_final_identity_features_and_no_radroots_dependencies() { @@ -104,6 +106,37 @@ fn crate_root_contains_only_the_approved_module_skeleton() { ); } +#[test] +fn package_documentation_covers_the_security_and_host_contract() { + for required in [ + "## Canonical surface", + "## Explicit provider and envelope flow", + "## Features and supported targets", + "## Security and serialization contract", + "## Side effects, cancellation, and commit points", + "## Intended consumers", + "public API baseline", + "implicit retry or fallback", + ] { + assert!(README.contains(required), "README is missing `{required}`"); + } + assert!(ROOT.contains("#![doc = include_str!(\"../README.md\")]")); + assert!(MANIFEST.contains("name = \"explicit_memory_provider\"")); + assert!(MANIFEST.contains("required-features = [\"memory\"]")); + for required in [ + "MemoryProvider::new()", + "provider.provision(", + "EncryptedEnvelope::seal", + "EncryptedEnvelope::decode", + "decoded.open(&provider)", + ] { + assert!( + EXAMPLE.contains(required), + "example is missing `{required}`" + ); + } +} + fn table_keys<'a>(source: &'a str, table: &str) -> BTreeSet<&'a str> { let Some((_, body)) = source.split_once(table) else { return BTreeSet::new();