Sealing: Encryption of Secrets #1
5 changed files with 206 additions and 0 deletions
94
Cargo.lock
generated
94
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
7
migrations/20250326160659_sealing.sql
Normal file
7
migrations/20250326160659_sealing.sql
Normal 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') )
|
||||||
|
);
|
||||||
|
|
@ -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
102
src/storage/sealing.rs
Normal 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")
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue