195 lines
6.3 KiB
Rust
Raw Normal View History

2026-04-30 15:04:33 +02:00
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.
2026-04-30 15:04:33 +02:00
#[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.
2026-04-30 15:04:33 +02:00
#[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.
2026-04-30 15:04:33 +02:00
#[arg(long)]
key: String,
2026-04-30 15:04:33 +02:00
},
/// Unseal a received GMS and store it (join a group).
Join {
/// Human-readable name to store the group under.
name: String,
/// Sealed GMS as hex string (from the inviter).
#[arg(long)]
sealed: String,
/// Account ID whose viewing secret key to use for decryption.
2026-04-30 15:04:33 +02:00
#[arg(long)]
account: String,
},
}
impl WalletSubcommand for GroupSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::New { name } => {
if wallet_core
.storage()
.user_data
.group_key_holder(&name)
2026-04-30 15:04:33 +02:00
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let holder = GroupKeyHolder::new();
wallet_core.insert_group_key_holder(name.clone(), holder);
2026-04-30 15:04:33 +02:00
wallet_core.store_persistent_data().await?;
println!("Created group '{name}'");
2026-04-30 15:04:33 +02:00
Ok(SubcommandReturnValue::Empty)
}
Self::Import { name, gms } => {
2026-04-30 15:04:33 +02:00
if wallet_core
.storage()
.user_data
.group_key_holder(&name)
2026-04-30 15:04:33 +02:00
.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"))?;
2026-04-30 15:04:33 +02:00
let holder = GroupKeyHolder::from_gms(gms_bytes);
wallet_core.insert_group_key_holder(name.clone(), holder);
2026-04-30 15:04:33 +02:00
wallet_core.store_persistent_data().await?;
println!("Imported group '{name}'");
2026-04-30 15:04:33 +02:00
Ok(SubcommandReturnValue::Empty)
}
Self::Export { name } => {
let holder = wallet_core
.storage()
.user_data
.group_key_holder(&name)
2026-04-30 15:04:33 +02:00
.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}");
2026-04-30 15:04:33 +02:00
}
}
Ok(SubcommandReturnValue::Empty)
}
Self::Remove { name } => {
if wallet_core.remove_group_key_holder(&name).is_none() {
2026-04-30 15:04:33 +02:00
anyhow::bail!("Group '{name}' not found");
}
wallet_core.store_persistent_data().await?;
println!("Removed group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
Self::Invite { name, key } => {
2026-04-30 15:04:33 +02:00
let holder = wallet_core
.storage()
.user_data
.group_key_holder(&name)
2026-04-30 15:04:33 +02:00
.context(format!("Group '{name}' not found"))?;
let key_bytes = hex::decode(&key).context("Invalid key hex")?;
let recipient_key =
nssa_core::encryption::shared_key_derivation::Secp256k1Point(key_bytes);
2026-04-30 15:04:33 +02:00
let sealed = holder.seal_for(&recipient_key);
2026-04-30 15:04:33 +02:00
println!("{}", hex::encode(&sealed));
Ok(SubcommandReturnValue::Empty)
}
Self::Join {
name,
sealed,
account,
} => {
if wallet_core
.storage()
.user_data
.group_key_holder(&name)
2026-04-30 15:04:33 +02:00
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
let account_id: nssa::AccountId = account.parse().context("Invalid account ID")?;
let (keychain, _, _) = wallet_core
2026-04-30 15:04:33 +02:00
.storage()
.user_data
.get_private_account(account_id)
.context("Private account not found")?;
let vsk = keychain.private_key_holder.viewing_secret_key;
let holder = GroupKeyHolder::unseal(&sealed_bytes, &vsk)
.map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
wallet_core.insert_group_key_holder(name.clone(), holder);
2026-04-30 15:04:33 +02:00
wallet_core.store_persistent_data().await?;
println!("Joined group '{name}'");
2026-04-30 15:04:33 +02:00
Ok(SubcommandReturnValue::Empty)
}
}
}
}