kasl/libs/secret.rs
1//! Secure credential storage and management with AES encryption.
2//!
3//! Provides functionality for securely storing and retrieving sensitive information
4//! such as passwords and API tokens using AES-256-CBC encryption.
5//!
6//! ## Features
7//!
8//! - **AES-256-CBC Encryption**: Industry-standard encryption for credential storage
9//! - **Compile-time Keys**: Encryption keys embedded during build process
10//! - **Secure Input**: Password prompting without echo to terminal
11//! - **File Protection**: Encrypted credentials stored in user data directory
12//! - **Memory Safety**: Credentials cleared from memory after use
13//!
14//! ## Usage
15//!
16//! ```rust
17//! use kasl::libs::secret::Secret;
18//!
19//! let secret = Secret::new(".jira_secret", "Enter your Jira password");
20//! let password = secret.get_or_prompt()?;
21//! let new_password = secret.prompt()?;
22//! ```
23
24use super::data_storage::DataStorage;
25use aes::Aes256;
26use anyhow::Result;
27use base64::prelude::*;
28use block_modes::block_padding::Pkcs7;
29use block_modes::{BlockMode, Cbc};
30use dialoguer::{theme::ColorfulTheme, Password};
31use std::fs::{self, File};
32use std::io::{Read, Write};
33use std::path::PathBuf;
34
35// Include generated metadata containing encryption keys
36// This file is created during the build process and contains
37// compile-time embedded encryption keys and initialization vectors
38include!(concat!(env!("OUT_DIR"), "/app_metadata.rs"));
39
40/// Type alias for AES-256-CBC cipher with PKCS7 padding.
41///
42/// This cipher configuration provides:
43/// - **AES-256**: Advanced Encryption Standard with 256-bit keys
44/// - **CBC Mode**: Cipher Block Chaining for secure block encryption
45/// - **PKCS7 Padding**: Standard padding scheme for block alignment
46type Aes256Cbc = Cbc<Aes256, Pkcs7>;
47
48/// Secure credential storage and management system.
49///
50/// This structure manages the complete lifecycle of sensitive credentials
51/// including user prompting, encryption, file storage, and decryption.
52/// It provides a high-level interface for secure credential handling.
53///
54/// ## Design Principles
55///
56/// - **Lazy Loading**: Passwords only decrypted when needed
57/// - **Immutable State**: Credential changes create new instances
58/// - **Error Recovery**: Graceful handling of encryption/decryption failures
59/// - **User Friendly**: Clear prompts and error messages
60///
61/// ## Internal State
62///
63/// The struct maintains both encrypted and decrypted state:
64/// - File-based encrypted storage for persistence
65/// - Memory-based decrypted storage for immediate use
66/// - Cryptographic keys for encryption/decryption operations
67///
68/// ## Lifecycle
69///
70/// 1. **Creation**: Initialize with file path and user prompt
71/// 2. **Retrieval**: Check for existing encrypted file
72/// 3. **Prompting**: Secure password input when needed
73/// 4. **Encryption**: AES encryption before file storage
74/// 5. **Decryption**: AES decryption when retrieving
75#[derive(Clone, Debug)]
76pub struct Secret {
77 /// Optional in-memory password storage.
78 ///
79 /// When present, contains the decrypted password for immediate use.
80 /// This avoids repeated decryption operations but increases memory
81 /// exposure time. Cleared when instance is dropped.
82 password: Option<String>,
83
84 /// User-facing prompt text for password input.
85 ///
86 /// Displayed when prompting for credentials through the terminal.
87 /// Should be descriptive and indicate which service needs authentication.
88 /// Examples: "Enter your Jira password", "GitLab API token"
89 prompt: String,
90
91 /// File system path for encrypted credential storage.
92 ///
93 /// Points to the location where encrypted credentials are stored.
94 /// Typically in the user's application data directory with a
95 /// service-specific filename (e.g., ".jira_secret").
96 secret_file_path: PathBuf,
97
98 /// AES encryption key for credential protection.
99 ///
100 /// 256-bit key used for AES encryption/decryption operations.
101 /// Embedded at compile time from build environment or defaults.
102 /// Should be kept consistent across application versions.
103 key: Vec<u8>,
104
105 /// Initialization vector for AES-CBC encryption.
106 ///
107 /// Fixed IV used with AES-CBC mode for deterministic encryption.
108 /// While using a fixed IV reduces security, it allows for consistent
109 /// file-based storage without additional key derivation complexity.
110 iv: Vec<u8>,
111}
112
113impl Secret {
114 /// Creates a new Secret instance for credential management.
115 ///
116 /// This constructor initializes a new secret manager with the specified
117 /// file storage location and user prompt text. It loads encryption keys
118 /// from compile-time embedded metadata and prepares the file path within
119 /// the application's data directory.
120 ///
121 /// ## Key Management
122 ///
123 /// Encryption keys are loaded from compile-time metadata:
124 /// - Keys embedded during build process for security
125 /// - Consistent keys across application installations
126 /// - No runtime key generation or derivation needed
127 ///
128 /// ## File Path Resolution
129 ///
130 /// The secret file path is resolved using the application's data storage:
131 /// - Platform-appropriate user data directory
132 /// - Service-specific filename for credential isolation
133 /// - Automatic directory creation when needed
134 ///
135 /// # Arguments
136 ///
137 /// * `secret_name` - Filename for storing encrypted credentials (e.g., ".jira_secret")
138 /// * `prompt` - User-facing text for password prompts (e.g., "Enter your Jira password")
139 ///
140 /// # Returns
141 ///
142 /// A new Secret instance ready for credential operations.
143 ///
144 /// # Examples
145 ///
146 /// ```rust
147 /// use kasl::libs::secret::Secret;
148 ///
149 /// // Jira password management
150 /// let jira_secret = Secret::new(".jira_secret", "Enter your Jira password");
151 ///
152 /// // GitLab token management
153 /// let gitlab_secret = Secret::new(".gitlab_token", "Enter your GitLab API token");
154 ///
155 /// // SI Server credentials
156 /// let si_secret = Secret::new(".si_credentials", "Enter your SI Server password");
157 /// ```
158 ///
159 /// # Error Handling
160 ///
161 /// Path resolution errors are handled gracefully by falling back to
162 /// the current directory if the data storage path cannot be created.
163 pub fn new(secret_name: &str, prompt: &str) -> Self {
164 // Load compile-time embedded encryption keys
165 let key = APP_METADATA_ENCRYPTION_KEY.to_vec();
166 let iv = APP_METADATA_ENCRYPTION_IV.to_vec();
167
168 // Resolve secret file path in application data directory
169 let secret_file_path = DataStorage::new().get_path(secret_name).unwrap_or_else(|_| PathBuf::from(secret_name));
170
171 Self {
172 password: None,
173 secret_file_path,
174 prompt: prompt.to_owned(),
175 key,
176 iv,
177 }
178 }
179
180 /// Creates a new Secret instance with the specified password.
181 ///
182 /// This internal method creates a copy of the current Secret with
183 /// a different password value. Used for maintaining immutable state
184 /// while updating the in-memory password storage.
185 ///
186 /// # Arguments
187 ///
188 /// * `password` - The password to store in the new instance
189 ///
190 /// # Returns
191 ///
192 /// A new Secret instance with the updated password.
193 fn set_password(&self, password: &str) -> Self {
194 Self {
195 password: Some(password.to_owned()),
196 ..self.clone()
197 }
198 }
199
200 /// Retrieves password from cache or prompts user if not available.
201 ///
202 /// This method implements the primary credential retrieval logic:
203 /// 1. Check if encrypted file exists and is readable
204 /// 2. Attempt to decrypt existing credentials
205 /// 3. Prompt user for new credentials if decryption fails
206 /// 4. Encrypt and store new credentials for future use
207 ///
208 /// ## Caching Behavior
209 ///
210 /// - **Cache Hit**: Return decrypted password from file
211 /// - **Cache Miss**: Prompt user and store new password
212 /// - **Decryption Error**: Re-prompt user (file may be corrupted)
213 ///
214 /// ## Error Recovery
215 ///
216 /// If decryption fails (corrupted file, wrong keys, etc.), the method
217 /// gracefully falls back to prompting the user for new credentials.
218 /// This ensures the application can recover from storage corruption.
219 ///
220 /// # Returns
221 ///
222 /// Returns the user's password, either from cache or fresh input.
223 ///
224 /// # Errors
225 ///
226 /// Returns an error if:
227 /// - User cancels password prompt
228 /// - File system operations fail
229 /// - Encryption operations fail
230 ///
231 /// # Examples
232 ///
233 /// ```rust
234 /// use kasl::libs::secret::Secret;
235 ///
236 /// let secret = Secret::new(".api_token", "Enter API token");
237 ///
238 /// // First call prompts user and caches result
239 /// let token = secret.get_or_prompt()?;
240 ///
241 /// // Subsequent calls use cached value
242 /// let same_token = secret.get_or_prompt()?;
243 /// ```
244 pub fn get_or_prompt(&self) -> Result<String> {
245 // Check if encrypted credentials file exists
246 if fs::metadata(&self.secret_file_path).is_ok() {
247 // Attempt to decrypt existing credentials
248 if let Ok(password) = self.decrypt() {
249 return Ok(password);
250 }
251 // Decryption failed - file may be corrupted, continue to prompt
252 }
253
254 // No cached credentials or decryption failed - prompt user
255 self.prompt()
256 }
257
258 /// Prompts user for password and stores it securely.
259 ///
260 /// This method handles the complete password input and storage workflow:
261 /// 1. Display secure password prompt (no echo)
262 /// 2. Encrypt the entered password
263 /// 3. Store encrypted data to file
264 /// 4. Return the entered password
265 ///
266 /// ## Security Features
267 ///
268 /// - **No Echo**: Password characters not displayed on screen
269 /// - **Immediate Encryption**: Password encrypted before file storage
270 /// - **Memory Clearing**: Original password cleared after encryption
271 ///
272 /// ## User Experience
273 ///
274 /// The prompt uses a colorful theme for better visibility and
275 /// provides clear instructions to the user. Password input is
276 /// handled securely without displaying characters.
277 ///
278 /// # Returns
279 ///
280 /// Returns the password entered by the user.
281 ///
282 /// # Errors
283 ///
284 /// Returns an error if:
285 /// - User cancels password input (Ctrl+C)
286 /// - Password encryption fails
287 /// - File system write operations fail
288 ///
289 /// # Examples
290 ///
291 /// ```rust
292 /// use kasl::libs::secret::Secret;
293 ///
294 /// let secret = Secret::new(".password", "Enter your password");
295 ///
296 /// // Force password prompt (ignores cache)
297 /// let password = secret.prompt()?;
298 /// ```
299 pub fn prompt(&self) -> Result<String> {
300 // Display secure password prompt
301 let password = Password::with_theme(&ColorfulTheme::default()).with_prompt(&self.prompt).interact()?;
302
303 // Encrypt and store the password
304 self.set_password(&password).encrypt()?;
305
306 Ok(password)
307 }
308
309 /// Encrypts the stored password and saves it to file.
310 ///
311 /// This method performs the complete encryption and storage workflow:
312 /// 1. Initialize AES-256-CBC cipher with embedded keys
313 /// 2. Encrypt the password using PKCS7 padding
314 /// 3. Encode encrypted data as Base64 for safe storage
315 /// 4. Write encoded data to the secret file
316 ///
317 /// ## Encryption Process
318 ///
319 /// - **Input**: Plain text password from memory
320 /// - **Cipher**: AES-256-CBC with compile-time keys
321 /// - **Padding**: PKCS7 for block alignment
322 /// - **Encoding**: Base64 for text-safe storage
323 /// - **Output**: Encrypted file in application data directory
324 ///
325 /// ## File System Operations
326 ///
327 /// - Creates parent directories if they don't exist
328 /// - Overwrites existing credential files
329 /// - Uses platform-appropriate file permissions
330 ///
331 /// # Returns
332 ///
333 /// Returns a new Secret instance for method chaining.
334 ///
335 /// # Errors
336 ///
337 /// Returns an error if:
338 /// - No password is set in memory
339 /// - AES encryption fails
340 /// - Base64 encoding fails
341 /// - File system write operations fail
342 ///
343 /// # Examples
344 ///
345 /// ```rust
346 /// let secret = Secret::new(".test", "Test password")
347 /// .set_password("my_secret")
348 /// .encrypt()?;
349 /// ```
350 fn encrypt(&self) -> Result<Self> {
351 // Initialize AES cipher with embedded keys
352 let cipher = Aes256Cbc::new_from_slices(&self.key, &self.iv)?;
353
354 // Get password from memory
355 let password = &self.password.clone().unwrap();
356
357 // Encrypt password with PKCS7 padding
358 let ciphertext = cipher.encrypt_vec(&password.as_bytes());
359
360 // Encode as Base64 for safe file storage
361 let encoded = BASE64_STANDARD.encode(&ciphertext);
362
363 // Ensure parent directory exists
364 if let Some(parent) = self.secret_file_path.parent() {
365 let _ = fs::create_dir_all(parent);
366 }
367
368 // Write encrypted data to file
369 let mut file = File::create(&self.secret_file_path)?;
370 file.write_all(encoded.as_bytes())?;
371
372 Ok(self.clone())
373 }
374
375 /// Decrypts stored credentials from file.
376 ///
377 /// This method performs the complete decryption workflow:
378 /// 1. Read Base64-encoded data from credential file
379 /// 2. Decode Base64 to get raw encrypted bytes
380 /// 3. Initialize AES cipher with embedded keys
381 /// 4. Decrypt data and remove PKCS7 padding
382 /// 5. Convert decrypted bytes to UTF-8 string
383 ///
384 /// ## Decryption Process
385 ///
386 /// - **Input**: Base64-encoded encrypted file
387 /// - **Decoding**: Base64 to raw encrypted bytes
388 /// - **Cipher**: AES-256-CBC with compile-time keys
389 /// - **Padding**: PKCS7 removal for original data
390 /// - **Output**: Plain text password string
391 ///
392 /// ## Error Recovery
393 ///
394 /// The method handles various failure modes:
395 /// - **File Not Found**: Returns error for missing credentials
396 /// - **Invalid Base64**: Returns error for corrupted encoding
397 /// - **Decryption Failure**: Returns error for wrong keys or corrupted data
398 /// - **Invalid UTF-8**: Returns error for corrupted password data
399 ///
400 /// # Returns
401 ///
402 /// Returns the decrypted password as a string.
403 ///
404 /// # Errors
405 ///
406 /// Returns an error if:
407 /// - Credential file doesn't exist or can't be read
408 /// - Base64 decoding fails (corrupted file)
409 /// - AES decryption fails (wrong keys, corrupted data)
410 /// - Decrypted data is not valid UTF-8
411 ///
412 /// # Examples
413 ///
414 /// ```rust
415 /// let secret = Secret::new(".existing_secret", "Password");
416 ///
417 /// match secret.decrypt() {
418 /// Ok(password) => println!("Retrieved cached password"),
419 /// Err(_) => println!("No cached password or decryption failed"),
420 /// }
421 /// ```
422 fn decrypt(&self) -> Result<String> {
423 // Read Base64-encoded data from file
424 let mut file = File::open(&self.secret_file_path)?;
425 let mut encoded = String::new();
426 file.read_to_string(&mut encoded)?;
427
428 // Decode Base64 to get encrypted bytes
429 let ciphertext = BASE64_STANDARD.decode(encoded)?;
430
431 // Initialize AES cipher with embedded keys
432 let cipher = Aes256Cbc::new_from_slices(&self.key, &self.iv)?;
433
434 // Decrypt data and remove padding
435 let decrypted_ciphertext = cipher.decrypt_vec(&ciphertext)?;
436
437 // Convert decrypted bytes to UTF-8 string
438 let decrypted_password = String::from_utf8(decrypted_ciphertext)?;
439
440 Ok(decrypted_password)
441 }
442}