use anyhow::{Context as _, Result}; use clap::Subcommand; use key_protocol::key_management::group_key_holder::GroupKeyHolder; use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, }; /// Group key management commands. #[derive(Subcommand, Debug, Clone)] pub enum GroupSubcommand { /// Create a new group with a fresh random GMS. New { /// Human-readable name for the group. name: String, }, /// Import a group from raw GMS bytes. Import { /// Human-readable name for the group. name: String, /// Raw GMS as 64-character hex string. #[arg(long)] gms: String, }, /// Export the raw GMS hex for backup or manual distribution. Export { /// Group name. name: String, }, /// List all groups. #[command(visible_alias = "ls")] List, /// Remove a group from the wallet. Remove { /// Group name. name: String, }, /// Seal the group's GMS for a recipient (invite). Invite { /// Group name. name: String, /// Recipient's sealing public key as hex string. #[arg(long)] key: String, }, /// Unseal a received GMS and store it (join a group). /// Uses the wallet's dedicated sealing key (generated via `new-sealing-key`). Join { /// Human-readable name to store the group under. name: String, /// Sealed GMS as hex string (from the inviter). #[arg(long)] sealed: String, }, /// Generate a dedicated sealing key pair for GMS distribution. /// Share the printed public key with group members so they can seal GMS for you. NewSealingKey, } impl WalletSubcommand for GroupSubcommand { async fn handle_subcommand( self, wallet_core: &mut WalletCore, ) -> Result { match self { Self::New { name } => { if wallet_core .storage() .user_data .group_key_holder(&name) .is_some() { anyhow::bail!("Group '{name}' already exists"); } let holder = GroupKeyHolder::new(); wallet_core.insert_group_key_holder(name.clone(), holder); wallet_core.store_persistent_data().await?; println!("Created group '{name}'"); Ok(SubcommandReturnValue::Empty) } Self::Import { name, gms } => { if wallet_core .storage() .user_data .group_key_holder(&name) .is_some() { anyhow::bail!("Group '{name}' already exists"); } let gms_bytes: [u8; 32] = hex::decode(&gms) .context("Invalid GMS hex")? .try_into() .map_err(|_err| anyhow::anyhow!("GMS must be exactly 32 bytes"))?; let holder = GroupKeyHolder::from_gms(gms_bytes); wallet_core.insert_group_key_holder(name.clone(), holder); wallet_core.store_persistent_data().await?; println!("Imported group '{name}'"); Ok(SubcommandReturnValue::Empty) } Self::Export { name } => { let holder = wallet_core .storage() .user_data .group_key_holder(&name) .context(format!("Group '{name}' not found"))?; let gms_hex = hex::encode(holder.dangerous_raw_gms()); println!("Group: {name}"); println!("GMS: {gms_hex}"); Ok(SubcommandReturnValue::Empty) } Self::List => { let holders = &wallet_core.storage().user_data.group_key_holders; if holders.is_empty() { println!("No groups found"); } else { for name in holders.keys() { println!("{name}"); } } Ok(SubcommandReturnValue::Empty) } Self::Remove { name } => { if wallet_core.remove_group_key_holder(&name).is_none() { anyhow::bail!("Group '{name}' not found"); } wallet_core.store_persistent_data().await?; println!("Removed group '{name}'"); Ok(SubcommandReturnValue::Empty) } Self::Invite { name, key } => { let holder = wallet_core .storage() .user_data .group_key_holder(&name) .context(format!("Group '{name}' not found"))?; let key_bytes = hex::decode(&key).context("Invalid key hex")?; let recipient_key: key_protocol::key_management::group_key_holder::SealingPublicKey = nssa_core::encryption::shared_key_derivation::Secp256k1Point(key_bytes); let sealed = holder.seal_for(&recipient_key); println!("{}", hex::encode(&sealed)); Ok(SubcommandReturnValue::Empty) } Self::Join { name, sealed } => { if wallet_core .storage() .user_data .group_key_holder(&name) .is_some() { anyhow::bail!("Group '{name}' already exists"); } let sealing_key = wallet_core.storage().user_data.sealing_secret_key.context( "No sealing key found. Run 'wallet group new-sealing-key' first.", )?; let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?; let holder = GroupKeyHolder::unseal(&sealed_bytes, &sealing_key) .map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?; wallet_core.insert_group_key_holder(name.clone(), holder); wallet_core.store_persistent_data().await?; println!("Joined group '{name}'"); Ok(SubcommandReturnValue::Empty) } Self::NewSealingKey => { if wallet_core.storage().user_data.sealing_secret_key.is_some() { anyhow::bail!("Sealing key already exists. Each wallet has one sealing key."); } let mut secret: nssa_core::encryption::Scalar = [0_u8; 32]; rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut secret); let public_key = nssa_core::encryption::shared_key_derivation::Secp256k1Point::from_scalar( secret, ); wallet_core.set_sealing_secret_key(secret); wallet_core.store_persistent_data().await?; println!("Sealing key generated."); println!("Public key: {}", hex::encode(&public_key.0)); println!("Share this public key with group members so they can seal GMS for you."); Ok(SubcommandReturnValue::Empty) } } } }