Sealing: Encryption of Secrets #1

Merged
C0ffeeCode merged 7 commits from sealing into dev 2025-04-03 10:08:08 +02:00
5 changed files with 206 additions and 0 deletions
Showing only changes of commit 1accd45648 - Show all commits

94
Cargo.lock generated
View file

@ -17,6 +17,42 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -223,6 +259,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.3" version = "1.0.3"
@ -290,9 +336,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@ -851,6 +907,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -1085,6 +1150,12 @@ version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@ -1168,6 +1239,18 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 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]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.11.0" version = "1.11.0"
@ -1393,6 +1476,7 @@ checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
name = "rvault-server" name = "rvault-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aes-gcm-siv",
"axum", "axum",
"dotenvy", "dotenvy",
"env_logger", "env_logger",
@ -2054,6 +2138,16 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View file

@ -16,6 +16,7 @@ serde_json = "1.0.117"
json-patch = "4.0.0" json-patch = "4.0.0"
# serde_with = "3.8.1" # serde_with = "3.8.1"
dotenvy = "0.15.7" dotenvy = "0.15.7"
aes-gcm-siv = "0.11.1"
# utoipa = { version = "4.2.0", features = ["axum_extras"] } # utoipa = { version = "4.2.0", features = ["axum_extras"] }
sqlx = { version = "0.8.3", features = [ sqlx = { version = "0.8.3", features = [

View file

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

View file

@ -48,6 +48,8 @@ async fn main() {
.layer(middleware::from_fn(set_default_content_type_json)) .layer(middleware::from_fn(set_default_content_type_json))
.with_state(pool.clone()); .with_state(pool.clone());
storage::sealing::get_or_init_root_key(&pool).await;
warn!("Listening on {}", listen_addr.to_string()); warn!("Listening on {}", listen_addr.to_string());
// Start listening // Start listening
let listener = TcpListener::bind(listen_addr).await.unwrap(); let listener = TcpListener::bind(listen_addr).await.unwrap();

102
src/storage/sealing.rs Normal file
View file

@ -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<u8>),
Uninitialized,
// Portion(Vec<String>),
}
static KEY_MAPS: 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
}
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<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"),
}
.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 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<u8>, 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")
}