lib

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

README.md (8865B)


      1 # radroots_secrets
      2 
      3 `radroots_secrets` defines the bounded secret-provider, key-wrapping, and
      4 authenticated-envelope contracts for Radroots. Hosts choose and configure a
      5 provider explicitly; this crate does not own application authorization,
      6 credential UI, account policy, runtime execution, or global provider state.
      7 
      8 The crate is pre-release and publication remains disabled. Its Cargo version
      9 is frozen at `0.1.0-alpha` until the coordinated release contract explicitly
     10 changes it.
     11 
     12 ## Canonical surface
     13 
     14 | Module | Responsibility |
     15 | --- | --- |
     16 | `id` | Validated, redacted identifiers, backend kinds, key versions, and single-owner references. |
     17 | `provider` | Provider capabilities, exact selection policy, and the executor-neutral provider SPI. |
     18 | `wrapping` | Bounded zeroizing plaintext, wrapped values, requests, and the key-wrapping SPI. |
     19 | `envelope` | Versioned XChaCha20-Poly1305 envelope encoding with authenticated provider metadata. |
     20 | `memory` | Opt-in, process-local adapter for development and deterministic tests. |
     21 | `file` | Opt-in encrypted file adapter with secure path, permission, and durable-write rules. |
     22 | `keyring` | Opt-in operating-system credential-store adapter with lazy native access. |
     23 | `error` | Normalized errors that do not expose provider-native messages or secret values. |
     24 
     25 The curated root exports only `EncryptedEnvelope`, `Error`, `SecretId`,
     26 `SecretRef`, `SecretProvider`, and `KeyWrapping`. Supporting request, policy,
     27 adapter, and value types remain in their owning modules so security boundaries
     28 stay explicit. The reviewed Rust surface is recorded in the
     29 [public API baseline](../../contracts/api_baselines/radroots_secrets.txt).
     30 
     31 ## Explicit provider and envelope flow
     32 
     33 The memory adapter is empty when constructed. This example provisions an
     34 explicit key, supplies a separate explicit data key and nonce, seals one value,
     35 serializes the envelope, and opens it again:
     36 
     37 ```rust
     38 # #[cfg(feature = "memory")]
     39 # fn main() -> Result<(), Box<dyn std::error::Error>> {
     40 use futures_executor::block_on;
     41 use radroots_secrets::EncryptedEnvelope;
     42 use radroots_secrets::context::{
     43     EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId,
     44 };
     45 use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest};
     46 use radroots_secrets::id::{BackendKind, KeyVersion};
     47 use radroots_secrets::memory::MemoryProvider;
     48 use radroots_secrets::wrapping::SecretMaterial;
     49 use radroots_secrets::{SecretId, SecretRef};
     50 
     51 let reference = SecretRef::new(
     52     SecretId::parse("example-profile-key")?,
     53     BackendKind::Memory,
     54     KeyVersion::new(1)?,
     55 );
     56 let provider = MemoryProvider::new();
     57 provider.provision(
     58     &reference,
     59     SecretMaterial::from_slice(&[0x41; 32])?,
     60 )?;
     61 
     62 let plaintext = SecretMaterial::from_slice(b"private profile value")?;
     63 let data_key = SecretMaterial::from_slice(&[0x41; 32])?;
     64 let context = EnvelopeContext::new(
     65     EnvelopePurpose::parse("radroots.private_profile")?,
     66     EnvelopeSubject::parse("profile", "example-profile")?,
     67     PayloadSchemaId::parse("radroots.private_profile.v1")?,
     68 );
     69 let request = SealRequest::new(
     70     reference,
     71     context.clone(),
     72     &plaintext,
     73     SealMaterial::new(data_key, Nonce::new([0x24; 24])),
     74 );
     75 let encoded = block_on(EncryptedEnvelope::seal(&provider, request))?.encode()?;
     76 let decoded = EncryptedEnvelope::decode(&encoded)?;
     77 let opened = block_on(decoded.open(&provider, &context))?;
     78 
     79 opened.expose_secret(|bytes| assert_eq!(bytes, b"private profile value"));
     80 # Ok(())
     81 # }
     82 # #[cfg(not(feature = "memory"))]
     83 # fn main() {}
     84 ```
     85 
     86 A runnable version is available at
     87 [`examples/explicit_memory_provider.rs`](examples/explicit_memory_provider.rs).
     88 Its fixed key and nonce exist only to make the example deterministic. Production
     89 hosts must supply cryptographically strong key material and a unique nonce for
     90 every encryption under the same data key.
     91 
     92 ## Features and supported targets
     93 
     94 | Feature | Default | Effect |
     95 | --- | --- | --- |
     96 | `std` | yes | Enables standard-library integration; it performs no I/O by itself. |
     97 | `serde` | yes | Serializes validated identifiers and encoded envelopes, never plaintext material or capability references. |
     98 | `memory` | no | Enables the explicit process-local adapter; requires `std`. |
     99 | `file` | no | Enables encrypted file persistence; requires `std`. |
    100 | `keyring` | no | Enables lazy operating-system credential-store access; requires `std`. |
    101 
    102 The core contracts compile without the standard library. `file` and `keyring`
    103 are native host adapters; `memory` is available on standard-library targets.
    104 No feature installs a global provider, starts a runtime, generates key
    105 material, selects a fallback, opens storage, or performs a credential prompt.
    106 
    107 ## Security and serialization contract
    108 
    109 `SecretMaterial` is bounded, single-owner, zeroizing plaintext. It implements
    110 neither `Clone` nor serialization and exposes bytes only inside an explicit
    111 closure. `SecretRef`, seal requests, and seal material are likewise not
    112 cloneable or serializable. Secret identifiers are serialized only where a
    113 documented wire or storage format requires them; their ordinary diagnostics
    114 remain redacted.
    115 
    116 `EncryptedEnvelope` v2 authenticates its format version, cipher, key source,
    117 backend, key version, secret identifier, purpose, typed subject, payload
    118 schema, nonce, wrapped data key, and ciphertext length. Normal open requires an
    119 independently derived expected context and rejects legacy v1. Decode validates
    120 all lengths and enum values before provider access. Envelope serialization is
    121 a persistence contract; Rust layout and debug output are not. Unknown versions,
    122 ciphers, key sources, malformed lengths, context mismatches, backend mismatches,
    123 and authentication failures fail closed.
    124 
    125 Legacy v1 bytes remain decodeable for migration inventory, but normal open
    126 always rejects them. An authorized host migration boundary must explicitly
    127 construct `LegacyV1ResealAuthority`, supply the independently derived v2
    128 context and expected provider reference, validate the owning payload schema,
    129 and provide a fresh data key and nonce. The migration primitive rejects key or
    130 nonce reuse and returns only a new v2 envelope plus a plaintext commitment;
    131 transient plaintext and key material remain single-owner zeroizing values.
    132 
    133 Provider-native error strings are normalized before crossing the public
    134 boundary. Callers must still avoid logging plaintext, serialized identifiers,
    135 encoded envelopes, or provider configuration.
    136 
    137 ## Side effects, cancellation, and commit points
    138 
    139 Constructing `MemoryProvider` and `KeyringProvider`, querying capabilities,
    140 validating identifiers, selecting a provider, and encoding or decoding an
    141 already-built envelope do not access secret storage. `KeyringProvider` creates
    142 native credential entries lazily when an explicit operation begins.
    143 
    144 Memory provisioning commits when the in-process map accepts the value. File
    145 provisioning commits when the no-clobber entry is durably renamed and its
    146 directory is synchronized. File rotation commits the new version before
    147 removing the old version and can resume that boundary. Keyring provisioning
    148 commits when the native store accepts the credential. Removal is idempotent;
    149 rotation requires a higher version of the same identifier.
    150 
    151 The SPIs return executor-neutral futures and do not create cancellation tokens
    152 or background work. Dropping a future before its provider commit point cancels
    153 only work the provider has not committed. After a durable or native commit,
    154 the host must inspect or retry the exact operation; it must not assume that
    155 dropping the future rolled the operation back. The built-in adapters perform no
    156 implicit retry or fallback.
    157 
    158 ## Intended consumers
    159 
    160 Direct consumers are storage adapters, SDK signing hosts, Myc custody hosts,
    161 and other first-party runtimes that own provider selection and authorization.
    162 Applications should normally consume these contracts through `radroots_sdk`.
    163 External provider implementations may implement `SecretProvider` and
    164 `KeyWrapping` without depending on a built-in adapter.
    165 
    166 This package must not acquire actor authorization, signing policy, account
    167 management, application database ownership, UI prompts, executor ownership,
    168 global sessions, network transport, or provider fallback. Those responsibilities
    169 remain with their dedicated packages and hosts.
    170 
    171 ## Package charter
    172 
    173 The authoritative responsibility, dependency, feature, module, root-export,
    174 and forbidden-scope contract is the
    175 [Radroots crates Release V1 specification](../../contracts/crates/release_v1/radroots_crates_release_v1.toml).
    176 The reviewed surface is the
    177 [`radroots_secrets` API baseline](../../contracts/api_baselines/radroots_secrets.txt).
    178 
    179 ## Copyright
    180 
    181 Except as otherwise noted, all files in the `radroots_secrets` distribution
    182 are copyright (c) 2025 Tyson Lupul. See `LICENSE` for usage, redistribution,
    183 and warranty terms.