Skip to main content

secret_manager/
lib.rs

1//! Distributed secret-key rotation and in-process caching.
2//!
3//! # Overview
4//!
5//! `secret-manager` manages a *ring buffer* of versioned encryption keys shared across a
6//! cluster.  Keys are generated, encrypted, and persisted by a **rotator**; they are fetched,
7//! decrypted, and cached in memory by a **syncer**.  The two roles are intentionally
8//! decoupled so you can deploy them in whatever topology fits your system.
9//!
10//! ## Core concepts
11//!
12//! | Concept | Type | Purpose |
13//! |---------|------|---------|
14//! | Key group | [`GroupId`] | Logical namespace for a set of rotating keys |
15//! | Ring buffer | [`InMemorySecretGroup<V, S>`] | In-process cache; `V` slots, each key `S` bytes |
16//! | Version | `u8` | Slot index (0 … V-1); wraps modulo V, **not** at 255 |
17//! | Rotator | [`KeyRotator`] | Background task — generates and stores new keys |
18//! | Syncer | [`SecretSyncer`] | Background task — polls storage and updates the ring |
19//! | Manager | [`SecretManager`] | Convenience facade that runs both together |
20//!
21//! # Usage patterns
22//!
23//! ## All-in-one: `SecretManager`
24//!
25//! Use this when a single service instance should both rotate **and** consume keys:
26//!
27//! ```rust,no_run
28//! # use secret_manager::*;
29//! # use async_trait::async_trait;
30//! # use std::{sync::Arc, time::{Duration, SystemTime}};
31//! # use tokio_util::sync::CancellationToken;
32//! # #[derive(Clone)]
33//! # struct MyBackend;
34//! # #[async_trait]
35//! # impl SecretBackend for MyBackend {
36//! #     type Error = std::convert::Infallible;
37//! #     async fn load_all(&self, _: &str) -> Result<Vec<KeyRecord>, Self::Error> { Ok(vec![]) }
38//! #     async fn poll_new(&self, _: &str, _: SystemTime, _: i64) -> Result<Vec<KeyRecord>, Self::Error> { Ok(vec![]) }
39//! # }
40//! # #[async_trait]
41//! # impl SecretRotationBackend for MyBackend {
42//! #     type Error = std::convert::Infallible;
43//! #     async fn latest_key_info(&self, _: &str) -> Result<Option<(u8, SystemTime)>, Self::Error> { Ok(None) }
44//! #     async fn try_insert_key(&self, _: &str, _: Option<u8>, _: u8, _: &Encrypted, _: SystemTime) -> Result<bool, Self::Error> { Ok(true) }
45//! # }
46//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
47//! # let (backend, encryptor) = (MyBackend, NoOpEncryptor);
48//! let group = Arc::new(InMemorySecretGroup::<256, 32>::new(0, [0u8; 32]));
49//!
50//! // `backend` implements both `SecretBackend` and `SecretRotationBackend`.
51//! // `encryptor` implements `KeyEncryptor` (e.g. `LocalEncryptor`).
52//! let manager = SecretManager::new(
53//!     "payments-signing",
54//!     Arc::clone(&group),
55//!     backend,
56//!     encryptor,
57//!     Duration::from_secs(3600), // rotate every hour
58//!     Duration::from_secs(30),   // propagation delay before activating a new key
59//!     None,                      // poll interval (default: 5 s)
60//!     None,                      // key generator (default: CSPRNG)
61//! );
62//!
63//! let token = CancellationToken::new();
64//! let handle = manager.start(token.clone()).await?;
65//!
66//! // Use `group.current()` to get the active signing key.
67//! // Use `group.resolve(version)` to verify tokens issued with an older key.
68//!
69//! token.cancel();
70//! handle.wait().await; // wait for background tasks to stop cleanly
71//! # Ok(()) }
72//! ```
73//!
74//! ## Rotation-only: `KeyRotator`
75//!
76//! Deploy a single dedicated rotation service that writes keys to storage while the rest of
77//! your fleet only reads them via syncers:
78//!
79//! ```rust,no_run
80//! # use secret_manager::*;
81//! # use async_trait::async_trait;
82//! # use std::{time::{Duration, SystemTime}};
83//! # use tokio_util::sync::CancellationToken;
84//! # struct MyBackend;
85//! # #[async_trait]
86//! # impl SecretRotationBackend for MyBackend {
87//! #     type Error = std::convert::Infallible;
88//! #     async fn latest_key_info(&self, _: &str) -> Result<Option<(u8, SystemTime)>, Self::Error> { Ok(None) }
89//! #     async fn try_insert_key(&self, _: &str, _: Option<u8>, _: u8, _: &Encrypted, _: SystemTime) -> Result<bool, Self::Error> { Ok(true) }
90//! # }
91//! # async fn example() {
92//! # let (backend, encryptor) = (MyBackend, NoOpEncryptor);
93//! let rotator: KeyRotator<_, _, 256, 32> = KeyRotator::new(
94//!     "session-tokens",
95//!     backend,                   // implements `SecretRotationBackend`
96//!     Duration::from_secs(3600),
97//!     Duration::from_secs(30),
98//!     encryptor,
99//!     || [0u8; 32],
100//! );
101//! rotator.run(CancellationToken::new()).await;
102//! # }
103//! ```
104//!
105//! ## Read-only: `SecretSyncer`
106//!
107//! Reader instances that never rotate — they only follow the key stream from storage:
108//!
109//! ```rust,no_run
110//! # use secret_manager::*;
111//! # use async_trait::async_trait;
112//! # use std::{sync::Arc, time::{Duration, SystemTime}};
113//! # use tokio_util::sync::CancellationToken;
114//! # #[derive(Clone)]
115//! # struct MyBackend;
116//! # #[async_trait]
117//! # impl SecretBackend for MyBackend {
118//! #     type Error = std::convert::Infallible;
119//! #     async fn load_all(&self, _: &str) -> Result<Vec<KeyRecord>, Self::Error> { Ok(vec![]) }
120//! #     async fn poll_new(&self, _: &str, _: SystemTime, _: i64) -> Result<Vec<KeyRecord>, Self::Error> { Ok(vec![]) }
121//! # }
122//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
123//! # let (backend, encryptor) = (MyBackend, NoOpEncryptor);
124//! let group = Arc::new(InMemorySecretGroup::<256, 32>::new(0, [0u8; 32]));
125//! let mut syncer: SecretSyncer<_, _, 256, 32> = SecretSyncer::new(
126//!     "api-tokens",
127//!     Arc::clone(&group),
128//!     backend,   // implements `SecretBackend`
129//!     encryptor,
130//!     Duration::from_secs(3600), // used to compute smart poll intervals
131//!     None,                      // poll interval override
132//! );
133//!
134//! let token = CancellationToken::new();
135//! let cursor = syncer.initial_load(&token).await?;
136//! tokio::spawn(syncer.run(token, cursor));
137//! # Ok(()) }
138//! ```
139//!
140//! # Encryptors
141//!
142//! | Type | Crate feature | Notes |
143//! |------|--------------|-------|
144//! | [`NoOpEncryptor`] | *(always)* | Plaintext — testing / storage-layer encryption |
145//! | [`LocalEncryptor`] | *(always)* | AES-256-GCM-SIV with a local 32-byte key |
146//! | `KmsEncryptor` | `aws-kms` | AWS KMS — KMS manages the IV; requires network |
147//!
148//! # Backends
149//!
150//! | Type | Crate feature |
151//! |------|--------------|
152//! | `DieselPgSecretBackend` | `pg-diesel-async` |
153//! | `SqlxPgSecretBackend` | `pg-sqlx` |
154//!
155//! Implement [`SecretBackend`] + [`SecretRotationBackend`] together to bring your own backend.
156
157mod backend;
158#[cfg(feature = "pg-diesel-async")]
159mod diesel_pg_backend;
160mod encryptor;
161mod local_encryptor;
162#[cfg(feature = "aws-kms")]
163mod aws_kms_encryptor;
164mod no_op_encryptor;
165mod manager;
166#[cfg(any(feature = "pg-diesel-async", feature = "pg-sqlx"))]
167mod pg_queries;
168mod rotator;
169mod secret_rotation;
170#[cfg(feature = "pg-sqlx")]
171mod sqlx_pg_backend;
172mod syncer;
173mod util;
174
175/// Identifies a logical group of rotating keys.
176///
177/// A group ID is a human-readable label stored as `VARCHAR(32)` in the database.
178/// Keep it short, lowercase, and slug-style (e.g. `"payments-signing"`, `"session-tokens"`).
179/// Values longer than 32 characters will be rejected by the storage backend.
180pub type GroupId = String;
181
182
183
184pub use backend::{KeyRecord, SecretBackend};
185pub use encryptor::{Encrypted, EncryptorError, KeyEncryptor};
186pub use local_encryptor::LocalEncryptor;
187pub use manager::{SecretManager, SecretManagerHandle};
188pub use no_op_encryptor::NoOpEncryptor;
189pub use rotator::{KeyRotator, SecretRotationBackend};
190pub use secret_rotation::{InMemorySecretGroup, SecretGroup};
191pub use syncer::SecretSyncer;
192
193#[cfg(any(test, feature = "aws-kms"))]
194pub use aws_kms_encryptor::KmsEncryptor;
195#[cfg(any(test, feature = "pg-diesel-async"))]
196pub use diesel_pg_backend::{DieselPgSecretBackend, DieselPgSecretBackendError};
197#[cfg(any(test, feature = "pg-sqlx"))]
198pub use sqlx_pg_backend::{SqlxPgSecretBackend, SqlxPgSecretBackendError};