1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
//! This crate contains the rust implementation of [SecureStore](https://neosmart.net/blog/2020/securestore-open-secrets-format/),
//! an open standard for cross-language/cross-platform secrets storage and
//! retrieval. A SecureStore is represented on-disk as a plain-text,
//! human-readable (JSON) file, intended to be stored and versioned alongside
//! the code using it. Refer to [the accompanying article](https://neosmart.net/blog/2020/securestore-open-secrets-format/)
//! for more information on the SecureStore protocol.
//!
//! SecureStore vaults are created by or loaded from an existing vault and
//! represented in memory as instances of [`SecretsManager`], the primary type
//! exposed by this crate. Typically, one `SecretsManager` instance should be
//! created to service all retrieval and storage requests of secrets for an app.
//!
//! For maximum flexibility and per the SecureStore protocol, the private keys
//! used to encrypt or decrypt secrets in the vault can come from different
//! sources (that may possibly even be used interchangeably); this key source is
//! specified as a variant of the [`KeySource`] enum at the time of creating or
//! loading a `SecretsManager` instance.
//!
//! For best results, this crate should be used alongside the
//! [`ssclient`](https://github.com/neosmart/securestore-rs/tree/master/ssclient)
//! companion CLI app (available via `cargo install ssclient`). Typically, a new
//! SecureStore vault is created with `ssclient create ...` and loaded with
//! secrets at the command line with a series of `ssclient set ...`
//! commands; this crate is then used by your application's logic at runtime to
//! load the store created by `ssclient` (via [`SecretsManager::load()`]) and
//! retrieve the secrets (via [`SecretsManager::get()`]), although all the
//! functionality needed to create and initialize a new store and its secrets
//! directly yourself (without `ssclient`) is also available via the
//! `SecretsManager` API.
//!
//! A full and annotated example of using `ssclient` and this `securestore`
//! crate in tandem [may be found online](https://github.com/neosmart/securestore-rs/blob/master/README.md),
//! but an abbreviated version is shown below.
//!
//! # Example
//!
//! First, create a new store and set some secret value at the command line with
//! the companion `ssclient` crate:
//!
//! ```sh
//! $ cargo install ssclient
//! $ ssclient create secrets.json --export-key secrets.key
//! $ ssclient -k secrets.key set db_password pgsql123
//! ```
//!
//! Then in your code, load the store with the newly-created key file and
//! retrieve the secret:
//! ```rust
//! use securestore::{KeySource, SecretsManager};
//! use std::path::Path;
//! #
//! # let mut sman = SecretsManager::new(KeySource::Csprng).unwrap();
//! # sman.set("db_password", "pgsql123");
//! # sman.export_key("secrets.key");
//! # sman.save_as("secrets.json");
//! # drop (sman);
//!
//! let key_path = Path::new("secrets.key");
//! let sman = SecretsManager::load("secrets.json", KeySource::Path(key_path))
//!     .expect("Failed to load secrets store!");
//! let db_password = sman.get("db_password")
//!     .expect("Couldn't get db_password from vault!");
//! # drop(sman);
//! # std::fs::remove_file("secrets.key").unwrap();
//! # std::fs::remove_file("secrets.json").unwrap();
//!
//! assert_eq!(db_password, String::from("pgsql123"));
//! ```
mod errors;
mod serial;
mod shared;
#[cfg(test)]
mod tests;

use self::shared::{CryptoKeys, EncryptedBlob, Vault};
pub use crate::errors::{Error, ErrorKind};
pub use crate::serial::{BinaryDeserializable, BinarySerializable};
use openssl::rand;
use std::fs::File;
use std::path::{Path, PathBuf};

