docbox_secrets/
json.rs

1//! # JSON Secret Manager
2//!
3//! Encrypted local JSON based secrets manager, secrets are stored within a local
4//! JSON file encrypted using [age](https://github.com/str4d/rage) encryption
5//!
6//! Intended for self-hosted environments where AWS secrets manager is not available
7//!
8//! ## Environment Variables
9//!
10//! * `DOCBOX_SECRET_MANAGER_KEY` - Specifies the encryption key to use
11//! * `DOCBOX_SECRET_MANAGER_PATH` - Path to the encrypted JSON file
12
13use crate::{Secret, SecretManager};
14use age::secrecy::SecretString;
15use anyhow::Context;
16use serde::{Deserialize, Serialize};
17use std::{collections::HashMap, fmt::Debug, path::PathBuf, str::FromStr};
18
19#[derive(Clone, Deserialize, Serialize)]
20pub struct JsonSecretManagerConfig {
21    /// Encryption key to use
22    pub key: String,
23
24    /// Path to the encrypted JSON file
25    pub path: PathBuf,
26}
27
28impl Debug for JsonSecretManagerConfig {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("JsonSecretManagerConfig")
31            .field("path", &self.path)
32            .finish()
33    }
34}
35
36impl JsonSecretManagerConfig {
37    /// Load a config from environment variables
38    pub fn from_env() -> anyhow::Result<Self> {
39        let key = std::env::var("DOCBOX_SECRET_MANAGER_KEY")
40            .context("missing DOCBOX_SECRET_MANAGER_KEY secret key to access store")?;
41        let path = std::env::var("DOCBOX_SECRET_MANAGER_PATH")
42            .context("missing DOCBOX_SECRET_MANAGER_PATH file path to access store")?;
43
44        Ok(Self {
45            key,
46            path: PathBuf::from_str(&path)?,
47        })
48    }
49}
50
51// Local encrypted JSON based secret manager
52pub struct JsonSecretManager {
53    path: PathBuf,
54    key: SecretString,
55}
56
57/// Temporary structure secrets are loaded into when loaded from a file
58#[derive(Deserialize, Serialize)]
59struct SecretFile {
60    /// Secrets contained within the file as key-value pair
61    secrets: HashMap<String, String>,
62}
63
64impl JsonSecretManager {
65    pub fn from_config(config: JsonSecretManagerConfig) -> Self {
66        let key = SecretString::from(config.key);
67
68        Self {
69            path: config.path,
70            key,
71        }
72    }
73
74    async fn read_file(&self) -> anyhow::Result<SecretFile> {
75        let bytes = tokio::fs::read(&self.path).await?;
76        let identity = age::scrypt::Identity::new(self.key.clone());
77        let decrypted = age::decrypt(&identity, &bytes)?;
78        let file = serde_json::from_slice(&decrypted)?;
79        Ok(file)
80    }
81
82    async fn write_file(&self, file: SecretFile) -> anyhow::Result<()> {
83        let bytes = serde_json::to_string(&file)?;
84        let recipient = age::scrypt::Recipient::new(self.key.clone());
85        let encrypted = age::encrypt(&recipient, bytes.as_bytes())?;
86        tokio::fs::write(&self.path, encrypted).await?;
87        Ok(())
88    }
89}
90
91impl SecretManager for JsonSecretManager {
92    async fn get_secret(&self, name: &str) -> anyhow::Result<Option<Secret>> {
93        let file = match self.read_file().await {
94            Ok(value) => value,
95            Err(_) => return Ok(None),
96        };
97
98        let secret = file.secrets.get(name);
99        Ok(secret.map(|value| Secret::String(value.clone())))
100    }
101
102    async fn set_secret(&self, name: &str, value: &str) -> anyhow::Result<()> {
103        let mut secrets = if self.path.exists() {
104            self.read_file().await?
105        } else {
106            SecretFile {
107                secrets: Default::default(),
108            }
109        };
110
111        secrets.secrets.insert(name.to_string(), value.to_string());
112        self.write_file(secrets).await?;
113        Ok(())
114    }
115
116    async fn delete_secret(&self, name: &str) -> anyhow::Result<()> {
117        let mut secrets = if self.path.exists() {
118            self.read_file().await?
119        } else {
120            SecretFile {
121                secrets: Default::default(),
122            }
123        };
124
125        secrets.secrets.remove(name);
126        self.write_file(secrets).await?;
127        Ok(())
128    }
129}