Sealing: Encryption of Secrets #1

Merged
C0ffeeCode merged 7 commits from sealing into dev 2025-04-03 10:08:08 +02:00
8 changed files with 386 additions and 208 deletions
Showing only changes of commit d77237aefe - Show all commits

View file

@ -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

View file

@ -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{

View file

@ -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,
)

View file

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

View file

@ -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");
}
}

View 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) {
let progressed_something = {
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;
match &*read_lock {
KeyEnum::MainKey(_) => {
if matches!(*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 => {
return UnsealResult::AlreadyDone;
} else if matches!(*read_lock, 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;
return UnsealResult::Uninitialized;
}
}
// todo!()
},
}
};
info!(
"Progress on unsealing: {}",
match progressed_something {
KeyEnum::MainKey(_) => "done and ready",
_ => "not yet ready",
// 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<u8>,
}
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");
Self {
nonce: *nonce,
protected_data: data.into(),
}
}
pub async fn encrypt(data: &String) -> ([u8; 12], Vec<u8>) {
/// 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 {
Aes256GcmSiv::new_from_slice(key)
} else {
panic!("Cannot seal secret since the vault is not unsealed")
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(());
}
.expect("Failed to create new AesGcmSiv cipher from variable size key");
}
} 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."
);
}

View file

@ -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"
);
}

View file

@ -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()
}