/// A `KeySource` specifies the source of the encryption/decryption keys used by
/// a [`SecretsManager`] instance when loading or interacting with a SecureStore
/// vault.
///
/// Note that it is possible for different `KeySource` variants to be equivalent
/// and used interchangeably. For instance, you can derive the secret keys from
/// a password (via [`KeySource::Password`]) when reading/writing a SecureStore
/// vault from the command line (via the companion cli app/crate, `ssclient`)
/// but then export a copy of the keys derived from that password to a keyfile
/// and use that when accessing the vault from your code in production (as a
/// [`KeySource::Path`] variant). See [`SecretsManager::export_key()`] or
/// the `ssclient` documentation for more info.
///
/// Note that when creating a new vault with [`KeySource::Csprng`] the generated
/// private keys should be exported via [`SecretsManager::export_key()`]
/// before dropping the `SecretsManager` instance; the exported keyfile should
/// then be used the next time the vault is loaded (via [`KeySource::Path`]).
#[non_exhaustive]
#[derive(Clone)]
pub enum KeySource<'a> {
    /// Load the keys from a keyfile on-disk. Both binary and PEM keyfiles are
    /// supported.
    Path(&'a Path),
    /// Derive keys from the specified password.
    ///
    /// You most likely do not want to use this `KeySource` variant directly;
    /// instead use [`ssclient`] with a password when managing the secrets
    /// in the SecureStore vault at the command line, and use `ssclient` to
    /// export a keyfile equivalent to that password to use to retrieve
    /// passwords at runtime (via [`KeySource::Path`] or
    /// [`KeySource::from_file()`).
    ///
    /// [`ssclient`]: https://neosmart.net/blog/2020/securestore-open-secrets-format/
    Password(&'a str),
    /// Automatically generate a new key file from a secure RNG, for use with
    /// [`SecretsManager::new()`] only.
    ///
    /// [`SecretsManager::export_key()`] should be used to export the
    /// keys before the `SecretsManager` instance is disposed or else the
    /// generated key will be lost and secrets will not be decryptable. The
    /// store should subsequently be loaded with [`KeySource::Path`] pointing to
    /// the exported key's path.
    Csprng,
}

mod private {
    pub trait Sealed {}
}

/// This type is used internally for generic function overload purposes. See and
/// use [`KeySource`] instead.
pub trait GenericKeySource: private::Sealed {
    fn key_source<'a>(&'a self) -> KeySource<'a>;
}

impl<'a> KeySource<'a> {
    // This is purposely named like an enum variant for backwards compatibility.
    #[doc(hidden)]
    #[allow(non_snake_case)]
    pub fn File<P: AsRef<Path> + 'a>(path: P) -> impl GenericKeySource + 'a {
        path
    }

    /// Use in lieu of `KeySource::Path` for cases where `path` implements
    /// `AsRef<Path>` but isn't specifically a `&Path` itself.
    pub fn from_file<P: AsRef<Path> + 'a>(path: P) -> impl GenericKeySource + 'a {
        path
    }
}

impl<P: AsRef<Path>> private::Sealed for P {}
impl<P: AsRef<Path>> GenericKeySource for P {
    fn key_source<'a>(&'a self) -> KeySource<'a> {
        KeySource::Path(self.as_ref())
    }
}

impl<'a> private::Sealed for KeySource<'a> {}
impl GenericKeySource for KeySource<'_> {
    fn key_source<'a>(&'a self) -> KeySource<'a> {
        self.clone()
    }
}

/// `SecretsManager` is the primary interface used for interacting with this
/// crate, and is an in-memory representation of an encrypted SecureStore vault.
///
/// An existing plain-text SecureStore vault can be loaded with
/// [`SecretsManager::load()`] or a new vault can be created with [`new()`] and
/// then saved to disk with [`save()`] or [`save_as()`].
///
/// Individual secrets can be set, retrieved, and removed with
/// [`SecretsManager::set()`], [`get()`], and [`remove()`] respectively. The
/// names/keys of all secrets stored in this vault can be enumerated via
/// [`SecretsManager::keys()`].
///
/// [`new()`]: Self::new()
/// [`save()`]: Self::save()
/// [`save_as()`]: Self::save_as()
/// [`get()`]: Self::get()
/// [`remove()`]: Self::remove()
pub struct SecretsManager {
    vault: Vault,
    path: Option<PathBuf>,
    cryptokeys: CryptoKeys,
}

