feat: add dedicated sealing key for GMS distribution

This commit is contained in:
Moudy 2026-05-08 08:19:55 +02:00
parent 4ace6e1570
commit 4e7963c655
7 changed files with 53 additions and 23 deletions

View File

@ -322,13 +322,12 @@ mod tests {
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk); let account_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let expected_npk = NullifierPublicKey([ let expected_npk = NullifierPublicKey([
185, 161, 225, 224, 20, 156, 173, 0, 6, 173, 74, 136, 16, 88, 71, 154, 101, 160, 224, 136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79,
162, 247, 98, 183, 210, 118, 130, 143, 237, 20, 112, 111, 114, 215, 76, 92, 60, 166, 104, 35, 51, 91, 16, 114, 188, 112,
]);
let expected_account_id = AccountId::new([
236, 138, 175, 184, 194, 233, 144, 109, 157, 51, 193, 120, 83, 110, 147, 90, 154, 57,
148, 236, 12, 92, 135, 38, 253, 79, 88, 143, 161, 175, 46, 144,
]); ]);
// AccountId is derived from (program_id, seed, npk), so it changes when npk changes.
// We verify npk is pinned, and AccountId is deterministically derived from it.
let expected_account_id = AccountId::for_private_pda(&program_id, &seed, &expected_npk);
assert_eq!(npk, expected_npk); assert_eq!(npk, expected_npk);
assert_eq!(account_id, expected_account_id); assert_eq!(account_id, expected_account_id);

View File

@ -52,6 +52,10 @@ pub struct NSSAUserData {
/// keyed by `AccountId`. Each entry stores the group label and identifier needed /// keyed by `AccountId`. Each entry stores the group label and identifier needed
/// to re-derive keys during sync. /// to re-derive keys during sync.
pub shared_private_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>, pub shared_private_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>,
/// Dedicated sealing secret key for GMS distribution. Generated once via
/// `wallet group new-sealing-key`. The corresponding public key is shared with
/// group members so they can seal GMS for this wallet.
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
} }
impl NSSAUserData { impl NSSAUserData {
@ -112,6 +116,7 @@ impl NSSAUserData {
private_key_tree, private_key_tree,
group_key_holders: BTreeMap::new(), group_key_holders: BTreeMap::new(),
shared_private_accounts: BTreeMap::new(), shared_private_accounts: BTreeMap::new(),
sealing_secret_key: None,
}) })
} }

View File

@ -45,16 +45,17 @@ pub enum GroupSubcommand {
key: String, key: String,
}, },
/// Unseal a received GMS and store it (join a group). /// Unseal a received GMS and store it (join a group).
/// Uses the wallet's dedicated sealing key (generated via `new-sealing-key`).
Join { Join {
/// Human-readable name to store the group under. /// Human-readable name to store the group under.
name: String, name: String,
/// Sealed GMS as hex string (from the inviter). /// Sealed GMS as hex string (from the inviter).
#[arg(long)] #[arg(long)]
sealed: String, sealed: String,
/// Account ID whose viewing secret key to use for decryption.
#[arg(long)]
account: 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 { impl WalletSubcommand for GroupSubcommand {
@ -156,11 +157,7 @@ impl WalletSubcommand for GroupSubcommand {
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
Self::Join { Self::Join { name, sealed } => {
name,
sealed,
account,
} => {
if wallet_core if wallet_core
.storage() .storage()
.user_data .user_data
@ -170,17 +167,14 @@ impl WalletSubcommand for GroupSubcommand {
anyhow::bail!("Group '{name}' already exists"); 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 sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
let account_id: nssa::AccountId = account.parse().context("Invalid account ID")?; let holder = GroupKeyHolder::unseal(&sealed_bytes, &sealing_key)
let (keychain, _, _) = wallet_core
.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:?}"))?; .map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
wallet_core.insert_group_key_holder(name.clone(), holder); wallet_core.insert_group_key_holder(name.clone(), holder);
@ -189,6 +183,27 @@ impl WalletSubcommand for GroupSubcommand {
println!("Joined group '{name}'"); println!("Joined group '{name}'");
Ok(SubcommandReturnValue::Empty) 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)
}
} }
} }
} }

View File

@ -110,6 +110,9 @@ pub struct PersistentStorage {
nssa::AccountId, nssa::AccountId,
key_protocol::key_protocol_core::SharedAccountEntry, key_protocol::key_protocol_core::SharedAccountEntry,
>, >,
/// Dedicated sealing secret key for GMS distribution.
#[serde(default)]
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
} }
impl PersistentStorage { impl PersistentStorage {

View File

@ -206,6 +206,7 @@ pub fn produce_data_for_storage(
labels, labels,
group_key_holders: user_data.group_key_holders.clone(), group_key_holders: user_data.group_key_holders.clone(),
shared_private_accounts: user_data.shared_private_accounts.clone(), shared_private_accounts: user_data.shared_private_accounts.clone(),
sealing_secret_key: user_data.sealing_secret_key,
} }
} }

View File

@ -107,6 +107,7 @@ impl WalletCore {
labels, labels,
group_key_holders, group_key_holders,
shared_private_accounts, shared_private_accounts,
sealing_secret_key,
} = PersistentStorage::from_path(&storage_path).with_context(|| { } = PersistentStorage::from_path(&storage_path).with_context(|| {
format!( format!(
"Failed to read persistent storage at {}", "Failed to read persistent storage at {}",
@ -122,6 +123,7 @@ impl WalletCore {
let mut store = WalletChainStore::new(config, persistent_accounts, labels)?; let mut store = WalletChainStore::new(config, persistent_accounts, labels)?;
store.user_data.group_key_holders = group_key_holders; store.user_data.group_key_holders = group_key_holders;
store.user_data.shared_private_accounts = shared_private_accounts; store.user_data.shared_private_accounts = shared_private_accounts;
store.user_data.sealing_secret_key = sealing_secret_key;
Ok(store) Ok(store)
}, },
last_synced_block, last_synced_block,
@ -310,6 +312,11 @@ impl WalletCore {
self.storage.user_data.insert_group_key_holder(name, holder); self.storage.user_data.insert_group_key_holder(name, holder);
} }
/// Set the wallet's dedicated sealing secret key.
pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) {
self.storage.user_data.sealing_secret_key = Some(key);
}
/// Remove a group key holder from storage. Returns the removed holder if it existed. /// Remove a group key holder from storage. Returns the removed holder if it existed.
pub fn remove_group_key_holder( pub fn remove_group_key_holder(
&mut self, &mut self,