Skip to main content

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