saorsa_core/identity/
encryption.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2
3// This file is part of the Saorsa P2P network.
4
5// Licensed under the AGPL-3.0 license:
6// <https://www.gnu.org/licenses/agpl-3.0.html>
7
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU Affero General Public License for more details.
12
13// You should have received a copy of the GNU Affero General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16// Copyright 2024 P2P Foundation
17// SPDX-License-Identifier: AGPL-3.0-or-later
18
19//! Identity encryption utilities for secure data transport and storage
20
21use crate::error::SecurityError;
22use crate::{P2PError, Result};
23use argon2::{
24    Algorithm, Argon2, Params, Version,
25    password_hash::{PasswordHasher, SaltString, rand_core::RngCore},
26};
27use saorsa_pqc::{ChaCha20Poly1305Cipher, SymmetricEncryptedMessage, SymmetricKey};
28// TODO: Replace with saorsa-pqc HKDF once correct import path is found
29use hkdf::Hkdf;
30use serde::{Deserialize, Serialize};
31use sha2::Sha256;
32
33/// Size of ChaCha20Poly1305 key in bytes
34const CHACHA_KEY_SIZE: usize = 32;
35
36/// Size of salt for key derivation
37const SALT_SIZE: usize = 32;
38
39/// Default Argon2id parameters for device password encryption
40const DEVICE_ARGON2_MEMORY: u32 = 32768; // 32MB
41const DEVICE_ARGON2_TIME: u32 = 2;
42const DEVICE_ARGON2_PARALLELISM: u32 = 2;
43
44/// Encrypted data container for identity sync packages
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct EncryptedData {
47    /// The encrypted message from saorsa-pqc
48    pub encrypted_message: SymmetricEncryptedMessage,
49    /// The salt used for key derivation
50    pub salt: [u8; SALT_SIZE],
51}
52
53/// Encrypt data with a device password for sync packages
54pub fn encrypt_with_device_password(data: &[u8], device_password: &str) -> Result<EncryptedData> {
55    // Generate random salt
56    let mut salt = [0u8; SALT_SIZE];
57    let mut rng = rand::thread_rng();
58    rng.fill_bytes(&mut salt);
59
60    // Derive key from device password using Argon2id
61    let key_bytes = derive_key_from_password(device_password, &salt)?;
62    let symmetric_key = SymmetricKey::from_bytes(key_bytes);
63
64    // Encrypt data with ChaCha20Poly1305
65    let cipher = ChaCha20Poly1305Cipher::new(&symmetric_key);
66    let (ciphertext, nonce) = cipher.encrypt(data, None).map_err(|e| {
67        P2PError::Security(SecurityError::EncryptionFailed(
68            format!("ChaCha20Poly1305 encryption failed: {:?}", e).into(),
69        ))
70    })?;
71
72    let encrypted_message = SymmetricEncryptedMessage::new(ciphertext, nonce, None);
73
74    Ok(EncryptedData {
75        encrypted_message,
76        salt,
77    })
78}
79
80/// Decrypt data with a device password
81pub fn decrypt_with_device_password(
82    encrypted: &EncryptedData,
83    device_password: &str,
84) -> Result<Vec<u8>> {
85    // Derive key from device password
86    let key_bytes = derive_key_from_password(device_password, &encrypted.salt)?;
87    let symmetric_key = SymmetricKey::from_bytes(key_bytes);
88
89    // Decrypt data
90    let cipher = ChaCha20Poly1305Cipher::new(&symmetric_key);
91    let plaintext = cipher
92        .decrypt(
93            &encrypted.encrypted_message.ciphertext,
94            &encrypted.encrypted_message.nonce,
95            None,
96        )
97        .map_err(|e| {
98            P2PError::Security(SecurityError::DecryptionFailed(
99                format!("ChaCha20Poly1305 decryption failed: {:?}", e).into(),
100            ))
101        })?;
102
103    Ok(plaintext)
104}
105
106/// Derive a ChaCha20Poly1305 key from a password using Argon2id
107fn derive_key_from_password(
108    password: &str,
109    salt: &[u8; SALT_SIZE],
110) -> Result<[u8; CHACHA_KEY_SIZE]> {
111    // Configure Argon2id
112    let argon2 = Argon2::new(
113        Algorithm::Argon2id,
114        Version::V0x13,
115        Params::new(
116            DEVICE_ARGON2_MEMORY,
117            DEVICE_ARGON2_TIME,
118            DEVICE_ARGON2_PARALLELISM,
119            Some(CHACHA_KEY_SIZE),
120        )
121        .map_err(|e| {
122            P2PError::Security(SecurityError::InvalidKey(
123                format!("Invalid Argon2 params: {}", e).into(),
124            ))
125        })?,
126    );
127
128    // Create salt string
129    let salt_string = SaltString::encode_b64(salt).map_err(|e| {
130        P2PError::Security(SecurityError::InvalidKey(
131            format!("Failed to encode salt: {}", e).into(),
132        ))
133    })?;
134
135    // Derive key
136    let hash = argon2
137        .hash_password(password.as_bytes(), &salt_string)
138        .map_err(|e| {
139            P2PError::Security(SecurityError::KeyGenerationFailed(
140                format!("Argon2id key derivation failed: {}", e).into(),
141            ))
142        })?;
143
144    let hash_output = hash.hash.ok_or_else(|| {
145        P2PError::Security(SecurityError::KeyGenerationFailed(
146            "No hash output from Argon2".to_string().into(),
147        ))
148    })?;
149
150    let key_bytes = hash_output.as_bytes();
151    if key_bytes.len() < CHACHA_KEY_SIZE {
152        return Err(P2PError::Security(SecurityError::KeyGenerationFailed(
153            "Insufficient key material from Argon2".to_string().into(),
154        )));
155    }
156
157    let mut result = [0u8; CHACHA_KEY_SIZE];
158    result.copy_from_slice(&key_bytes[..CHACHA_KEY_SIZE]);
159    Ok(result)
160}
161
162/// Encrypt data with a shared secret (for peer-to-peer encryption)
163pub fn encrypt_with_shared_secret(
164    data: &[u8],
165    shared_secret: &[u8; 32],
166    info: &[u8],
167) -> Result<EncryptedData> {
168    // Generate random salt for HKDF
169    let mut salt = [0u8; SALT_SIZE];
170    let mut rng = rand::thread_rng();
171    rng.fill_bytes(&mut salt);
172
173    // TODO: Use saorsa-pqc HKDF-SHA3 when available
174    let hkdf = Hkdf::<Sha256>::new(Some(&salt), shared_secret);
175    let mut key_bytes = [0u8; CHACHA_KEY_SIZE];
176    hkdf.expand(info, &mut key_bytes).map_err(|e| {
177        P2PError::Security(SecurityError::KeyGenerationFailed(
178            format!("HKDF-SHA3 expansion failed: {:?}", e).into(),
179        ))
180    })?;
181
182    let symmetric_key = SymmetricKey::from_bytes(key_bytes);
183
184    // Encrypt data with ChaCha20Poly1305
185    let cipher = ChaCha20Poly1305Cipher::new(&symmetric_key);
186    let (ciphertext, nonce) = cipher.encrypt(data, None).map_err(|e| {
187        P2PError::Security(SecurityError::EncryptionFailed(
188            format!("ChaCha20Poly1305 encryption failed: {:?}", e).into(),
189        ))
190    })?;
191
192    let encrypted_message = SymmetricEncryptedMessage::new(ciphertext, nonce, None);
193
194    Ok(EncryptedData {
195        encrypted_message,
196        salt,
197    })
198}
199
200/// Decrypt data with a shared secret
201pub fn decrypt_with_shared_secret(
202    encrypted: &EncryptedData,
203    shared_secret: &[u8; 32],
204    info: &[u8],
205) -> Result<Vec<u8>> {
206    // TODO: Use saorsa-pqc HKDF-SHA3 when available
207    let hkdf = Hkdf::<Sha256>::new(Some(&encrypted.salt), shared_secret);
208    let mut key_bytes = [0u8; CHACHA_KEY_SIZE];
209    hkdf.expand(info, &mut key_bytes).map_err(|e| {
210        P2PError::Security(SecurityError::KeyGenerationFailed(
211            format!("HKDF-SHA3 expansion failed: {:?}", e).into(),
212        ))
213    })?;
214
215    let symmetric_key = SymmetricKey::from_bytes(key_bytes);
216
217    // Decrypt data
218    let cipher = ChaCha20Poly1305Cipher::new(&symmetric_key);
219    let plaintext = cipher
220        .decrypt(
221            &encrypted.encrypted_message.ciphertext,
222            &encrypted.encrypted_message.nonce,
223            None,
224        )
225        .map_err(|e| {
226            P2PError::Security(SecurityError::DecryptionFailed(
227                format!("ChaCha20Poly1305 decryption failed: {:?}", e).into(),
228            ))
229        })?;
230
231    Ok(plaintext)
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_device_password_encryption() {
240        let data = b"Secret identity data";
241        let password = "MyDevicePassword123!";
242
243        // Encrypt
244        let encrypted =
245            encrypt_with_device_password(data, password).expect("Encryption should succeed");
246
247        // Verify encrypted data exists
248        assert!(!encrypted.encrypted_message.ciphertext.is_empty());
249
250        // Decrypt
251        let decrypted =
252            decrypt_with_device_password(&encrypted, password).expect("Decryption should succeed");
253
254        assert_eq!(decrypted, data);
255    }
256
257    #[test]
258    fn test_encryption_serialization() {
259        let data = b"Test data for serialization";
260        let password = "SerializeTest123!";
261
262        // Create encrypted data
263        let encrypted =
264            encrypt_with_device_password(data, password).expect("Encryption should succeed");
265
266        // Serialize
267        let serialized = bincode::serialize(&encrypted).expect("Serialization should succeed");
268
269        // Deserialize
270        let deserialized: EncryptedData =
271            bincode::deserialize(&serialized).expect("Deserialization should succeed");
272
273        // Verify fields match
274        assert_eq!(
275            encrypted.encrypted_message.ciphertext,
276            deserialized.encrypted_message.ciphertext
277        );
278        assert_eq!(encrypted.salt, deserialized.salt);
279
280        // Verify can decrypt after deserialize
281        let decrypted = decrypt_with_device_password(&deserialized, password)
282            .expect("Decryption should succeed");
283
284        assert_eq!(decrypted, data);
285    }
286
287    #[test]
288    fn test_wrong_password_fails() {
289        let data = b"Secret identity data";
290        let password = "MyDevicePassword123!";
291        let wrong_password = "WrongPassword456!";
292
293        // Encrypt
294        let encrypted =
295            encrypt_with_device_password(data, password).expect("Encryption should succeed");
296
297        // Try to decrypt with wrong password
298        let result = decrypt_with_device_password(&encrypted, wrong_password);
299
300        assert!(result.is_err());
301    }
302
303    #[test]
304    fn test_shared_secret_encryption() {
305        let data = b"Peer to peer message";
306        let shared_secret = [42u8; 32];
307        let info = b"p2p-identity-sync";
308
309        // Encrypt
310        let encrypted = encrypt_with_shared_secret(data, &shared_secret, info)
311            .expect("Encryption should succeed");
312
313        // Decrypt
314        let decrypted = decrypt_with_shared_secret(&encrypted, &shared_secret, info)
315            .expect("Decryption should succeed");
316
317        assert_eq!(decrypted, data);
318    }
319
320    #[test]
321    fn test_different_info_fails() {
322        let data = b"Peer to peer message";
323        let shared_secret = [42u8; 32];
324        let info1 = b"p2p-identity-sync";
325        let info2 = b"different-context";
326
327        // Encrypt with info1
328        let encrypted = encrypt_with_shared_secret(data, &shared_secret, info1)
329            .expect("Encryption should succeed");
330
331        // Try to decrypt with info2
332        let result = decrypt_with_shared_secret(&encrypted, &shared_secret, info2);
333
334        assert!(result.is_err());
335    }
336
337    #[test]
338    fn test_encryption_produces_unique_nonces() {
339        let data = b"Test data";
340        let password = "TestPassword123!";
341
342        // Encrypt same data twice
343        let encrypted1 =
344            encrypt_with_device_password(data, password).expect("First encryption should succeed");
345        let encrypted2 =
346            encrypt_with_device_password(data, password).expect("Second encryption should succeed");
347
348        // Salts should be different
349        assert_ne!(encrypted1.salt, encrypted2.salt);
350        // Ciphertexts should be different due to different salts and nonces
351        assert_ne!(
352            encrypted1.encrypted_message.ciphertext,
353            encrypted2.encrypted_message.ciphertext
354        );
355    }
356}