From 27e2850b5caa6779b53f2fc47aa15f9099c238dd Mon Sep 17 00:00:00 2001 From: Moudy Date: Fri, 8 May 2026 23:59:08 +0200 Subject: [PATCH] refactor: make SealingPublicKey a newtype wrapper --- integration_tests/tests/shared_accounts.rs | 2 +- .../src/key_management/group_key_holder.rs | 43 ++++++++++++++----- wallet/src/cli/group.rs | 15 +++---- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index fddb9e6b..ecf3a4b4 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -102,7 +102,7 @@ async fn group_invite_join_key_agreement() -> Result<()> { .sealing_secret_key .context("Sealing key not found")?; let sealing_pk = - nssa_core::encryption::shared_key_derivation::Secp256k1Point::from_scalar(sealing_sk); + key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk); let holder = ctx .wallet() diff --git a/key_protocol/src/key_management/group_key_holder.rs b/key_protocol/src/key_management/group_key_holder.rs index 3f77c531..609c45ed 100644 --- a/key_protocol/src/key_management/group_key_holder.rs +++ b/key_protocol/src/key_management/group_key_holder.rs @@ -12,10 +12,30 @@ use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey}; /// Public key used to seal a `GroupKeyHolder` for distribution to a recipient. /// -/// Structurally identical to `ViewingPublicKey` (both are secp256k1 points), but given -/// a distinct alias to clarify intent: viewing keys encrypt account state, sealing keys -/// encrypt the GMS for off-chain distribution. -pub type SealingPublicKey = Secp256k1Point; +/// Wraps a secp256k1 point but is a distinct type from `ViewingPublicKey` to enforce +/// key separation: viewing keys encrypt account state, sealing keys encrypt the GMS +/// for off-chain distribution. +pub struct SealingPublicKey(Secp256k1Point); + +impl SealingPublicKey { + /// Derive the sealing public key from a secret scalar. + #[must_use] + pub fn from_scalar(scalar: Scalar) -> Self { + Self(Secp256k1Point::from_scalar(scalar)) + } + + /// Construct from raw serialized bytes (e.g. received from another wallet). + #[must_use] + pub const fn from_bytes(bytes: Vec) -> Self { + Self(Secp256k1Point(bytes)) + } + + /// Returns the raw bytes for display or transmission. + #[must_use] + pub fn to_bytes(&self) -> &[u8] { + &self.0.0 + } +} /// Secret key used to unseal a `GroupKeyHolder` received from another member. pub type SealingSecretKey = Scalar; @@ -144,7 +164,7 @@ impl GroupKeyHolder { let mut ephemeral_scalar: Scalar = [0_u8; 32]; OsRng.fill_bytes(&mut ephemeral_scalar); let ephemeral_pubkey = Secp256k1Point::from_scalar(ephemeral_scalar); - let shared = SharedSecretKey::new(&ephemeral_scalar, recipient_key); + let shared = SharedSecretKey::new(&ephemeral_scalar, &recipient_key.0); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -386,7 +406,7 @@ mod tests { let recipient_vpk = recipient_keys.generate_viewing_public_key(); let recipient_vsk = recipient_keys.viewing_secret_key; - let sealed = holder.seal_for(&recipient_vpk); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); @@ -417,7 +437,7 @@ mod tests { .produce_private_key_holder(None) .viewing_secret_key; - let sealed = holder.seal_for(&recipient_vpk); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -432,7 +452,7 @@ mod tests { let recipient_vpk = recipient_keys.generate_viewing_public_key(); let recipient_vsk = recipient_keys.viewing_secret_key; - let mut sealed = holder.seal_for(&recipient_vpk); + let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); // Flip a byte in the ciphertext portion (after ephemeral_pubkey + nonce) let last = sealed.len() - 1; sealed[last] ^= 0xFF; @@ -451,8 +471,9 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let sealed_a = holder.seal_for(&recipient_vpk); - let sealed_b = holder.seal_for(&recipient_vpk); + let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0); + let sealed_a = holder.seal_for(&sealing_key); + let sealed_b = holder.seal_for(&sealing_key); assert_ne!(sealed_a, sealed_b); } @@ -514,7 +535,7 @@ mod tests { let bob_vpk = bob_keys.generate_viewing_public_key(); let bob_vsk = bob_keys.viewing_secret_key; - let sealed = alice_holder.seal_for(&bob_vpk); + let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0)); let bob_holder = GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); diff --git a/wallet/src/cli/group.rs b/wallet/src/cli/group.rs index e7e7c136..e1dd9159 100644 --- a/wallet/src/cli/group.rs +++ b/wallet/src/cli/group.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result}; use clap::Subcommand; -use key_protocol::key_management::group_key_holder::GroupKeyHolder; +use key_protocol::key_management::group_key_holder::{GroupKeyHolder, SealingPublicKey}; use crate::{ WalletCore, @@ -99,8 +99,10 @@ impl WalletSubcommand for GroupSubcommand { .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 recipient_key = + key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes( + key_bytes, + ); let sealed = holder.seal_for(&recipient_key); println!("{}", hex::encode(&sealed)); @@ -141,16 +143,13 @@ impl WalletSubcommand for GroupSubcommand { 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, - ); + let public_key = SealingPublicKey::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!("Public key: {}", hex::encode(public_key.to_bytes())); println!("Share this public key with group members so they can seal GMS for you."); Ok(SubcommandReturnValue::Empty) }