diff --git a/artifacts/test_program_methods/private_pda_spender.bin b/artifacts/test_program_methods/private_pda_spender.bin index cc602ee4..ca1c8bd6 100644 Binary files a/artifacts/test_program_methods/private_pda_spender.bin and b/artifacts/test_program_methods/private_pda_spender.bin differ diff --git a/key_protocol/src/key_management/group_key_holder.rs b/key_protocol/src/key_management/group_key_holder.rs index 533906a1..3f77c531 100644 --- a/key_protocol/src/key_management/group_key_holder.rs +++ b/key_protocol/src/key_management/group_key_holder.rs @@ -322,13 +322,12 @@ mod tests { let account_id = AccountId::for_private_pda(&program_id, &seed, &npk); let expected_npk = NullifierPublicKey([ - 185, 161, 225, 224, 20, 156, 173, 0, 6, 173, 74, 136, 16, 88, 71, 154, 101, 160, 224, - 162, 247, 98, 183, 210, 118, 130, 143, 237, 20, 112, 111, 114, - ]); - 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, + 136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79, + 215, 76, 92, 60, 166, 104, 35, 51, 91, 16, 114, 188, 112, ]); + // 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!(account_id, expected_account_id); diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 3adea616..20bea342 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -52,6 +52,10 @@ pub struct NSSAUserData { /// keyed by `AccountId`. Each entry stores the group label and identifier needed /// to re-derive keys during sync. pub shared_private_accounts: BTreeMap, + /// 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, } impl NSSAUserData { @@ -112,6 +116,7 @@ impl NSSAUserData { private_key_tree, group_key_holders: BTreeMap::new(), shared_private_accounts: BTreeMap::new(), + sealing_secret_key: None, }) } diff --git a/wallet/src/cli/group.rs b/wallet/src/cli/group.rs index 0a1d8d54..f1d93b75 100644 --- a/wallet/src/cli/group.rs +++ b/wallet/src/cli/group.rs @@ -45,16 +45,17 @@ pub enum GroupSubcommand { 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, - /// 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 { @@ -156,11 +157,7 @@ impl WalletSubcommand for GroupSubcommand { Ok(SubcommandReturnValue::Empty) } - Self::Join { - name, - sealed, - account, - } => { + Self::Join { name, sealed } => { if wallet_core .storage() .user_data @@ -170,17 +167,14 @@ impl WalletSubcommand for GroupSubcommand { 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 account_id: nssa::AccountId = account.parse().context("Invalid account ID")?; - 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) + 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); @@ -189,6 +183,27 @@ impl WalletSubcommand for GroupSubcommand { 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) + } } } } diff --git a/wallet/src/config.rs b/wallet/src/config.rs index d8e186bd..79a4e3c9 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -110,6 +110,9 @@ pub struct PersistentStorage { nssa::AccountId, key_protocol::key_protocol_core::SharedAccountEntry, >, + /// Dedicated sealing secret key for GMS distribution. + #[serde(default)] + pub sealing_secret_key: Option, } impl PersistentStorage { diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 57416c55..bc53edc0 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -206,6 +206,7 @@ pub fn produce_data_for_storage( labels, group_key_holders: user_data.group_key_holders.clone(), shared_private_accounts: user_data.shared_private_accounts.clone(), + sealing_secret_key: user_data.sealing_secret_key, } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 1ff65ce9..307b253a 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -107,6 +107,7 @@ impl WalletCore { labels, group_key_holders, shared_private_accounts, + sealing_secret_key, } = PersistentStorage::from_path(&storage_path).with_context(|| { format!( "Failed to read persistent storage at {}", @@ -122,6 +123,7 @@ impl WalletCore { let mut store = WalletChainStore::new(config, persistent_accounts, labels)?; store.user_data.group_key_holders = group_key_holders; store.user_data.shared_private_accounts = shared_private_accounts; + store.user_data.sealing_secret_key = sealing_secret_key; Ok(store) }, last_synced_block, @@ -310,6 +312,11 @@ impl WalletCore { 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. pub fn remove_group_key_holder( &mut self,