encrypted_json_kv/
database.rs

1/********************************************************************************
2 *   Encrypted KV store for json blobs based on sled                            *
3 *   Copyright (C) 2020 Famedly GmbH                                            *
4 *                                                                              *
5 *   This program is free software: you can redistribute it and/or modify       *
6 *   it under the terms of the GNU Affero General Public License as             *
7 *   published by the Free Software Foundation, either version 3 of the         *
8 *   License, or (at your option) any later version.                            *
9 *                                                                              *
10 *   This program is distributed in the hope that it will be useful,            *
11 *   but WITHOUT ANY WARRANTY; without even the implied warranty of             *
12 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the               *
13 *   GNU Affero General Public License for more details.                        *
14 *                                                                              *
15 *   You should have received a copy of the GNU Affero General Public License   *
16 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.     *
17 ********************************************************************************/
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use sled;
21use sodiumoxide::crypto::{pwhash, secretbox};
22
23use thiserror::Error;
24
25use std::{fs, path::PathBuf};
26
27use crate::{crypto::derive_key, EncryptedValue, EncryptionError};
28
29pub(crate) const CURRENT_DATABASE_VERSION: usize = 0;
30
31/// A KV store based on sled, storing serde_json::Value values in an encrypted fashion
32pub struct Database {
33    path: Option<PathBuf>,
34    db: sled::Db,
35    key: secretbox::Key,
36}
37
38#[derive(Error, Debug)]
39pub enum DatabaseOpenError {
40    #[error("file io broke")]
41    Io(#[from] std::io::Error),
42    #[error("the database isn't configured properly")]
43    DbConfig(#[from] DbConfigError),
44    #[error("the version on disk is not supported by the current version")]
45    UnsupportedVersion,
46    #[error("error from sled")]
47    Sled(#[from] sled::Error),
48    #[error("internal config file couldn't be parsed")]
49    TomlRead(#[from] toml::de::Error),
50    #[error("couldn't serialize config file")]
51    TomlWrite(#[from] toml::ser::Error),
52}
53
54#[derive(Error, Debug)]
55pub enum DatabaseError {
56    #[error("error from sled")]
57    Sled(#[from] sled::Error),
58    #[error("")]
59    Encryption(#[from] EncryptionError),
60    #[error("")]
61    Serde(#[from] serde_json::Error),
62}
63
64#[derive(Error, Debug)]
65pub enum DatabaseSetPassphraseError {
66    #[error("toml")]
67    Toml(#[from] toml::ser::Error),
68    #[error("io")]
69    Io(#[from] std::io::Error),
70    #[error("database is temporary")]
71    Temporary,
72}
73
74impl Database {
75    /// Opens a new Database instance.
76    pub fn new(path: PathBuf, passphrase: &[u8]) -> Result<Self, DatabaseOpenError> {
77        // ensure that the passed path exists
78        fs::create_dir_all(&path)?;
79
80        let mut is_new_db = false;
81
82        // get config if exists, create new one otherwise
83        let mut db_config_path = path.clone();
84        db_config_path.push("encrypted_json_kv.toml");
85        let db_config = match fs::read_to_string(&db_config_path) {
86            Ok(db_config_string) => toml::from_str(&db_config_string)?,
87            Err(error) => match error.kind() {
88                // File not found, creating a new one
89                std::io::ErrorKind::NotFound => {
90                    let config = DbConfig::new(passphrase, None);
91                    fs::write(db_config_path, toml::to_string(&config)?)?;
92                    is_new_db = true;
93                    config
94                }
95                // Other error -> pass it to the caller
96                _ => Err(error)?,
97            },
98        };
99
100        // check db version
101        if db_config.version != CURRENT_DATABASE_VERSION {
102            return Err(DatabaseOpenError::UnsupportedVersion);
103        }
104
105        // decrypt key
106        let key = db_config.get_key(passphrase)?;
107
108        // open sled database
109        let mut db_path = path.clone();
110        db_path.push("sled");
111        let db = sled::Config::new()
112            .path(db_path)
113            .create_new(is_new_db)
114            .open()?;
115
116        // return database
117        Ok(Database { path: Some(path), db, key })
118    }
119
120    /// Opens a new temporary Database instance
121    pub fn temporary() -> Result<Self, DatabaseOpenError> {
122        let key = secretbox::gen_key();
123        let db = sled::Config::new()
124            .temporary(true)
125            .open()?;
126        Ok(Database { path: None, db, key })
127    }
128
129    pub fn set_passphrase(&self, passphrase: &[u8]) -> Result<(), DatabaseSetPassphraseError> {
130        match &self.path {
131            Some(path) => {
132                let mut db_config_path = path.clone();
133                db_config_path.push("encrypted_json_kv.toml");
134        let config = DbConfig::new(passphrase, None);
135        fs::write(db_config_path, toml::to_string(&config)?)?;
136        Ok(())
137            },
138            None => Err(DatabaseSetPassphraseError::Temporary)
139        }
140    }
141
142    /// Get a serde_json::Value from the database. The wrapping sled::Result will only
143    /// throw an error when something goes really wrong. The Option inside that Result
144    /// indicates whether the requested item is present
145    pub fn get(&self, key: &str) -> Result<Option<Value>, DatabaseError> {
146        match self.db.get(key)? {
147            Some(value) => Ok(Some(serde_json::from_slice::<EncryptedValue>(&value)?.decrypt(&self.key)?)),
148            None => Ok(None),
149        }
150    }
151
152    /// Insert a serde_json::Value into the database. The wrapping sled::Result will only
153    /// throw an error when something goes really wrong. The Option inside contains the
154    /// ciphertext of the just inserted value, if this insertion was successful.
155    pub fn insert(&self, key: &str, value: &Value) -> Result<(), DatabaseError> {
156        let value = EncryptedValue::encrypt(value, &self.key);
157        let value = serde_json::to_vec(&value).unwrap();
158        self.db.insert(key, value)?;
159        Ok(())
160    }
161
162    /// Remove a value from the database. The wrapping sled::Result will only throw an error when
163    /// something goes really wrong. The Option inside contains the old value, if one was present.
164    pub fn remove(&self, key: &str) -> Result<Option<Value>, DatabaseError> {
165        match self.db.remove(key)? {
166            Some(value) => Ok(Some(serde_json::from_slice::<EncryptedValue>(&value)?.decrypt(&self.key)?)),
167            None => Ok(None),
168        }
169    }
170
171    /// Iterate over all keys in the database
172    pub fn keys(&self) -> impl DoubleEndedIterator<Item = sled::Result<sled::IVec>> {
173        self.db.iter().keys()
174    }
175
176    /// Get encryption key
177    pub fn encryption_key(&self) -> &secretbox::Key {
178        &self.key
179    }
180}
181
182#[derive(Serialize, Deserialize)]
183pub(crate) struct DbConfig {
184    version: usize,
185    salt: pwhash::Salt,
186    encrypted_key: EncryptedValue,
187}
188
189impl DbConfig {
190    pub(crate) fn new(passphrase: &[u8], key: Option<secretbox::Key>) -> DbConfig {
191        let salt = pwhash::gen_salt();
192        let outer_key = derive_key(passphrase, salt);
193        let inner_key = key.unwrap_or_else(|| secretbox::gen_key());
194        let encrypted_key =
195            EncryptedValue::encrypt(&Value::String(base64::encode(inner_key)), &outer_key);
196        DbConfig {
197            version: CURRENT_DATABASE_VERSION,
198            salt,
199            encrypted_key,
200        }
201    }
202
203    pub(crate) fn get_key(&self, passphrase: &[u8]) -> Result<secretbox::Key, DbConfigError> {
204        let outer_key = derive_key(passphrase, self.salt);
205        let inner_key_value = self.encrypted_key.decrypt(&outer_key)?;
206        let inner_key_encoded = inner_key_value.as_str().ok_or_else(|| DbConfigError::WrongKeyType)?;
207        let inner_key_bytes = base64::decode(inner_key_encoded)?;
208        Ok(secretbox::Key::from_slice(&inner_key_bytes).ok_or_else(|| DbConfigError::WrongKeySize)?)
209    }
210}
211
212#[derive(Error, Debug)]
213pub enum DbConfigError {
214    #[error("couldn't decrypt database key")]
215    Encryption(#[from] EncryptionError),
216    #[error("base64 decoding of the key didn't work")]
217    Encoding(#[from] base64::DecodeError),
218    #[error("key was not a string after decrypting and parsing")]
219    WrongKeyType,
220    #[error("key has the wrong size")]
221    WrongKeySize,
222}