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 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);

View File

@ -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<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 {
@ -112,6 +116,7 @@ impl NSSAUserData {
private_key_tree,
group_key_holders: BTreeMap::new(),
shared_private_accounts: BTreeMap::new(),
sealing_secret_key: None,
})
}

View File

@ -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)
}
}
}
}

View File

@ -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<nssa_core::encryption::Scalar>,
}
impl PersistentStorage {

View File

@ -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,
}
}

View File

@ -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,