use anyhow::Result;
use base64::Engine;
use chacha20poly1305::{
ChaCha20Poly1305, Key, Nonce,
aead::{Aead, AeadCore, KeyInit},
};
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
use zeroize::Zeroizing;
pub struct KeyManager {
iterations: u32,
use_keyring: bool,
keyring_service: String,
}
impl KeyManager {
pub async fn new(iterations: u32, use_keyring: bool) -> Result<Self> {
Ok(Self {
iterations,
use_keyring,
keyring_service: "com.saorsalabs.communitas".to_string(),
})
}
pub async fn derive_key(&self, password: &str, salt: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
let password = password.to_string();
let salt = salt.to_vec();
let iterations = self.iterations;
tokio::task::spawn_blocking(move || {
let mut key = Zeroizing::new(vec![0u8; 32]);
pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, iterations, &mut key);
Ok(key) as Result<Zeroizing<Vec<u8>>>
})
.await
.map_err(|e| anyhow::anyhow!("Failed to derive key: {}", e))?
}
pub async fn hash_password(&self, password: &str) -> Result<Vec<u8>> {
let password = password.to_string();
tokio::task::spawn_blocking(move || {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(b"communitas:password:v1:");
hasher.update(password.as_bytes());
Ok(hasher.finalize().as_bytes().to_vec())
})
.await
.map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e))?
}
pub async fn store_in_keyring(&self, four_words: &str, key: &[u8]) -> Result<()> {
if !self.use_keyring {
return Ok(());
}
use keyring::Entry;
let entry = Entry::new(&self.keyring_service, four_words)
.map_err(|e| anyhow::anyhow!("Failed to create keyring entry: {}", e))?;
let key_b64 = base64::engine::general_purpose::STANDARD.encode(key);
entry
.set_password(&key_b64)
.map_err(|e| anyhow::anyhow!("Failed to store key in keyring: {}", e))?;
Ok(())
}
pub async fn get_from_keyring(&self, four_words: &str) -> Result<Zeroizing<Vec<u8>>> {
if !self.use_keyring {
return Err(anyhow::anyhow!("Keyring not enabled"));
}
use keyring::Entry;
let entry = Entry::new(&self.keyring_service, four_words)
.map_err(|e| anyhow::anyhow!("Failed to access keyring entry: {}", e))?;
let key_b64 = entry
.get_password()
.map_err(|e| anyhow::anyhow!("Failed to retrieve key from keyring: {}", e))?;
let key = Zeroizing::new(
base64::engine::general_purpose::STANDARD
.decode(&key_b64)
.map_err(|e| anyhow::anyhow!("Failed to decode key from keyring: {}", e))?,
);
Ok(key)
}
pub async fn delete_from_keyring(&self, four_words: &str) -> Result<()> {
if !self.use_keyring {
return Ok(());
}
use keyring::Entry;
let entry = Entry::new(&self.keyring_service, four_words)
.map_err(|e| anyhow::anyhow!("Failed to access keyring entry: {}", e))?;
entry
.delete_credential()
.map_err(|e| anyhow::anyhow!("Failed to delete key from keyring: {}", e))?;
Ok(())
}
pub async fn derive_subkey(&self, master_key: &[u8], context: &str) -> Result<Vec<u8>> {
use blake3::derive_key;
let context_string = format!("communitas:subkey:{}:v1", context);
let subkey = derive_key(&context_string, master_key);
Ok(subkey.to_vec())
}
pub fn generate_nonce() -> Nonce {
use rand::SeedableRng;
ChaCha20Poly1305::generate_nonce(&mut rand::rngs::StdRng::from_entropy())
}
pub fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
let cipher_key = Key::from(
*<&[u8; 32]>::try_from(key).map_err(|_| anyhow::anyhow!("Invalid key length"))?,
);
let cipher = ChaCha20Poly1305::new(&cipher_key);
let nonce = Self::generate_nonce();
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|e| anyhow::anyhow!("Encryption failed: {}", e))?;
let mut result = nonce.to_vec();
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn decrypt(&self, key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
if ciphertext_with_nonce.len() < 12 {
return Err(anyhow::anyhow!("Invalid ciphertext: too short"));
}
let (nonce_bytes, ciphertext) = ciphertext_with_nonce.split_at(12);
let nonce = Nonce::from(
*<&[u8; 12]>::try_from(nonce_bytes)
.map_err(|_| anyhow::anyhow!("Invalid nonce length"))?,
);
let cipher_key = Key::from(
*<&[u8; 32]>::try_from(key).map_err(|_| anyhow::anyhow!("Invalid key length"))?,
);
let cipher = ChaCha20Poly1305::new(&cipher_key);
let plaintext = cipher
.decrypt(&nonce, ciphertext)
.map_err(|e| anyhow::anyhow!("Decryption failed: {}", e))?;
Ok(Zeroizing::new(plaintext))
}
pub fn generate_salt() -> Vec<u8> {
use rand::{Rng, SeedableRng};
let mut salt = vec![0u8; 32];
rand::rngs::StdRng::from_entropy().fill(&mut salt[..]);
salt
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_key_derivation() {
let manager = KeyManager::new(1000, false).await.unwrap(); let salt = KeyManager::generate_salt();
let key1 = manager.derive_key("test_password", &salt).await.unwrap();
let key2 = manager.derive_key("test_password", &salt).await.unwrap();
assert_eq!(&*key1, &*key2);
let key3 = manager
.derive_key("different_password", &salt)
.await
.unwrap();
assert_ne!(&*key1, &*key3);
let salt2 = KeyManager::generate_salt();
let key4 = manager.derive_key("test_password", &salt2).await.unwrap();
assert_ne!(&*key1, &*key4);
}
#[tokio::test]
async fn test_encryption_decryption() {
let manager = KeyManager::new(1000, false).await.unwrap();
let key = vec![0u8; 32];
let plaintext = b"Hello, encrypted world!";
let ciphertext = manager.encrypt(&key, plaintext).unwrap();
assert_ne!(&ciphertext[12..], plaintext);
let decrypted = manager.decrypt(&key, &ciphertext).unwrap();
assert_eq!(&*decrypted, plaintext);
let wrong_key = vec![1u8; 32];
assert!(manager.decrypt(&wrong_key, &ciphertext).is_err());
}
#[tokio::test]
async fn test_password_hashing() {
let manager = KeyManager::new(1000, false).await.unwrap();
let hash1 = manager.hash_password("test_password").await.unwrap();
let hash2 = manager.hash_password("test_password").await.unwrap();
assert_eq!(hash1, hash2);
let hash3 = manager.hash_password("different_password").await.unwrap();
assert_ne!(hash1, hash3);
}
#[tokio::test]
async fn test_subkey_derivation() {
let manager = KeyManager::new(1000, false).await.unwrap();
let master_key = vec![0u8; 32];
let subkey1 = manager.derive_subkey(&master_key, "files").await.unwrap();
let subkey2 = manager
.derive_subkey(&master_key, "metadata")
.await
.unwrap();
assert_ne!(subkey1, subkey2);
let subkey3 = manager.derive_subkey(&master_key, "files").await.unwrap();
assert_eq!(subkey1, subkey3);
}
}