p2p_chat/crypto/
storage.rs

1//! This module handles the encryption of data at rest.
2//!
3//! It uses Argon2 for key derivation and ChaCha20Poly1305 for symmetric encryption.
4use anyhow::{anyhow, Result};
5use argon2::{Argon2, Params};
6use chacha20poly1305::{
7    aead::{Aead, AeadCore, KeyInit},
8    ChaCha20Poly1305, Key, Nonce,
9};
10
11/// A context for encrypting and decrypting data at rest.
12///
13/// The key is derived from a password and salt using Argon2id.
14#[derive(Clone)]
15pub struct StorageEncryption {
16    key: [u8; 32], // Derived via Argon2id
17}
18
19impl StorageEncryption {
20    /// Creates a new `StorageEncryption` context.
21    ///
22    /// # Arguments
23    ///
24    /// * `password` - The password to use for key derivation.
25    /// * `salt` - A 16-byte salt to use for key derivation.
26    ///
27    /// # Errors
28    ///
29    /// This function will return an error if the salt is not 16 bytes long or if
30    /// key derivation fails.
31    pub fn new(password: &str, salt: &[u8]) -> Result<Self> {
32        if salt.len() != 16 {
33            return Err(anyhow!("Salt must be 16 bytes"));
34        }
35
36        let params = Params::new(15000, 2, 1, Some(32))
37            .map_err(|e| anyhow!("Argon2 params error: {:?}", e))?;
38        let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
39
40        let mut key = [0u8; 32];
41        argon2
42            .hash_password_into(password.as_bytes(), salt, &mut key)
43            .map_err(|e| anyhow!("Argon2 key derivation failed: {}", e))?;
44
45        Ok(Self { key })
46    }
47
48    /// Generates a new random 16-byte salt.
49    pub fn generate_salt() -> [u8; 16] {
50        let mut salt = [0u8; 16];
51        getrandom::getrandom(&mut salt).expect("Failed to generate random salt");
52        salt
53    }
54
55    /// Encrypts a byte slice.
56    ///
57    /// # Arguments
58    ///
59    /// * `data` - The data to encrypt.
60    ///
61    /// # Returns
62    ///
63    /// A vector containing the nonce and the ciphertext.
64    ///
65    /// # Errors
66    ///
67    /// This function will return an error if encryption fails.
68    pub fn encrypt_value(&self, data: &[u8]) -> Result<Vec<u8>> {
69        let cipher = ChaCha20Poly1305::new(Key::from_slice(&self.key));
70        let nonce = ChaCha20Poly1305::generate_nonce(&mut rand::rngs::OsRng);
71
72        let ciphertext = cipher
73            .encrypt(&nonce, data)
74            .map_err(|e| anyhow!("Encryption failed: {}", e))?;
75
76        let mut result = nonce.to_vec();
77        result.extend_from_slice(&ciphertext);
78
79        Ok(result)
80    }
81
82    /// Decrypts a byte slice.
83    ///
84    /// # Arguments
85    ///
86    /// * `ciphertext` - The data to decrypt (nonce prepended).
87    ///
88    /// # Returns
89    ///
90    /// A vector containing the plaintext.
91    ///
92    /// # Errors
93    ///
94    /// This function will return an error if decryption fails.
95    pub fn decrypt_value(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
96        if ciphertext.len() < 12 {
97            return Err(anyhow!("Ciphertext too short"));
98        }
99
100        let (nonce_bytes, encrypted_data) = ciphertext.split_at(12);
101        let nonce = Nonce::from_slice(nonce_bytes);
102
103        let cipher = ChaCha20Poly1305::new(Key::from_slice(&self.key));
104
105        let plaintext = cipher
106            .decrypt(nonce, encrypted_data)
107            .map_err(|e| anyhow!("Decryption failed: {}", e))?;
108
109        Ok(plaintext)
110    }
111
112    /// Derives a hash of a recipient's public key.
113    ///
114    /// This is used to create a unique identifier for a recipient in the mailbox
115    /// storage, without revealing the public key itself.
116    ///
117    /// # Arguments
118    ///
119    /// * `public_key` - The public key of the recipient.
120    pub fn derive_recipient_hash(public_key: &[u8]) -> [u8; 32] {
121        use sha2::{Digest, Sha256};
122        let mut hasher = Sha256::new();
123        hasher.update(b"p2p-messenger-recipient-");
124        hasher.update(public_key);
125        hasher.finalize().into()
126    }
127}