// We aren't manually implementing Send/Sync for `SecretsManager`, but we need
// to make sure that it implements them all the same for ergonomic reasons.
const _: () = {
    // It is sufficient to declare the generic function pointers; calling them
    // too would require using `const fn` with Send/Sync constraints which wasn't
    // stabilized until rustc 1.61.0
    fn assert_send<T: Send>() {}
    let _ = assert_send::<SecretsManager>;
    fn assert_sync<T: Sync>() {}
    let _ = assert_sync::<SecretsManager>;
};

impl SecretsManager {
    fn create_sentinel(keys: &CryptoKeys) -> EncryptedBlob {
        let mut random = [0u8; shared::IV_SIZE * 2];
        rand::rand_bytes(&mut random).expect("Failed to create sentinel");
        EncryptedBlob::encrypt(&keys, &random)
    }

    /// Creates a new instance of `SecretsManager`, encrypting its secrets with
    /// the specified [`KeySource`].
    ///
    /// Note that the usage of [`KeySource::Path`] is taken to mean that there
    /// is an existing compatible private key already available at the
    /// specified path. To generate a new key file, use [`KeySource::Csprng`]
    /// then export the generated key to the desired path with
    /// [`export_key()`](Self::export_key).
    ///
    /// Most users will likely prefer to create a new SecureStore vault and
    /// manage its secrets by using the companion CLI utility [`ssclient`],
    /// then [`load()`](Self::load) the SecureStore at runtime to retrieve
    /// its secrets.
    ///
    /// [`ssclient`]: https://github.com/neosmart/securestore-rs/tree/master/ssclient
    pub fn new<K: GenericKeySource>(key_source: K) -> Result<SecretsManager, Error> {
        let key_source = key_source.key_source();
        let mut vault = Vault::new();
        let keys = key_source.extract_keys(&vault.iv)?;
        vault.sentinel = Some(Self::create_sentinel(&keys));
        Ok(SecretsManager {
            cryptokeys: keys,
            path: None,
            vault,
        })
    }

    /// Load the contents of an on-disk SecureStore vault located at `path`
    /// into a new `SecretsManager` instance, decrypting its contents with
    /// the [`KeySource`] specified by the `key_source` parameter.
    ///
    /// Note that changes to the vault are not written to disk unless and until
    /// [`save()`] or [`save_as()`] is called.
    ///
    /// ## Panics:
    /// In debug mode, if an attempt is made to load an existing vault but
    /// `key_source` is set to [`KeySource::Csprng`] (which should only be
    /// used when initializing a new secrets vault). In release mode, this does
    /// not panic but the vault will invariably fail to decrypt.
    ///
    /// [`save()`]: SecretsManager::save()
    /// [`save_as()`]: SecretsManager::save_as()
    ///
    /// ## Example:
    ///
    /// First, in the shell:
    /// ```sh
    /// ssclient create secrets.json --export-key secrets.key
    /// ssclient set password mYpassWORD123
    /// ```
    ///
    /// Then, in rust:
    /// ```no_run
    /// use securestore::SecretsManager;
    ///
    /// let secrets = SecretsManager::load("secrets.json", "secrets.key").unwrap();
    /// let password = secrets.get("password").unwrap();
    /// assert_eq!(password, String::from("mYpassWORD123"));
    /// ```
    pub fn load<P: AsRef<Path>, K: GenericKeySource>(
        path: P,
        key_source: K,
    ) -> Result<SecretsManager, Error> {
        let key_source = key_source.key_source();
        // We intentionally only panic here in debug mode, only because we try to avoid
        // panicking in production if possible. This isn't a logic error (the code will
        // still run and everything will work without any incorrect behavior) but the
        // user will just never get the desired results (loading an existing store with
        // a newly generated key will just always fail to decrypt the store contents).
        // Tl;dr it's not unsafe or technically incorrect, just stupid.
        if matches!(key_source, KeySource::Csprng) {
            debug_assert!(
                false,
                concat!(
                    "It is incorrect to call SecretsManager::load() ",
                    "except with an existing key source!"
                )
            );
        }

        let path = path.as_ref();

        let mut vault = Vault::from_file(path)?;
        let keys = key_source.extract_keys(&vault.iv)?;

        // The sentinel is an optional part of the spec that prevents inadvertently
        // adding two secrets with two different passwords. It is not intended to
        // have any effects on the security or entropy of the store.
        if let Some(ref sentinel) = vault.sentinel {
            sentinel.decrypt(&keys)?;
        } else {
            vault.sentinel = Some(Self::create_sentinel(&keys));
        }

        let sman = SecretsManager {
            cryptokeys: keys,
            path: Some(PathBuf::from(path)),
            vault,
        };
        Ok(sman)
    }

