From 786d0b2899d0a06a316e0abfdb2b5af29e52b293 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 1 Sep 2025 18:12:13 -0300 Subject: [PATCH] add tag computation --- .../privacy_preserving_transaction/message.rs | 94 ++++++++++++++++--- nssa/src/state.rs | 24 +++-- 2 files changed, 100 insertions(+), 18 deletions(-) diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 4877e3d..244a81f 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -1,16 +1,45 @@ use nssa_core::{ - Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, - encryption::{Ciphertext, EphemeralPublicKey}, + encryption::{Ciphertext, EphemeralPublicKey, IncomingViewingPublicKey}, }; +use sha2::{Digest, Sha256}; use crate::{Address, error::NssaError}; +pub type ViewTag = u8; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct EncryptedAccountData { pub(crate) ciphertext: Ciphertext, pub(crate) epk: EphemeralPublicKey, - pub(crate) view_tag: u8, + pub(crate) view_tag: ViewTag, +} + +impl EncryptedAccountData { + fn new( + ciphertext: Ciphertext, + npk: NullifierPublicKey, + ivk: IncomingViewingPublicKey, + epk: EphemeralPublicKey, + ) -> Self { + let view_tag = Self::compute_view_tag(npk, ivk); + Self { + ciphertext, + epk, + view_tag, + } + } + + /// Computes the tag as the first byte of SHA256("/NSSA/v0.1/ViewTag" || Npk || Ivk) + pub fn compute_view_tag(npk: NullifierPublicKey, ivk: IncomingViewingPublicKey) -> ViewTag { + let mut hasher = Sha256::new(); + hasher.update(b"/NSSA/v0.1/ViewTag"); + hasher.update(npk.to_byte_array()); + hasher.update(ivk.to_bytes()); + let digest: [u8; 32] = hasher.finalize().into(); + digest[0] + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -27,10 +56,14 @@ impl Message { pub fn try_from_circuit_output( public_addresses: Vec
, nonces: Vec, - ephemeral_public_keys: Vec, + public_keys: Vec<( + NullifierPublicKey, + IncomingViewingPublicKey, + EphemeralPublicKey, + )>, output: PrivacyPreservingCircuitOutput, ) -> Result { - if ephemeral_public_keys.len() != output.ciphertexts.len() { + if public_keys.len() != output.ciphertexts.len() { return Err(NssaError::InvalidInput( "Ephemeral public keys and ciphertexts length mismatch".into(), )); @@ -39,11 +72,9 @@ impl Message { let encrypted_private_post_states = output .ciphertexts .into_iter() - .zip(ephemeral_public_keys) - .map(|(ciphertext, epk)| EncryptedAccountData { - ciphertext, - epk, - view_tag: 0, // TODO: implement + .zip(public_keys) + .map(|(ciphertext, (npk, ivk, epk))| { + EncryptedAccountData::new(ciphertext, npk, ivk, epk) }) .collect(); Ok(Self { @@ -61,9 +92,17 @@ impl Message { pub mod tests { use std::io::Cursor; - use nssa_core::{Commitment, Nullifier, NullifierPublicKey, account::Account}; + use nssa_core::{ + Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, + account::Account, + encryption::{EphemeralPublicKey, IncomingViewingPublicKey}, + }; + use sha2::{Digest, Sha256}; - use crate::{Address, privacy_preserving_transaction::message::Message}; + use crate::{ + Address, + privacy_preserving_transaction::message::{EncryptedAccountData, Message}, + }; pub fn message_for_tests() -> Message { let account1 = Account::default(); @@ -108,4 +147,35 @@ pub mod tests { assert_eq!(message, message_from_cursor); } + + #[test] + fn test_encrypted_account_data_constructor() { + let npk = NullifierPublicKey::from(&[1; 32]); + let ivk = IncomingViewingPublicKey::from(&[2; 32]); + let account = Account::default(); + let commitment = Commitment::new(&npk, &account); + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &ivk); + let epk = EphemeralPublicKey::from_scalar(esk); + let ciphertext = EncryptionScheme::encrypt(&account, &shared_secret, &commitment, 2); + let encrypted_account_data = + EncryptedAccountData::new(ciphertext.clone(), npk.clone(), ivk.clone(), epk.clone()); + + let expected_view_tag = { + let mut hasher = Sha256::new(); + hasher.update(b"/NSSA/v0.1/ViewTag"); + hasher.update(npk.to_byte_array()); + hasher.update(ivk.to_bytes()); + let digest: [u8; 32] = hasher.finalize().into(); + digest[0] + }; + + assert_eq!(encrypted_account_data.ciphertext, ciphertext); + assert_eq!(encrypted_account_data.epk, epk); + assert_eq!( + encrypted_account_data.view_tag, + EncryptedAccountData::compute_view_tag(npk, ivk) + ); + assert_eq!(encrypted_account_data.view_tag, expected_view_tag); + } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 93ee06c..f975804 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -801,7 +801,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![sender_keys.address()], vec![sender_nonce], - vec![epk], + vec![(recipient_keys.npk(), recipient_keys.ivk(), epk)], output, ) .unwrap(); @@ -854,8 +854,16 @@ pub mod tests { ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![], vec![], vec![epk_1, epk_2], output).unwrap(); + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![ + (sender_keys.npk(), sender_keys.ivk(), epk_1), + (recipient_keys.npk(), recipient_keys.ivk(), epk_2), + ], + output, + ) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); @@ -899,9 +907,13 @@ pub mod tests { ) .unwrap(); - let message = - Message::try_from_circuit_output(vec![*recipient_address], vec![], vec![epk], output) - .unwrap(); + let message = Message::try_from_circuit_output( + vec![*recipient_address], + vec![], + vec![(sender_keys.npk(), sender_keys.ivk(), epk)], + output, + ) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]);