prover/
encryption.rs

1//! Cryptographic key derivation and note encryption.
2//!
3//! This module implements two key derivation schemes:
4//!
5//! 1. **Encryption Keys (X25519)**: For encrypting/decrypting note data
6//!    off-chain. Derived from Freighter signature using SHA-256.
7//!
8//! 2. **Note Identity Keys (BN254)**: For proving ownership in ZK circuits.
9//!    Also derived from Freighter signature using SHA-256 with domain
10//!    separation.
11//!
12//! Both key types are deterministically derived from wallet signatures,
13//! ensuring users can recover all keys using only their wallet seed phrase.
14//!
15//! We use SHA-256 as the hash function for both key derivation and encryption.
16//! We use sha instead of Poseidon2 because:
17//! - It won't be used in the circuit context
18//! - SHA is well-established and its security has been more researched than
19//!   Poseidon2
20//!
21//! # Key Architecture
22//!
23//! ```text
24//! Freighter Wallet (Ed25519)
25//!        │
26//!        ├── signMessage("Sign to access Privacy Pool [v1]")
27//!        │          │
28//!        │          └── SHA-256 → X25519 Encryption Keypair
29//!        │
30//!        └── signMessage("Privacy Pool Spending Key [v1]")
31//!                   │
32//!                   └── SHA-256 → BN254 Note Private Key
33//!                                      │
34//!                                      └── Poseidon2 → Note Public Key
35//! ```
36
37use alloc::{format, string::String, vec::Vec};
38use ark_bn254::Fr;
39use ark_ff::PrimeField;
40use ark_serialize::CanonicalSerialize;
41use crypto_secretbox::{KeyInit, Nonce, XSalsa20Poly1305, aead::Aead};
42use sha2::{Digest, Sha256};
43use wasm_bindgen::prelude::*;
44use x25519_dalek::{PublicKey, StaticSecret};
45
46/// Encryption key derivation (X25519). Used for off-chain note
47/// encryption/decryption Derive X25519 encryption keypair deterministically
48/// from a Freighter signature.
49///
50/// This keypair is used for encrypting note data (amount, blinding) so that
51/// only the recipient can decrypt it. The encryption scheme is
52/// X25519-XSalsa20-Poly1305.
53///
54/// # Derivation
55/// ```text
56/// signature (64 bytes) → SHA-256 → 32-byte seed → X25519 keypair
57/// ```
58///
59/// # Arguments
60/// * `signature` - Stellar Ed25519 signature from signing "Sign to access
61///   Privacy Pool [v1]"
62///
63/// # Returns
64/// 64 bytes: `[public_key (32), private_key (32)]`
65#[wasm_bindgen]
66pub fn derive_keypair_from_signature(signature: &[u8]) -> Result<Vec<u8>, JsValue> {
67    derive_keypair_from_signature_internal(signature).map_err(|e| JsValue::from_str(&e))
68}
69
70fn derive_keypair_from_signature_internal(signature: &[u8]) -> Result<Vec<u8>, String> {
71    if signature.len() != 64 {
72        return Err("Signature must be 64 bytes (Ed25519)".into());
73    }
74
75    // Hash signature to get a 32-byte seed
76    let mut hasher = Sha256::new();
77    hasher.update(signature);
78    let seed = hasher.finalize();
79
80    // Generate X25519 keypair from seed
81    let mut secret_bytes = [0u8; 32];
82    secret_bytes.copy_from_slice(&seed);
83
84    let secret = StaticSecret::from(secret_bytes);
85    let public = PublicKey::from(&secret);
86
87    // Return [public_key (32), private_key (32)]
88    let mut result = Vec::with_capacity(64);
89    result.extend_from_slice(public.as_bytes());
90    result.extend_from_slice(&secret.to_bytes());
91
92    Ok(result)
93}
94
95/// Derive private key (BN254 scalar) deterministically from a Freighter
96/// signature for note identity. Used for ZK circuit ownership proofs
97///
98/// This private key is used inside ZK circuits to prove ownership of notes.
99/// The corresponding public key is derived via Poseidon2 hash
100///
101/// # Derivation
102/// ```text
103/// signature (64 bytes) → SHA-256 → 32-byte BN254 scalar (note private key)
104/// ```
105///
106/// # Arguments
107/// * `signature` - Stellar Ed25519 signature from signing "Privacy Pool
108///   Spending Key [v1]"
109///
110/// # Returns
111/// 32 bytes: Note private key (BN254 scalar, little-endian)
112#[wasm_bindgen]
113pub fn derive_note_private_key(signature: &[u8]) -> Result<Vec<u8>, JsValue> {
114    if signature.len() != 64 {
115        return Err(JsValue::from_str("Signature must be 64 bytes (Ed25519)"));
116    }
117
118    // Hash signature to get 32-byte key
119    // As SHA-256 might be larger than BN254 field, we apply module reduction.
120    let mut hasher = Sha256::new();
121    hasher.update(signature);
122    let key = hasher.finalize();
123
124    // Reduce to BN254 module
125    let field = Fr::from_le_bytes_mod_order(&key);
126
127    // Serialize into bytes
128    let mut result: Vec<u8> = Vec::with_capacity(32);
129    field
130        .serialize_compressed(&mut result)
131        .expect("Serialization failed");
132
133    Ok(result)
134}
135
136/// Generate a cryptographically random blinding factor for a note.
137///
138/// Each note requires a unique blinding factor to ensure commitments are unique
139/// even when amount and recipient are the same.
140///
141/// # Returns
142/// 32 bytes: Random BN254 scalar (little-endian), reduced to field modulus
143///
144/// # Note
145/// Unlike the private keys above, blinding factors are NOT derived
146/// deterministically. They are random per-note and must be stored for later
147/// use.
148#[wasm_bindgen]
149pub fn generate_random_blinding() -> Result<Vec<u8>, JsValue> {
150    let mut random_bytes = [0u8; 32];
151    getrandom::getrandom(&mut random_bytes)
152        .map_err(|e| JsValue::from_str(&format!("Random generation failed: {}", e)))?;
153
154    // Reduce to BN254 field
155    let scalar = Fr::from_le_bytes_mod_order(&random_bytes);
156
157    // Serialize back to little-endian bytes
158    let mut result = Vec::with_capacity(32);
159    scalar
160        .serialize_compressed(&mut result)
161        .map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))?;
162    Ok(result)
163}
164
165/// Encrypt note data using X25519-XSalsa20-Poly1305 (NaCl crypto_box).
166///
167/// When sending a note to someone, we encrypt the sensitive data (amount and
168/// blinding) with their X25519 public key. Only they can decrypt it.
169///
170/// # Output Format
171/// ```text
172/// [ephemeral_pubkey (32)] [nonce (24)] [ciphertext (40) + tag (16)]
173/// Total: 112 bytes minimum
174/// ```
175///
176/// # Arguments
177/// * `recipient_pubkey_bytes` - Recipient's X25519 encryption public key (32
178///   bytes)
179/// * `plaintext` - Note data: `[amount (8 bytes LE)] [blinding (32 bytes)]` =
180///   40 bytes
181///
182/// # Returns
183/// Encrypted data (112 bytes)
184#[wasm_bindgen]
185pub fn encrypt_note_data(
186    recipient_pubkey_bytes: &[u8],
187    plaintext: &[u8],
188) -> Result<Vec<u8>, JsValue> {
189    encrypt_note_data_internal(recipient_pubkey_bytes, plaintext).map_err(|e| JsValue::from_str(&e))
190}
191
192fn encrypt_note_data_internal(
193    recipient_pubkey_bytes: &[u8],
194    plaintext: &[u8],
195) -> Result<Vec<u8>, String> {
196    if recipient_pubkey_bytes.len() != 32 {
197        return Err("Recipient public key must be 32 bytes".into());
198    }
199    if plaintext.len() != 40 {
200        return Err("Plaintext must be 40 bytes (8 amount + 32 blinding)".into());
201    }
202
203    // Generate ephemeral secret key using getrandom directly
204    let mut ephemeral_bytes = [0u8; 32];
205    getrandom::getrandom(&mut ephemeral_bytes)
206        .map_err(|e| format!("Failed to generate ephemeral key: {}", e))?;
207
208    let ephemeral_secret = StaticSecret::from(ephemeral_bytes);
209    let ephemeral_public = PublicKey::from(&ephemeral_secret);
210
211    // ECDH: derive shared secret
212    let recipient_public = PublicKey::from(
213        *<&[u8; 32]>::try_from(recipient_pubkey_bytes)
214            .map_err(|_| "Invalid recipient public key")?,
215    );
216    let shared_secret = ephemeral_secret.diffie_hellman(&recipient_public);
217
218    // Setup XSalsa20Poly1305 cipher with shared secret
219    let cipher = XSalsa20Poly1305::new(shared_secret.as_bytes().into());
220
221    // Generate random nonce (24 bytes for XSalsa20) using getrandom
222    let mut nonce_bytes = [0u8; 24];
223    getrandom::getrandom(&mut nonce_bytes)
224        .map_err(|e| format!("Failed to generate nonce: {}", e))?;
225    let nonce = Nonce::from(nonce_bytes);
226
227    // Encrypt plaintext
228    let ciphertext = cipher
229        .encrypt(&nonce, plaintext)
230        .map_err(|e| format!("Encryption failed: {:?}", e))?;
231
232    // Pack: [ephemeral_pubkey (32)] [nonce (24)] [ciphertext + tag]
233    // 32 (pubkey) + 24 (nonce) = 56 bytes overhead
234    let capacity = ciphertext
235        .len()
236        .checked_add(56)
237        .expect("Integer overflow on encryption output size");
238    let mut result = Vec::with_capacity(capacity);
239    result.extend_from_slice(ephemeral_public.as_bytes());
240    result.extend_from_slice(&nonce_bytes);
241    result.extend_from_slice(&ciphertext);
242
243    Ok(result)
244}
245
246/// Decrypt note data using X25519-XSalsa20-Poly1305.
247///
248/// When scanning for notes addressed to us, we try to decrypt each encrypted
249/// output. If decryption succeeds, the note was sent to us.
250///
251/// # Arguments
252/// * `private_key_bytes` - Our X25519 encryption private key (32 bytes)
253/// * `encrypted_data` - Encrypted data from on-chain event (112+ bytes)
254///
255/// # Returns
256/// - Success: `[amount (8 bytes LE)] [blinding (32 bytes)]` = 40 bytes
257/// - Failure: Empty vec (note was not addressed to us)
258#[wasm_bindgen]
259pub fn decrypt_note_data(
260    private_key_bytes: &[u8],
261    encrypted_data: &[u8],
262) -> Result<Vec<u8>, JsValue> {
263    decrypt_note_data_internal(private_key_bytes, encrypted_data).map_err(|e| JsValue::from_str(&e))
264}
265
266fn decrypt_note_data_internal(
267    private_key_bytes: &[u8],
268    encrypted_data: &[u8],
269) -> Result<Vec<u8>, String> {
270    if private_key_bytes.len() != 32 {
271        return Err("Private key must be 32 bytes".into());
272    }
273
274    // Minimum size: ephemeral_pubkey (32) + nonce (24) + min ciphertext (40) + tag
275    // (16) = 112
276    if encrypted_data.len() < 112 {
277        return Err("Encrypted data too short".into());
278    }
279
280    // Extract components
281    let ephemeral_pubkey = &encrypted_data[0..32];
282    let nonce_bytes = &encrypted_data[32..56];
283    let ciphertext_with_tag = &encrypted_data[56..];
284
285    // Setup our private key
286    let our_secret = StaticSecret::from(
287        *<&[u8; 32]>::try_from(private_key_bytes).map_err(|_| "Invalid private key")?,
288    );
289
290    // ECDH: derive shared secret
291    let ephemeral_public = PublicKey::from(
292        *<&[u8; 32]>::try_from(ephemeral_pubkey).map_err(|_| "Invalid ephemeral public key")?,
293    );
294    let shared_secret = our_secret.diffie_hellman(&ephemeral_public);
295
296    // Setup XSalsa20Poly1305 cipher
297    let cipher = XSalsa20Poly1305::new(shared_secret.as_bytes().into());
298
299    // Create nonce from bytes (convert to array first)
300    let mut nonce_array = [0u8; 24];
301    nonce_array.copy_from_slice(nonce_bytes);
302    let nonce = Nonce::from(nonce_array);
303
304    // Decrypt
305    match cipher.decrypt(&nonce, ciphertext_with_tag) {
306        Ok(plaintext) => Ok(plaintext),
307        Err(_) => {
308            // Decryption failed - this note output is not for us
309            Ok(Vec::new()) // Return empty vec
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_derive_keypair_determinism() {
320        let signature = [1u8; 64];
321        let keys1 = derive_keypair_from_signature_internal(&signature).expect("Derivation failed");
322        let keys2 = derive_keypair_from_signature_internal(&signature).expect("Derivation failed");
323        assert_eq!(keys1, keys2);
324        assert_eq!(keys1.len(), 64);
325    }
326
327    #[test]
328    fn test_encryption_roundtrip() {
329        let recipient_sig = [2u8; 64];
330        let recip_keys =
331            derive_keypair_from_signature_internal(&recipient_sig).expect("Derivation failed");
332        let pub_key = &recip_keys[0..32];
333        let priv_key = &recip_keys[32..64];
334
335        // 8 bytes amount + 32 bytes blinding = 40 bytes
336        let amount = [10u8; 8];
337        let blinding = [20u8; 32];
338        let mut plaintext = Vec::with_capacity(40);
339        plaintext.extend_from_slice(&amount);
340        plaintext.extend_from_slice(&blinding);
341
342        let encrypted = encrypt_note_data_internal(pub_key, &plaintext).expect("Encryption failed");
343        assert!(encrypted.len() >= 112);
344
345        let decrypted =
346            decrypt_note_data_internal(priv_key, &encrypted).expect("Decryption failed");
347        assert_eq!(decrypted, plaintext);
348    }
349
350    #[test]
351    fn test_decrypt_failure_wrong_key() {
352        let alice_sig = [3u8; 64];
353        let bob_sig = [4u8; 64];
354
355        let alice_keys =
356            derive_keypair_from_signature_internal(&alice_sig).expect("Derivation failed");
357        let bob_keys = derive_keypair_from_signature_internal(&bob_sig).expect("Derivation failed");
358
359        // Encrypt for Alice
360        let alice_pub = &alice_keys[0..32];
361        let plaintext = [0u8; 40];
362        let encrypted =
363            encrypt_note_data_internal(alice_pub, &plaintext).expect("Encryption failed");
364
365        // Bob tries to decrypt
366        let bob_priv = &bob_keys[32..64];
367        let decrypted = decrypt_note_data_internal(bob_priv, &encrypted)
368            .expect("Decryption should handle failure gracefully");
369
370        // Should return empty vec on failure as per implementation
371        assert!(decrypted.is_empty());
372    }
373
374    #[test]
375    fn test_invalid_input_lengths() {
376        let sig = [5u8; 64];
377        let keys = derive_keypair_from_signature_internal(&sig)
378            .expect("Derivation failed in test_invalid_input_lengths");
379        let pub_key = &keys[0..32];
380
381        // Invalid plaintext length
382        let res = encrypt_note_data_internal(pub_key, &[0u8; 39]);
383        assert!(res.is_err());
384
385        // Invalid pubkey length
386        let res = encrypt_note_data_internal(&[0u8; 31], &[0u8; 40]);
387        assert!(res.is_err());
388    }
389}