mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-05-12 12:49:36 +00:00
210 lines
7.3 KiB
Rust
210 lines
7.3 KiB
Rust
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<SubcommandReturnValue> {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|