1#![doc = include_str!("../README.md")]
2
3pub mod args;
4
5use async_trait::async_trait;
6use cache_loader_async::{
7 backing::{LruCacheBacking, TtlCacheBacking, TtlMeta},
8 cache_api::{CacheEntry, LoadingCache, WithMeta},
9};
10use rand::rngs::ThreadRng;
11use serde::{Deserialize, Serialize};
12#[cfg(unix)]
13use std::os::unix::prelude::{MetadataExt, PermissionsExt};
14use std::{
15 collections::HashMap,
16 io::ErrorKind,
17 path::PathBuf,
18 time::{Duration, SystemTime},
19};
20use streambed::{
21 crypto::{self, KEY_SIZE, SALT_SIZE},
22 secret_store::{
23 AppRoleAuthReply, AuthToken, Error, GetSecretReply, SecretData, SecretStore,
24 UserPassAuthReply,
25 },
26};
27use tokio::{
28 fs,
29 io::{AsyncReadExt, AsyncWriteExt},
30 time::Instant,
31};
32
33const AUTHORIZED_SECRET_TTL: Duration = Duration::from_secs(60 * 5);
34const USERPASS_LEASE_TIME: Duration = Duration::from_secs(86400 * 7);
35
36type TtlCache = LoadingCache<
37 String,
38 Option<GetSecretReply>,
39 Error,
40 TtlCacheBacking<
41 String,
42 CacheEntry<Option<GetSecretReply>, Error>,
43 LruCacheBacking<String, (CacheEntry<Option<GetSecretReply>, Error>, Instant)>,
44 >,
45>;
46
47#[derive(Deserialize, Serialize)]
48struct StorableSecretData {
49 version: u32,
50 secret_data: SecretData,
51}
52
53#[derive(Debug, Deserialize, Serialize)]
54struct TokenData {
55 username: String,
56 expires: u128,
57}
58
59#[derive(Debug, Deserialize)]
60struct ClientToken {
61 data: TokenData,
62 signature: String,
63}
64
65#[derive(Clone)]
72pub struct FileSecretStore {
73 cache: TtlCache,
74 max_secrets_cached: usize,
75 root_path: PathBuf,
76 root_secret: [u8; KEY_SIZE],
77 ttl_field: Option<String>,
78 unauthorized_timeout: Duration,
79}
80
81impl FileSecretStore {
82 pub fn new<P: Into<PathBuf>>(
83 root_path: P,
84 root_secret: &[u8; KEY_SIZE],
85 unauthorized_timeout: Duration,
86 max_secrets_cached: usize,
87 ttl_field: Option<&str>,
88 ) -> Self {
89 let root_path = root_path.into();
90 Self::with_new_cache(
91 root_path,
92 root_secret,
93 unauthorized_timeout,
94 max_secrets_cached,
95 ttl_field.map(|s| s.to_string()),
96 )
97 }
98
99 fn with_new_cache(
100 root_path: PathBuf,
101 root_secret: &[u8; KEY_SIZE],
102 unauthorized_timeout: Duration,
103 max_secrets_cached: usize,
104 ttl_field: Option<String>,
105 ) -> Self {
106 let retained_root_path = root_path.clone();
107 let retained_root_secret = root_secret;
108 let retained_ttl_field = ttl_field.clone();
109
110 let root_secret = *root_secret;
111
112 let cache: TtlCache = LoadingCache::with_meta_loader(
113 TtlCacheBacking::with_backing(
114 unauthorized_timeout,
115 LruCacheBacking::new(max_secrets_cached),
116 ),
117 move |secret_path| {
118 let task_root_path = root_path.clone();
119 let task_ttl_field = ttl_field.clone();
120
121 async move {
122 let mut result = Err(Error::Unauthorized);
123 match fs::File::open(task_root_path.join(secret_path)).await {
124 Ok(mut file) => {
125 let mut buf = Vec::new();
126 if file.read_to_end(&mut buf).await.is_ok()
127 && buf.len() >= crypto::SALT_SIZE
128 {
129 let (salt, bytes) = buf.split_at_mut(crypto::SALT_SIZE);
130 if let Ok(salt) = salt.try_into() {
131 crypto::decrypt(bytes, &root_secret, &salt);
132 if let Ok(stored) =
133 postcard::from_bytes::<StorableSecretData>(bytes)
134 {
135 let secret_data = stored.secret_data;
136 let mut lease_duration = None;
137 if let Some(ttl_field) = task_ttl_field {
138 if let Some(ttl) = secret_data.data.get(&ttl_field) {
139 if let Ok(ttl_duration) =
140 ttl.parse::<humantime::Duration>()
141 {
142 lease_duration = Some(ttl_duration.into());
143 }
144 }
145 }
146
147 result = Ok(Some(GetSecretReply {
148 lease_duration: lease_duration
149 .unwrap_or(AUTHORIZED_SECRET_TTL)
150 .as_secs(),
151 data: secret_data,
152 }))
153 .with_meta(lease_duration.map(TtlMeta::from))
154 }
155 }
156 }
157 }
158 Err(e) if e.kind() == ErrorKind::NotFound => {
159 result = Ok(None).with_meta(None);
160 }
161 Err(_) => (),
162 }
163 result
164 }
165 },
166 );
167
168 Self {
169 cache,
170 root_path: retained_root_path,
171 root_secret: *retained_root_secret,
172 max_secrets_cached,
173 ttl_field: retained_ttl_field,
174 unauthorized_timeout,
175 }
176 }
177
178 pub fn with_new_auth_prepared(ss: &Self) -> Self {
179 Self::with_new_cache(
180 ss.root_path.clone(),
181 &ss.root_secret,
182 ss.unauthorized_timeout,
183 ss.max_secrets_cached,
184 ss.ttl_field.clone(),
185 )
186 }
187
188 fn hashed(password: &str, salt: &[u8; SALT_SIZE]) -> Vec<u8> {
189 crypto::hash(password.as_bytes(), salt)
190 }
191}
192
193#[async_trait]
194impl SecretStore for FileSecretStore {
195 async fn approle_auth(
199 &self,
200 _role_id: &str,
201 _secret_id: &str,
202 ) -> Result<AppRoleAuthReply, Error> {
203 Ok(AppRoleAuthReply {
204 auth: AuthToken {
205 client_token: "some-token".to_string(),
206 lease_duration: u64::MAX,
207 },
208 })
209 }
210
211 async fn create_secret(&self, secret_path: &str, secret_data: SecretData) -> Result<(), Error> {
212 match fs::metadata(&self.root_path).await {
213 #[cfg(unix)]
214 Ok(attrs) if attrs.permissions().mode() & 0o077 != 0 => Err(Error::Unauthorized),
215 Ok(attrs) => {
216 let mut result = Err(Error::Unauthorized);
217
218 let path = self.root_path.join(secret_path);
219 if let Some(parent) = path.parent() {
220 let _ = fs::create_dir_all(parent).await;
221 }
222
223 let mut file_options = fs::OpenOptions::new();
224 let mut open_options = file_options.create(true).write(true);
225 #[cfg(unix)]
226 {
227 open_options = open_options.mode(attrs.mode());
228 }
229
230 if let Ok(mut file) = open_options.open(path).await {
231 let stored = StorableSecretData {
232 version: 0,
233 secret_data,
234 };
235 if let Ok(mut bytes) = postcard::to_stdvec(&stored) {
236 let salt = {
237 let mut rng = ThreadRng::default();
238 crypto::salt(&mut rng)
239 };
240 crypto::encrypt(&mut bytes, &self.root_secret, &salt);
241 let mut buf = Vec::with_capacity(SALT_SIZE + bytes.len());
242 buf.extend(salt);
243 buf.extend(bytes);
244
245 if file.write_all(&buf).await.is_ok() && file.sync_all().await.is_ok() {
246 result = Ok(());
247 let _ = self.cache.remove(secret_path.to_string()).await;
249 }
250 }
251 }
252 result
253 }
254 Err(_) => Err(Error::Unauthorized),
255 }
256 }
257
258 async fn get_secret(&self, secret_path: &str) -> Result<Option<GetSecretReply>, Error> {
259 match fs::metadata(&self.root_path).await {
260 #[cfg(unix)]
261 Ok(attrs) if attrs.permissions().mode() & 0o077 != 0 => Err(Error::Unauthorized),
262 Ok(_) => self
263 .cache
264 .get(secret_path.to_string())
265 .await
266 .map_err(|e| e.as_loading_error().unwrap().clone()), Err(_) => Err(Error::Unauthorized),
268 }
269 }
270
271 async fn userpass_auth(
272 &self,
273 username: &str,
274 password: &str,
275 ) -> Result<UserPassAuthReply, Error> {
276 if let Ok(Some(data)) = self
277 .get_secret(&format!("auth/userpass/users/{username}"))
278 .await
279 {
280 if let Some(data_password) = data.data.data.get("password") {
281 if let Ok(data_password) = hex::decode(data_password) {
282 let (salt, _) = data_password.split_at(SALT_SIZE);
283 if let Ok(salt) = salt.try_into() {
284 let password = Self::hashed(password, &salt);
285 if password == data_password {
286 let now = SystemTime::now();
287 let expires = now
288 .checked_add(USERPASS_LEASE_TIME)
289 .unwrap_or(now)
290 .duration_since(SystemTime::UNIX_EPOCH)
291 .map(|t| t.as_millis())
292 .unwrap_or(0);
293 let data =
294 format!(r#"{{"username":"{username}","expires":{expires}}}"#);
295 let signature =
296 hex::encode(crypto::sign(data.as_bytes(), &self.root_secret));
297 return Ok(UserPassAuthReply {
298 auth: AuthToken {
299 client_token: base64::encode(format!(
300 r#"{{"data":{data},"signature":"{signature}"}}"#
301 )),
302 lease_duration: USERPASS_LEASE_TIME.as_secs(),
303 },
304 });
305 }
306 }
307 }
308 }
309 }
310 Err(Error::Unauthorized)
311 }
312
313 async fn token_auth(&self, token: &str) -> Result<(), Error> {
314 if let Ok(token) = base64::decode(token) {
315 if let Ok(client_token) = serde_json::from_slice::<ClientToken>(&token) {
316 let data = serde_json::to_string(&client_token.data).unwrap();
317 if let Ok(signature) = hex::decode(&client_token.signature) {
318 if crypto::verify(data.as_bytes(), &self.root_secret, &signature) {
319 let now = SystemTime::now()
320 .duration_since(SystemTime::UNIX_EPOCH)
321 .map(|t| t.as_millis())
322 .unwrap_or(0);
323 if now <= client_token.data.expires {
324 return Ok(());
325 }
326 }
327 }
328 }
329 }
330 Err(Error::Unauthorized)
331 }
332
333 async fn userpass_create_update_user(
334 &self,
335 _current_username: &str,
336 username: &str,
337 password: &str,
338 ) -> Result<(), Error> {
339 let salt = {
340 let mut rng = ThreadRng::default();
341 crypto::salt(&mut rng)
342 };
343 let password = Self::hashed(password, &salt);
344 let mut data = HashMap::new();
345 data.insert("password".to_string(), hex::encode(password));
346 let data = SecretData { data };
347 self.create_secret(&format!("auth/userpass/users/{username}"), data)
348 .await
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use std::{collections::HashMap, env};
355
356 use streambed::crypto;
357 use test_log::test;
358
359 use super::*;
360
361 #[test(tokio::test)]
362 #[cfg(unix)]
363 async fn test_set_get_secret() {
364 let confidant_dir = env::temp_dir().join("test_set_get_secret");
365 let _ = fs::remove_dir_all(&confidant_dir).await;
366 let _ = fs::create_dir_all(&confidant_dir).await;
367 println!("Writing to {}", confidant_dir.to_string_lossy());
368
369 let ss = FileSecretStore::new(
370 confidant_dir.clone(),
371 &[0; crypto::KEY_SIZE],
372 Duration::from_secs(1),
373 1,
374 Some("ttl"),
375 );
376
377 ss.approle_auth("role_id", "secret_id").await.unwrap();
379
380 let mut data = HashMap::new();
381 data.insert("key".to_string(), "value".to_string());
382 data.insert("ttl".to_string(), "60m".to_string());
383 let data = SecretData { data };
384
385 fs::set_permissions(&confidant_dir, PermissionsExt::from_mode(0o755))
387 .await
388 .unwrap();
389
390 assert!(ss.create_secret("some.secret", data.clone()).await.is_err());
394
395 fs::set_permissions(&confidant_dir, PermissionsExt::from_mode(0o700))
397 .await
398 .unwrap();
399
400 assert!(ss.create_secret("some.secret", data.clone()).await.is_ok());
401
402 assert!(ss.get_secret("some.other.secret").await.unwrap().is_none());
405
406 assert_eq!(
408 ss.get_secret("some.secret").await,
409 Ok(Some(GetSecretReply {
410 lease_duration: 3600,
411 data
412 }))
413 );
414
415 assert!(ss
417 .userpass_create_update_user("mitchellh", "mitchellh", "foo")
418 .await
419 .is_ok());
420
421 let user_ss = FileSecretStore::with_new_auth_prepared(&ss);
423 let userpass_auth = user_ss.userpass_auth("mitchellh", "foo").await.unwrap();
424
425 let bad_user_ss = FileSecretStore::with_new_auth_prepared(&ss);
427 assert!(bad_user_ss
428 .userpass_auth("mitchellh", "foo2")
429 .await
430 .is_err());
431
432 let token_ss = FileSecretStore::with_new_auth_prepared(&ss);
434 token_ss
435 .token_auth(&userpass_auth.auth.client_token)
436 .await
437 .unwrap();
438 }
439}