diff --git a/Cargo.lock b/Cargo.lock index c5c28d4..531828a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,42 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -223,6 +259,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -290,9 +336,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "der" version = "0.7.9" @@ -851,6 +907,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1085,6 +1150,12 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking" version = "2.2.1" @@ -1168,6 +1239,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -1393,6 +1476,7 @@ checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" name = "rvault-server" version = "0.1.0" dependencies = [ + "aes-gcm-siv", "axum", "dotenvy", "env_logger", @@ -2054,6 +2138,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index bed1b56..f7da202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0.117" json-patch = "4.0.0" # serde_with = "3.8.1" dotenvy = "0.15.7" +aes-gcm-siv = "0.11.1" # utoipa = { version = "4.2.0", features = ["axum_extras"] } sqlx = { version = "0.8.3", features = [ diff --git a/migrations/20250326160659_sealing.sql b/migrations/20250326160659_sealing.sql new file mode 100644 index 0000000..b76a9ae --- /dev/null +++ b/migrations/20250326160659_sealing.sql @@ -0,0 +1,7 @@ +-- Sealing Key + +CREATE TABLE root_key ( + version INTEGER PRIMARY KEY CHECK ( version = 1 ), + encrypted_key BLOB NOT NULL, + type TEXT NOT NULL CHECK ( type IN ('dev_only') ) +); diff --git a/src/main.rs b/src/main.rs index 62d5a48..c788a6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,8 @@ async fn main() { .layer(middleware::from_fn(set_default_content_type_json)) .with_state(pool.clone()); + storage::sealing::get_or_init_root_key(&pool).await; + warn!("Listening on {}", listen_addr.to_string()); // Start listening let listener = TcpListener::bind(listen_addr).await.unwrap(); diff --git a/src/storage/sealing.rs b/src/storage/sealing.rs new file mode 100644 index 0000000..5f61a5a --- /dev/null +++ b/src/storage/sealing.rs @@ -0,0 +1,102 @@ +use aes_gcm_siv::{ + aead::{Aead, OsRng}, AeadCore, Aes256GcmSiv, KeyInit +}; +use log::{info, warn}; +use tokio::sync::RwLock; + +use super::DbPool; + +#[derive(PartialEq)] +enum KeyEnum { + /// Final key + MainKey(Vec), + Uninitialized, + // Portion(Vec), +} + +static KEY_MAPS: RwLock = RwLock::const_new(KeyEnum::Uninitialized); + +pub async fn get_or_init_root_key(pool: &DbPool) { + let mut value = KEY_MAPS.write().await; + + match *value { + KeyEnum::MainKey(_) => panic!("Root key already initialized!"), + KeyEnum::Uninitialized => get_root_key_db(pool, &mut value).await, + } + // Write lock on KEY_MAPS is hold throughout the process and set at last + // to avoid secrets being sealed with a new but discarded key +} + +async fn get_root_key_db(pool: &DbPool, value: &mut KeyEnum) { + let rk = sqlx::query!("SELECT encrypted_key, type FROM root_key ORDER BY version LIMIT 1") + .fetch_optional(pool) + .await + .expect("Failed to optionally read root key from the database"); + + let v = match rk { + Some(v) => v, + None => { + warn!("No root key was found in the database!"); + init_new_root_key(pool, value).await; + return; + }, + }; + info!("Root key of type {} found in the database", v.r#type); + match &*v.r#type { + "dev_only" => { + warn!("Root key is of type {}. This is INSECURE and must only be used for development purposes!", v.r#type); + *value = KeyEnum::MainKey(v.encrypted_key); + return; + }, + _ => panic!("Unknown root key type in database"), + } +} + +async fn init_new_root_key(pool: &DbPool, value: &mut KeyEnum) { + warn!("Initializing new root key!"); + let key = Aes256GcmSiv::generate_key(&mut OsRng); + + let key = key.as_slice().to_owned(); + + let _ = sqlx::query!( + " + INSERT INTO root_key (encrypted_key, type, version) + VALUES ($1, 'dev_only', 1) + ", + key + ) + .execute(pool) + .await + .expect("Failed to write new root key to the database"); + + *value = KeyEnum::MainKey(key); + info!("Initialized new root key!"); +} + +pub async fn seal(data: &String) -> ([u8; 12], Vec) { + let cipher = match &*KEY_MAPS.read().await { + KeyEnum::MainKey(key) => Aes256GcmSiv::new_from_slice(key), + KeyEnum::Uninitialized => panic!("Cannot seal secret since the vault is not unsealed"), + } + .expect("Failed to create new AesGcmSiv cipher from variable size key"); + + let nonce: aes_gcm_siv::aead::generic_array::GenericArray::NonceSize> = + Aes256GcmSiv::generate_nonce(&mut OsRng); // 96-bits; unique per message + let enc = cipher.encrypt(&nonce, data.as_bytes()).unwrap(); + debug_assert!(nonce.len() == 12); + let nonce = nonce.as_slice().try_into().expect("Nonce should be exactly 12 bytes"); + (nonce, enc) +} + +pub async fn unseal(nonce: Vec, data: impl AsRef<[u8]>) -> String { + assert!(nonce.len() == 12); + let cipher = match &*KEY_MAPS.read().await { + KeyEnum::MainKey(key) => Aes256GcmSiv::new_from_slice(key), + KeyEnum::Uninitialized => panic!("Cannot seal secret since the vault is not unsealed"), + } + .expect("Failed to create new AesGcmSiv cipher from variable size key"); + + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(&nonce); + let enc = cipher.decrypt(nonce, data.as_ref()).unwrap(); + String::from_utf8(enc).expect("Failed to parse as utf8") +}