Sealing: Encryption of Secrets #1
9 changed files with 220 additions and 60 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -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
1
Cargo.lock
generated
|
|
@ -1478,6 +1478,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"aes-gcm-siv",
|
||||
"axum",
|
||||
"base64",
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
"json-patch",
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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') )
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
struct ProtectedRK {
|
||||
pub protection_type: String,
|
||||
pub encrypted_key: Vec<u8>,
|
||||
}
|
||||
|
||||
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")
|
||||
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");
|
||||
|
||||
|
|
|
|||
31
src/storage/sealing/simple.rs
Normal file
31
src/storage/sealing/simple.rs
Normal 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()
|
||||
}
|
||||
47
src/sys.rs
47
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<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>) {}
|
||||
|
|
|
|||
Loading…
Reference in a new issue