feat: add default keypair creation and selection functionality
This commit is contained in:
		| @@ -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<u8> { | ||||
|         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<u8> { | ||||
|         let mut key = vec![0u8; key_len]; | ||||
|   | ||||
| @@ -68,7 +68,7 @@ impl<S: KVStore> Vault<S> { | ||||
|             .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<S: KVStore> Vault<S> { | ||||
|         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<S: KVStore> Vault<S> { | ||||
|         // 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<S: KVStore> Vault<S> { | ||||
|             .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<S: KVStore> Vault<S> { | ||||
|             )); | ||||
|         } | ||||
|         // 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<S: KVStore> Vault<S> { | ||||
|  | ||||
|     // --- 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<String, VaultError> { | ||||
|         // 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<S: KVStore> Vault<S> { | ||||
|             )); | ||||
|         } | ||||
|         // 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<S: KVStore> Vault<S> { | ||||
|             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<S: KVStore> Vault<S> { | ||||
|             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)); | ||||
|   | ||||
| @@ -13,6 +13,7 @@ pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'st | ||||
| ) { | ||||
|     engine.register_type::<RhaiSessionManager<S>>(); | ||||
|     engine.register_fn("select_keypair", RhaiSessionManager::<S>::select_keypair); | ||||
|     engine.register_fn("select_default_keypair", RhaiSessionManager::<S>::select_default_keypair); | ||||
|     engine.register_fn("sign", RhaiSessionManager::<S>::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<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> 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<rhai::Blob, String> { | ||||
|             let sm = self.inner.lock().unwrap(); | ||||
|             // Try to get the current keyspace name from session state if possible | ||||
|   | ||||
| @@ -125,6 +125,30 @@ impl<S: KVStore + Send + Sync> SessionManager<S> { | ||||
|         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<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
| @@ -283,6 +307,30 @@ impl<S: KVStore> SessionManager<S> { | ||||
|         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<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|   | ||||
		Reference in New Issue
	
	Block a user