Skip to main content

amaters_sdk_rust/
fhe.rs

1//! FHE (Fully Homomorphic Encryption) key management and operations
2//!
3//! This module provides client-side encryption and decryption capabilities.
4//! The actual FHE operations are feature-gated and require the `fhe` feature.
5
6use crate::error::{Result, SdkError};
7use amaters_core::{CipherBlob, Key};
8use std::path::Path;
9
10/// FHE client keys for encryption/decryption
11///
12/// When the `fhe` feature is enabled, this wraps TFHE client keys for real
13/// homomorphic encryption. Without the feature, it acts as a passthrough stub.
14#[derive(Clone)]
15pub struct FheKeys {
16    #[cfg(feature = "fhe")]
17    client_key: tfhe::ClientKey,
18    #[cfg(feature = "fhe")]
19    server_key: tfhe::ServerKey,
20    #[cfg(not(feature = "fhe"))]
21    _placeholder: (),
22}
23
24impl FheKeys {
25    /// Generate new FHE keys
26    ///
27    /// This is a computationally expensive operation (can take several seconds)
28    /// when the `fhe` feature is enabled.
29    pub fn generate() -> Result<Self> {
30        #[cfg(feature = "fhe")]
31        {
32            let config = tfhe::ConfigBuilder::default().build();
33            let (client_key, server_key) = tfhe::generate_keys(config);
34            Ok(Self {
35                client_key,
36                server_key,
37            })
38        }
39        #[cfg(not(feature = "fhe"))]
40        {
41            Ok(Self { _placeholder: () })
42        }
43    }
44
45    /// Get reference to the client key
46    #[cfg(feature = "fhe")]
47    pub fn client_key(&self) -> &tfhe::ClientKey {
48        &self.client_key
49    }
50
51    /// Get reference to the server key
52    #[cfg(feature = "fhe")]
53    pub fn server_key(&self) -> &tfhe::ServerKey {
54        &self.server_key
55    }
56
57    /// Set this instance's server key as the global TFHE server key.
58    ///
59    /// Must be called before performing any FHE arithmetic, comparison, or boolean operations.
60    /// TFHE operations require a thread-local server key to be set.
61    #[cfg(feature = "fhe")]
62    pub fn set_as_global_server_key(&self) {
63        tfhe::set_server_key(self.server_key.clone());
64    }
65
66    /// Load keys from a file
67    ///
68    /// Reads serialized key data from the given path and deserializes
69    /// using oxicode (when `fhe` + `serialization` features are enabled).
70    pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
71        #[cfg(feature = "fhe")]
72        {
73            let bytes = std::fs::read(path.as_ref())
74                .map_err(|e| SdkError::Fhe(format!("failed to read key file: {}", e)))?;
75            #[cfg(feature = "serialization")]
76            {
77                let (client_key, server_key): (tfhe::ClientKey, tfhe::ServerKey) =
78                    oxicode::serde::decode_serde(&bytes)
79                        .map_err(|e| SdkError::Fhe(format!("failed to deserialize keys: {}", e)))?;
80                Ok(Self {
81                    client_key,
82                    server_key,
83                })
84            }
85            #[cfg(not(feature = "serialization"))]
86            {
87                let _ = bytes;
88                Err(SdkError::Fhe(
89                    "serialization feature required for key file loading".to_string(),
90                ))
91            }
92        }
93        #[cfg(not(feature = "fhe"))]
94        {
95            let _ = path;
96            Ok(Self { _placeholder: () })
97        }
98    }
99
100    /// Save keys to a file
101    ///
102    /// Serializes keys using oxicode and writes to the given path
103    /// (when `fhe` + `serialization` features are enabled).
104    pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
105        #[cfg(feature = "fhe")]
106        {
107            #[cfg(feature = "serialization")]
108            {
109                let bytes = oxicode::serde::encode_serde(&(&self.client_key, &self.server_key))
110                    .map_err(|e| SdkError::Fhe(format!("failed to serialize keys: {}", e)))?;
111                std::fs::write(path.as_ref(), &bytes)
112                    .map_err(|e| SdkError::Fhe(format!("failed to write key file: {}", e)))?;
113                Ok(())
114            }
115            #[cfg(not(feature = "serialization"))]
116            {
117                let _ = path;
118                Err(SdkError::Fhe(
119                    "serialization feature required for key file saving".to_string(),
120                ))
121            }
122        }
123        #[cfg(not(feature = "fhe"))]
124        {
125            let _ = path;
126            Ok(())
127        }
128    }
129
130    /// Serialize keys to bytes using oxicode
131    #[cfg(feature = "serialization")]
132    pub fn to_bytes(&self) -> Result<Vec<u8>> {
133        #[cfg(feature = "fhe")]
134        {
135            oxicode::serde::encode_serde(&(&self.client_key, &self.server_key)).map_err(|e| {
136                SdkError::Serialization(format!("failed to serialize FHE keys: {}", e))
137            })
138        }
139        #[cfg(not(feature = "fhe"))]
140        {
141            Ok(Vec::new())
142        }
143    }
144
145    /// Deserialize keys from bytes using oxicode
146    #[cfg(feature = "serialization")]
147    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
148        #[cfg(feature = "fhe")]
149        {
150            let (client_key, server_key): (tfhe::ClientKey, tfhe::ServerKey) =
151                oxicode::serde::decode_serde(bytes).map_err(|e| {
152                    SdkError::Serialization(format!("failed to deserialize FHE keys: {}", e))
153                })?;
154            Ok(Self {
155                client_key,
156                server_key,
157            })
158        }
159        #[cfg(not(feature = "fhe"))]
160        {
161            let _ = bytes;
162            Ok(Self { _placeholder: () })
163        }
164    }
165}
166
167/// FHE encryptor for client-side encryption
168pub struct FheEncryptor {
169    keys: FheKeys,
170}
171
172impl FheEncryptor {
173    /// Create a new encryptor with generated keys
174    pub fn new() -> Result<Self> {
175        Ok(Self {
176            keys: FheKeys::generate()?,
177        })
178    }
179
180    /// Create an encryptor with existing keys
181    pub fn with_keys(keys: FheKeys) -> Self {
182        Self { keys }
183    }
184
185    /// Get a reference to the keys
186    pub fn keys(&self) -> &FheKeys {
187        &self.keys
188    }
189
190    /// Encrypt a value
191    ///
192    /// When `fhe` is enabled, encrypts each byte using TFHE FheUint8 and
193    /// serializes the resulting ciphertexts into a single CipherBlob.
194    /// Without `fhe`, this is a passthrough that wraps plaintext as-is (NOT secure).
195    pub fn encrypt(&self, plaintext: &[u8]) -> Result<CipherBlob> {
196        #[cfg(feature = "fhe")]
197        {
198            use tfhe::prelude::FheTryEncrypt;
199
200            // Encrypt each byte as an FheUint8 and collect serialized ciphertexts
201            let mut encrypted_parts: Vec<Vec<u8>> = Vec::with_capacity(plaintext.len());
202            for &byte in plaintext {
203                let encrypted: tfhe::FheUint8 =
204                    tfhe::FheUint8::try_encrypt(byte, self.keys.client_key())
205                        .map_err(|e| SdkError::Fhe(format!("failed to encrypt byte: {}", e)))?;
206                // Serialize each encrypted value
207                #[cfg(feature = "serialization")]
208                {
209                    let serialized = oxicode::serde::encode_serde(&encrypted).map_err(|e| {
210                        SdkError::Fhe(format!("failed to serialize encrypted byte: {}", e))
211                    })?;
212                    encrypted_parts.push(serialized);
213                }
214                #[cfg(not(feature = "serialization"))]
215                {
216                    let _ = encrypted;
217                    return Err(SdkError::Fhe(
218                        "serialization feature required for FHE encryption".to_string(),
219                    ));
220                }
221            }
222            // Pack: [count(u64)] [len1(u64)][data1] [len2(u64)][data2] ...
223            let count = plaintext.len() as u64;
224            let total_size = 8 + encrypted_parts.iter().map(|p| 8 + p.len()).sum::<usize>();
225            let mut blob_data = Vec::with_capacity(total_size);
226            blob_data.extend_from_slice(&count.to_le_bytes());
227            for part in &encrypted_parts {
228                let len = part.len() as u64;
229                blob_data.extend_from_slice(&len.to_le_bytes());
230                blob_data.extend_from_slice(part);
231            }
232            Ok(CipherBlob::new(blob_data))
233        }
234        #[cfg(not(feature = "fhe"))]
235        {
236            // For testing: just wrap the plaintext as-is
237            // WARNING: This is NOT secure - only for development
238            Ok(CipherBlob::new(plaintext.to_vec()))
239        }
240    }
241
242    /// Decrypt a ciphertext
243    ///
244    /// When `fhe` is enabled, deserializes FheUint8 ciphertexts from the blob
245    /// and decrypts each one. Without `fhe`, returns the raw blob data (NOT secure).
246    pub fn decrypt(&self, ciphertext: &CipherBlob) -> Result<Vec<u8>> {
247        #[cfg(feature = "fhe")]
248        {
249            use tfhe::prelude::FheDecrypt;
250
251            let data = ciphertext.to_vec();
252            if data.len() < 8 {
253                return Err(SdkError::Fhe("ciphertext too short".to_string()));
254            }
255            let count = u64::from_le_bytes(
256                data[..8]
257                    .try_into()
258                    .map_err(|_| SdkError::Fhe("invalid ciphertext header".to_string()))?,
259            ) as usize;
260
261            let mut offset = 8usize;
262            let mut plaintext = Vec::with_capacity(count);
263
264            for _ in 0..count {
265                if offset + 8 > data.len() {
266                    return Err(SdkError::Fhe(
267                        "ciphertext truncated: missing length field".to_string(),
268                    ));
269                }
270                let part_len = u64::from_le_bytes(
271                    data[offset..offset + 8]
272                        .try_into()
273                        .map_err(|_| SdkError::Fhe("invalid ciphertext part length".to_string()))?,
274                ) as usize;
275                offset += 8;
276
277                if offset + part_len > data.len() {
278                    return Err(SdkError::Fhe(
279                        "ciphertext truncated: insufficient data".to_string(),
280                    ));
281                }
282
283                #[cfg(feature = "serialization")]
284                {
285                    let encrypted: tfhe::FheUint8 = oxicode::serde::decode_serde(
286                        &data[offset..offset + part_len],
287                    )
288                    .map_err(|e| {
289                        SdkError::Fhe(format!("failed to deserialize encrypted byte: {}", e))
290                    })?;
291                    let byte: u8 = encrypted.decrypt(self.keys.client_key());
292                    plaintext.push(byte);
293                }
294                #[cfg(not(feature = "serialization"))]
295                {
296                    return Err(SdkError::Fhe(
297                        "serialization feature required for FHE decryption".to_string(),
298                    ));
299                }
300
301                offset += part_len;
302            }
303
304            Ok(plaintext)
305        }
306        #[cfg(not(feature = "fhe"))]
307        {
308            // For testing: just unwrap the data as-is
309            // WARNING: This is NOT secure - only for development
310            Ok(ciphertext.to_vec())
311        }
312    }
313
314    /// Encrypt a key
315    pub fn encrypt_key(&self, key: &Key) -> Result<CipherBlob> {
316        #[cfg(feature = "fhe")]
317        {
318            self.encrypt(key.as_bytes())
319        }
320        #[cfg(not(feature = "fhe"))]
321        {
322            // For testing: just wrap the key bytes
323            Ok(CipherBlob::new(key.to_vec()))
324        }
325    }
326
327    /// Batch encrypt multiple values
328    pub fn encrypt_batch(&self, plaintexts: &[&[u8]]) -> Result<Vec<CipherBlob>> {
329        plaintexts.iter().map(|p| self.encrypt(p)).collect()
330    }
331}
332
333impl Default for FheEncryptor {
334    fn default() -> Self {
335        Self::new().expect("failed to create default encryptor")
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_fhe_keys_generate_no_fhe() {
345        #[cfg(not(feature = "fhe"))]
346        {
347            let keys = FheKeys::generate().expect("generate keys should succeed");
348            // Verify save_to_file works (no-op in stub mode)
349            let dir = std::env::temp_dir();
350            let path = dir.join("test_fhe_keys_generate");
351            keys.save_to_file(&path)
352                .expect("save_to_file should succeed in stub mode");
353        }
354    }
355
356    #[test]
357    fn test_encrypt_decrypt_roundtrip_no_fhe() {
358        #[cfg(not(feature = "fhe"))]
359        {
360            let encryptor = FheEncryptor::new().expect("create encryptor");
361            let plaintext = b"hello world roundtrip test";
362            let ciphertext = encryptor.encrypt(plaintext).expect("encrypt");
363            let decrypted = encryptor.decrypt(&ciphertext).expect("decrypt");
364
365            // In stub mode, it should be identity
366            assert_eq!(decrypted, plaintext);
367        }
368    }
369
370    #[test]
371    fn test_file_save_load_roundtrip_no_fhe() {
372        #[cfg(not(feature = "fhe"))]
373        {
374            let dir = std::env::temp_dir();
375            let path = dir.join("test_fhe_keys_save_load");
376
377            let keys = FheKeys::generate().expect("generate keys");
378            keys.save_to_file(&path).expect("save keys");
379
380            let _loaded = FheKeys::load_from_file(&path).expect("load keys");
381            // In stub mode, both are placeholder values, so just verify no error
382        }
383    }
384
385    #[cfg(feature = "serialization")]
386    #[test]
387    fn test_serialization_roundtrip_no_fhe() {
388        #[cfg(not(feature = "fhe"))]
389        {
390            let keys = FheKeys::generate().expect("generate keys");
391            let bytes = keys.to_bytes().expect("serialize keys");
392            let _restored = FheKeys::from_bytes(&bytes).expect("deserialize keys");
393            // In stub mode, to_bytes returns empty vec, from_bytes accepts anything
394        }
395    }
396
397    #[test]
398    fn test_batch_encrypt_no_fhe() {
399        #[cfg(not(feature = "fhe"))]
400        {
401            let encryptor = FheEncryptor::new().expect("create encryptor");
402            let data: Vec<&[u8]> = vec![b"one", b"two", b"three"];
403
404            let encrypted = encryptor.encrypt_batch(&data).expect("batch encrypt");
405            assert_eq!(encrypted.len(), 3);
406
407            // Verify each can be decrypted back
408            for (i, ct) in encrypted.iter().enumerate() {
409                let decrypted = encryptor.decrypt(ct).expect("decrypt");
410                assert_eq!(decrypted, data[i]);
411            }
412        }
413    }
414
415    #[test]
416    fn test_encrypt_key_no_fhe() {
417        #[cfg(not(feature = "fhe"))]
418        {
419            let encryptor = FheEncryptor::new().expect("create encryptor");
420            let key = Key::new(b"test-key-data".to_vec());
421            let cipher = encryptor.encrypt_key(&key).expect("encrypt key");
422            let decrypted = encryptor.decrypt(&cipher).expect("decrypt");
423            assert_eq!(decrypted, key.as_bytes());
424        }
425    }
426
427    #[test]
428    fn test_encryptor_with_keys() {
429        #[cfg(not(feature = "fhe"))]
430        {
431            let keys = FheKeys::generate().expect("generate keys");
432            let encryptor = FheEncryptor::with_keys(keys);
433            let _keys_ref = encryptor.keys();
434            let plaintext = b"test with_keys";
435            let ciphertext = encryptor.encrypt(plaintext).expect("encrypt");
436            let decrypted = encryptor.decrypt(&ciphertext).expect("decrypt");
437            assert_eq!(decrypted, plaintext);
438        }
439    }
440
441    #[test]
442    fn test_empty_plaintext_no_fhe() {
443        #[cfg(not(feature = "fhe"))]
444        {
445            let encryptor = FheEncryptor::new().expect("create encryptor");
446            let plaintext = b"";
447            let ciphertext = encryptor.encrypt(plaintext).expect("encrypt empty");
448            let decrypted = encryptor.decrypt(&ciphertext).expect("decrypt empty");
449            assert_eq!(decrypted, plaintext);
450        }
451    }
452}