    /// Saves changes to the underlying vault specified by the path supplied
    /// during construction of this `SecretsManager` instance.
    ///
    /// Note that changes to a `SecretsManager` instance and its underlying
    /// vault are transient and will be lost unlesss they are flushed to
    /// disk via [`save()`](Self::save()) or [`save_as()`](Self::save_as()).
    ///
    /// ## Panics:
    /// If a call to `save()` is made on a `SecretsManager` initialized with
    /// `SecretsManager::new()` rather than opened with `load()`; use
    /// `save_as()` instead.
    ///
    /// [`load()`]: Self::load()
    pub fn save(&self) -> Result<(), Error> {
        match self.path.as_ref() {
            Some(path) => self.vault.save(path),
            None => panic!(concat!(
                "Cannot call save() on a newly-created store without a path. ",
                "Use SecretsManager::save_as(&path) instead!"
            )),
        }
    }

    /// Export the current vault plus any changes that have been made to it to
    /// the path specified by the `path` argument.
    ///
    /// Note that changes to a `SecretsManager` instance and its underlying
    /// vault are transient and will be lost unlesss they are flushed to
    /// disk via [`save()`](Self::save()) or [`save_as()`](Self::save_as()).
    pub fn save_as<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
        self.vault.save(path.as_ref())
    }

    /// Exports the private key(s) resident in memory to a path on-disk. Note
    /// that in addition to being used for exporting existing keys previously
    /// loaded into the secrets store and keys newly generated by the secrets
    /// store, it can also be used to export keys derived from passwords to
    /// their equivalent keyfiles to facilitate subsequent passwordless access.
    ///
    /// As of securestore 0.100, the keyfile is written in a PEM-like format,
    /// with ASCII armor and a base64-encoded payload. Previous versions
    /// exported a binary version of the keyfile. Both are fully supported.
    ///
    /// ## Example:
    ///
    /// ```rust
    /// use securestore::{SecretsManager, KeySource};
    ///
    /// let vault_pass = KeySource::Password("myVaultPass123");
    /// let mut sman = SecretsManager::new(vault_pass).unwrap();
    /// sman.set("password", "password123");
    /// sman.save_as("secrets.json").unwrap();
    /// sman.export_key("passwordless.key").unwrap();
    ///
    /// // We can now use either the vault password "myVaultPass123" or the
    /// // equivalent keyfile "passwordless.key" to load the store and access
    /// // the secrets.
    ///
    /// let vault_key = KeySource::from_file("passwordless.key");
    /// let sman = SecretsManager::load("secrets.json", vault_key).unwrap();
    /// assert_eq!("password123", sman.get("password").unwrap());
    ///
    /// # std::fs::remove_file("secrets.json").unwrap();
    /// # std::fs::remove_file("passwordless.key").unwrap();
    /// ```
    pub fn export_key<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
        self.cryptokeys.export(path)
    }

    #[doc(hidden)]
    #[inline]
    /// A backwards-compatibile alias for export_key()
    pub fn export_keyfile<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
        self.export_key(path)
    }

    /// Decrypt and retrieve the single secret identified by `name` from the
    /// loaded store as a `String`. If the secret cannot be found, an
    /// [`Error`] with [`Error::kind()`] set to
    /// [`ErrorKind::SecretNotFound`] is returned.
    ///
    /// See [`get_as()`](Self::get_as) to retrieve either binary secrets or
    /// secrets of arbitrary types implementing [`BinaryDeserializable`].
    pub fn get(&self, name: &str) -> Result<String, Error> {
        self.get_as::<String>(name)
    }

    /// Decrypt and retrieve the single secret identified by `name` from the
    /// loaded store, deserializing it to the requested type.
    /// If the secret cannot be found, an [`Error`] with [`Error::kind()`] set
    /// to [`ErrorKind::SecretNotFound`] is returned.
    ///
    /// Out-of-the-box, this crate supports retrieving `String` and `Vec<u8>`
    /// secrets. [`BinaryDeserializable`] may be implemented to support
    /// directly retrieving arbitrary types, but it is preferred to
    /// internally deserialize from one of the primitive supported types
    /// previously mentioned after calling [`get()`](Self::get()) to ensure
    /// maximum compatibility with other SecureStore clients.
    pub fn get_as<T: BinaryDeserializable>(&self, name: &str) -> Result<T, Error> {
        match self.vault.secrets.get(name) {
            None => ErrorKind::SecretNotFound.into(),
            Some(blob) => {
                let decrypted = blob.decrypt(&self.cryptokeys)?;
                T::deserialize(decrypted)
                    .map_err(|e| Error::from_inner(ErrorKind::DeserializationError, e))
            }
        }
    }

    /// Add a new secret or replace the existing secret identified by `name`
    /// with the value `value` to the store.
    ///
    /// Out-of-the-box, this crate supports `String`, `&str`, `Vec<u8>`, and
    /// `&[u8]` secrets. [`BinarySerializable`] may be implemented to
    /// support directly setting arbitrary types, but it is preferred to
    /// internally serialize to one of the primitive supported types
    /// previously mentioned before calling [`set()`](Self::set()) to ensure
    /// maximum compatibility with other SecureStore clients.
    pub fn set<T: BinarySerializable>(&mut self, name: &str, value: T) {
        let encrypted = EncryptedBlob::encrypt(&self.cryptokeys, T::serialize(&value));
        self.vault.secrets.insert(name.to_string(), encrypted);
    }

    /// Remove the secret identified by `name` from the store. If there is no
    /// secret by that name, an [`Error`] with [`Error::kind()`] set to
    /// [`ErrorKind::SecretNotFound`] is returned.
    pub fn remove(&mut self, name: &str) -> Result<(), Error> {
        self.vault
            .secrets
            .remove(name)
            .ok_or_else(|| ErrorKind::SecretNotFound.into())
            .map(|_| ())
    }

    /// Retrieve a list of the names of secrets stored in the vault.
    pub fn keys<'a>(&'a self) -> impl Iterator<Item = &'a str> {
        self.vault.secrets.keys().map(|s| s.as_str())
    }
}

