2025-12-22 21:34:01 +00:00

295 lines
8.8 KiB
Rust

use serde::{Deserialize, Serialize};
use sharks::{Share, Sharks};
use crate::config::SealConfig;
use crate::crypto::CryptoBackend;
use crate::error::{CryptoError, CryptoResult, Result, VaultError};
use crate::storage::StorageBackend;
/// Master key used to encrypt all secrets in the vault
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MasterKey {
pub key_data: Vec<u8>,
}
impl MasterKey {
/// Generate a new random 32-byte master key
pub async fn generate(crypto: &dyn CryptoBackend) -> CryptoResult<Self> {
let key_data = crypto.random_bytes(32).await?;
Ok(Self { key_data })
}
}
/// State of the vault seal mechanism
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SealState {
/// Vault is sealed (master key is split across shares)
Sealed,
/// Vault is unsealed (master key is reconstructed and available)
Unsealed,
}
/// Seal mechanism using Shamir Secret Sharing
#[derive(Debug)]
pub struct SealMechanism {
threshold: u8,
shares_count: usize,
state: SealState,
master_key: Option<MasterKey>,
}
impl SealMechanism {
/// Create a new seal mechanism from configuration
pub fn new(config: &SealConfig) -> Result<Self> {
// Validate threshold and shares
if config.shamir.threshold == 0 || config.shamir.shares == 0 {
return Err(VaultError::crypto(
"Threshold and shares must be greater than 0".to_string(),
));
}
if config.shamir.threshold > config.shamir.shares {
return Err(VaultError::crypto(
"Threshold cannot be greater than shares count".to_string(),
));
}
if config.shamir.threshold > 255 {
return Err(VaultError::crypto(
"Threshold cannot exceed 255".to_string(),
));
}
Ok(Self {
threshold: config.shamir.threshold as u8,
shares_count: config.shamir.shares,
state: SealState::Sealed,
master_key: None,
})
}
/// Get current seal state
pub fn state(&self) -> SealState {
self.state
}
/// Check if vault is sealed
pub fn is_sealed(&self) -> bool {
self.state == SealState::Sealed
}
/// Initialize the vault with a new master key (first initialization)
pub async fn init(
&mut self,
crypto: &dyn CryptoBackend,
storage: &dyn StorageBackend,
) -> Result<SealInitResult> {
if self.state == SealState::Unsealed {
return Err(VaultError::crypto(
"Vault is already initialized".to_string(),
));
}
// Generate new master key
let master_key = MasterKey::generate(crypto).await?;
// Split into Shamir shares
let shares = self.split_into_shares(&master_key)?;
// Store shares (would be distributed to operators in production)
let share_storage = ShareStorage {
shares: shares.iter().map(|s| s.to_vec()).collect(),
threshold: self.threshold,
shares_count: self.shares_count as u8,
};
let share_json =
serde_json::to_string(&share_storage).map_err(|e| VaultError::crypto(e.to_string()))?;
storage
.store_secret(
"sys/seal/shares",
&crate::storage::EncryptedData {
ciphertext: share_json.as_bytes().to_vec(),
nonce: vec![],
algorithm: "plain".to_string(),
},
)
.await
.map_err(|e| VaultError::storage(e.to_string()))?;
self.master_key = Some(master_key);
self.state = SealState::Unsealed;
Ok(SealInitResult {
shares: shares.iter().map(hex::encode).collect(),
threshold: self.threshold,
})
}
/// Unseal the vault using provided shares
pub fn unseal(&mut self, shares_data: &[&[u8]]) -> CryptoResult<()> {
use std::convert::TryFrom;
if shares_data.len() < self.threshold as usize {
return Err(CryptoError::InvalidAlgorithm(format!(
"Need at least {} shares to unseal, got {}",
self.threshold,
shares_data.len()
)));
}
// Parse shares from byte slices
let shares: std::result::Result<Vec<Share>, _> = shares_data
.iter()
.map(|data| Share::try_from(*data).map_err(|_| "Invalid share format"))
.collect();
let shares = shares
.map_err(|_| CryptoError::InvalidAlgorithm("Failed to parse shares".to_string()))?;
// Reconstruct master key from shares
let sharks = Sharks(self.threshold);
let reconstructed = sharks.recover(shares.as_slice()).map_err(|e| {
CryptoError::InvalidAlgorithm(format!("Failed to reconstruct secret: {:?}", e))
})?;
self.master_key = Some(MasterKey {
key_data: reconstructed,
});
self.state = SealState::Unsealed;
Ok(())
}
/// Seal the vault (clear master key from memory)
pub fn seal(&mut self) {
self.master_key = None;
self.state = SealState::Sealed;
}
/// Get the master key (panics if vault is sealed)
pub fn master_key(&self) -> Result<&MasterKey> {
self.master_key
.as_ref()
.ok_or_else(|| VaultError::crypto("Vault is sealed".to_string()))
}
/// Split master key into Shamir shares
fn split_into_shares(&self, key: &MasterKey) -> CryptoResult<Vec<Vec<u8>>> {
let sharks = Sharks(self.threshold);
let dealer = sharks.dealer(&key.key_data);
let shares: Vec<Vec<u8>> = dealer
.take(self.shares_count)
.map(|share| Vec::<u8>::from(&share))
.collect();
if shares.len() != self.shares_count {
return Err(CryptoError::InvalidAlgorithm(
"Failed to generate required number of shares".to_string(),
));
}
Ok(shares)
}
}
/// Result of vault initialization
#[derive(Debug, Serialize)]
pub struct SealInitResult {
pub shares: Vec<String>, // Hex-encoded shares
pub threshold: u8,
}
/// Storage structure for vault shares
#[derive(Debug, Serialize, Deserialize)]
struct ShareStorage {
shares: Vec<Vec<u8>>,
threshold: u8,
shares_count: u8,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_seal_mechanism_creation() {
let shamir_config = crate::config::ShamirSealConfig {
threshold: 3,
shares: 5,
};
let config = SealConfig {
seal_type: "shamir".to_string(),
shamir: shamir_config,
auto_unseal: Default::default(),
};
let seal = SealMechanism::new(&config).expect("Failed to create seal");
assert_eq!(seal.state(), SealState::Sealed);
assert!(seal.is_sealed());
}
#[test]
fn test_invalid_threshold() {
let shamir_config = crate::config::ShamirSealConfig {
threshold: 5,
shares: 3, // threshold > shares
};
let config = SealConfig {
seal_type: "shamir".to_string(),
shamir: shamir_config,
auto_unseal: Default::default(),
};
assert!(SealMechanism::new(&config).is_err());
}
#[test]
fn test_shamir_reconstruct() {
let shamir_config = crate::config::ShamirSealConfig {
threshold: 2,
shares: 3,
};
let config = SealConfig {
seal_type: "shamir".to_string(),
shamir: shamir_config,
auto_unseal: Default::default(),
};
let seal = SealMechanism::new(&config).expect("Failed to create seal");
let key = MasterKey {
key_data: vec![42u8; 32],
};
let shares = seal.split_into_shares(&key).expect("Failed to split");
assert_eq!(shares.len(), 3);
// Test reconstruction with threshold shares
let mut seal2 = SealMechanism::new(&config).expect("Failed to create seal");
let share_refs: Vec<&[u8]> = vec![&shares[0], &shares[1]];
seal2.unseal(&share_refs).expect("Failed to unseal");
assert!(!seal2.is_sealed());
assert_eq!(seal2.master_key().unwrap().key_data, key.key_data);
}
#[test]
fn test_seal_unseal_cycle() {
let shamir_config = crate::config::ShamirSealConfig {
threshold: 2,
shares: 3,
};
let config = SealConfig {
seal_type: "shamir".to_string(),
shamir: shamir_config,
auto_unseal: Default::default(),
};
let mut seal = SealMechanism::new(&config).expect("Failed to create seal");
// Initially sealed
assert!(seal.is_sealed());
// Seal again (should be no-op)
seal.seal();
assert!(seal.is_sealed());
}
}