promocrypt_core/
encryption.rs

1//! Cryptographic operations for promocrypt-core.
2//!
3//! Implements the two-key encryption system using:
4//! - Argon2id for key derivation
5//! - AES-256-GCM for authenticated encryption
6//! - AES-256-SIV for deterministic storage encryption
7
8use argon2::{Algorithm, Argon2, Params, Version};
9use base64::{Engine, engine::general_purpose::STANDARD};
10use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
11use ring::digest::{SHA256, digest};
12use ring::rand::{SecureRandom, SystemRandom};
13
14use crate::error::{PromocryptError, Result};
15
16/// Argon2 memory cost in KiB (64 MB)
17pub const ARGON2_MEMORY_COST: u32 = 65536;
18
19/// Argon2 time cost (iterations)
20pub const ARGON2_TIME_COST: u32 = 3;
21
22/// Argon2 parallelism
23pub const ARGON2_PARALLELISM: u32 = 1;
24
25/// Salt size in bytes
26pub const SALT_SIZE: usize = 16;
27
28/// Nonce size for AES-256-GCM
29pub const NONCE_SIZE: usize = 12;
30
31/// Tag size for AES-256-GCM
32pub const TAG_SIZE: usize = 16;
33
34/// Encrypted key size (32 bytes key + 16 bytes tag)
35pub const ENCRYPTED_KEY_SIZE: usize = 48;
36
37/// Derive a 32-byte key from a password/secret using Argon2id.
38///
39/// # Arguments
40/// * `input` - Password, secret, or machine ID bytes
41/// * `salt` - 16-byte random salt
42///
43/// # Returns
44/// 32-byte derived key
45pub fn derive_key(input: &[u8], salt: &[u8; SALT_SIZE]) -> Result<[u8; 32]> {
46    let params = Params::new(
47        ARGON2_MEMORY_COST,
48        ARGON2_TIME_COST,
49        ARGON2_PARALLELISM,
50        Some(32),
51    )
52    .map_err(|e| PromocryptError::EncryptionFailed(format!("Argon2 params error: {}", e)))?;
53
54    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
55
56    let mut output = [0u8; 32];
57    argon2
58        .hash_password_into(input, salt, &mut output)
59        .map_err(|e| PromocryptError::EncryptionFailed(format!("Argon2 hash error: {}", e)))?;
60
61    Ok(output)
62}
63
64/// Generate a random 32-byte key.
65pub fn generate_random_key() -> Result<[u8; 32]> {
66    let rng = SystemRandom::new();
67    let mut key = [0u8; 32];
68    rng.fill(&mut key).map_err(|_| {
69        PromocryptError::EncryptionFailed("Failed to generate random key".to_string())
70    })?;
71    Ok(key)
72}
73
74/// Generate a random 16-byte salt.
75pub fn generate_salt() -> Result<[u8; SALT_SIZE]> {
76    let rng = SystemRandom::new();
77    let mut salt = [0u8; SALT_SIZE];
78    rng.fill(&mut salt)
79        .map_err(|_| PromocryptError::EncryptionFailed("Failed to generate salt".to_string()))?;
80    Ok(salt)
81}
82
83/// Generate a random 12-byte nonce.
84pub fn generate_nonce() -> Result<[u8; NONCE_SIZE]> {
85    let rng = SystemRandom::new();
86    let mut nonce = [0u8; NONCE_SIZE];
87    rng.fill(&mut nonce)
88        .map_err(|_| PromocryptError::EncryptionFailed("Failed to generate nonce".to_string()))?;
89    Ok(nonce)
90}
91
92/// Encrypt data with AES-256-GCM.
93///
94/// # Arguments
95/// * `key` - 32-byte encryption key
96/// * `plaintext` - Data to encrypt
97/// * `nonce` - 12-byte nonce (must be unique per encryption with same key)
98///
99/// # Returns
100/// Ciphertext with appended authentication tag (plaintext.len() + 16 bytes)
101pub fn encrypt(key: &[u8; 32], plaintext: &[u8], nonce: &[u8; NONCE_SIZE]) -> Result<Vec<u8>> {
102    let unbound_key = UnboundKey::new(&AES_256_GCM, key)
103        .map_err(|_| PromocryptError::EncryptionFailed("Invalid key".to_string()))?;
104    let less_safe_key = LessSafeKey::new(unbound_key);
105
106    let nonce = Nonce::assume_unique_for_key(*nonce);
107
108    // ring encrypts in-place, so we need to allocate space for the tag
109    let mut in_out = plaintext.to_vec();
110    in_out.reserve(TAG_SIZE);
111
112    less_safe_key
113        .seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
114        .map_err(|_| PromocryptError::EncryptionFailed("Encryption failed".to_string()))?;
115
116    Ok(in_out)
117}
118
119/// Decrypt data with AES-256-GCM.
120///
121/// # Arguments
122/// * `key` - 32-byte decryption key
123/// * `ciphertext` - Encrypted data with appended tag
124/// * `nonce` - 12-byte nonce used during encryption
125///
126/// # Returns
127/// Decrypted plaintext
128pub fn decrypt(key: &[u8; 32], ciphertext: &[u8], nonce: &[u8; NONCE_SIZE]) -> Result<Vec<u8>> {
129    if ciphertext.len() < TAG_SIZE {
130        return Err(PromocryptError::DecryptionFailed);
131    }
132
133    let unbound_key =
134        UnboundKey::new(&AES_256_GCM, key).map_err(|_| PromocryptError::DecryptionFailed)?;
135    let less_safe_key = LessSafeKey::new(unbound_key);
136
137    let nonce = Nonce::assume_unique_for_key(*nonce);
138
139    let mut in_out = ciphertext.to_vec();
140    let plaintext = less_safe_key
141        .open_in_place(nonce, Aad::empty(), &mut in_out)
142        .map_err(|_| PromocryptError::DecryptionFailed)?;
143
144    Ok(plaintext.to_vec())
145}
146
147/// Encrypt a 32-byte data key for storage.
148///
149/// Used to encrypt the data_key with either machineID or secret.
150///
151/// # Arguments
152/// * `data_key` - 32-byte key to encrypt
153/// * `password_or_machine_id` - Password/secret or machine ID to derive encryption key from
154/// * `salt` - Salt for key derivation
155/// * `nonce` - Nonce for encryption
156///
157/// # Returns
158/// 48 bytes: encrypted key (32) + tag (16)
159pub fn encrypt_data_key(
160    data_key: &[u8; 32],
161    password_or_machine_id: &[u8],
162    salt: &[u8; SALT_SIZE],
163    nonce: &[u8; NONCE_SIZE],
164) -> Result<[u8; ENCRYPTED_KEY_SIZE]> {
165    let derived_key = derive_key(password_or_machine_id, salt)?;
166    let ciphertext = encrypt(&derived_key, data_key, nonce)?;
167
168    if ciphertext.len() != ENCRYPTED_KEY_SIZE {
169        return Err(PromocryptError::EncryptionFailed(format!(
170            "Unexpected ciphertext length: {}",
171            ciphertext.len()
172        )));
173    }
174
175    let mut result = [0u8; ENCRYPTED_KEY_SIZE];
176    result.copy_from_slice(&ciphertext);
177    Ok(result)
178}
179
180/// Decrypt a 32-byte data key from storage.
181///
182/// # Arguments
183/// * `encrypted` - 48 bytes: encrypted key + tag
184/// * `password_or_machine_id` - Password/secret or machine ID to derive decryption key from
185/// * `salt` - Salt used during encryption
186/// * `nonce` - Nonce used during encryption
187///
188/// # Returns
189/// Decrypted 32-byte data key
190pub fn decrypt_data_key(
191    encrypted: &[u8; ENCRYPTED_KEY_SIZE],
192    password_or_machine_id: &[u8],
193    salt: &[u8; SALT_SIZE],
194    nonce: &[u8; NONCE_SIZE],
195) -> Result<[u8; 32]> {
196    let derived_key = derive_key(password_or_machine_id, salt)?;
197    let plaintext = decrypt(&derived_key, encrypted, nonce)?;
198
199    if plaintext.len() != 32 {
200        return Err(PromocryptError::DecryptionFailed);
201    }
202
203    let mut result = [0u8; 32];
204    result.copy_from_slice(&plaintext);
205    Ok(result)
206}
207
208/// Encrypt data with a data key (for config/mutable sections).
209///
210/// # Arguments
211/// * `data_key` - 32-byte data key
212/// * `plaintext` - Data to encrypt
213/// * `nonce` - 12-byte nonce
214///
215/// # Returns
216/// Ciphertext with appended tag
217pub fn encrypt_data(
218    data_key: &[u8; 32],
219    plaintext: &[u8],
220    nonce: &[u8; NONCE_SIZE],
221) -> Result<Vec<u8>> {
222    encrypt(data_key, plaintext, nonce)
223}
224
225/// Decrypt data with a data key.
226///
227/// # Arguments
228/// * `data_key` - 32-byte data key
229/// * `ciphertext` - Encrypted data with tag
230/// * `nonce` - 12-byte nonce used during encryption
231///
232/// # Returns
233/// Decrypted plaintext
234pub fn decrypt_data(
235    data_key: &[u8; 32],
236    ciphertext: &[u8],
237    nonce: &[u8; NONCE_SIZE],
238) -> Result<Vec<u8>> {
239    decrypt(data_key, ciphertext, nonce)
240}
241
242// ==================== Storage Encryption (Deterministic) ====================
243//
244// For database storage, we need deterministic encryption so that:
245// 1. Same code always encrypts to the same ciphertext (for lookups)
246// 2. The encryption is still authenticated
247//
248// We use AES-256-GCM with a synthetic nonce derived from the key and plaintext.
249// This is similar to AES-SIV but using GCM for the AEAD.
250
251/// Derive a deterministic nonce from the key and plaintext.
252///
253/// Uses HMAC-SHA256 truncated to 12 bytes.
254fn derive_deterministic_nonce(key: &[u8; 32], plaintext: &[u8]) -> [u8; NONCE_SIZE] {
255    // Create a synthetic IV using: SHA256(key || plaintext)
256    let mut input = Vec::with_capacity(32 + plaintext.len());
257    input.extend_from_slice(key);
258    input.extend_from_slice(plaintext);
259
260    let hash = digest(&SHA256, &input);
261    let mut nonce = [0u8; NONCE_SIZE];
262    nonce.copy_from_slice(&hash.as_ref()[..NONCE_SIZE]);
263    nonce
264}
265
266/// Encrypt a code for database storage (deterministic).
267///
268/// Uses deterministic encryption so the same code always produces the same
269/// ciphertext, allowing for database lookups without decryption.
270///
271/// # Arguments
272/// * `storage_key` - 32-byte storage encryption key
273/// * `code` - The promotional code to encrypt
274///
275/// # Returns
276/// Base64-encoded ciphertext (deterministic: same code = same output)
277pub fn encrypt_code_for_storage(storage_key: &[u8; 32], code: &str) -> Result<String> {
278    let plaintext = code.as_bytes();
279    let nonce = derive_deterministic_nonce(storage_key, plaintext);
280    let ciphertext = encrypt(storage_key, plaintext, &nonce)?;
281
282    // Include nonce in output for decryption (even though it's derivable)
283    // Format: base64(nonce || ciphertext)
284    let mut output = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
285    output.extend_from_slice(&nonce);
286    output.extend_from_slice(&ciphertext);
287
288    Ok(STANDARD.encode(&output))
289}
290
291/// Decrypt a code from database storage.
292///
293/// # Arguments
294/// * `storage_key` - 32-byte storage encryption key
295/// * `encrypted` - Base64-encoded ciphertext
296///
297/// # Returns
298/// The decrypted promotional code
299pub fn decrypt_code_from_storage(storage_key: &[u8; 32], encrypted: &str) -> Result<String> {
300    let data = STANDARD
301        .decode(encrypted)
302        .map_err(|_| PromocryptError::DecryptionFailed)?;
303
304    if data.len() < NONCE_SIZE + TAG_SIZE {
305        return Err(PromocryptError::DecryptionFailed);
306    }
307
308    let mut nonce = [0u8; NONCE_SIZE];
309    nonce.copy_from_slice(&data[..NONCE_SIZE]);
310
311    let ciphertext = &data[NONCE_SIZE..];
312    let plaintext = decrypt(storage_key, ciphertext, &nonce)?;
313
314    String::from_utf8(plaintext).map_err(|_| PromocryptError::DecryptionFailed)
315}
316
317/// Encrypt multiple codes for storage (batch operation).
318pub fn encrypt_codes_for_storage(storage_key: &[u8; 32], codes: &[&str]) -> Result<Vec<String>> {
319    codes
320        .iter()
321        .map(|code| encrypt_code_for_storage(storage_key, code))
322        .collect()
323}
324
325/// Decrypt multiple codes from storage (batch operation).
326pub fn decrypt_codes_from_storage(
327    storage_key: &[u8; 32],
328    encrypted: &[&str],
329) -> Result<Vec<String>> {
330    encrypted
331        .iter()
332        .map(|enc| decrypt_code_from_storage(storage_key, enc))
333        .collect()
334}
335
336/// Hash a secret for storage in history (using SHA256).
337pub fn hash_secret(secret: &str) -> String {
338    let hash = digest(&SHA256, secret.as_bytes());
339    hex::encode(hash.as_ref())
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_derive_key() {
348        let password = b"test-password";
349        let salt = [0u8; SALT_SIZE];
350
351        let key1 = derive_key(password, &salt).unwrap();
352        let key2 = derive_key(password, &salt).unwrap();
353
354        assert_eq!(key1, key2);
355        assert_eq!(key1.len(), 32);
356    }
357
358    #[test]
359    fn test_derive_key_different_inputs() {
360        let salt = [0u8; SALT_SIZE];
361
362        let key1 = derive_key(b"password1", &salt).unwrap();
363        let key2 = derive_key(b"password2", &salt).unwrap();
364
365        assert_ne!(key1, key2);
366    }
367
368    #[test]
369    fn test_derive_key_different_salts() {
370        let password = b"test-password";
371
372        let key1 = derive_key(password, &[0u8; SALT_SIZE]).unwrap();
373        let key2 = derive_key(password, &[1u8; SALT_SIZE]).unwrap();
374
375        assert_ne!(key1, key2);
376    }
377
378    #[test]
379    fn test_encrypt_decrypt_roundtrip() {
380        let key = generate_random_key().unwrap();
381        let nonce = generate_nonce().unwrap();
382        let plaintext = b"Hello, World!";
383
384        let ciphertext = encrypt(&key, plaintext, &nonce).unwrap();
385        let decrypted = decrypt(&key, &ciphertext, &nonce).unwrap();
386
387        assert_eq!(decrypted, plaintext);
388    }
389
390    #[test]
391    fn test_encrypt_decrypt_empty() {
392        let key = generate_random_key().unwrap();
393        let nonce = generate_nonce().unwrap();
394        let plaintext = b"";
395
396        let ciphertext = encrypt(&key, plaintext, &nonce).unwrap();
397        let decrypted = decrypt(&key, &ciphertext, &nonce).unwrap();
398
399        assert_eq!(decrypted, plaintext);
400    }
401
402    #[test]
403    fn test_decrypt_wrong_key() {
404        let key1 = generate_random_key().unwrap();
405        let key2 = generate_random_key().unwrap();
406        let nonce = generate_nonce().unwrap();
407        let plaintext = b"Secret data";
408
409        let ciphertext = encrypt(&key1, plaintext, &nonce).unwrap();
410        let result = decrypt(&key2, &ciphertext, &nonce);
411
412        assert!(result.is_err());
413    }
414
415    #[test]
416    fn test_decrypt_wrong_nonce() {
417        let key = generate_random_key().unwrap();
418        let nonce1 = generate_nonce().unwrap();
419        let nonce2 = generate_nonce().unwrap();
420        let plaintext = b"Secret data";
421
422        let ciphertext = encrypt(&key, plaintext, &nonce1).unwrap();
423        let result = decrypt(&key, &ciphertext, &nonce2);
424
425        assert!(result.is_err());
426    }
427
428    #[test]
429    fn test_data_key_encrypt_decrypt() {
430        let data_key = generate_random_key().unwrap();
431        let password = b"my-secret-password";
432        let salt = generate_salt().unwrap();
433        let nonce = generate_nonce().unwrap();
434
435        let encrypted = encrypt_data_key(&data_key, password, &salt, &nonce).unwrap();
436        let decrypted = decrypt_data_key(&encrypted, password, &salt, &nonce).unwrap();
437
438        assert_eq!(data_key, decrypted);
439    }
440
441    #[test]
442    fn test_data_key_wrong_password() {
443        let data_key = generate_random_key().unwrap();
444        let salt = generate_salt().unwrap();
445        let nonce = generate_nonce().unwrap();
446
447        let encrypted = encrypt_data_key(&data_key, b"correct-password", &salt, &nonce).unwrap();
448        let result = decrypt_data_key(&encrypted, b"wrong-password", &salt, &nonce);
449
450        assert!(result.is_err());
451    }
452
453    #[test]
454    fn test_generate_salt_unique() {
455        let salt1 = generate_salt().unwrap();
456        let salt2 = generate_salt().unwrap();
457
458        // Statistically should never be equal
459        assert_ne!(salt1, salt2);
460    }
461
462    #[test]
463    fn test_generate_nonce_unique() {
464        let nonce1 = generate_nonce().unwrap();
465        let nonce2 = generate_nonce().unwrap();
466
467        assert_ne!(nonce1, nonce2);
468    }
469
470    #[test]
471    fn test_ciphertext_size() {
472        let key = generate_random_key().unwrap();
473        let nonce = generate_nonce().unwrap();
474
475        // Test various plaintext sizes
476        for size in [0, 1, 16, 32, 100, 1000] {
477            let plaintext = vec![0u8; size];
478            let ciphertext = encrypt(&key, &plaintext, &nonce).unwrap();
479            assert_eq!(ciphertext.len(), size + TAG_SIZE);
480        }
481    }
482}