impl<'a> KeySource<'a> {
    fn extract_keys(&self, iv: &[u8; shared::IV_SIZE]) -> Result<CryptoKeys, Error> {
        match &self {
            KeySource::Csprng => {
                let mut buffer = [0u8; shared::KEY_COUNT * shared::KEY_LENGTH];
                rand::rand_bytes(&mut buffer).expect("Key generation failure!");

                CryptoKeys::import(&buffer[..])
            }
            KeySource::Path(path) => {
                let attr = std::fs::metadata(path)?;
                if (attr.len() as usize) < (shared::KEY_COUNT * shared::KEY_LENGTH) {
                    return ErrorKind::InvalidKeyfile.into();
                }

                let file = File::open(path)?;
                CryptoKeys::import(&file)
            }
            KeySource::Password(password) => {
                use openssl::pkcs5::pbkdf2_hmac;

                let mut key_data = [0u8; shared::KEY_COUNT * shared::KEY_LENGTH];
                pbkdf2_hmac(
                    password.as_bytes(),
                    iv,
                    shared::PBKDF2_ROUNDS,
                    shared::PBKDF2_DIGEST(),
                    &mut key_data,
                )
                .expect("PBKDF2 key generation failed!");

                CryptoKeys::import(&key_data[..])
            }
        }
    }
}