From d77237aefe7693f1772e121f4f94f61eee256ceb Mon Sep 17 00:00:00 2001 From: C0ffeeCode Date: Wed, 2 Apr 2025 18:59:33 +0200 Subject: [PATCH] Refactor: Secret struct and feature-gates - Shamir and its dependencies behind a default feature - Secret has its own struct --- Cargo.toml | 10 +- go_tests/tests/secret_test.go | 22 +-- src/engines/kv/data.rs | 20 +-- src/main.rs | 10 +- src/storage.rs | 2 +- src/storage/sealing.rs | 328 ++++++++++++++++++++++------------ src/storage/sealing/shamir.rs | 173 ++++++++++++------ src/storage/sealing/simple.rs | 29 ++- 8 files changed, 386 insertions(+), 208 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7e6a024..107a780 100644 --- a/Cargo.toml +++ b/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 diff --git a/go_tests/tests/secret_test.go b/go_tests/tests/secret_test.go index b58899f..b8c8890 100644 --- a/go_tests/tests/secret_test.go +++ b/go_tests/tests/secret_test.go @@ -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{ diff --git a/src/engines/kv/data.rs b/src/engines/kv/data.rs index c81d977..0e1c0c5 100644 --- a/src/engines/kv/data.rs +++ b/src/engines/kv/data.rs @@ -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, ) diff --git a/src/main.rs b/src/main.rs index 36bf56d..5cf8d2f 100644 --- a/src/main.rs +++ b/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) diff --git a/src/storage.rs b/src/storage.rs index 546b0e2..97cbb25 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -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"); } } diff --git a/src/storage/sealing.rs b/src/storage/sealing.rs index 1e1320b..8dc9580 100644 --- a/src/storage/sealing.rs +++ b/src/storage/sealing.rs @@ -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), /// Encrypted with single secret (protected_rk, nonce) - Simple(Vec, Vec), + Simple(SimpleSealing), + #[cfg(feature = "shamir")] // Shamir's Secret Sharing - Shamir(Vec, Vec), + Shamir(shamir::ShamirBucket), /// Unknown or not initialized Uninitialized, } -static ROOT_KEY_MAYBE: RwLock = RwLock::const_new(KeyEnum::Uninitialized); +trait Sealing { + fn new(protected_rk: Vec, nonce: Vec) -> Self; + + async fn unseal(&mut self, key: String) -> UnsealResult; +} struct ProtectedRK { pub protection_type: String, @@ -30,6 +37,8 @@ struct ProtectedRK { pub nonce: Option>, } +static ROOT_KEY_MAYBE: RwLock = 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 struct Secret { + pub nonce: [u8; 12], + pub protected_data: Vec, +} + +impl Secret { + pub fn new(data: D, nonce: N) -> Self + where + D: Into>, + N: AsRef<[u8]>, + { + let nonce_slice = nonce.as_ref(); + assert!( + nonce_slice.len() == 12, + "Nonce must be exactly 12 bytes long" + ); + + let nonce: &[u8; 12] = nonce_slice.try_into().expect("Nonce must be 12 bytes long"); + + Self { + nonce: *nonce, + protected_data: data.into(), + } + } + + /// 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 { + 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, + ::NonceSize, + > = Aes256GcmSiv::generate_nonce(&mut OsRng); // 96-bits; unique per message + 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_bytes(self) -> Result, ()> { + 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(&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::from_utf8(self.decrypt_bytes().await?).map_err(|e| { + error!("Failed to parse secret as UTF8: {e}"); + }) } } -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 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), + /// 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 } }; - info!( - "Progress on unsealing: {}", - match progressed_something { - KeyEnum::MainKey(_) => "done and ready", - _ => "not yet ready", - } + 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." ); - - let mut write_lock = ROOT_KEY_MAYBE.write().await; - *write_lock = progressed_something; -} - -pub async fn encrypt(data: &String) -> ([u8; 12], Vec) { - 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, - ::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 decrypt(nonce: Vec, data: impl AsRef<[u8]>) -> String { - assert!(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") } diff --git a/src/storage/sealing/shamir.rs b/src/storage/sealing/shamir.rs index 54cd5b3..2a9e513 100644 --- a/src/storage/sealing/shamir.rs +++ b/src/storage/sealing/shamir.rs @@ -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>; -static PORTIONS: RwLock> = 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, #[serde(rename = "v")] pub value: Vec, } +#[derive(PartialEq)] +pub struct ShamirBucket { + portions: Vec, + protected_rk: Vec, + nonce: Vec, +} + +impl Sealing for ShamirBucket { + fn new(protected_rk: Vec, nonce: Vec) -> 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 { let root_key = Aes256GcmSiv::generate_key(&mut OsRng); let nonce: GenericArray::NonceSize> = @@ -60,35 +138,6 @@ pub async fn init_shamir(pool: &DbPool, threshold: usize, limit: usize) -> Vec, key: String, nonce: &[u8]) -> Result, 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) -> Result { +fn join_keys(shares: &[ShamirPortion]) -> Result { let shares: Vec = shares .iter() - .map(|portion| P256Share { - identifier: IdentifierPrimeField::::from_slice(&portion.identifier).unwrap(), - value: ValuePrimeField::::from_slice(&portion.value).unwrap(), + .map(|portion| { + let identifier = IdentifierPrimeField::::from_slice(&portion.identifier) + .map_err(|e| { + info!("Portion could not be converted to IdentifierPrimeField: {e}"); + VsssErr::InvalidShare + })?; + let value = ValuePrimeField::::from_slice(&portion.value).map_err(|e| { + info!("Portion could not be converted to ValuePrimeField: {e}"); + VsssErr::InvalidShare + })?; + Ok(P256Share { identifier, value }) }) - .collect(); + .collect::>()?; + 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" + ); } diff --git a/src/storage/sealing/simple.rs b/src/storage/sealing/simple.rs index 6e6c74e..a832efc 100644 --- a/src/storage/sealing/simple.rs +++ b/src/storage/sealing/simple.rs @@ -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, Vec); + +impl Sealing for SimpleSealing { + fn new(protected_rk: Vec, nonce: Vec) -> 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::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, key: String, nonce: &[u8]) -> Vec { - 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() -}