diff --git a/Cargo.lock b/Cargo.lock index fcdab3fc..f0b0fa22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,6 +1943,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ + "getrandom 0.4.2", "hybrid-array", "rand_core 0.10.1", ] @@ -6418,6 +6419,7 @@ dependencies = [ "bytesize", "chacha20", "k256", + "ml-kem", "risc0-zkvm", "serde", "serde_json", diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 4d268b89..09246caf 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -7,7 +7,7 @@ use integration_tests::{ }; use log::info; use nssa::{AccountId, program::Program}; -use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; +use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey}; use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::cli::{ @@ -63,7 +63,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); - let to_vpk = Secp256k1Point::from_scalar(to_npk.0); + let to_vpk = ViewingPublicKey::from_seed(&to_npk.0, &[0u8; 32]); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: Some(format_private_account_id(from)), @@ -274,7 +274,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); - let to_vpk = Secp256k1Point::from_scalar(to_npk.0); + let to_vpk = ViewingPublicKey::from_seed(&to_npk.0, &[0u8; 32]); let from: AccountId = ctx.existing_public_accounts()[0]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index 518239e7..6ceb3efe 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -229,10 +229,10 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { // Fresh recipients — hardcoded npks not in any wallet. let recipient_npk_0 = NullifierPublicKey([0xAA; 32]); - let recipient_vpk_0 = ViewingPublicKey::from_scalar(recipient_npk_0.0); + let recipient_vpk_0 = ViewingPublicKey::from_seed(&recipient_npk_0.0, &[0u8; 32]); let recipient_npk_1 = NullifierPublicKey([0xBB; 32]); - let recipient_vpk_1 = ViewingPublicKey::from_scalar(recipient_npk_1.0); + let recipient_vpk_1 = ViewingPublicKey::from_seed(&recipient_npk_1.0, &[0u8; 32]); let amount_spend_0: u128 = 13; let amount_spend_1: u128 = 37; diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index c5d937e0..95b86d1d 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -101,9 +101,12 @@ async fn group_invite_join_key_agreement() -> Result<()> { .storage() .user_data .sealing_secret_key + .clone() .context("Sealing key not found")?; let sealing_pk = - key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk); + key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes( + nssa_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.r).0, + ); let holder = ctx .wallet() diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 1f132932..3a027277 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -209,8 +209,7 @@ pub async fn tps_test() -> Result<()> { fn build_privacy_transaction() -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); let sender_nsk = [1; 32]; - let sender_vsk = [99; 32]; - let sender_vpk = ViewingPublicKey::from_scalar(sender_vsk); + let sender_vpk = ViewingPublicKey::from_seed(&[99u8; 32], &[100u8; 32]); let sender_npk = NullifierPublicKey::from(&sender_nsk); let sender_pre = AccountWithMetadata::new( Account { @@ -223,8 +222,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { AccountId::for_regular_private_account(&sender_npk, 0), ); let recipient_nsk = [2; 32]; - let recipient_vsk = [99; 32]; - let recipient_vpk = ViewingPublicKey::from_scalar(recipient_vsk); + let recipient_vpk = ViewingPublicKey::from_seed(&[99u8; 32], &[100u8; 32]); let recipient_npk = NullifierPublicKey::from(&recipient_nsk); let recipient_pre = AccountWithMetadata::new( Account::default(), @@ -232,13 +230,13 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { AccountId::for_regular_private_account(&recipient_npk, 0), ); - let eph_holder_from = EphemeralKeyHolder::new(&sender_npk); - let sender_ss = eph_holder_from.calculate_shared_secret_sender(&sender_vpk); - let sender_epk = eph_holder_from.generate_ephemeral_public_key(); + let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk); + let sender_ss = eph_holder_from.calculate_shared_secret_sender(); + let sender_epk = eph_holder_from.ephemeral_public_key().clone(); - let eph_holder_to = EphemeralKeyHolder::new(&recipient_npk); - let recipient_ss = eph_holder_to.calculate_shared_secret_sender(&recipient_vpk); - let recipient_epk = eph_holder_from.generate_ephemeral_public_key(); + let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk); + let recipient_ss = eph_holder_to.calculate_shared_secret_sender(); + let recipient_epk = eph_holder_to.ephemeral_public_key().clone(); let balance_to_move: u128 = 1; let proof: MembershipProof = ( diff --git a/key_protocol/src/key_management/ephemeral_key_holder.rs b/key_protocol/src/key_management/ephemeral_key_holder.rs index 36600493..dc223390 100644 --- a/key_protocol/src/key_management/ephemeral_key_holder.rs +++ b/key_protocol/src/key_management/ephemeral_key_holder.rs @@ -1,54 +1,61 @@ use nssa_core::{ - NullifierPublicKey, SharedSecretKey, - encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey, - shared_key_derivation::Secp256k1Point}, + SharedSecretKey, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; -use rand::{RngCore as _, rngs::OsRng}; -use sha2::Digest as _; -#[derive(Debug)] -/// Ephemeral secret key holder. Non-clonable as intended for one-time use. Produces ephemeral -/// public keys. Can produce shared secret for sender. +/// Ephemeral key holder for the sender side of a KEM-based shared-secret exchange. +/// +/// Non-clonable as intended for one-time use: construction encapsulates once and +/// stores both the shared secret and the ciphertext (EphemeralPublicKey) that must +/// be sent to the receiver. pub struct EphemeralKeyHolder { - ephemeral_secret_key: EphemeralSecretKey, + shared_secret: SharedSecretKey, + ephemeral_public_key: EphemeralPublicKey, +} + +// Marvin-pq: SharedSecretKey does not implement Debug (intentional — leaking key material via +// debug output would be a security risk). We implement Debug manually here, redacting the +// shared secret while still allowing the ephemeral public key (KEM ciphertext) to be inspected. +impl std::fmt::Debug for EphemeralKeyHolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EphemeralKeyHolder") + .field("shared_secret", &"") + .field("ephemeral_public_key", &self.ephemeral_public_key) + .finish() + } } impl EphemeralKeyHolder { #[must_use] - pub fn new(receiver_nullifier_public_key: &NullifierPublicKey) -> Self { - let mut nonce_bytes = [0; 16]; - OsRng.fill_bytes(&mut nonce_bytes); - let mut hasher = sha2::Sha256::new(); - hasher.update(receiver_nullifier_public_key); - hasher.update(nonce_bytes); - + pub fn new(receiver_viewing_public_key: &ViewingPublicKey) -> Self { + let (shared_secret, ephemeral_public_key) = + SharedSecretKey::encapsulate(receiver_viewing_public_key); Self { - ephemeral_secret_key: hasher.finalize().into(), + shared_secret, + ephemeral_public_key, } } + /// Returns the KEM ciphertext to be transmitted to the receiver as the EphemeralPublicKey. #[must_use] - pub fn generate_ephemeral_public_key(&self) -> EphemeralPublicKey { - EphemeralPublicKey::from_scalar(self.ephemeral_secret_key) + pub fn ephemeral_public_key(&self) -> &EphemeralPublicKey { + &self.ephemeral_public_key } + /// Returns the sender-side shared secret (established at construction time). #[must_use] - pub fn calculate_shared_secret_sender( - &self, - _receiver_viewing_public_key: &ViewingPublicKey, - ) -> SharedSecretKey { - SharedSecretKey::new(&self.ephemeral_secret_key, &Secp256k1Point::from_scalar(self.ephemeral_secret_key)) + pub fn calculate_shared_secret_sender(&self) -> SharedSecretKey { + self.shared_secret } } +/// Encapsulates a fresh shared secret toward `vpk` and returns `(shared_secret, ciphertext)`. +/// +/// Used when the local side is acting as an "ephemeral receiver" — i.e. generating a +/// one-sided encryption that only the holder of the VSK can decrypt. #[must_use] pub fn produce_one_sided_shared_secret_receiver( - _vpk: &ViewingPublicKey, + vpk: &ViewingPublicKey, ) -> (SharedSecretKey, EphemeralPublicKey) { - let mut esk = [0; 32]; - OsRng.fill_bytes(&mut esk); - ( - SharedSecretKey::new(&esk, &Secp256k1Point::from_scalar(esk)), - EphemeralPublicKey::from_scalar(esk), - ) + SharedSecretKey::encapsulate(vpk) } diff --git a/key_protocol/src/key_management/group_key_holder.rs b/key_protocol/src/key_management/group_key_holder.rs index f345789d..7aec47bb 100644 --- a/key_protocol/src/key_management/group_key_holder.rs +++ b/key_protocol/src/key_management/group_key_holder.rs @@ -1,44 +1,39 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; use nssa_core::{ SharedSecretKey, - encryption::{Scalar, shared_key_derivation::Secp256k1Point}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{PdaSeed, ProgramId}, }; use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; -use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey}; +use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}; /// Public key used to seal a `GroupKeyHolder` for distribution to a recipient. /// -/// 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); +/// Wraps the ML-KEM-768 encapsulation key bytes (1184 bytes). Distinct from +/// `ViewingPublicKey` to enforce key separation: viewing keys encrypt account state, +/// sealing keys encrypt the GMS for off-chain distribution. +pub struct SealingPublicKey(Vec); 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). + /// Construct from raw serialized encapsulation-key bytes (e.g. received from another wallet). #[must_use] pub const fn from_bytes(bytes: Vec) -> Self { - Self(Secp256k1Point(bytes)) + Self(bytes) } /// Returns the raw bytes for display or transmission. #[must_use] pub fn to_bytes(&self) -> &[u8] { - &self.0.0 + &self.0 } } /// Secret key used to unseal a `GroupKeyHolder` received from another member. -pub type SealingSecretKey = Scalar; +/// Holds the two 32-byte FIPS 203 seed halves `d` and `r`. +pub type SealingSecretKey = ViewingSecretKey; /// Manages shared viewing keys for a group of controllers owning private PDAs. /// @@ -151,20 +146,25 @@ impl GroupKeyHolder { SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None) } + // Marvin-pq: seal_for/unseal switched from ECDH (Secp256k1) to ML-KEM-768. + // Wire format changed from: + // ephemeral_pubkey (33) || nonce (12) || ciphertext+tag (48) = 93 bytes + // to: + // kem_ciphertext (1088) || nonce (12) || ciphertext+tag (48) = 1148 bytes + // SealingSecretKey is now the FIPS 203 seed pair (d, r) = ViewingSecretKey. + /// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`]. /// - /// Uses an ephemeral ECDH key exchange to derive a shared secret, then AES-256-GCM - /// to encrypt the payload. The returned bytes are - /// `ephemeral_pubkey (33) || nonce (12) || ciphertext+tag (48)` = 93 bytes. + /// Uses ML-KEM-768 encapsulation to derive a shared secret, then AES-256-GCM to encrypt + /// the payload. The returned bytes are + /// `kem_ciphertext (1088) || nonce (12) || ciphertext+tag (48)` = 1148 bytes. /// - /// Each call generates a fresh ephemeral key, so two seals of the same holder produce + /// Each call generates a fresh KEM encapsulation, so two seals of the same holder produce /// different ciphertexts. #[must_use] pub fn seal_for(&self, recipient_key: &SealingPublicKey) -> Vec { - 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.0); + let vpk = ViewingPublicKey(recipient_key.0.clone()); + let (shared, kem_ct) = SharedSecretKey::encapsulate(&vpk); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -176,12 +176,12 @@ impl GroupKeyHolder { .encrypt(&nonce, self.gms.as_ref()) .expect("AES-GCM encryption should not fail with valid key/nonce"); - let capacity = 33_usize + let capacity = 1088_usize .checked_add(12) .and_then(|n| n.checked_add(ciphertext.len())) .expect("seal capacity overflow"); let mut out = Vec::with_capacity(capacity); - out.extend_from_slice(&ephemeral_pubkey.0); + out.extend_from_slice(&kem_ct.0); out.extend_from_slice(&nonce_bytes); out.extend_from_slice(&ciphertext); out @@ -189,20 +189,23 @@ impl GroupKeyHolder { /// Decrypts a sealed `GroupKeyHolder` using the recipient's [`SealingSecretKey`]. /// - /// Returns `Err` if the ciphertext is too short, the ECDH point is invalid, or the - /// AES-GCM authentication tag doesn't verify (wrong key or tampered data). + /// Returns `Err` if the ciphertext is too short or the AES-GCM authentication tag + /// doesn't verify (wrong key or tampered data). pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result { - const HEADER_LEN: usize = 33 + 12; + // Marvin-pq: kem_ciphertext (1088) + nonce (12) = header, then AES-GCM tag (16) minimum. + const KEM_CT_LEN: usize = 1088; + const HEADER_LEN: usize = KEM_CT_LEN + 12; const MIN_LEN: usize = HEADER_LEN + 16; + if sealed.len() < MIN_LEN { return Err(SealError::TooShort); } - // MIN_LEN (61) > HEADER_LEN (45), so all slicing below is in bounds. - let ephemeral_pubkey = Secp256k1Point(sealed[..33].to_vec()); - let nonce = aes_gcm::Nonce::from_slice(&sealed[33..HEADER_LEN]); + + let kem_ct = EphemeralPublicKey(sealed[..KEM_CT_LEN].to_vec()); + let nonce = aes_gcm::Nonce::from_slice(&sealed[KEM_CT_LEN..HEADER_LEN]); let ciphertext = &sealed[HEADER_LEN..]; - let shared = SharedSecretKey::new(own_key, &ephemeral_pubkey); + let shared = SharedSecretKey::decapsulate(&kem_ct, &own_key.d, &own_key.r); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -219,7 +222,7 @@ impl GroupKeyHolder { Ok(Self::from_gms(gms)) } - /// Derives an AES-256 key from the ECDH shared secret via SHA-256 with a domain prefix. + /// Derives an AES-256 key from the ML-KEM shared secret via SHA-256 with a domain prefix. fn seal_kdf(shared: &SharedSecretKey) -> [u8; 32] { const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeySeal/AES\x00\x00\x00\x00\x00\x00"; let mut hasher = sha2::Sha256::new(); @@ -325,8 +328,7 @@ mod tests { /// Pins the end-to-end derivation for a fixed (GMS, `ProgramId`, `PdaSeed`). Any change /// to `secret_spending_key_for_pda`, the `PrivateKeyHolder` nsk/npk chain, or the - /// `AccountId::for_private_pda` formula breaks this test. Mirrors the pinned-value - /// pattern from `for_private_pda_matches_pinned_value` in `nssa_core`. + /// `AccountId::for_private_pda` formula breaks this test. #[test] fn pinned_end_to_end_derivation_for_private_pda() { use nssa_core::{account::AccountId, program::ProgramId}; @@ -345,8 +347,6 @@ mod tests { 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, u128::MAX); @@ -354,10 +354,7 @@ mod tests { assert_eq!(account_id, expected_account_id); } - /// Wallets persist `GroupKeyHolder` to disk and reload it on startup. This test pins - /// the serde round-trip: serialize, deserialize, and assert the derived keys for a - /// sample seed match on both sides. A silent encoding drift would corrupt every - /// group-owned account. + /// Wallets persist `GroupKeyHolder` to disk and reload it on startup. #[test] fn gms_serde_round_trip_preserves_derivation() { let original = GroupKeyHolder::from_gms([7_u8; 32]); @@ -377,10 +374,7 @@ mod tests { } /// A `GroupKeyHolder` constructed from the same 32 bytes as a personal - /// `SecretSpendingKey` must not derive the same `NullifierPublicKey` as the personal - /// path, so a private PDA cannot be spent by a personal nullifier even under - /// adversarial key-material reuse. The safety rests on the group path's distinct - /// domain-separation prefix plus the seed mix-in (see `secret_spending_key_for_pda`). + /// `SecretSpendingKey` must not derive the same `NullifierPublicKey`. #[test] fn group_derivation_does_not_collide_with_personal_path_at_shared_bytes() { let shared_bytes = [13_u8; 32]; @@ -405,10 +399,10 @@ mod tests { let recipient_ssk = SecretSpendingKey([7_u8; 32]); let recipient_keys = recipient_ssk.produce_private_key_holder(None); let recipient_vpk = recipient_keys.generate_viewing_public_key(); - let _recipient_vsk = recipient_keys.viewing_secret_key; + let recipient_vsk = recipient_keys.viewing_secret_key.clone(); let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - let restored = GroupKeyHolder::unseal(&sealed, &Scalar::default()).expect("unseal"); + let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); @@ -433,13 +427,13 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let wrong_ssk = SecretSpendingKey([99_u8; 32]); - let _wrong_vsk = wrong_ssk + let wrong_vsk = SecretSpendingKey([99_u8; 32]) .produce_private_key_holder(None) - .viewing_secret_key; + .viewing_secret_key + .clone(); let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - let result = GroupKeyHolder::unseal(&sealed, &Scalar::default()); + let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -451,18 +445,18 @@ mod tests { let recipient_ssk = SecretSpendingKey([7_u8; 32]); let recipient_keys = recipient_ssk.produce_private_key_holder(None); let recipient_vpk = recipient_keys.generate_viewing_public_key(); - let _recipient_vsk = recipient_keys.viewing_secret_key; + let recipient_vsk = recipient_keys.viewing_secret_key.clone(); let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - // Flip a byte in the ciphertext portion (after ephemeral_pubkey + nonce) + // Flip a byte in the AES-GCM ciphertext portion (after KEM ciphertext + nonce). let last = sealed.len() - 1; sealed[last] ^= 0xFF; - let result = GroupKeyHolder::unseal(&sealed, &Scalar::default()); + let result = GroupKeyHolder::unseal(&sealed, &recipient_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } - /// Two seals of the same holder produce different ciphertexts (ephemeral randomness). + /// Two seals of the same holder produce different ciphertexts (KEM randomness). #[test] fn two_seals_produce_different_ciphertexts() { let holder = GroupKeyHolder::from_gms([42_u8; 32]); @@ -481,14 +475,15 @@ mod tests { /// Sealed payload is too short. #[test] fn unseal_too_short_fails() { - let vsk: SealingSecretKey = [7_u8; 32]; + let vsk = SealingSecretKey { + d: [7_u8; 32], + r: [0_u8; 32], + }; let result = GroupKeyHolder::unseal(&[0_u8; 10], &vsk); assert!(matches!(result, Err(super::SealError::TooShort))); } - /// Degenerate GMS values (all-zeros, all-ones, single-bit) must still produce valid, - /// non-zero, pairwise-distinct npks. Rules out accidental "if gms == default { return - /// default }" style shortcuts in the derivation. + /// Degenerate GMS values must still produce valid, non-zero, pairwise-distinct npks. #[test] fn degenerate_gms_produces_distinct_non_zero_keys() { let seed = PdaSeed::new([1; 32]); @@ -526,21 +521,18 @@ mod tests { let pda_seed = PdaSeed::new([42_u8; 32]); let program_id: nssa_core::program::ProgramId = [1; 8]; - // Derive Alice's keys let alice_keys = alice_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed); let alice_npk = alice_keys.generate_nullifier_public_key(); - // Seal GMS for Bob using Bob's viewing key, Bob unseals let bob_ssk = SecretSpendingKey([77_u8; 32]); let bob_keys = bob_ssk.produce_private_key_holder(None); let bob_vpk = bob_keys.generate_viewing_public_key(); - let _bob_vsk = bob_keys.viewing_secret_key; + let bob_vsk = bob_keys.viewing_secret_key.clone(); let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0)); let bob_holder = - GroupKeyHolder::unseal(&sealed, &Scalar::default()).expect("Bob should unseal the GMS"); + GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); - // Key agreement: both derive identical NPK and AccountId let bob_npk = bob_holder .derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed) .generate_nullifier_public_key(); diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index b0ddd1b5..af285ddd 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -1,5 +1,6 @@ use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; +use sha2::Digest as _; use crate::key_management::{ KeyChain, @@ -30,9 +31,7 @@ impl ChildKeysPrivate { .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(None); - let vsk = SecretSpendingKey::generate_viewing_secret_key( - ssk.generate_viewing_secret_seed_key(None), - ); + let vsk = ssk.generate_viewing_secret_seed_key(None); let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from(&vsk); @@ -57,10 +56,18 @@ impl ChildKeysPrivate { #[must_use] pub fn nth_child(&self, cci: u32) -> Self { - let mut input = vec![]; + let mut parent_hash = sha2::Sha256::new(); + parent_hash.update(b"LEE/keys"); + parent_hash.update([0u8; 16]); + parent_hash.update([9_u8]); + parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key); + parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d); + parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.r); + let parent_pt = parent_hash.finalize(); + let mut input = vec![]; input.extend_from_slice(b"LEE_seed_priv"); - input.extend_from_slice(&self.value.0.private_key_holder.nullifier_secret_key); + input.extend_from_slice(&parent_pt); #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] input.extend_from_slice(&cci.to_be_bytes()); @@ -76,9 +83,7 @@ impl ChildKeysPrivate { .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(Some(cci)); - let vsk = SecretSpendingKey::generate_viewing_secret_key( - ssk.generate_viewing_secret_seed_key(Some(cci)), - ); + let vsk = ssk.generate_viewing_secret_seed_key(Some(cci)); let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from(&vsk); @@ -125,7 +130,7 @@ mod tests { use nssa_core::{NullifierPublicKey, NullifierSecretKey}; use super::*; - use crate::key_management; + use crate::key_management::{self, secret_holders::ViewingSecretKey}; #[test] fn master_key_generation() { @@ -158,14 +163,88 @@ mod tests { 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, ]); + // Marvin-pq double check the d, r labels + let expected_vsk: ViewingSecretKey = ViewingSecretKey { + d: [ + 187, 143, 146, 12, 68, 148, 25, 203, 21, 92, 131, 2, 221, 81, 117, 62, 98, 194, + 159, 177, 102, 254, 236, 182, 76, 242, 116, 219, 17, 166, 99, 36, + ], + r: [ + 80, 97, 83, 209, 145, 99, 168, 99, 89, 29, 153, 236, 82, 99, 134, 114, 168, 19, + 223, 69, 34, 47, 76, 76, 15, 97, 245, 184, 25, 103, 251, 82, + ], + }; + + let expected_vpk: [u8; 1184] = [ + 127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220, + 150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181, + 118, 237, 25, 129, 51, 160, 189, 51, 99, 184, 57, 28, 121, 240, 236, 2, 170, 198, 26, + 91, 172, 110, 52, 32, 186, 35, 179, 202, 234, 249, 15, 242, 100, 198, 168, 163, 120, + 205, 118, 85, 195, 210, 187, 95, 150, 154, 8, 68, 165, 237, 87, 166, 101, 57, 4, 18, + 11, 122, 235, 180, 199, 154, 165, 158, 55, 136, 30, 237, 43, 167, 215, 68, 80, 102, 0, + 71, 90, 130, 206, 240, 215, 69, 199, 83, 7, 60, 184, 128, 230, 184, 61, 93, 201, 204, + 165, 104, 9, 127, 220, 52, 246, 217, 131, 251, 2, 170, 133, 6, 51, 40, 224, 101, 61, + 16, 135, 32, 182, 201, 68, 58, 171, 54, 161, 184, 243, 38, 106, 200, 251, 17, 172, 8, + 24, 73, 230, 55, 85, 20, 147, 222, 165, 200, 116, 135, 47, 20, 227, 56, 220, 64, 120, + 215, 245, 58, 86, 102, 149, 252, 193, 163, 160, 59, 82, 138, 249, 171, 1, 54, 199, 193, + 171, 85, 38, 64, 56, 121, 106, 84, 57, 252, 94, 147, 16, 191, 196, 104, 47, 129, 84, + 21, 252, 160, 81, 207, 184, 199, 3, 177, 74, 117, 115, 175, 138, 108, 36, 198, 5, 32, + 15, 218, 3, 20, 19, 15, 251, 209, 86, 128, 139, 148, 78, 10, 34, 144, 149, 74, 102, 48, + 59, 70, 124, 47, 193, 100, 26, 9, 104, 178, 102, 156, 199, 242, 101, 147, 161, 87, 27, + 234, 192, 204, 41, 36, 43, 83, 219, 15, 211, 66, 91, 76, 73, 13, 113, 155, 203, 193, + 160, 130, 84, 103, 47, 70, 100, 147, 169, 65, 119, 84, 121, 122, 161, 76, 203, 144, + 248, 145, 22, 8, 46, 121, 44, 77, 20, 149, 66, 179, 56, 149, 231, 98, 184, 9, 64, 14, + 67, 196, 34, 8, 123, 21, 80, 169, 168, 223, 230, 133, 0, 66, 159, 230, 69, 201, 205, + 169, 105, 196, 21, 71, 84, 70, 58, 165, 165, 134, 186, 232, 60, 70, 51, 57, 239, 74, + 174, 116, 234, 36, 178, 49, 42, 168, 250, 104, 141, 106, 0, 109, 52, 86, 104, 243, 62, + 214, 137, 48, 107, 2, 152, 206, 227, 175, 147, 236, 19, 113, 27, 191, 231, 235, 167, + 114, 104, 23, 126, 203, 94, 242, 149, 171, 115, 170, 89, 244, 58, 29, 176, 73, 203, 44, + 8, 32, 9, 226, 32, 78, 246, 38, 235, 149, 133, 25, 243, 47, 124, 180, 200, 211, 165, + 137, 56, 169, 117, 31, 244, 65, 91, 135, 146, 158, 20, 75, 102, 32, 65, 250, 103, 199, + 36, 48, 31, 155, 164, 191, 222, 85, 37, 66, 243, 17, 120, 104, 0, 228, 83, 200, 116, 6, + 199, 106, 236, 139, 246, 216, 152, 241, 211, 85, 106, 200, 44, 231, 240, 66, 3, 193, + 147, 16, 145, 65, 49, 33, 53, 247, 69, 47, 44, 113, 86, 117, 6, 20, 193, 183, 128, 178, + 181, 21, 251, 99, 39, 149, 210, 146, 106, 181, 186, 7, 36, 63, 186, 234, 191, 164, 193, + 162, 127, 250, 122, 189, 219, 21, 92, 48, 86, 209, 184, 99, 160, 201, 162, 145, 20, + 138, 154, 18, 37, 180, 209, 165, 165, 51, 187, 78, 193, 175, 135, 6, 55, 216, 178, 10, + 40, 246, 98, 128, 80, 14, 38, 69, 113, 123, 54, 94, 43, 50, 106, 167, 17, 77, 163, 148, + 117, 225, 9, 7, 253, 240, 157, 96, 103, 33, 100, 37, 37, 20, 53, 138, 234, 55, 45, 232, + 154, 9, 150, 192, 116, 36, 119, 106, 95, 119, 34, 220, 84, 174, 19, 227, 33, 209, 96, + 197, 148, 230, 197, 59, 117, 130, 7, 116, 11, 0, 197, 16, 249, 151, 31, 4, 64, 29, 165, + 247, 110, 176, 166, 4, 112, 136, 101, 208, 7, 179, 38, 183, 134, 58, 107, 207, 160, 38, + 159, 67, 112, 20, 225, 199, 179, 133, 117, 144, 54, 199, 15, 204, 80, 154, 116, 84, 88, + 109, 113, 5, 207, 226, 21, 62, 247, 122, 14, 156, 9, 8, 76, 26, 148, 67, 196, 128, 176, + 78, 51, 161, 151, 75, 248, 154, 31, 168, 9, 4, 3, 107, 222, 245, 178, 21, 84, 7, 25, + 155, 118, 97, 135, 63, 89, 233, 11, 207, 148, 155, 38, 106, 104, 102, 140, 104, 67, + 149, 20, 30, 196, 44, 197, 128, 34, 182, 80, 30, 32, 137, 34, 212, 164, 177, 164, 12, + 115, 41, 156, 111, 71, 230, 120, 111, 218, 25, 117, 218, 75, 167, 32, 37, 57, 50, 99, + 181, 203, 40, 105, 248, 150, 114, 121, 73, 127, 198, 191, 161, 44, 56, 213, 243, 71, 2, + 56, 192, 243, 107, 179, 27, 96, 21, 116, 169, 64, 15, 97, 166, 151, 200, 11, 40, 204, + 71, 168, 220, 9, 55, 43, 146, 244, 212, 166, 192, 180, 189, 237, 162, 42, 29, 33, 52, + 193, 4, 178, 157, 244, 28, 209, 44, 26, 36, 147, 126, 94, 164, 37, 47, 115, 38, 23, + 165, 96, 106, 140, 42, 69, 146, 194, 93, 71, 175, 49, 147, 32, 246, 97, 94, 41, 116, + 127, 174, 18, 16, 14, 163, 17, 180, 213, 203, 166, 33, 139, 214, 18, 170, 27, 41, 59, + 175, 200, 101, 14, 128, 45, 179, 167, 136, 232, 138, 56, 124, 145, 75, 233, 132, 161, + 196, 164, 72, 80, 60, 187, 38, 90, 90, 17, 66, 134, 59, 2, 165, 29, 76, 24, 38, 211, + 177, 83, 119, 20, 239, 59, 77, 34, 3, 42, 47, 60, 89, 46, 103, 168, 120, 17, 199, 50, + 17, 103, 107, 48, 8, 53, 220, 159, 212, 65, 198, 80, 8, 11, 235, 97, 203, 196, 240, 44, + 56, 121, 77, 91, 196, 160, 129, 242, 149, 226, 57, 106, 180, 76, 161, 203, 18, 37, 166, + 153, 44, 40, 28, 74, 8, 11, 6, 166, 54, 10, 103, 247, 23, 35, 7, 47, 173, 133, 71, 85, + 3, 168, 250, 120, 126, 174, 37, 80, 128, 107, 7, 161, 130, 155, 136, 92, 48, 215, 119, + 196, 124, 85, 157, 234, 2, 166, 137, 65, 121, 222, 112, 47, 17, 43, 23, 111, 88, 5, + 195, 41, 8, 191, 227, 21, 173, 35, 199, 196, 188, 162, 191, 195, 204, 137, 54, 16, 73, + 178, 150, 249, 234, 22, 216, 123, 157, 144, 218, 118, 53, 193, 67, 65, 84, 162, 244, + 165, 24, 110, 246, 146, 228, 212, 180, 150, 116, 201, 37, 128, 76, 41, 188, 42, 79, + 148, 52, 196, 176, 178, 224, 48, 168, 13, 129, 193, 131, 185, 131, 93, 40, 145, 56, + 180, 29, 153, 83, 39, 69, 232, 96, 238, 137, 104, 150, 2, 202, 239, 149, 248, 154, 115, + 115, 127, 3, 8, 32, 61, 96, 66, 25, 181, 14, 72, 73, 97, 186, 134, 140, 33, 69, 33, 74, + ]; assert!(expected_ssk == keys.value.0.secret_spending_key); assert!(expected_ccc == keys.ccc); assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key); assert!(expected_npk == keys.value.0.nullifier_public_key); - // vsk is now a 64-byte ML-KEM seed; vpk is a 1184-byte encapsulation key — byte - // vectors are asserted non-empty rather than against the old EC point values. - // assert!(!keys.value.0.private_key_holder.viewing_secret_key.0.is_empty()); - // assert!(!keys.value.0.viewing_public_key.0.is_empty()); + assert!(expected_vsk == keys.value.0.private_key_holder.viewing_secret_key); + assert!(expected_vpk == keys.value.0.viewing_public_key.to_bytes()); } #[test] @@ -177,21 +256,112 @@ mod tests { 114, 39, 38, 118, 197, 205, 225, ]; + // Marvin-pq this test currently fails let root_node = ChildKeysPrivate::root(seed); let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32); - let expected_nsk: NullifierSecretKey = [ - 124, 61, 40, 92, 33, 135, 3, 41, 200, 234, 3, 69, 102, 184, 57, 191, 106, 151, 194, - 192, 103, 132, 141, 112, 249, 108, 192, 117, 24, 48, 70, 216, - ]; - let expected_npk = nssa_core::NullifierPublicKey([ - 116, 231, 246, 189, 145, 240, 37, 59, 219, 223, 216, 246, 116, 171, 223, 55, 197, 200, - 134, 192, 221, 40, 218, 167, 239, 5, 11, 95, 147, 247, 162, 226, + let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([ + 215, 207, 70, 52, 161, 220, 88, 88, 241, 149, 81, 130, 217, 214, 252, 170, 51, 232, + 230, 158, 195, 173, 174, 37, 27, 101, 49, 35, 79, 13, 44, 225, ]); + let expected_ccc = [ + 113, 136, 96, 232, 12, 136, 185, 254, 36, 103, 64, 44, 238, 176, 240, 92, 219, 184, + 143, 35, 183, 54, 170, 15, 126, 56, 115, 21, 89, 142, 236, 217, + ]; + + let expected_nsk: NullifierSecretKey = [ + 27, 167, 3, 140, 113, 16, 209, 83, 21, 77, 65, 91, 26, 191, 203, 102, 66, 140, 157, + 220, 101, 104, 227, 135, 216, 215, 216, 126, 194, 196, 43, 34, + ]; + let expected_npk = nssa_core::NullifierPublicKey([ + 30, 208, 29, 96, 156, 95, 79, 16, 182, 0, 10, 194, 209, 90, 35, 177, 110, 224, 247, 67, + 219, 114, 113, 16, 42, 27, 220, 96, 151, 124, 8, 65, + ]); + + // Marvin-pq double check the d, r labels + let expected_vsk: ViewingSecretKey = ViewingSecretKey { + d: [ + 81, 154, 68, 152, 72, 163, 82, 17, 125, 156, 193, 135, 129, 93, 227, 55, 224, 104, + 119, 232, 13, 101, 241, 20, 175, 72, 192, 186, 176, 246, 140, 211, + ], + r: [ + 31, 40, 109, 41, 185, 61, 173, 79, 102, 171, 158, 245, 232, 71, 57, 157, 142, 117, + 184, 235, 216, 71, 55, 44, 33, 156, 167, 133, 184, 92, 47, 174, + ], + }; + + let expected_vpk: [u8; 1184] = [ + 67, 150, 145, 133, 41, 124, 194, 102, 104, 131, 195, 8, 168, 170, 200, 40, 210, 84, 85, + 117, 50, 99, 52, 23, 144, 23, 22, 140, 187, 76, 49, 224, 189, 64, 249, 72, 219, 35, 49, + 162, 146, 121, 27, 179, 183, 215, 84, 177, 62, 37, 103, 97, 209, 201, 8, 162, 38, 109, + 87, 44, 103, 136, 112, 236, 120, 60, 235, 130, 60, 212, 209, 77, 77, 220, 28, 156, 34, + 7, 31, 35, 179, 102, 21, 54, 77, 99, 157, 210, 247, 151, 214, 182, 30, 57, 219, 40, 42, + 188, 32, 30, 134, 126, 7, 22, 51, 241, 152, 8, 96, 5, 87, 168, 64, 62, 81, 247, 33, + 228, 44, 180, 203, 60, 49, 66, 247, 143, 113, 106, 189, 44, 11, 182, 213, 247, 9, 22, + 3, 208, 125, 2, 8, 103, 195, 202, 21, 33, 72, 139, 233, 19, 171, 172, 69, 253, 212, 37, + 197, 66, 165, 207, 168, 69, 18, 24, 1, 100, 200, 175, 163, 247, 115, 17, 124, 84, 183, + 92, 96, 142, 204, 149, 2, 90, 53, 110, 246, 188, 135, 240, 160, 231, 145, 23, 90, 209, + 93, 166, 17, 119, 240, 49, 67, 234, 41, 187, 71, 23, 152, 159, 54, 206, 207, 26, 11, + 32, 134, 202, 185, 201, 25, 59, 199, 182, 18, 236, 175, 254, 227, 195, 98, 52, 139, + 162, 172, 195, 102, 178, 115, 59, 113, 108, 96, 89, 175, 145, 71, 202, 231, 153, 69, 3, + 25, 60, 43, 215, 35, 70, 119, 16, 235, 98, 184, 252, 50, 36, 161, 244, 57, 13, 214, + 115, 106, 225, 166, 7, 59, 44, 130, 197, 85, 69, 220, 81, 10, 1, 130, 227, 225, 47, 78, + 251, 49, 232, 55, 2, 66, 64, 180, 220, 65, 140, 231, 188, 172, 153, 153, 152, 15, 186, + 74, 6, 39, 16, 251, 216, 165, 145, 134, 3, 88, 131, 80, 114, 156, 119, 72, 130, 54, + 159, 202, 23, 7, 130, 127, 156, 252, 113, 108, 85, 22, 120, 104, 12, 151, 187, 102, 64, + 96, 137, 184, 68, 201, 20, 196, 196, 226, 220, 139, 174, 76, 109, 1, 179, 81, 156, 26, + 136, 238, 106, 41, 197, 18, 16, 179, 91, 9, 8, 213, 123, 108, 58, 3, 102, 12, 87, 92, + 217, 207, 166, 131, 17, 218, 134, 170, 27, 129, 145, 0, 65, 85, 99, 163, 97, 78, 228, + 15, 54, 85, 201, 58, 204, 160, 250, 66, 41, 36, 165, 78, 50, 137, 78, 197, 103, 57, 79, + 26, 14, 167, 104, 165, 129, 128, 90, 104, 148, 121, 135, 24, 126, 139, 235, 84, 183, + 165, 115, 111, 83, 48, 184, 55, 84, 250, 115, 171, 195, 91, 114, 213, 104, 51, 110, 86, + 148, 37, 139, 83, 49, 165, 171, 144, 90, 19, 91, 195, 111, 82, 185, 133, 211, 24, 186, + 48, 230, 172, 190, 6, 65, 230, 26, 139, 8, 9, 34, 54, 28, 103, 84, 116, 38, 252, 105, + 86, 123, 40, 31, 39, 64, 14, 253, 215, 147, 182, 218, 111, 148, 2, 18, 3, 197, 4, 129, + 107, 136, 89, 122, 56, 47, 9, 179, 66, 227, 24, 193, 32, 4, 172, 210, 29, 152, 114, + 134, 65, 249, 201, 178, 16, 206, 209, 39, 193, 109, 91, 122, 194, 26, 206, 37, 227, 55, + 160, 214, 85, 196, 64, 97, 96, 66, 80, 34, 177, 83, 200, 44, 137, 175, 149, 114, 42, + 229, 168, 248, 96, 106, 110, 182, 155, 62, 27, 179, 229, 139, 9, 213, 181, 116, 59, + 118, 142, 91, 23, 165, 80, 43, 118, 18, 41, 143, 125, 59, 102, 61, 224, 120, 186, 10, + 63, 119, 241, 168, 196, 87, 117, 138, 3, 151, 1, 129, 76, 154, 87, 200, 114, 124, 90, + 212, 182, 54, 94, 20, 165, 243, 88, 77, 76, 152, 69, 19, 164, 106, 196, 204, 46, 239, + 116, 42, 179, 65, 79, 39, 145, 63, 169, 199, 142, 6, 103, 118, 130, 49, 184, 208, 203, + 36, 162, 216, 9, 188, 17, 86, 45, 35, 20, 178, 218, 121, 164, 243, 145, 57, 208, 130, + 26, 27, 28, 100, 161, 148, 195, 54, 66, 114, 108, 146, 135, 66, 69, 232, 33, 197, 213, + 131, 107, 31, 19, 162, 155, 164, 161, 103, 8, 192, 127, 188, 196, 252, 2, 155, 18, 130, + 105, 53, 235, 200, 87, 203, 162, 95, 50, 158, 96, 210, 1, 45, 8, 26, 3, 192, 201, 182, + 148, 192, 157, 106, 5, 161, 248, 66, 89, 56, 141, 126, 243, 143, 68, 90, 133, 193, 181, + 198, 3, 169, 72, 66, 215, 195, 38, 37, 196, 103, 229, 89, 162, 210, 118, 12, 233, 162, + 95, 164, 107, 97, 11, 120, 255, 164, 60, 117, 37, 108, 144, 185, 167, 40, 124, 69, 23, + 37, 148, 222, 233, 43, 50, 16, 58, 53, 252, 8, 102, 88, 109, 28, 18, 22, 5, 49, 66, + 149, 114, 203, 95, 216, 175, 10, 87, 206, 46, 9, 101, 212, 226, 84, 4, 231, 161, 106, + 185, 31, 6, 101, 27, 54, 49, 85, 54, 84, 12, 250, 4, 49, 184, 134, 186, 23, 146, 54, + 90, 186, 134, 129, 68, 10, 241, 201, 65, 251, 69, 110, 127, 220, 148, 38, 250, 148, 83, + 32, 100, 131, 83, 133, 195, 54, 132, 63, 229, 85, 34, 172, 126, 68, 99, 197, 18, 197, + 91, 221, 234, 66, 203, 156, 73, 46, 0, 54, 205, 11, 52, 172, 114, 193, 127, 171, 134, + 109, 92, 37, 124, 181, 167, 191, 209, 148, 232, 26, 136, 230, 133, 181, 248, 117, 11, + 45, 156, 136, 117, 144, 126, 239, 230, 144, 90, 57, 109, 158, 167, 19, 131, 215, 136, + 85, 136, 10, 49, 9, 146, 64, 81, 28, 171, 53, 78, 40, 225, 94, 238, 70, 174, 125, 186, + 155, 177, 202, 157, 63, 39, 152, 44, 105, 184, 140, 179, 204, 32, 210, 109, 35, 150, + 194, 14, 98, 148, 176, 73, 185, 49, 135, 135, 244, 151, 147, 17, 103, 35, 242, 130, 3, + 158, 198, 152, 83, 240, 198, 254, 145, 181, 67, 163, 14, 237, 249, 179, 252, 220, 67, + 239, 7, 118, 131, 229, 137, 172, 151, 57, 121, 138, 204, 6, 208, 52, 168, 236, 123, + 104, 68, 36, 141, 25, 168, 56, 199, 40, 200, 52, 97, 59, 55, 184, 196, 234, 204, 108, + 75, 65, 177, 82, 207, 127, 128, 157, 0, 68, 163, 127, 152, 85, 123, 209, 163, 21, 119, + 62, 250, 236, 58, 229, 220, 99, 209, 147, 10, 177, 115, 172, 96, 192, 80, 240, 66, 191, + 138, 91, 52, 200, 132, 126, 255, 69, 98, 12, 140, 8, 158, 2, 153, 66, 211, 74, 242, + 147, 148, 209, 6, 161, 76, 149, 158, 209, 163, 20, 76, 75, 192, 193, 162, 71, 134, 72, + 160, 192, 10, 203, 4, 213, 23, 140, 196, 39, 231, 39, 16, 209, 228, 112, 244, 29, 27, + 181, 190, 19, 134, 116, 173, 135, 190, 118, 4, 214, 194, 189, 224, 164, 91, 211, 182, + 162, 226, + ]; + + assert!(expected_ssk == child_node.value.0.secret_spending_key); + assert!(expected_ccc == child_node.ccc); assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key); assert!(expected_npk == child_node.value.0.nullifier_public_key); - // assert!(!child_node.value.0.private_key_holder.viewing_secret_key.0.is_empty()); - // assert!(!child_node.value.0.viewing_public_key.0.is_empty()); + assert!(expected_vsk == child_node.value.0.private_key_holder.viewing_secret_key); + assert!(expected_vpk == child_node.value.0.viewing_public_key.to_bytes()); } } diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index 676a90aa..58655578 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -1,6 +1,6 @@ use nssa_core::{ NullifierPublicKey, SharedSecretKey, - encryption::{EphemeralPublicKey, ViewingPublicKey, Scalar}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use secret_holders::{PrivateKeyHolder, SecretSpendingKey, SeedHolder}; use serde::{Deserialize, Serialize}; @@ -71,19 +71,14 @@ impl KeyChain { ephemeral_public_key_sender: &EphemeralPublicKey, _index: Option, ) -> SharedSecretKey { - SharedSecretKey::new( - &Scalar::default(), - ephemeral_public_key_sender, - ) + let vsk = &self.private_key_holder.viewing_secret_key; + SharedSecretKey::decapsulate(ephemeral_public_key_sender, &vsk.d, &vsk.r) } } #[cfg(test)] mod tests { - use aes_gcm::aead::OsRng; use base58::ToBase58 as _; - use k256::{AffinePoint, elliptic_curve::group::GroupEncoding as _}; - use rand::RngCore as _; use super::*; use crate::key_management::{ @@ -106,14 +101,10 @@ mod tests { fn calculate_shared_secret_receiver() { let account_id_key_holder = KeyChain::new_os_random(); - // Generate a random ephemeral public key sender - let mut scalar = [0; 32]; - OsRng.fill_bytes(&mut scalar); - let ephemeral_public_key_sender = EphemeralPublicKey::from_scalar(scalar); + // Create a proper KEM ciphertext by encapsulating toward this key chain's VPK. + let (_, epk) = SharedSecretKey::encapsulate(&account_id_key_holder.viewing_public_key); - // Calculate shared secret - let _shared_secret = account_id_key_holder - .calculate_shared_secret_receiver(&ephemeral_public_key_sender, None); + let _shared_secret = account_id_key_holder.calculate_shared_secret_receiver(&epk, None); } #[test] @@ -135,12 +126,6 @@ mod tests { println!("======Prerequisites======"); println!(); - println!( - "Group generator {:?}", - hex::encode(AffinePoint::GENERATOR.to_bytes()) - ); - println!(); - println!("======Holders======"); println!(); @@ -188,13 +173,11 @@ mod tests { fn non_trivial_chain_index() { let keys = account_with_chain_index_2_for_tests(); - let eph_key_holder = EphemeralKeyHolder::new(&keys.nullifier_public_key); + let eph_key_holder = EphemeralKeyHolder::new(&keys.viewing_public_key); - let key_sender = eph_key_holder.calculate_shared_secret_sender(&keys.viewing_public_key); - let key_receiver = keys.calculate_shared_secret_receiver( - &eph_key_holder.generate_ephemeral_public_key(), - Some(2), - ); + let key_sender = eph_key_holder.calculate_shared_secret_sender(); + let key_receiver = + keys.calculate_shared_secret_receiver(eph_key_holder.ephemeral_public_key(), Some(2)); assert_eq!(key_sender.0, key_receiver.0); } diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 7fae51cb..49592b75 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -1,10 +1,7 @@ use bip39::Mnemonic; use common::HashType; use ml_kem; -use nssa_core::{ - NullifierPublicKey, NullifierSecretKey, - encryption::ViewingPublicKey, -}; +use nssa_core::{NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey}; use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -120,7 +117,7 @@ impl SecretSpendingKey { #[must_use] #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] - pub fn generate_viewing_secret_seed_key(&self, index: Option) -> [u8; 64] { + pub fn generate_viewing_secret_seed_key(&self, index: Option) -> ViewingSecretKey { const PREFIX: &[u8; 8] = b"LEE/keys"; const SUFFIX_1: &[u8; 1] = &[2]; const SUFFIX_2: &[u8; 19] = &[0; 19]; @@ -136,9 +133,20 @@ impl SecretSpendingKey { bytes.extend_from_slice(SUFFIX_1); bytes.extend_from_slice(&index.to_be_bytes()); bytes.extend_from_slice(SUFFIX_2); - let bytes: [u8; 64] = bytes.try_into().expect("`generate_viewing_secret_key`: bytes must be exactly 64"); + let bytes: [u8; 64] = bytes + .try_into() + .expect("`generate_viewing_secret_key`: bytes must be exactly 64"); - hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed") + let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed"); + + ViewingSecretKey { + d: *full_seed + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + r: *full_seed + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"), + } } #[must_use] @@ -153,9 +161,7 @@ impl SecretSpendingKey { pub fn produce_private_key_holder(&self, index: Option) -> PrivateKeyHolder { PrivateKeyHolder { nullifier_secret_key: self.generate_nullifier_secret_key(index), - viewing_secret_key: Self::generate_viewing_secret_key( - self.generate_viewing_secret_seed_key(index), - ), + viewing_secret_key: self.generate_viewing_secret_seed_key(index), } } } @@ -211,10 +217,8 @@ mod tests { assert_eq!(seed_holder.seed.len(), 64); let top_secret_key_holder = seed_holder.produce_top_secret_key_holder(); - - let _vsk = SecretSpendingKey::generate_viewing_secret_key( - top_secret_key_holder.generate_viewing_secret_seed_key(None), - ); + // Marvin-pq should drop seed from the fucntion name + let _vsk = top_secret_key_holder.generate_viewing_secret_seed_key(None); } #[test] diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index e973655e..d4747d39 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -55,7 +55,7 @@ pub struct NSSAUserData { /// 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, + pub sealing_secret_key: Option, } impl NSSAUserData { diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index d9e80af4..3a26e99d 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -17,6 +17,7 @@ bytemuck.workspace = true bytesize.workspace = true base58.workspace = true k256 = { workspace = true, optional = true } +ml-kem = { workspace = true, optional = true, features = ["getrandom"] } chacha20 = { version = "0.10" } [dev-dependencies] @@ -24,4 +25,4 @@ serde_json.workspace = true [features] default = [] -host = ["dep:k256"] +host = ["dep:k256", "dep:ml-kem"] diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs index ac9317c2..354169a8 100644 --- a/nssa/core/src/encoding.rs +++ b/nssa/core/src/encoding.rs @@ -7,7 +7,7 @@ use std::io::Read as _; #[cfg(feature = "host")] use crate::Nullifier; #[cfg(feature = "host")] -use crate::encryption::shared_key_derivation::Secp256k1Point; +use crate::encryption::{EphemeralPublicKey, shared_key_derivation::Secp256k1Point}; #[cfg(feature = "host")] use crate::error::NssaCoreError; use crate::{ @@ -173,6 +173,26 @@ impl Secp256k1Point { } } +// Marvin-pq: EphemeralPublicKey is now the ML-KEM-768 ciphertext (1088 bytes) produced by +// SharedSecretKey::encapsulate. It replaces the old Secp256k1Point (33 bytes) on the wire. +// Fixed size: 1088 bytes for ML-KEM-768 (EncodedUSize + EncodedVSize per FIPS 203 §7.2). +#[cfg(feature = "host")] +impl EphemeralPublicKey { + /// Serializes the ML-KEM-768 ciphertext to bytes (always 1088 bytes). + #[must_use] + pub fn to_bytes(&self) -> Vec { + self.0.clone() + } + + /// Deserializes an ML-KEM-768 ciphertext from a cursor. + /// Reads exactly 1088 bytes — the fixed ciphertext size for ML-KEM-768. + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let mut value = vec![0u8; 1088]; + cursor.read_exact(&mut value)?; + Ok(Self(value)) + } +} + impl AccountId { #[must_use] pub const fn to_bytes(&self) -> [u8; 32] { diff --git a/nssa/core/src/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index 4b675d0e..0c44fef6 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -6,7 +6,7 @@ use chacha20::{ use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] -pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}; +pub use shared_key_derivation::{EphemeralPublicKey, ViewingPublicKey}; use crate::{Commitment, account::Account, program::PrivateAccountKind}; #[cfg(feature = "host")] diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs index d1022399..44607ea5 100644 --- a/nssa/core/src/encryption/shared_key_derivation.rs +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -7,16 +7,16 @@ use std::fmt::Write as _; use borsh::{BorshDeserialize, BorshSerialize}; use k256::{ - AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint, - elliptic_curve::{ - PrimeField as _, - sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, - }, + AffinePoint, FieldBytes, ProjectivePoint, + elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}, }; +use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed}; use serde::{Deserialize, Serialize}; use crate::{SharedSecretKey, encryption::Scalar}; +/// A compressed secp256k1 point (33 bytes). +/// Kept for backward compatibility with Phase-2+ callers; no longer used as `EphemeralPublicKey`. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Secp256k1Point(pub Vec); @@ -44,10 +44,12 @@ impl Secp256k1Point { } } -pub type EphemeralSecretKey = Scalar; -pub type EphemeralPublicKey = Secp256k1Point; +/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the +/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct EphemeralPublicKey(pub Vec); -/// ML-KEM 768 encapsulation key bytes (1184 bytes, opaque to this crate). +/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate). #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct ViewingPublicKey(pub Vec); @@ -56,35 +58,153 @@ impl ViewingPublicKey { pub fn to_bytes(&self) -> &[u8] { &self.0 } -} -impl From<&EphemeralSecretKey> for EphemeralPublicKey { - fn from(value: &EphemeralSecretKey) -> Self { - Self::from_scalar(*value) + /// Derive the ML-KEM-768 encapsulation key from the FIPS 203 seed halves `d` and `r`. + /// Allows any crate to construct a VPK from raw seed bytes without importing + /// `key_protocol::ViewingSecretKey`. + #[must_use] + pub fn from_seed(d: &[u8; 32], r: &[u8; 32]) -> Self { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(d); + seed[32..].copy_from_slice(r); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + Self(dk.encapsulation_key().to_bytes().to_vec()) } } impl SharedSecretKey { - /// Creates a new shared secret key from a scalar and a point. + /// Sender: encapsulate a fresh shared secret toward `vpk`. + /// + /// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction + /// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via + /// [`decapsulate`][Self::decapsulate]. #[must_use] - pub fn new(_scalar: &[u8; 32], _point: &Secp256k1Point) -> Self { - let scalar = &Scalar::default(); - let point = &Secp256k1Point::from_scalar(*scalar); + pub fn encapsulate(vpk: &ViewingPublicKey) -> (Self, EphemeralPublicKey) { + let ek_bytes: ml_kem::kem::Key = vpk + .0 + .as_slice() + .try_into() + .expect("ViewingPublicKey must be 1184 bytes (ML-KEM-768 encapsulation key)"); + let ek = ml_kem::EncapsulationKey768::new(&ek_bytes) + .expect("ViewingPublicKey bytes must encode a valid ML-KEM-768 encapsulation key"); + let (ct, ss) = ek.encapsulate(); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) + } - let scalar = k256::Scalar::from_repr((*scalar).into()).unwrap(); - let point: [u8; 33] = point.0.clone().try_into().unwrap(); + /// Sender: deterministically encapsulate a shared secret toward `vpk`. + /// + /// The KEM randomness is derived as `SHA-256(message_hash || output_index_le)`, + /// making the ciphertext reproducible from the same `(vpk, message_hash, output_index)` + /// triple. Use a distinct `output_index` for each private account output in the same + /// transaction to ensure per-output EPK uniqueness. + #[must_use] + pub fn encapsulate_deterministic( + vpk: &ViewingPublicKey, + message_hash: &[u8; 32], + output_index: u32, + ) -> (Self, EphemeralPublicKey) { + use risc0_zkvm::sha::{Impl, Sha256 as _}; - let encoded = EncodedPoint::from_bytes(point).unwrap(); - let pubkey_affine = AffinePoint::from_encoded_point(&encoded).unwrap(); + let mut input = Vec::with_capacity(36); + input.extend_from_slice(message_hash); + input.extend_from_slice(&output_index.to_le_bytes()); + let hash = Impl::hash_bytes(&input); + let m: ml_kem::B32 = ml_kem::array::Array::try_from(hash.as_bytes() as &[u8]) + .expect("SHA-256 output is 32 bytes"); - let shared = ProjectivePoint::from(pubkey_affine) * scalar; - let shared_affine = shared.to_affine(); + let ek_bytes: ml_kem::kem::Key = vpk + .0 + .as_slice() + .try_into() + .expect("ViewingPublicKey must be 1184 bytes (ML-KEM-768 encapsulation key)"); + let ek = ml_kem::EncapsulationKey768::new(&ek_bytes) + .expect("ViewingPublicKey bytes must encode a valid ML-KEM-768 encapsulation key"); + let (ct, ss) = ek.encapsulate_deterministic(&m); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) + } - let shared_affine_encoded = shared_affine.to_encoded_point(false); - let x_bytes_slice = shared_affine_encoded.x().unwrap(); - let mut x_bytes = [0_u8; 32]; - x_bytes.copy_from_slice(x_bytes_slice); - - Self(x_bytes) + /// Receiver: decapsulate the shared secret from a KEM ciphertext. + /// + /// `d` and `r` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed. + #[must_use] + pub fn decapsulate(ciphertext: &EphemeralPublicKey, d: &[u8; 32], r: &[u8; 32]) -> Self { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(d); + seed[32..].copy_from_slice(r); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + let ss = dk + .decapsulate_slice(&ciphertext.0) + .expect("EphemeralPublicKey must be 1088 bytes (ML-KEM-768 ciphertext)"); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + Self(ss_bytes) + } +} + +#[cfg(test)] +mod tests { + use ml_kem::KeyExport as _; + + use super::*; + + #[test] + fn encapsulate_decapsulate_round_trip() { + let d = [1u8; 32]; + let r = [2u8; 32]; + + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d); + seed[32..].copy_from_slice(&r); + + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + let ek_bytes = dk.encapsulation_key().to_bytes(); + let vpk = ViewingPublicKey(ek_bytes.to_vec()); + + let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk); + let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &r); + + assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match"); + assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes"); + assert_eq!( + vpk.0.len(), + 1184, + "ML-KEM-768 encapsulation key is 1184 bytes" + ); + } + + #[test] + fn different_vpks_produce_different_shared_secrets() { + let (d1, r1) = ([1u8; 32], [2u8; 32]); + let (d2, r2) = ([3u8; 32], [4u8; 32]); + + let vpk1 = { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d1); + seed[32..].copy_from_slice(&r1); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec()) + }; + let vpk2 = { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d2); + seed[32..].copy_from_slice(&r2); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec()) + }; + + let (ss1, _) = SharedSecretKey::encapsulate(&vpk1); + let (ss2, _) = SharedSecretKey::encapsulate(&vpk2); + + assert_ne!(ss1.0, ss2.0); } } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 09e37664..27cfd9b4 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -243,8 +243,8 @@ mod tests { let expected_sender_pre = sender.clone(); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 0).0; let (output, proof) = execute_and_prove( vec![sender, recipient], @@ -337,11 +337,11 @@ mod tests { Commitment::new(&recipient_account_id, &expected_private_account_2), ]; - let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk()); + let shared_secret_1 = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0).0; - let esk_2 = [5; 32]; - let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.vpk()); + let shared_secret_2 = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 1).0; let (output, proof) = execute_and_prove( vec![sender_pre, recipient], @@ -412,8 +412,8 @@ mod tests { )) .unwrap(); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0u8; 32], 0).0; let program_with_deps = ProgramWithDependencies::new( validity_window_chain_caller, @@ -443,7 +443,8 @@ mod tests { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -480,7 +481,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret_pda = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; // PDA (new, mask 3) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); @@ -518,7 +520,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret_pda = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); @@ -572,7 +575,8 @@ mod tests { let shared_keys = test_private_account_keys_1(); let shared_npk = shared_keys.npk(); let shared_identifier: u128 = 42; - let shared_secret = SharedSecretKey::new(&[55; 32], &shared_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0u8; 32], 0).0; // Sender: public account with balance, owned by auth-transfer let sender_id = AccountId::new([99; 32]); @@ -619,7 +623,7 @@ mod tests { let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let pre = AccountWithMetadata::new(Account::default(), true, account_id); @@ -648,7 +652,7 @@ mod tests { let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let sender = AccountWithMetadata::new( Account { @@ -690,7 +694,7 @@ mod tests { let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let account = Account { program_owner: program.id(), @@ -736,7 +740,7 @@ mod tests { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let auth_transfer_id = auth_transfer.id(); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); @@ -790,7 +794,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -816,7 +821,7 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let auth_transfer_id = auth_transfer.id(); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index d86273d8..97643cd1 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -143,7 +143,7 @@ pub mod tests { Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::ViewingPublicKey, program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -246,13 +246,11 @@ pub mod tests { #[test] fn encrypted_account_data_constructor() { let npk = NullifierPublicKey::from(&[1; 32]); - let vpk = ViewingPublicKey::from_scalar([2; 32]); + let vpk = ViewingPublicKey::from_seed(&[2u8; 32], &[3u8; 32]); let account = Account::default(); let account_id = nssa_core::account::AccountId::for_regular_private_account(&npk, 0); let commitment = Commitment::new(&account_id, &account); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &vpk); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0u8; 32], 0); let ciphertext = EncryptionScheme::encrypt( &account, &PrivateAccountKind::Regular(0), diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 8cf1d68e..4ab7ff87 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -365,7 +365,7 @@ pub mod tests { BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, - encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, + encryption::ViewingPublicKey, program::{ BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId, TimestampValidityWindow, WrappedBalanceSum, @@ -478,7 +478,8 @@ pub mod tests { pub struct TestPrivateKeys { pub nsk: NullifierSecretKey, - pub vsk: Scalar, + pub d: [u8; 32], + pub r: [u8; 32], } impl TestPrivateKeys { @@ -487,7 +488,7 @@ pub mod tests { } pub fn vpk(&self) -> ViewingPublicKey { - ViewingPublicKey::from_scalar(self.vsk) + ViewingPublicKey::from_seed(&self.d, &self.r) } } @@ -1265,14 +1266,16 @@ pub mod tests { pub fn test_private_account_keys_1() -> TestPrivateKeys { TestPrivateKeys { nsk: [13; 32], - vsk: [31; 32], + d: [31; 32], + r: [32; 32], } } pub fn test_private_account_keys_2() -> TestPrivateKeys { TestPrivateKeys { nsk: [38; 32], - vsk: [83; 32], + d: [83; 32], + r: [84; 32], } } @@ -1293,9 +1296,8 @@ pub mod tests { let recipient = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 0); let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], @@ -1342,13 +1344,11 @@ pub mod tests { let recipient_pre = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk()); - let epk_1 = EphemeralPublicKey::from_scalar(esk_1); + let (shared_secret_1, epk_1) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0); - let esk_2 = [3; 32]; - let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.vpk()); - let epk_2 = EphemeralPublicKey::from_scalar(esk_2); + let (shared_secret_2, epk_2) = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 1); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], @@ -1409,9 +1409,8 @@ pub mod tests { *recipient_account_id, ); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], @@ -1916,14 +1915,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, nsk: recipient_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -1962,14 +1971,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2008,14 +2027,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2054,14 +2083,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2100,14 +2139,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2144,14 +2193,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2170,7 +2229,8 @@ pub mod tests { let program = Program::simple_balance_transfer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2211,7 +2271,8 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2247,7 +2308,8 @@ pub mod tests { let npk_a = keys_a.npk(); let npk_b = keys_b.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys_b.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0u8; 32], 0).0; // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in @@ -2281,7 +2343,8 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([77; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2319,7 +2382,8 @@ pub mod tests { let npk = keys.npk(); let claim_seed = PdaSeed::new([77; 32]); let wrong_delegated_seed = PdaSeed::new([88; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2356,8 +2420,8 @@ pub mod tests { let keys_a = test_private_account_keys_1(); let keys_b = test_private_account_keys_2(); let seed = PdaSeed::new([55; 32]); - let shared_a = SharedSecretKey::new(&[66; 32], &keys_a.vpk()); - let shared_b = SharedSecretKey::new(&[77; 32], &keys_b.vpk()); + let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0u8; 32], 0).0; + let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0u8; 32], 0).0; let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX); let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX); @@ -2402,7 +2466,8 @@ pub mod tests { let program = Program::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0; let seed = PdaSeed::new([99; 32]); // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = @@ -2501,7 +2566,8 @@ pub mod tests { (&sender_keys.npk(), 0), ); - let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0).0; let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), @@ -2841,9 +2907,8 @@ pub mod tests { AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id); - let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0); let (output, proof) = execute_and_prove( vec![sender_pre, recipient_pre], @@ -2942,13 +3007,11 @@ pub mod tests { None, ); - let from_esk = [3; 32]; - let from_ss = SharedSecretKey::new(&from_esk, &from_keys.vpk()); - let from_epk = EphemeralPublicKey::from_scalar(from_esk); + let (from_ss, from_epk) = + SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0u8; 32], 0); - let to_esk = [3; 32]; - let to_ss = SharedSecretKey::new(&to_esk, &to_keys.vpk()); - let to_epk = EphemeralPublicKey::from_scalar(to_esk); + let (to_ss, to_epk) = + SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0u8; 32], 1); let mut dependencies = HashMap::new(); @@ -3245,9 +3308,8 @@ pub mod tests { let program = Program::authenticated_transfer_program(); // Set up parameters for the new account - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0); // Balance to initialize the account with (0 for a new account) let balance: u128 = 0; @@ -3298,9 +3360,8 @@ pub mod tests { AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0)); let program = Program::claimer(); - let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0); let (output, proof) = execute_and_prove( vec![unauthorized_account], @@ -3348,9 +3409,8 @@ pub mod tests { let claimer_program = Program::claimer(); // Set up parameters for claiming the new account - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0); let balance: u128 = 0; @@ -3398,8 +3458,8 @@ pub mod tests { }; let noop_program = Program::noop(); - let esk2 = [4; 32]; - let shared_secret2 = SharedSecretKey::new(&esk2, &private_keys.vpk()); + let shared_secret2 = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0).0; // Step 3: Try to execute noop program with authentication but without initialization let res = execute_and_prove( @@ -3483,7 +3543,8 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3509,7 +3570,8 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3555,8 +3617,8 @@ pub mod tests { let balance_to_transfer = 10_u128; let instruction = (balance_to_transfer, auth_transfers.id()); - let recipient_esk = [3; 32]; - let recipient = SharedSecretKey::new(&recipient_esk, &recipient_keys.vpk()); + let recipient = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 0).0; let mut dependencies = HashMap::new(); dependencies.insert(auth_transfers.id(), auth_transfers); @@ -3712,9 +3774,8 @@ pub mod tests { let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0u8; 32], 0); let instruction = ( block_validity_window, @@ -3782,9 +3843,8 @@ pub mod tests { let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0u8; 32], 0); let instruction = ( BlockValidityWindow::new_unbounded(), @@ -4338,8 +4398,10 @@ pub mod tests { ..Account::default() }; - let alice_shared_0 = SharedSecretKey::new(&[10; 32], &alice_keys.vpk()); - let alice_shared_1 = SharedSecretKey::new(&[11; 32], &alice_keys.vpk()); + let (alice_shared_0, alice_epk_0) = + SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0u8; 32], 0); + let (alice_shared_1, alice_epk_1) = + SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0u8; 32], 1); // Fund alice_pda_0 { @@ -4365,11 +4427,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![funder_id], vec![funder_nonce], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([10; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())], output, ) .unwrap(); @@ -4407,11 +4465,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![funder_id], vec![funder_nonce], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([11; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())], output, ) .unwrap(); @@ -4457,11 +4511,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![recipient_id], vec![Nonce(0)], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([10; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())], output, ) .unwrap(); @@ -4501,11 +4551,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![recipient_id], vec![], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([11; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())], output, ) .unwrap(); diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index df9aa87c..2a9ee5bf 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -1043,14 +1043,13 @@ mod tests { use nssa_core::{ InputAccountIdentity, SharedSecretKey, account::AccountWithMetadata, - encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use testnet_initial_state::PrivateAccountPublicInitialData; let nsk: nssa_core::NullifierSecretKey = [7; 32]; let npk = nssa_core::NullifierPublicKey::from(&nsk); - let vsk: EphemeralSecretKey = [8; 32]; - let vpk = ViewingPublicKey::from_scalar(vsk); + let vpk = ViewingPublicKey::from_seed(&[8u8; 32], &[9u8; 32]); let genesis_account = Account { program_owner: Program::authenticated_transfer_program().id(), @@ -1068,9 +1067,7 @@ mod tests { SequencerCoreWithMockClients::start_from_config(config).await; // Attempt to re-initialize the same genesis account via a privacy-preserving transaction - let esk = [9; 32]; - let shared_secret = SharedSecretKey::new(&esk, &vpk); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0u8; 32], 0); let (output, proof) = execute_and_prove( vec![AccountWithMetadata::new( diff --git a/testnet_initial_state/src/lib.rs b/testnet_initial_state/src/lib.rs index b5c91d4d..7902f6d2 100644 --- a/testnet_initial_state/src/lib.rs +++ b/testnet_initial_state/src/lib.rs @@ -4,7 +4,8 @@ use key_protocol::key_management::{ secret_holders::{PrivateKeyHolder, SecretSpendingKey}, }; use nssa::{Account, AccountId, Data, PrivateKey, PublicKey, V03State}; -use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; +use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey}; +use key_protocol::key_management::secret_holders::ViewingSecretKey; use serde::{Deserialize, Serialize}; const PRIVATE_KEY_PUB_ACC_A: [u8; 32] = [ @@ -47,15 +48,7 @@ const VSK_PRIV_ACC_B: [u8; 32] = [ 154, 161, 34, 208, 74, 27, 1, 119, 13, 88, 128, ]; -const VPK_PRIV_ACC_A: [u8; 33] = [ - 2, 210, 206, 38, 213, 4, 182, 198, 220, 47, 93, 148, 61, 84, 148, 250, 158, 45, 8, 81, 48, 80, - 46, 230, 87, 210, 47, 204, 76, 58, 214, 167, 81, -]; -const VPK_PRIV_ACC_B: [u8; 33] = [ - 2, 79, 110, 46, 203, 29, 206, 205, 18, 86, 27, 189, 104, 103, 113, 181, 110, 53, 78, 172, 11, - 171, 190, 18, 126, 214, 81, 77, 192, 154, 58, 195, 238, -]; const NPK_PRIV_ACC_A: [u8; 32] = [ 167, 108, 50, 153, 74, 47, 151, 188, 140, 79, 195, 31, 181, 9, 40, 167, 201, 32, 175, 129, 45, @@ -134,20 +127,20 @@ pub fn initial_priv_accounts_private_keys() -> Vec Result { - if self.viewing_public_key_len == 33 { + if self.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts(self.viewing_public_key, self.viewing_public_key_len) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok(nssa_core::encryption::ViewingPublicKey(slice.to_vec())) } else { Err(WalletFfiError::InvalidKeyValue) } diff --git a/wallet/src/cli/group.rs b/wallet/src/cli/group.rs index e1dd9159..66ee056f 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, SealingPublicKey}; +use key_protocol::key_management::{group_key_holder::{GroupKeyHolder, SealingPublicKey}, secret_holders::ViewingSecretKey}; use crate::{ WalletCore, @@ -120,7 +120,7 @@ impl WalletSubcommand for GroupSubcommand { } let sealing_key = - wallet_core.storage().user_data.sealing_secret_key.context( + wallet_core.storage().user_data.sealing_secret_key.clone().context( "No sealing key found. Run 'wallet group new-sealing-key' first.", )?; @@ -141,9 +141,13 @@ impl WalletSubcommand for GroupSubcommand { 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 = SealingPublicKey::from_scalar(secret); + let mut d = [0_u8; 32]; + let mut r = [0_u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d); + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut r); + let secret = ViewingSecretKey { d, r }; + let ek_bytes = nssa_core::encryption::ViewingPublicKey::from_seed(&d, &r).0; + let public_key = SealingPublicKey::from_bytes(ek_bytes); wallet_core.set_sealing_secret_key(secret); wallet_core.store_persistent_data().await?; diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index f02ff99c..a1816422 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -405,10 +405,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { let to_npk = nssa_core::NullifierPublicKey(to_npk); let to_vpk_res = hex::decode(to_vpk)?; - let mut to_vpk = [0_u8; 33]; - to_vpk.copy_from_slice(&to_vpk_res); - let to_vpk = - nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); + let to_vpk = nssa_core::encryption::ViewingPublicKey(to_vpk_res); let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core) .send_private_transfer_to_outer_account( @@ -487,10 +484,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { let to_npk = nssa_core::NullifierPublicKey(to_npk); let to_vpk_res = hex::decode(to_vpk)?; - let mut to_vpk = [0_u8; 33]; - to_vpk.copy_from_slice(&to_vpk_res); - let to_vpk = - nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); + let to_vpk = nssa_core::encryption::ViewingPublicKey(to_vpk_res); let (tx_hash, _) = NativeTokenTransfer(wallet_core) .send_shielded_transfer_to_outer_account( diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 73bb6c2c..e2e390ae 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -898,11 +898,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { let recipient_npk = nssa_core::NullifierPublicKey(recipient_npk); let recipient_vpk_res = hex::decode(recipient_vpk)?; - let mut recipient_vpk = [0_u8; 33]; - recipient_vpk.copy_from_slice(&recipient_vpk_res); - let recipient_vpk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( - recipient_vpk.to_vec(), - ); + let recipient_vpk = nssa_core::encryption::ViewingPublicKey(recipient_vpk_res); let (tx_hash, [secret_sender, _]) = Token(wallet_core) .send_transfer_transaction_private_foreign_account( @@ -1018,11 +1014,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { let holder_npk = nssa_core::NullifierPublicKey(holder_npk); let holder_vpk_res = hex::decode(holder_vpk)?; - let mut holder_vpk = [0_u8; 33]; - holder_vpk.copy_from_slice(&holder_vpk_res); - let holder_vpk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( - holder_vpk.to_vec(), - ); + let holder_vpk = nssa_core::encryption::ViewingPublicKey(holder_vpk_res); let (tx_hash, [secret_definition, _]) = Token(wallet_core) .send_mint_transaction_private_foreign_account( @@ -1184,11 +1176,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { let recipient_npk = nssa_core::NullifierPublicKey(recipient_npk); let recipient_vpk_res = hex::decode(recipient_vpk)?; - let mut recipient_vpk = [0_u8; 33]; - recipient_vpk.copy_from_slice(&recipient_vpk_res); - let recipient_vpk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( - recipient_vpk.to_vec(), - ); + let recipient_vpk = nssa_core::encryption::ViewingPublicKey(recipient_vpk_res); let (tx_hash, _) = Token(wallet_core) .send_transfer_transaction_shielded_foreign_account( @@ -1326,11 +1314,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { let holder_npk = nssa_core::NullifierPublicKey(holder_npk); let holder_vpk_res = hex::decode(holder_vpk)?; - let mut holder_vpk = [0_u8; 33]; - holder_vpk.copy_from_slice(&holder_vpk_res); - let holder_vpk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( - holder_vpk.to_vec(), - ); + let holder_vpk = nssa_core::encryption::ViewingPublicKey(holder_vpk_res); let (tx_hash, _) = Token(wallet_core) .send_mint_transaction_shielded_foreign_account( diff --git a/wallet/src/config.rs b/wallet/src/config.rs index e5d43024..7e92e566 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -112,7 +112,7 @@ pub struct PersistentStorage { >, /// Dedicated sealing secret key for GMS distribution. #[serde(default)] - pub sealing_secret_key: Option, + pub sealing_secret_key: Option, } impl PersistentStorage { diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 6b7cfa00..89a103e1 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -206,7 +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, + sealing_secret_key: user_data.sealing_secret_key.clone(), } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 24ac19b9..ec5547b3 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -317,7 +317,7 @@ impl WalletCore { } /// Set the wallet's dedicated sealing secret key. - pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) { + pub fn set_sealing_secret_key(&mut self, key: key_protocol::key_management::secret_holders::ViewingSecretKey) { self.storage.user_data.sealing_secret_key = Some(key); } @@ -791,7 +791,7 @@ impl WalletCore { continue; } - let shared_secret = SharedSecretKey::new(&vsk, &encrypted_data.epk); + let shared_secret = SharedSecretKey::decapsulate(&encrypted_data.epk, &vsk.d, &vsk.r); let commitment = &tx.message.new_commitments[ciph_id]; if let Some((_kind, new_acc)) = nssa_core::EncryptionScheme::decrypt( diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 603fdd0c..fbc282f7 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -121,9 +121,9 @@ impl AccountManager { } => { let acc = nssa_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let pre = AccountPreparedData { nsk: None, npk, @@ -150,9 +150,9 @@ impl AccountManager { } => { let acc = nssa_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, account_id); - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let pre = AccountPreparedData { nsk: None, npk, @@ -348,9 +348,9 @@ async fn private_key_tree_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, account_id); - let eph_holder = EphemeralKeyHolder::new(&from_npk); - let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&from_vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); Ok(AccountPreparedData { nsk: Some(nsk), @@ -393,9 +393,9 @@ async fn private_shared_acc_preparation( None }; - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); Ok(AccountPreparedData { nsk: exists.then_some(nsk), @@ -419,7 +419,7 @@ mod tests { let acc = PrivacyPreservingAccount::PrivateShared { nsk: [0; 32], npk: NullifierPublicKey([1; 32]), - vpk: ViewingPublicKey::from_scalar([2; 32]), + vpk: ViewingPublicKey::from_seed(&[2u8; 32], &[3u8; 32]), identifier: 42, }; assert!(acc.is_private());