licenz_core/
encrypted_store.rs1use crate::error::{LicenseError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9use zeroize::Zeroizing;
10
11pub const MIN_PASSPHRASE_LENGTH: usize = 12;
13
14pub const ENCRYPTED_STORE_VERSION: u8 = 1;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct EncryptedKeyStore {
20 encrypted_data: Vec<u8>,
22 nonce: [u8; 12],
24 salt: [u8; 32],
26 version: u8,
28}
29
30impl EncryptedKeyStore {
31 pub fn encrypt(private_key_pem: &str, passphrase: &str) -> Result<Self> {
35 if passphrase.len() < MIN_PASSPHRASE_LENGTH {
37 return Err(LicenseError::InvalidKeyFormat(format!(
38 "Passphrase must be at least {} characters",
39 MIN_PASSPHRASE_LENGTH
40 )));
41 }
42
43 let salt: [u8; 32] = rand::random();
45 let nonce: [u8; 12] = rand::random();
46
47 let derived_key = derive_key(passphrase.as_bytes(), &salt)?;
48
49 let encrypted_data = encrypt_aes_gcm(private_key_pem.as_bytes(), &derived_key, &nonce)?;
51
52 Ok(Self {
53 encrypted_data,
54 nonce,
55 salt,
56 version: ENCRYPTED_STORE_VERSION,
57 })
58 }
59
60 pub fn decrypt(&self, passphrase: &str) -> Result<String> {
62 if self.version != ENCRYPTED_STORE_VERSION {
63 return Err(LicenseError::InvalidKeyFormat(format!(
64 "Unsupported encrypted key store version: {} (expected {})",
65 self.version, ENCRYPTED_STORE_VERSION
66 )));
67 }
68
69 let derived_key = derive_key(passphrase.as_bytes(), &self.salt)?;
70
71 let decrypted =
73 decrypt_aes_gcm(&self.encrypted_data, &derived_key, &self.nonce).map_err(|_| {
74 LicenseError::InvalidKeyFormat(
75 "Decryption failed - incorrect passphrase or corrupted data".into(),
76 )
77 })?;
78
79 String::from_utf8(decrypted).map_err(|e| LicenseError::InvalidKeyFormat(e.to_string()))
80 }
81
82 pub fn save(&self, path: &Path) -> Result<()> {
84 let data = bincode::serde::encode_to_vec(self, bincode::config::standard())
85 .map_err(|e| LicenseError::SerializationError(e.to_string()))?;
86
87 std::fs::write(path, data)?;
88
89 #[cfg(unix)]
91 {
92 use std::os::unix::fs::PermissionsExt;
93 let perms = std::fs::Permissions::from_mode(0o600);
94 std::fs::set_permissions(path, perms)?;
95 }
96
97 Ok(())
98 }
99
100 pub fn load(path: &Path) -> Result<Self> {
102 let data = std::fs::read(path)?;
103 let (store, _len) = bincode::serde::decode_from_slice(&data, bincode::config::standard())
104 .map_err(|e| LicenseError::InvalidKeyFormat(e.to_string()))?;
105 Ok(store)
106 }
107
108 pub fn backup_key_file(
110 private_key_path: &Path,
111 backup_path: &Path,
112 passphrase: &str,
113 ) -> Result<()> {
114 let pem = std::fs::read_to_string(private_key_path)?;
115 let store = Self::encrypt(&pem, passphrase)?;
116 store.save(backup_path)?;
117 Ok(())
118 }
119
120 pub fn restore_key_file(
122 backup_path: &Path,
123 private_key_path: &Path,
124 passphrase: &str,
125 ) -> Result<()> {
126 let store = Self::load(backup_path)?;
127 let pem = store.decrypt(passphrase)?;
128
129 std::fs::write(private_key_path, &pem)?;
130
131 #[cfg(unix)]
133 {
134 use std::os::unix::fs::PermissionsExt;
135 let perms = std::fs::Permissions::from_mode(0o600);
136 std::fs::set_permissions(private_key_path, perms)?;
137 }
138
139 Ok(())
140 }
141}
142
143fn derive_key(passphrase: &[u8], salt: &[u8; 32]) -> Result<Zeroizing<[u8; 32]>> {
145 use argon2::{Algorithm, Argon2, Params, Version};
146
147 let params = Params::new(19_456, 2, 1, Some(32))
148 .map_err(|e| LicenseError::KeyGenerationFailed(format!("Argon2 params: {}", e)))?;
149 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
150 let mut key = Zeroizing::new([0u8; 32]);
151 argon2
152 .hash_password_into(passphrase, salt.as_slice(), key.as_mut())
153 .map_err(|e| LicenseError::KeyGenerationFailed(format!("Argon2id: {}", e)))?;
154 Ok(key)
155}
156
157fn encrypt_aes_gcm(
159 plaintext: &[u8],
160 key: &Zeroizing<[u8; 32]>,
161 nonce: &[u8; 12],
162) -> Result<Vec<u8>> {
163 use aes_gcm::{
164 aead::{Aead, KeyInit},
165 Aes256Gcm, Nonce,
166 };
167
168 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
169 .map_err(|e| LicenseError::KeyGenerationFailed(e.to_string()))?;
170
171 let nonce = Nonce::from_slice(nonce);
172
173 cipher
174 .encrypt(nonce, plaintext)
175 .map_err(|e| LicenseError::KeyGenerationFailed(e.to_string()))
176}
177
178fn decrypt_aes_gcm(
180 ciphertext: &[u8],
181 key: &Zeroizing<[u8; 32]>,
182 nonce: &[u8; 12],
183) -> std::result::Result<Vec<u8>, ()> {
184 use aes_gcm::{
185 aead::{Aead, KeyInit},
186 Aes256Gcm, Nonce,
187 };
188
189 let cipher = Aes256Gcm::new_from_slice(key.as_ref()).map_err(|_| ())?;
190 let nonce = Nonce::from_slice(nonce);
191
192 cipher.decrypt(nonce, ciphertext).map_err(|_| ())
193}
194
195pub fn validate_passphrase(passphrase: &str) -> std::result::Result<(), Vec<&'static str>> {
197 let mut errors = Vec::new();
198
199 if passphrase.len() < MIN_PASSPHRASE_LENGTH {
200 errors.push("Passphrase must be at least 12 characters");
201 }
202
203 if !passphrase.chars().any(|c| c.is_uppercase()) {
204 errors.push("Passphrase should contain at least one uppercase letter");
205 }
206
207 if !passphrase.chars().any(|c| c.is_lowercase()) {
208 errors.push("Passphrase should contain at least one lowercase letter");
209 }
210
211 if !passphrase.chars().any(|c| c.is_numeric()) {
212 errors.push("Passphrase should contain at least one number");
213 }
214
215 if errors.is_empty() {
216 Ok(())
217 } else {
218 Err(errors)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use tempfile::TempDir;
226
227 #[test]
228 fn test_encrypt_decrypt_round_trip() {
229 let original = "-----BEGIN PRIVATE KEY-----\ntest key content\n-----END PRIVATE KEY-----";
230 let passphrase = "SecurePass123!";
231
232 let store = EncryptedKeyStore::encrypt(original, passphrase).unwrap();
233 assert_eq!(store.version, ENCRYPTED_STORE_VERSION);
234 let decrypted = store.decrypt(passphrase).unwrap();
235
236 assert_eq!(original, decrypted);
237 }
238
239 #[test]
240 fn test_wrong_passphrase_fails() {
241 let original = "test key content";
242 let passphrase = "SecurePass123!";
243
244 let store = EncryptedKeyStore::encrypt(original, passphrase).unwrap();
245 let result = store.decrypt("WrongPassword1!");
246
247 assert!(result.is_err());
248 }
249
250 #[test]
251 fn test_passphrase_too_short() {
252 let result = EncryptedKeyStore::encrypt("key", "short");
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn test_file_round_trip() {
258 let temp_dir = TempDir::new().unwrap();
259 let backup_path = temp_dir.path().join("key.backup");
260
261 let original = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
262 let passphrase = "SecurePass123!";
263
264 let store = EncryptedKeyStore::encrypt(original, passphrase).unwrap();
265 store.save(&backup_path).unwrap();
266
267 let loaded = EncryptedKeyStore::load(&backup_path).unwrap();
268 let decrypted = loaded.decrypt(passphrase).unwrap();
269
270 assert_eq!(original, decrypted);
271 }
272
273 #[test]
274 fn test_validate_passphrase() {
275 assert!(validate_passphrase("SecurePass123!").is_ok());
276 assert!(validate_passphrase("short").is_err());
277 assert!(validate_passphrase("alllowercase123").is_err());
278 }
279}