Refactor: Secret struct and feature-gates
- Shamir and its dependencies behind a default feature - Secret has its own struct
This commit is contained in:
parent
6eb02c8412
commit
d77237aefe
8 changed files with 386 additions and 208 deletions
10
Cargo.toml
10
Cargo.toml
|
|
@ -3,6 +3,12 @@ name = "rvault-server"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["shamir"]
|
||||
# default = ["insecure-dev-sealing"]
|
||||
insecure-dev-sealing = []
|
||||
shamir = ["vsss-rs", "p256"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.27"
|
||||
env_logger = "0.11.7"
|
||||
|
|
@ -28,8 +34,8 @@ sqlx = { version = "0.8.3", features = [
|
|||
] }
|
||||
|
||||
aes-gcm-siv = "0.11.1"
|
||||
vsss-rs = { version = "5.1.0", default-features = false, features = ["zeroize", "std"] }
|
||||
p256 = { version = "0.13.2", default-features = false, features = ["std", "ecdsa"] }
|
||||
vsss-rs = { version = "5.1.0", optional = true, default-features = false, features = ["zeroize", "std"] }
|
||||
p256 = { version = "0.13.2", optional = true, default-features = false, features = ["std", "ecdsa"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -40,18 +40,18 @@ func TestMain(m *testing.M) {
|
|||
}
|
||||
|
||||
// Requires in-code portions
|
||||
func TestUnseal(t *testing.T) {
|
||||
abc := []string{
|
||||
"eyJpIjpbMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwxXSwidiI6WzE4OCw2NiwxMTksMTQ0LDE1OSw3MCw4NiwxMTUsMTIwLDI1MywxMjQsOTYsMTM5LDk0LDQ1LDE2NiwyMTMsMzYsMTE1LDU4LDg5LDE0OCw2MCwyOCwxNTAsMTE2LDU3LDg5LDIwMCw5NywxNDYsMjEzXX0=",
|
||||
"eyJpIjpbMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwyXSwidiI6WzE1OCwyNDQsNzEsOTUsMTIyLDEzOCwyNDEsMjEzLDQ1LDE1NiwxMTgsNCwxNzYsNiwxNTcsMTkyLDE2MSwxNjEsNDMsMTc1LDE5NSw4NywxODAsMTAwLDE1NiwxNCwxNDgsMTUsMTc4LDkwLDY3LDExOF19",
|
||||
}
|
||||
for i := range abc {
|
||||
if _, err := Client.Sys().Unseal(abc[i]); err != nil {
|
||||
t.Fatal("Error unsealing", err)
|
||||
}
|
||||
// func TestUnseal(t *testing.T) {
|
||||
// abc := []string{
|
||||
// "eyJpIjpbMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwxXSwidiI6WzE4OCw2NiwxMTksMTQ0LDE1OSw3MCw4NiwxMTUsMTIwLDI1MywxMjQsOTYsMTM5LDk0LDQ1LDE2NiwyMTMsMzYsMTE1LDU4LDg5LDE0OCw2MCwyOCwxNTAsMTE2LDU3LDg5LDIwMCw5NywxNDYsMjEzXX0=",
|
||||
// "eyJpIjpbMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwyXSwidiI6WzE1OCwyNDQsNzEsOTUsMTIyLDEzOCwyNDEsMjEzLDQ1LDE1NiwxMTgsNCwxNzYsNiwxNTcsMTkyLDE2MSwxNjEsNDMsMTc1LDE5NSw4NywxODAsMTAwLDE1NiwxNCwxNDgsMTUsMTc4LDkwLDY3LDExOF19",
|
||||
// }
|
||||
// for i := range abc {
|
||||
// if _, err := Client.Sys().Unseal(abc[i]); err != nil {
|
||||
// t.Fatal("Error unsealing", err)
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
// }
|
||||
// }
|
||||
|
||||
func kv2Write(t *testing.T, mount string, path string) {
|
||||
data := map[string]any{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
use super::structs::KvV2WriteRequest;
|
||||
use crate::{
|
||||
DbPool,
|
||||
common::HttpError,
|
||||
engines::{
|
||||
EnginePath,
|
||||
kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper},
|
||||
},
|
||||
storage::sealing::{decrypt, encrypt},
|
||||
common::HttpError, engines::{
|
||||
kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath
|
||||
}, storage::sealing::Secret, DbPool
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
|
|
@ -39,13 +35,13 @@ struct SecretDataInternal {
|
|||
|
||||
impl SecretDataInternal {
|
||||
pub async fn into_external(self) -> KvSecretData {
|
||||
let secret = decrypt(self.nonce, &self.encrypted_data).await;
|
||||
let secret = Secret::new(self.encrypted_data, self.nonce).decrypt().await;
|
||||
KvSecretData {
|
||||
created_time: self.created_time,
|
||||
deletion_time: self.deletion_time,
|
||||
version_number: self.version_number,
|
||||
secret_path: self.secret_path,
|
||||
secret_data: secret,
|
||||
secret_data: secret.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,12 +121,12 @@ pub async fn post_data(
|
|||
|
||||
let content = serde_json::to_string(&secret.data).unwrap();
|
||||
|
||||
let (nonce, enc) = encrypt(&content).await;
|
||||
let Secret { nonce, protected_data } = Secret::encrypt(&content).await.unwrap();
|
||||
let nonce = nonce.as_slice();
|
||||
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
let res_m = sqlx::query!("
|
||||
let _ = sqlx::query!("
|
||||
INSERT INTO kv2_metadata (engine_path, secret_path, cas_required, created_time, max_versions, updated_time)
|
||||
VALUES ($1, $2, 0, $3, 100, $3)
|
||||
ON CONFLICT(engine_path, secret_path) DO NOTHING;
|
||||
|
|
@ -141,7 +137,7 @@ pub async fn post_data(
|
|||
engine_path,
|
||||
kv_path,
|
||||
nonce,
|
||||
enc,
|
||||
protected_data,
|
||||
ts,
|
||||
secret.version,
|
||||
)
|
||||
|
|
|
|||
10
src/main.rs
10
src/main.rs
|
|
@ -1,12 +1,12 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::Request,
|
||||
http::StatusCode,
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use log::*;
|
||||
use std::{env, net::SocketAddr, str::FromStr};
|
||||
|
|
@ -51,14 +51,10 @@ async fn main() {
|
|||
.with_state(pool.clone());
|
||||
|
||||
if !storage::sealing::prepare_unseal(&pool).await {
|
||||
// storage::sealing::init_insecure_in_db(&pool).await;
|
||||
let user_key = storage::sealing::shamir::init_shamir(&pool, 2, 5).await;
|
||||
let success = storage::sealing::prepare_unseal(&pool).await;
|
||||
warn!("New sealing password generated: ");
|
||||
assert!(success, "Vault ought to have been initialized just now but it is not.");
|
||||
storage::sealing::init_default(&pool).await;
|
||||
}
|
||||
|
||||
warn!("Listening on {}", listen_addr.to_string());
|
||||
warn!("Listening on {listen_addr}");
|
||||
// Start listening
|
||||
let listener = TcpListener::bind(listen_addr).await.unwrap();
|
||||
axum::serve(listener, app)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub async fn create_pool(db_url: String) -> DbPool {
|
|||
if db_url.starts_with("sqlite:") && db_url != ("sqlite::memory:") {
|
||||
let path = db_url.replace("sqlite:", "");
|
||||
if !Path::new(&path).exists() {
|
||||
warn!("Sqlite database does not exist, creating file {}", path);
|
||||
warn!("Sqlite database does not exist, creating file {path}");
|
||||
File::create(&path).expect("Failed to create database file");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
pub mod simple;
|
||||
#[cfg(feature = "shamir")]
|
||||
pub mod shamir;
|
||||
pub mod simple;
|
||||
|
||||
use aes_gcm_siv::{
|
||||
AeadCore, Aes256GcmSiv, KeyInit,
|
||||
aead::{Aead, OsRng},
|
||||
};
|
||||
use log::{error, info, warn};
|
||||
use simple::SimpleSealing;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::DbPool;
|
||||
|
|
@ -15,14 +17,19 @@ enum KeyEnum {
|
|||
/// Final key
|
||||
MainKey(Vec<u8>),
|
||||
/// Encrypted with single secret (protected_rk, nonce)
|
||||
Simple(Vec<u8>, Vec<u8>),
|
||||
Simple(SimpleSealing),
|
||||
#[cfg(feature = "shamir")]
|
||||
// Shamir's Secret Sharing
|
||||
Shamir(Vec<u8>, Vec<u8>),
|
||||
Shamir(shamir::ShamirBucket),
|
||||
/// Unknown or not initialized
|
||||
Uninitialized,
|
||||
}
|
||||
|
||||
static ROOT_KEY_MAYBE: RwLock<KeyEnum> = RwLock::const_new(KeyEnum::Uninitialized);
|
||||
trait Sealing {
|
||||
fn new(protected_rk: Vec<u8>, nonce: Vec<u8>) -> Self;
|
||||
|
||||
async fn unseal(&mut self, key: String) -> UnsealResult;
|
||||
}
|
||||
|
||||
struct ProtectedRK {
|
||||
pub protection_type: String,
|
||||
|
|
@ -30,6 +37,8 @@ struct ProtectedRK {
|
|||
pub nonce: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
static ROOT_KEY_MAYBE: RwLock<KeyEnum> = RwLock::const_new(KeyEnum::Uninitialized);
|
||||
|
||||
/// Returns `true` if vault is initialized or unsealed.
|
||||
/// Returns `false` if uninitialized (nothing in the database).
|
||||
pub async fn prepare_unseal(pool: &DbPool) -> bool {
|
||||
|
|
@ -63,35 +72,35 @@ pub async fn prepare_unseal(pool: &DbPool) -> bool {
|
|||
);
|
||||
|
||||
let mut lock = lock.await;
|
||||
match &*v.protection_type {
|
||||
let nonce = v.nonce.expect("Simple encryption but the nonce is missing");
|
||||
let res = match &*v.protection_type {
|
||||
#[cfg(feature = "insecure-dev-sealing")]
|
||||
"dev_only" => {
|
||||
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,
|
||||
v.nonce.expect("Simple encryption but the nonce is missing"),
|
||||
);
|
||||
}
|
||||
"shamir" => {
|
||||
*lock = KeyEnum::Shamir(
|
||||
v.encrypted_key,
|
||||
v.nonce.expect("Simple encryption but the nonce is missing"),
|
||||
);
|
||||
KeyEnum::MainKey(v.encrypted_key)
|
||||
}
|
||||
#[cfg(not(feature = "insecure-dev-sealing"))]
|
||||
"dev_only" => panic!(
|
||||
r#"Database is insecure but "insecure-dev-sealing" is not enabled for this build!"#
|
||||
),
|
||||
"simple" => KeyEnum::Simple(SimpleSealing::new(v.encrypted_key, nonce)),
|
||||
#[cfg(feature = "shamir")]
|
||||
"shamir" => KeyEnum::Shamir(shamir::ShamirBucket::new(v.encrypted_key, nonce)),
|
||||
#[cfg(not(feature = "shamir"))]
|
||||
"shamir" => panic!(r#"Feature "shamir" is not enabled for this build!"#),
|
||||
_ => panic!("Unknown root key type in database"),
|
||||
}
|
||||
};
|
||||
*lock = res;
|
||||
true
|
||||
}
|
||||
|
||||
/// Must NOT be used in production.
|
||||
/// Token is plainly stored in the database and will be unsealed directly by [prepare_unseal]!
|
||||
/// Danger!
|
||||
#[allow(unused)]
|
||||
#[cfg(feature = "insecure-dev-sealing")]
|
||||
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();
|
||||
|
|
@ -99,7 +108,7 @@ pub async fn init_insecure_in_db(pool: &DbPool) {
|
|||
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", None).await;
|
||||
write_new_root_key(pool, root_key, "dev_only", Some(b"")).await;
|
||||
}
|
||||
|
||||
async fn write_new_root_key(
|
||||
|
|
@ -132,105 +141,194 @@ pub async fn reseal(pool: &DbPool) {
|
|||
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!(),
|
||||
KeyEnum::Shamir(_, _) => todo!(),
|
||||
// pub async fn sealing_status() {
|
||||
// let lock = ROOT_KEY_MAYBE.read().await;
|
||||
// match &*lock {
|
||||
// KeyEnum::MainKey(_) => todo!(),
|
||||
// KeyEnum::Simple(_, _) => todo!(),
|
||||
// KeyEnum::Uninitialized => todo!(),
|
||||
// KeyEnum::Shamir(_, _) => todo!(),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub async fn provide_key(key: String) -> UnsealResult {
|
||||
// First, check if we need to write-lock at all
|
||||
{
|
||||
let read_lock = ROOT_KEY_MAYBE.read().await;
|
||||
if matches!(*read_lock, KeyEnum::MainKey(_)) {
|
||||
info!("Providing keys is useless since vault is already unlocked");
|
||||
return UnsealResult::AlreadyDone;
|
||||
} else if matches!(*read_lock, KeyEnum::Uninitialized) {
|
||||
error!("Cannot process provided key when the vault is uninitialized");
|
||||
return UnsealResult::Uninitialized;
|
||||
}
|
||||
}
|
||||
|
||||
// A write lock is necessary.
|
||||
let mut write_lock = ROOT_KEY_MAYBE.write().await;
|
||||
let rk = match &mut *write_lock {
|
||||
KeyEnum::MainKey(_) | KeyEnum::Uninitialized => {
|
||||
unreachable!("Should have been checked above")
|
||||
}
|
||||
KeyEnum::Simple(simple) => simple.unseal(key).await,
|
||||
#[cfg(feature = "shamir")]
|
||||
KeyEnum::Shamir(shamir) => shamir.unseal(key).await,
|
||||
};
|
||||
let rk = match rk {
|
||||
UnsealResult::DoneConfidential(rk) => rk,
|
||||
UnsealResult::Done => unreachable!(),
|
||||
reject_action => return reject_action,
|
||||
};
|
||||
*write_lock = KeyEnum::MainKey(rk);
|
||||
|
||||
info!("Unsealing done; Vault ready");
|
||||
UnsealResult::Done
|
||||
}
|
||||
|
||||
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, nonce) => {
|
||||
KeyEnum::MainKey(simple::unseal(protected_rk, key, nonce).await)
|
||||
}
|
||||
KeyEnum::Uninitialized => {
|
||||
error!("Cannot process provided key when the vault is uninitialized");
|
||||
return;
|
||||
}
|
||||
KeyEnum::Shamir(protected_rk, nonce) => {
|
||||
let rk = shamir::unseal(protected_rk, key, nonce).await;
|
||||
match rk {
|
||||
Ok(rk) => KeyEnum::MainKey(rk),
|
||||
Err(e) => {
|
||||
match e {
|
||||
vsss_rs::Error::SharingMinThreshold => {
|
||||
info!("Shamir portion provided. Sharing threshold not reached.")
|
||||
},
|
||||
vsss_rs::Error::SharingLimitLessThanThreshold => unreachable!(),
|
||||
vsss_rs::Error::InvalidSizeRequest => todo!(),
|
||||
vsss_rs::Error::SharingInvalidIdentifier => todo!(),
|
||||
vsss_rs::Error::SharingDuplicateIdentifier => {
|
||||
warn!("The supplied Shamir portion is already known. Duplication ignored.")
|
||||
},
|
||||
vsss_rs::Error::SharingMaxRequest => todo!(),
|
||||
vsss_rs::Error::InvalidShare => todo!(),
|
||||
vsss_rs::Error::InvalidGenerator(_) => todo!(),
|
||||
vsss_rs::Error::InvalidSecret => todo!(),
|
||||
vsss_rs::Error::InvalidShareConversion => todo!(),
|
||||
vsss_rs::Error::NotImplemented => todo!(),
|
||||
vsss_rs::Error::InvalidShareElement => todo!(),
|
||||
vsss_rs::Error::NotEnoughShareIdentifiers => todo!(),
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// todo!()
|
||||
},
|
||||
}
|
||||
};
|
||||
pub struct Secret {
|
||||
pub nonce: [u8; 12],
|
||||
pub protected_data: Vec<u8>,
|
||||
}
|
||||
|
||||
info!(
|
||||
"Progress on unsealing: {}",
|
||||
match progressed_something {
|
||||
KeyEnum::MainKey(_) => "done and ready",
|
||||
_ => "not yet ready",
|
||||
}
|
||||
impl Secret {
|
||||
pub fn new<D, N>(data: D, nonce: N) -> Self
|
||||
where
|
||||
D: Into<Vec<u8>>,
|
||||
N: AsRef<[u8]>,
|
||||
{
|
||||
let nonce_slice = nonce.as_ref();
|
||||
assert!(
|
||||
nonce_slice.len() == 12,
|
||||
"Nonce must be exactly 12 bytes long"
|
||||
);
|
||||
|
||||
let mut write_lock = ROOT_KEY_MAYBE.write().await;
|
||||
*write_lock = progressed_something;
|
||||
}
|
||||
let nonce: &[u8; 12] = nonce_slice.try_into().expect("Nonce must be 12 bytes long");
|
||||
|
||||
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")
|
||||
Self {
|
||||
nonce: *nonce,
|
||||
protected_data: data.into(),
|
||||
}
|
||||
.expect("Failed to create new AesGcmSiv cipher from variable size key");
|
||||
}
|
||||
|
||||
/// Encrypt a secret
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if .
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the vault is uninitialized or an unknown error occurs.
|
||||
pub async fn encrypt(data: &String) -> Result<Self, ()> {
|
||||
let cipher = if let KeyEnum::MainKey(key) = &*ROOT_KEY_MAYBE.read().await {
|
||||
match Aes256GcmSiv::new_from_slice(key) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to create new AesGcmSiv cipher from variable size key: {e}");
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Cannot encrypt secret since the vault is not unsealed");
|
||||
return Err(());
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
let enc = match cipher.encrypt(&nonce, data.as_bytes()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to encrypt secret with cipher: {e}");
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
debug_assert!(nonce.len() == 12, "Nonce should be exactly 12 bytes");
|
||||
let nonce = match nonce.as_slice().try_into() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Nonce should be exactly 12 bytes: {e}");
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
nonce,
|
||||
protected_data: enc,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn decrypt(nonce: Vec<u8>, data: impl AsRef<[u8]>) -> String {
|
||||
assert!(nonce.len() == 12);
|
||||
pub async fn decrypt_bytes(self) -> Result<Vec<u8>, ()> {
|
||||
assert!(self.nonce.len() == 12);
|
||||
let cipher = match &*ROOT_KEY_MAYBE.read().await {
|
||||
KeyEnum::MainKey(key) => Aes256GcmSiv::new_from_slice(key),
|
||||
_ => 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")
|
||||
let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(&self.nonce);
|
||||
let enc = match cipher.decrypt(nonce, self.protected_data.as_ref()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt secret with given nonce and cipher: {e}");
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
Ok(enc)
|
||||
}
|
||||
|
||||
pub async fn decrypt(self) -> Result<String, ()> {
|
||||
String::from_utf8(self.decrypt_bytes().await?).map_err(|e| {
|
||||
error!("Failed to parse secret as UTF8: {e}");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UnsealResult {
|
||||
/// Unsealing finished, with root key hidden
|
||||
Done,
|
||||
/// Was already unsealed, no action taken
|
||||
AlreadyDone,
|
||||
/// Could not unseal as the vault is uninitialized
|
||||
Uninitialized,
|
||||
|
||||
/// Unsealing finished, returns root key
|
||||
DoneConfidential(Vec<u8>),
|
||||
/// Unsealing attempt has been recorded but is not sufficient
|
||||
Unfinished,
|
||||
/// The provided or the set of previously provided portions are invalid.
|
||||
/// Unsealing has been reset.
|
||||
InvalidReset,
|
||||
/// Duplicate share
|
||||
Duplicate,
|
||||
/// Error processing share, invalid
|
||||
InvalidRejected,
|
||||
}
|
||||
|
||||
pub async fn init_default(pool: &DbPool) {
|
||||
#[cfg(feature = "insecure-dev-sealing")]
|
||||
let user_key = {
|
||||
storage::sealing::init_insecure_in_db(&pool).await;
|
||||
"INSECURE automatic unlock - TESTING ONLY"
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "insecure-dev-sealing"))]
|
||||
let user_key = {
|
||||
#[cfg(not(feature = "shamir"))]
|
||||
{
|
||||
simple::init_simple(&pool).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "shamir")]
|
||||
{
|
||||
shamir::init_shamir(&pool, 2, 5).await
|
||||
}
|
||||
};
|
||||
|
||||
let success = prepare_unseal(&pool).await;
|
||||
warn!("New sealing password generated: {user_key:?}");
|
||||
assert!(
|
||||
success,
|
||||
"Vault ought to have been initialized just now but it is not."
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,39 +3,117 @@ use aes_gcm_siv::{
|
|||
aead::{Aead, OsRng, generic_array::GenericArray},
|
||||
};
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
use log::warn;
|
||||
use log::{error, info, warn};
|
||||
use p256::{NonZeroScalar, Scalar, SecretKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::sync::RwLock;
|
||||
use vsss_rs::{
|
||||
DefaultShare, Error as VsssErr, IdentifierPrimeField, ReadableShareSet, ShareElement, ValuePrimeField
|
||||
DefaultShare, Error as VsssErr, IdentifierPrimeField, ReadableShareSet, ShareElement,
|
||||
ValuePrimeField,
|
||||
};
|
||||
use zeroize::ZeroizeOnDrop;
|
||||
|
||||
use crate::DbPool;
|
||||
|
||||
use super::write_new_root_key;
|
||||
use super::{write_new_root_key, Sealing, UnsealResult};
|
||||
|
||||
type P256Share = DefaultShare<IdentifierPrimeField<Scalar>, IdentifierPrimeField<Scalar>>;
|
||||
|
||||
static PORTIONS: RwLock<Vec<ShamirPortion>> = RwLock::const_new(Vec::new());
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ZeroizeOnDrop)]
|
||||
/// Differs from [P256Share] by containing Strings
|
||||
pub struct ShamirPortion {
|
||||
struct ShamirPortion {
|
||||
#[serde(rename = "i")]
|
||||
pub identifier: Vec<u8>,
|
||||
#[serde(rename = "v")]
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct ShamirBucket {
|
||||
portions: Vec<ShamirPortion>,
|
||||
protected_rk: Vec<u8>,
|
||||
nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Sealing for ShamirBucket {
|
||||
fn new(protected_rk: Vec<u8>, nonce: Vec<u8>) -> Self {
|
||||
Self {
|
||||
portions: Vec::with_capacity(2),
|
||||
protected_rk,
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
async fn unseal(&mut self, key: String) -> UnsealResult {
|
||||
let key = match BASE64_STANDARD.decode(key) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
warn!("Portion could not be decoded: {e}");
|
||||
return UnsealResult::InvalidRejected;
|
||||
}
|
||||
};
|
||||
let key_portion: ShamirPortion = match serde_json::from_slice(&key) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
info!("Portion could not be parsed: {e}");
|
||||
return UnsealResult::InvalidRejected;
|
||||
}
|
||||
};
|
||||
|
||||
if self.portions.contains(&key_portion) {
|
||||
warn!("The supplied Shamir portion is already known. Duplication ignored.");
|
||||
return UnsealResult::Duplicate;
|
||||
}
|
||||
self.portions.push(key_portion);
|
||||
|
||||
let abc = match join_keys(&self.portions) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return match e {
|
||||
VsssErr::SharingMinThreshold => {
|
||||
info!("Shamir portion provided. Sharing threshold not reached.");
|
||||
UnsealResult::Unfinished
|
||||
},
|
||||
VsssErr::SharingDuplicateIdentifier => unreachable!("Addition of duplicate keys should have been prevented by not recording them"),
|
||||
e => {
|
||||
error!("Unknown error occurred upon joining keys {e:?}");
|
||||
unreachable!()
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
.to_bytes();
|
||||
|
||||
let cipher = match Aes256GcmSiv::new_from_slice(&abc) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
info!("Cipher could not be created from slice: {e}");
|
||||
return UnsealResult::InvalidRejected;
|
||||
}
|
||||
};
|
||||
debug_assert_eq!(self.nonce.len(), 12);
|
||||
let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(&self.nonce);
|
||||
let root_key = cipher.decrypt(nonce, self.protected_rk.as_ref());
|
||||
match root_key {
|
||||
Ok(v) => UnsealResult::DoneConfidential(v),
|
||||
Err(_) => {
|
||||
// Err is opaque on purpose
|
||||
self.portions.clear();
|
||||
warn!(
|
||||
"Enough shares have been provided but the set of shares is invalid. The set of shares has been reset."
|
||||
);
|
||||
UnsealResult::InvalidReset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shamir Secret Sharing does not verify a portion for validity,
|
||||
/// unlike Feldman Verified Secret Sharing, which is built on Shamir.
|
||||
/// "Validation" happens by attempting to decrypt the root key.
|
||||
///
|
||||
/// # Returns
|
||||
/// List of key portions
|
||||
/// List of encoded key portions
|
||||
pub async fn init_shamir(pool: &DbPool, threshold: usize, limit: usize) -> Vec<String> {
|
||||
let root_key = Aes256GcmSiv::generate_key(&mut OsRng);
|
||||
let nonce: GenericArray<u8, <Aes256GcmSiv as AeadCore>::NonceSize> =
|
||||
|
|
@ -60,35 +138,6 @@ pub async fn init_shamir(pool: &DbPool, threshold: usize, limit: usize) -> Vec<S
|
|||
portions
|
||||
}
|
||||
|
||||
pub async fn unseal(protected_rk: &Vec<u8>, key: String, nonce: &[u8]) -> Result<Vec<u8>, VsssErr> {
|
||||
let key = BASE64_STANDARD.decode(key).unwrap();
|
||||
let key_portion: ShamirPortion = serde_json::from_slice(&key).unwrap();
|
||||
|
||||
let mut portions = PORTIONS.write().await;
|
||||
if portions.contains(&key_portion) {
|
||||
return Err(VsssErr::SharingDuplicateIdentifier);
|
||||
}
|
||||
portions.push(key_portion);
|
||||
|
||||
let abc = match join_keys(&*portions) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
.to_bytes();
|
||||
|
||||
let cipher = Aes256GcmSiv::new_from_slice(&abc).unwrap();
|
||||
debug_assert_eq!(nonce.len(), 12);
|
||||
let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(nonce);
|
||||
let root_key = cipher.decrypt(nonce, protected_rk.as_ref());
|
||||
match root_key {
|
||||
Ok(v) => return Ok(v),
|
||||
Err(_) => {
|
||||
// Err is opaque on purpose
|
||||
todo!("Error: While the threshold is reached, at least one portion is invalid. TODO: reset advised")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Vec of Base64 encoded JSON-wrapped identifier-value pairs
|
||||
fn share_keys(
|
||||
mut osrng: &mut OsRng,
|
||||
|
|
@ -122,18 +171,31 @@ fn share_keys(
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn join_keys(shares: &Vec<ShamirPortion>) -> Result<SecretKey, vsss_rs::Error> {
|
||||
fn join_keys(shares: &[ShamirPortion]) -> Result<SecretKey, vsss_rs::Error> {
|
||||
let shares: Vec<P256Share> = shares
|
||||
.iter()
|
||||
.map(|portion| P256Share {
|
||||
identifier: IdentifierPrimeField::<Scalar>::from_slice(&portion.identifier).unwrap(),
|
||||
value: ValuePrimeField::<Scalar>::from_slice(&portion.value).unwrap(),
|
||||
.map(|portion| {
|
||||
let identifier = IdentifierPrimeField::<Scalar>::from_slice(&portion.identifier)
|
||||
.map_err(|e| {
|
||||
info!("Portion could not be converted to IdentifierPrimeField: {e}");
|
||||
VsssErr::InvalidShare
|
||||
})?;
|
||||
let value = ValuePrimeField::<Scalar>::from_slice(&portion.value).map_err(|e| {
|
||||
info!("Portion could not be converted to ValuePrimeField: {e}");
|
||||
VsssErr::InvalidShare
|
||||
})?;
|
||||
Ok(P256Share { identifier, value })
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<_, VsssErr>>()?;
|
||||
|
||||
let scalar = shares.combine()?;
|
||||
let nzs_dup = NonZeroScalar::from_repr(scalar.0.into()).unwrap();
|
||||
let sk_dup = SecretKey::from(nzs_dup);
|
||||
Ok(sk_dup)
|
||||
// A little suboptimal thanks to CtOption
|
||||
let nzs = match NonZeroScalar::from_repr(scalar.0.into()).into_option() {
|
||||
Some(v) => v,
|
||||
None => return Err(VsssErr::InvalidShare),
|
||||
};
|
||||
let sk = SecretKey::from(nzs);
|
||||
Ok(sk)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -142,11 +204,20 @@ fn split_and_join() {
|
|||
let root_key = root_key.as_slice().to_owned();
|
||||
let kps = share_keys(&mut OsRng, 2, 5, &root_key);
|
||||
|
||||
let kps: Vec<_> = kps.iter().map(|f| {
|
||||
let b = BASE64_STANDARD.decode(f).unwrap();
|
||||
serde_json::from_slice(&b).unwrap()
|
||||
}).collect();
|
||||
let k = join_keys(&kps).unwrap();
|
||||
let kps: Vec<_> = kps
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let b = BASE64_STANDARD
|
||||
.decode(f)
|
||||
.expect("A portion could not be decoded from BASE64");
|
||||
serde_json::from_slice(&b).expect("A portion could not be parsed as a key pair")
|
||||
})
|
||||
.collect();
|
||||
let k = join_keys(&kps).expect("Error on joining key pairs");
|
||||
|
||||
assert_eq!(root_key, k.to_bytes().as_slice());
|
||||
assert_eq!(
|
||||
root_key,
|
||||
k.to_bytes().as_slice(),
|
||||
"Original key and re-combined key from shares are not equal"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,27 @@ use base64::{Engine, prelude::BASE64_STANDARD};
|
|||
|
||||
use crate::DbPool;
|
||||
|
||||
use super::write_new_root_key;
|
||||
use super::{write_new_root_key, Sealing, UnsealResult};
|
||||
|
||||
/// Pair of protected root key and nonce
|
||||
#[derive(PartialEq)]
|
||||
pub struct SimpleSealing(Vec<u8>, Vec<u8>);
|
||||
|
||||
impl Sealing for SimpleSealing {
|
||||
fn new(protected_rk: Vec<u8>, nonce: Vec<u8>) -> Self {
|
||||
Self(protected_rk, nonce)
|
||||
}
|
||||
|
||||
async fn unseal(&mut self, key: String) -> UnsealResult {
|
||||
let key = BASE64_STANDARD.decode(key).unwrap();
|
||||
let cipher = Aes256GcmSiv::new_from_slice(&key).unwrap();
|
||||
debug_assert_eq!(self.1.len(), 12);
|
||||
let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(self.1.as_slice());
|
||||
UnsealResult::DoneConfidential(cipher.decrypt(nonce, self.0.as_ref()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn init_simple(pool: &DbPool) -> String {
|
||||
let root_key = Aes256GcmSiv::generate_key(&mut OsRng);
|
||||
let nonce: GenericArray<u8, <Aes256GcmSiv as AeadCore>::NonceSize> =
|
||||
|
|
@ -26,11 +45,3 @@ pub async fn init_simple(pool: &DbPool) -> String {
|
|||
write_new_root_key(pool, protected_rk, "simple", Some(nonce.as_slice())).await;
|
||||
BASE64_STANDARD.encode(user_key)
|
||||
}
|
||||
|
||||
pub async fn unseal(protected_rk: &Vec<u8>, key: String, nonce: &[u8]) -> Vec<u8> {
|
||||
let key = BASE64_STANDARD.decode(key).unwrap();
|
||||
let cipher = Aes256GcmSiv::new_from_slice(&key).unwrap();
|
||||
debug_assert_eq!(nonce.len(), 12);
|
||||
let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(nonce);
|
||||
cipher.decrypt(nonce, protected_rk.as_ref()).unwrap()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue