streambed_confidant/
lib.rs

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/// A secret store implementation that uses the file system as its
66/// backing store.
67/// An unauthorized_timeout determines how long the server should wait before being
68/// requested again.
69/// A max_secrets_cached arg limits the number of secrets that can be held at any time.
70
71#[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    /// Authentication is a noop for this secret store, and it will always succeed.
196    /// Authentication is essentially implied given the user a host process is
197    /// assigned to.
198    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                            // We should be able to read our writes
248                            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()), // Unsure how we can deal with caching issues
267            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        // Establish a secret store for our service as a whole
378        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        // Set up the permissions to cause secret creation failure
386        fs::set_permissions(&confidant_dir, PermissionsExt::from_mode(0o755))
387            .await
388            .unwrap();
389
390        // This should fail as we don't have the correct file permissions.
391        // We are looking for the confidant dir to have owner permissions
392        // only + the ability for the owner to write.
393        assert!(ss.create_secret("some.secret", data.clone()).await.is_err());
394
395        // Let's now set up the correct permissions
396        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        // Try reading a secret that doesn't exist. It should fail by returning
403        // None.
404        assert!(ss.get_secret("some.other.secret").await.unwrap().is_none());
405
406        // Now read the secret we wrote before - all should be well.
407        assert_eq!(
408            ss.get_secret("some.secret").await,
409            Ok(Some(GetSecretReply {
410                lease_duration: 3600,
411                data
412            }))
413        );
414
415        // Create a user
416        assert!(ss
417            .userpass_create_update_user("mitchellh", "mitchellh", "foo")
418            .await
419            .is_ok());
420
421        // Login as that user
422        let user_ss = FileSecretStore::with_new_auth_prepared(&ss);
423        let userpass_auth = user_ss.userpass_auth("mitchellh", "foo").await.unwrap();
424
425        // Login as that user with the wrong password
426        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        // Auth login from the previous valid userpass token
433        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}