Feat (sealing): Simple Password sealing

Password is generated on first startup.
The password given to the user is not same as the one used to encrypt secrets
This commit is contained in:
Laurenz 2025-03-27 17:13:48 +01:00
parent 4d342e8b99
commit 88ed714e22
Signed by: C0ffeeCode
SSH key fingerprint: SHA256:prvFOyBjButRypyXm7X8lbbCkly2Dq1PF7e/mrsPVjw
9 changed files with 220 additions and 60 deletions

5
.gitignore vendored
View file

@ -7,6 +7,5 @@
*.pdf
target/
go_client/openapi.json
crates/storage-sled/sled_db
test.db
src/storage/database.db
*.db*

1
Cargo.lock generated
View file

@ -1478,6 +1478,7 @@ version = "0.1.0"
dependencies = [
"aes-gcm-siv",
"axum",
"base64",
"dotenvy",
"env_logger",
"json-patch",

View file

@ -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 = [

View file

@ -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') )
);

View file

@ -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<EnginePath>,
) -> Result<Response, ()> {
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<String>,
Extension(EnginePath(engine_path)): Extension<EnginePath>,
) -> Result<Response, Response> {
debug!("Secret: {}, path: {}", path, path);
debug!("Secret: {path}, path: {path}");
let del_time = UtcDateTime::now().unix_timestamp();

View file

@ -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

View file

@ -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,25 +13,36 @@ use super::DbPool;
enum KeyEnum {
/// Final key
MainKey(Vec<u8>),
/// Encrypted with single secret
Simple(Vec<u8>),
/// Unknown or not initialized
Uninitialized,
// Portion(Vec<String>),
// ShamirPortion(Vec<String>),
}
static KEY_MAPS: RwLock<KeyEnum> = RwLock::const_new(KeyEnum::Uninitialized);
static ROOT_KEY_MAYBE: RwLock<KeyEnum> = 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
struct ProtectedRK {
pub protection_type: String,
pub encrypted_key: Vec<u8>,
}
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")
/// 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;
}
}
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");
@ -37,62 +51,133 @@ async fn get_root_key_db(pool: &DbPool, value: &mut KeyEnum) {
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<u8>, 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<u8>) {
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<u8>) {
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<u8, <Aes256GcmSiv as aes_gcm_siv::AeadCore>::NonceSize> =
Aes256GcmSiv::generate_nonce(&mut OsRng); // 96-bits; unique per message
let nonce: aes_gcm_siv::aead::generic_array::GenericArray<
u8,
<Aes256GcmSiv as aes_gcm_siv::AeadCore>::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<u8>, data: impl AsRef<[u8]>) -> String {
pub async fn decrypt(nonce: Vec<u8>, 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");

View file

@ -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<u8>, key: String) -> Vec<u8> {
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()
}

View file

@ -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<DbPool> {
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<DbPool>) {
sealing::reseal(&pool).await;
}
#[derive(Deserialize)]
struct UnsealRequest {
/// Required, unless `reset` is true
pub key: Option<String>,
#[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<DbPool>, Json(req): Json<UnsealRequest>) -> 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<DbPool>) {}