diff --git a/vault/src/crypto.rs b/vault/src/crypto.rs index 04b93e1..081f195 100644 --- a/vault/src/crypto.rs +++ b/vault/src/crypto.rs @@ -11,8 +11,15 @@ use rand_core::{RngCore, OsRng as RandOsRng}; pub mod kdf { use super::*; - + /// Standard parameters for keyspace key derivation + pub const KEYSPACE_KEY_LENGTH: usize = 32; + pub const KEYSPACE_KEY_ITERATIONS: u32 = 10_000; + /// Derive a symmetric key for keyspace operations using standard parameters + /// Always uses PBKDF2 with SHA-256, 32 bytes output, and 10,000 iterations + pub fn keyspace_key(password: &[u8], salt: &[u8]) -> Vec { + derive_key_pbkdf2(password, salt, KEYSPACE_KEY_LENGTH, KEYSPACE_KEY_ITERATIONS) + } pub fn derive_key_pbkdf2(password: &[u8], salt: &[u8], key_len: usize, iterations: u32) -> Vec { let mut key = vec![0u8; key_len]; diff --git a/vault/src/lib.rs b/vault/src/lib.rs index 5b0c46f..f602ab3 100644 --- a/vault/src/lib.rs +++ b/vault/src/lib.rs @@ -68,7 +68,7 @@ impl Vault { .storage .get(name) .await - .map_err(|e| VaultError::Storage(format!("{e:?}")))? + .map_err(|e| VaultError::Storage(format!("{e:?}")))? .is_some() { debug!("keyspace '{}' already exists", name); @@ -84,7 +84,7 @@ impl Vault { debug!("salt: {:?}", salt); // 2. Derive key // Always use PBKDF2 for key derivation - let key = kdf::derive_key_pbkdf2(password, &salt, 32, 10_000); + let key = kdf::keyspace_key(password, &salt); debug!("derived key: {} bytes", key.len()); // 3. Prepare initial keyspace data let keyspace_data = KeyspaceData { keypairs: vec![] }; @@ -107,7 +107,7 @@ impl Vault { // 6. Compose metadata let metadata = KeyspaceMetadata { name: name.to_string(), - salt: salt.try_into().unwrap_or([0u8; 16]), + salt: salt.clone().try_into().unwrap_or([0u8; 16]), encrypted_blob, created_at: Some(crate::utils::now()), tags, @@ -125,6 +125,10 @@ impl Vault { .await .map_err(|e| VaultError::Storage(format!("{e:?}")))?; debug!("success"); + + // 8. Create default keypair, passing the salt we already have + self.create_default_keypair(name, password, &salt).await?; + Ok(()) } @@ -180,7 +184,7 @@ impl Vault { )); } // 2. Derive key - let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); + let key = kdf::keyspace_key(password, &metadata.salt); debug!("derived key: {} bytes", key.len()); let ciphertext = &metadata.encrypted_blob; @@ -213,6 +217,56 @@ impl Vault { // --- Keypair Management APIs --- + /// Create a default Ed25519 keypair for client identity + /// This keypair is deterministically generated from the password and salt + /// and will always be the first keypair in the keyspace + async fn create_default_keypair( + &mut self, + keyspace: &str, + password: &[u8], + salt: &[u8], + ) -> Result { + // 1. Derive a deterministic seed using standard PBKDF2 + let seed = kdf::keyspace_key(password, salt); + + // 2. Generate Ed25519 keypair from the seed + use ed25519_dalek::{SigningKey, VerifyingKey}; + + // Use the seed to create a deterministic keypair + let signing = SigningKey::from_bytes(seed.as_slice().try_into().unwrap()); + let verifying: VerifyingKey = (&signing).into(); + + let priv_bytes = signing.to_bytes().to_vec(); + let pub_bytes = verifying.to_bytes().to_vec(); + + // Create an ID for the default keypair + let id = hex::encode(&pub_bytes); + + // 3. Unlock the keyspace to get its data + let mut data = self.unlock_keyspace(keyspace, password).await?; + + // 4. Add to keypairs (as the first entry) + let entry = KeyEntry { + id: id.clone(), + key_type: KeyType::Ed25519, + private_key: priv_bytes, + public_key: pub_bytes, + metadata: Some(KeyMetadata { + name: Some("Default Identity".to_string()), + created_at: Some(crate::utils::now()), + tags: Some(vec!["default".to_string(), "identity".to_string()]), + }), + }; + + // Ensure it's the first keypair by inserting at index 0 + data.keypairs.insert(0, entry); + + // 5. Re-encrypt and store + self.save_keyspace(keyspace, password, &data).await?; + + Ok(id) + } + /// Add a new keypair to a keyspace (generates and stores a new keypair) /// Add a new keypair to a keyspace (generates and stores a new keypair) /// If key_type is None, defaults to Secp256k1. @@ -341,7 +395,7 @@ impl Vault { )); } // 2. Derive key - let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); + let key = kdf::keyspace_key(password, &metadata.salt); debug!("derived key: {} bytes", key.len()); // 3. Serialize plaintext let plaintext = match serde_json::to_vec(data) { @@ -500,7 +554,7 @@ impl Vault { hex::encode(&meta.salt) ); // 2. Derive key - let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); + let key = kdf::keyspace_key(password, &meta.salt); // 3. Generate nonce let nonce = random_salt(12); debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(&nonce)); @@ -547,7 +601,7 @@ impl Vault { hex::encode(&meta.salt) ); // 2. Derive key - let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); + let key = kdf::keyspace_key(password, &meta.salt); // 3. Extract nonce let nonce = &ciphertext[..12]; debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce)); diff --git a/vault/src/rhai_bindings.rs b/vault/src/rhai_bindings.rs index 0c64ce9..e8514fd 100644 --- a/vault/src/rhai_bindings.rs +++ b/vault/src/rhai_bindings.rs @@ -13,6 +13,7 @@ pub fn register_rhai_api>(); engine.register_fn("select_keypair", RhaiSessionManager::::select_keypair); + engine.register_fn("select_default_keypair", RhaiSessionManager::::select_default_keypair); engine.register_fn("sign", RhaiSessionManager::::sign); // No global constant registration: Rhai does not support this directly. // Scripts should receive the session manager as a parameter or via module scope. @@ -36,6 +37,11 @@ impl RhaiSessionMan // Use Mutex for interior mutability, &self is sufficient self.inner.lock().unwrap().select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}")) } + + pub fn select_default_keypair(&self) -> Result<(), String> { + self.inner.lock().unwrap().select_default_keypair() + .map_err(|e| format!("select_default_keypair error: {e}")) + } pub fn sign(&self, message: rhai::Blob) -> Result { let sm = self.inner.lock().unwrap(); // Try to get the current keyspace name from session state if possible diff --git a/vault/src/session.rs b/vault/src/session.rs index dfca9e7..22b30fa 100644 --- a/vault/src/session.rs +++ b/vault/src/session.rs @@ -125,6 +125,30 @@ impl SessionManager { self.unlocked_keyspace.is_some() } + /// Returns the default keypair (first keypair) for client identity, if any. + pub fn default_keypair(&self) -> Option<&KeyEntry> { + self.current_keyspace() + .and_then(|ks| ks.keypairs.first()) + } + + /// Selects the default keypair (first keypair) as the current keypair. + pub fn select_default_keypair(&mut self) -> Result<(), VaultError> { + let default_id = self + .default_keypair() + .map(|k| k.id.clone()) + .ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?; + + self.select_keypair(&default_id) + } + + /// Returns true if the current keypair is the default keypair (first keypair). + pub fn is_default_keypair_selected(&self) -> bool { + match (self.current_keypair(), self.default_keypair()) { + (Some(current), Some(default)) => current.id == default.id, + _ => false, + } + } + pub async fn sign(&self, message: &[u8]) -> Result, VaultError> { let (name, password, _) = self .unlocked_keyspace @@ -283,6 +307,30 @@ impl SessionManager { self.unlocked_keyspace.is_some() } + /// Returns the default keypair (first keypair) for client identity, if any. + pub fn default_keypair(&self) -> Option<&KeyEntry> { + self.current_keyspace() + .and_then(|ks| ks.keypairs.first()) + } + + /// Selects the default keypair (first keypair) as the current keypair. + pub fn select_default_keypair(&mut self) -> Result<(), VaultError> { + let default_id = self + .default_keypair() + .map(|k| k.id.clone()) + .ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?; + + self.select_keypair(&default_id) + } + + /// Returns true if the current keypair is the default keypair (first keypair). + pub fn is_default_keypair_selected(&self) -> bool { + match (self.current_keypair(), self.default_keypair()) { + (Some(current), Some(default)) => current.id == default.id, + _ => false, + } + } + pub async fn sign(&self, message: &[u8]) -> Result, VaultError> { let (name, password, _) = self .unlocked_keyspace