diff --git a/.gitignore b/.gitignore index 8cac0ad..57cc0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ *.pdf target/ go_client/openapi.json -crates/storage-sled/sled_db -test.db -src/storage/database.db + +*.db* diff --git a/Cargo.lock b/Cargo.lock index 531828a..d47dc2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,6 +1478,7 @@ version = "0.1.0" dependencies = [ "aes-gcm-siv", "axum", + "base64", "dotenvy", "env_logger", "json-patch", diff --git a/Cargo.toml b/Cargo.toml index f7da202..0488257 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ json-patch = "4.0.0" # serde_with = "3.8.1" dotenvy = "0.15.7" aes-gcm-siv = "0.11.1" +base64 = "0.22.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 index b76a9ae..63f703b 100644 --- a/migrations/20250326160659_sealing.sql +++ b/migrations/20250326160659_sealing.sql @@ -3,5 +3,5 @@ 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') ) + type TEXT NOT NULL CHECK ( type IN ('dev_only', 'simple') ) ); diff --git a/src/engines/kv/data.rs b/src/engines/kv/data.rs index 1854cd2..0810f94 100644 --- a/src/engines/kv/data.rs +++ b/src/engines/kv/data.rs @@ -2,7 +2,7 @@ use super::structs::KvV2WriteRequest; use crate::{ common::HttpError, engines::{ kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath - }, storage::sealing::{seal, unseal}, DbPool + }, storage::sealing::{encrypt, decrypt}, DbPool }; use axum::{ Extension, Json, @@ -35,7 +35,7 @@ struct SecretDataInternal { impl SecretDataInternal { pub async fn into_external(self) -> KvSecretData { - let secret = unseal(self.nonce, &self.encrypted_data).await; + let secret = decrypt(self.nonce, &self.encrypted_data).await; KvSecretData { created_time: self.created_time, deletion_time: self.deletion_time, @@ -53,9 +53,7 @@ pub async fn get_data( Extension(EnginePath(engine_path)): Extension, ) -> Result { debug!( - "Get request: Engine: {}, path: {}", - engine_path, - path, + "Get request: Engine: {engine_path}, path: {path}", ); let res = if params.version != 0 { @@ -89,13 +87,13 @@ pub async fn get_data( version: Some(secret_content.version_number), }; let return_secret = Json(return_secret); - info!("{:?}", return_secret); + info!("{return_secret:?}"); Ok(return_secret.into_response()) } Err(e) => match e { sqlx::Error::RowNotFound => { - warn!("Secret not found (could be correct behavior) {:?}", e); + warn!("Secret not found (could be correct behavior) {e:?}"); Ok(HttpError::simple( StatusCode::NOT_FOUND, "Secret not found within kv2 engine", @@ -125,7 +123,7 @@ pub async fn post_data( let content = serde_json::to_string(&secret.data).unwrap(); - let (nonce, enc) = seal(&content).await; + let (nonce, enc) = encrypt(&content).await; let nonce = nonce.as_slice(); let mut tx = pool.begin().await.unwrap(); @@ -169,7 +167,7 @@ pub async fn delete_data( Path(path): Path, Extension(EnginePath(engine_path)): Extension, ) -> Result { - debug!("Secret: {}, path: {}", path, path); + debug!("Secret: {path}, path: {path}"); let del_time = UtcDateTime::now().unix_timestamp(); diff --git a/src/main.rs b/src/main.rs index c788a6b..62c9ce6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,11 @@ 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; + if !storage::sealing::prepare_unseal(&pool).await { + // storage::sealing::init_insecure_in_db(&pool).await; + let user_key = storage::sealing::simple::init_simple(&pool).await; + warn!("New sealing password generated: {user_key}"); + } warn!("Listening on {}", listen_addr.to_string()); // Start listening diff --git a/src/storage/sealing.rs b/src/storage/sealing.rs index 5f61a5a..5e1991a 100644 --- a/src/storage/sealing.rs +++ b/src/storage/sealing.rs @@ -1,7 +1,10 @@ +pub mod simple; + use aes_gcm_siv::{ - aead::{Aead, OsRng}, AeadCore, Aes256GcmSiv, KeyInit + AeadCore, Aes256GcmSiv, KeyInit, + aead::{Aead, OsRng}, }; -use log::{info, warn}; +use log::{error, info, warn}; use tokio::sync::RwLock; use super::DbPool; @@ -10,89 +13,171 @@ use super::DbPool; enum KeyEnum { /// Final key MainKey(Vec), + /// Encrypted with single secret + Simple(Vec), + /// Unknown or not initialized Uninitialized, - // Portion(Vec), + // ShamirPortion(Vec), } -static KEY_MAPS: RwLock = RwLock::const_new(KeyEnum::Uninitialized); +static ROOT_KEY_MAYBE: RwLock = RwLock::const_new(KeyEnum::Uninitialized); -pub async fn get_or_init_root_key(pool: &DbPool) { - let mut value = KEY_MAPS.write().await; +struct ProtectedRK { + pub protection_type: String, + pub encrypted_key: Vec, +} - match *value { - KeyEnum::MainKey(_) => panic!("Root key already initialized!"), - KeyEnum::Uninitialized => get_root_key_db(pool, &mut value).await, +/// Returns `true` if vault is initialized or unsealed. +/// Returns `false` if uninitialized (nothing in the database). +pub async fn prepare_unseal(pool: &DbPool) -> bool { + { + if !matches!(*ROOT_KEY_MAYBE.read().await, KeyEnum::Uninitialized) { + info!("Vault unseal is already prepared"); + return true; + } } - // 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 lock = ROOT_KEY_MAYBE.write(); // Not awaited just here + + let rk = sqlx::query_as!( + ProtectedRK, + "SELECT encrypted_key, type as protection_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; - }, + return false; + } }; - info!("Root key of type {} found in the database", v.r#type); - match &*v.r#type { + info!( + "Root key of type {} found in the database", + v.protection_type + ); + + let mut lock = lock.await; + match &*v.protection_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; - }, + warn!( + "Root key is of type {}. This is INSECURE and must only be used for development purposes!", + v.protection_type + ); + *lock = KeyEnum::MainKey(v.encrypted_key); + } + "simple" => { + *lock = KeyEnum::Simple(v.encrypted_key); + } _ => panic!("Unknown root key type in database"), } + true } -async fn init_new_root_key(pool: &DbPool, value: &mut KeyEnum) { - warn!("Initializing new root key!"); - let key = Aes256GcmSiv::generate_key(&mut OsRng); +/// Must NOT be used in production. +/// Token is plainly stored in the database and will be unsealed directly by [prepare_unseal]! +/// Danger! +pub async fn init_insecure_in_db(pool: &DbPool) { + let root_key = Aes256GcmSiv::generate_key(&mut OsRng); + let root_key = root_key.as_slice().to_owned(); - let key = key.as_slice().to_owned(); + warn!( + "Danger: INSECURE! Generated root key is stored plainly in the database. Must ONLY be used for development!" + ); + write_new_root_key(pool, root_key, "dev_only").await; +} +async fn write_new_root_key(pool: &DbPool, protected_key: Vec, type_to_be: &str) { let _ = sqlx::query!( " INSERT INTO root_key (encrypted_key, type, version) - VALUES ($1, 'dev_only', 1) + VALUES ($1, $2, 1) ", - key + protected_key, + type_to_be ) .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"), +pub async fn reseal(pool: &DbPool) { + { + let mut lock = ROOT_KEY_MAYBE.write().await; + *lock = KeyEnum::Uninitialized; + } + prepare_unseal(pool).await; +} + +pub async fn sealing_status() { + let lock = ROOT_KEY_MAYBE.read().await; + match &*lock { + KeyEnum::MainKey(_) => todo!(), + KeyEnum::Simple(_) => todo!(), + KeyEnum::Uninitialized => todo!(), + } +} + +pub async fn provide_key(key: String) { + let progressed_something = { + let read_lock = ROOT_KEY_MAYBE.read().await; + match &*read_lock { + KeyEnum::MainKey(_) => { + info!("Providing keys is useless since vault is already unlocked"); + return; + } + KeyEnum::Simple(protected_rk) => { + KeyEnum::MainKey(simple::unseal(protected_rk, key).await) + } + KeyEnum::Uninitialized => { + error!("Cannot process provided key when the vault is uninitialized"); + return; + } + } + }; + + info!( + "Progress on unsealing: {}", + match progressed_something { + KeyEnum::MainKey(_) => "done and ready", + _ => "not yet ready", + } + ); + + let mut write_lock = ROOT_KEY_MAYBE.write().await; + *write_lock = progressed_something; +} + +pub async fn encrypt(data: &String) -> ([u8; 12], Vec) { + let cipher = if let KeyEnum::MainKey(key) = &*ROOT_KEY_MAYBE.read().await { + Aes256GcmSiv::new_from_slice(key) + } else { + 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 nonce: aes_gcm_siv::aead::generic_array::GenericArray< + u8, + ::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"); + 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 { +pub async fn decrypt(nonce: Vec, data: impl AsRef<[u8]>) -> String { assert!(nonce.len() == 12); - let cipher = match &*KEY_MAPS.read().await { + let cipher = match &*ROOT_KEY_MAYBE.read().await { KeyEnum::MainKey(key) => Aes256GcmSiv::new_from_slice(key), - KeyEnum::Uninitialized => panic!("Cannot seal secret since the vault is not unsealed"), + _ => panic!("Cannot seal secret since the vault is not unsealed"), } .expect("Failed to create new AesGcmSiv cipher from variable size key"); diff --git a/src/storage/sealing/simple.rs b/src/storage/sealing/simple.rs new file mode 100644 index 0000000..9de5b12 --- /dev/null +++ b/src/storage/sealing/simple.rs @@ -0,0 +1,31 @@ +use aes_gcm_siv::{aead::{Aead, OsRng}, Aes256GcmSiv, KeyInit}; +use base64::{prelude::BASE64_STANDARD, Engine}; + +use crate::DbPool; + +use super::write_new_root_key; + +pub async fn init_simple(pool: &DbPool) -> String { + // let root_key = write_new_root_key(pool, p_type).await; + let root_key = Aes256GcmSiv::generate_key(&mut OsRng); + let root_key = root_key.as_slice().to_owned(); + + let (user_key, protected_rk) = { + let key = Aes256GcmSiv::generate_key(&mut OsRng); + let cipher = Aes256GcmSiv::new(&key); + let nonce: &[u8; 12] = b"hello world!"; // TODO + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(nonce); + let enc = cipher.encrypt(nonce, root_key.as_slice()).unwrap(); + (key, enc) + }; + write_new_root_key(pool, protected_rk, "simple").await; + BASE64_STANDARD.encode(user_key) +} + +pub async fn unseal(protected_rk: &Vec, key: String) -> Vec { + let key = BASE64_STANDARD.decode(key).unwrap(); + let cipher = Aes256GcmSiv::new_from_slice(&key).unwrap(); + let nonce: &[u8; 12] = b"hello world!"; // TODO + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(nonce); + cipher.decrypt(nonce, protected_rk.as_ref()).unwrap() +} diff --git a/src/sys.rs b/src/sys.rs index fb4b994..8b297d6 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -1,8 +1,49 @@ -use axum::Router; +use axum::{ + extract::State, routing::{get, post}, Json, Router +}; +use serde::Deserialize; -use crate::storage::DbPool; +use crate::storage::{DbPool, sealing}; /// System routes pub fn sys_router(pool: DbPool) -> Router { - Router::new().with_state(pool) + Router::new() + .route("/seal", post(seal_post)) + .route("/seal-status", get(seal_status_get)) + .route("/unseal", post(unseal_post)) + .with_state(pool) } + +async fn seal_post(State(pool): State) { + sealing::reseal(&pool).await; +} + +#[derive(Deserialize)] +struct UnsealRequest { + /// Required, unless `reset` is true + pub key: Option, + #[serde(default)] + /// Specifies if previously-provided unseal keys are discarded and the unseal process is reset. + pub reset: bool, + + // #[serde(default)] + // /// Used to migrate the seal from shamir to autoseal or autoseal to shamir. Must be provided on all unseal key calls. + // pub migrate: bool, +} + +async fn unseal_post(State(pool): State, Json(req): Json) -> Result<(), ()> { + if req.reset { + sealing::reseal(&pool).await; + } + + if let Some(key) = req.key { + sealing::provide_key(key).await; + } else if !req.reset { + // No request key nor reset = bad request + return Err(()); + } + + Ok(()) +} + +async fn seal_status_get(State(pool): State) {}