git_crypt/
key.rs

1//! # Key Management
2//!
3//! This module handles encryption key storage, import, export, and lifecycle management.
4//!
5//! ## Key Storage
6//!
7//! Keys are stored in the git repository's internal directory:
8//! - **Default key path**: `.git/git-crypt/keys/default`
9//! - **Format**: Raw 32-byte binary data
10//! - **Permissions**: 0600 on Unix (owner read/write only)
11//! - **Never committed**: Keys stay in `.git/` directory
12//!
13//! ## Key Operations
14//!
15//! - **Generate**: Create new random 256-bit key
16//! - **Save/Load**: Persist keys to/from filesystem
17//! - **Export**: Save key to file for sharing
18//! - **Import**: Load key from shared file
19//!
20//! ## Security Considerations
21//!
22//! - Keys are stored unencrypted in `.git/git-crypt/`
23//! - File permissions are restricted to owner only (Unix)
24//! - Exported key files must be shared securely
25//! - Consider using GPG for team key distribution
26//!
27//! ## Unit Tests
28//!
29//! Run key management tests:
30//! ```bash
31//! cargo test key::
32//! ```
33//!
34//! Tests cover:
35//! - Directory path resolution
36//! - Initialization and duplicate detection
37//! - Key generation and persistence
38//! - Export and import workflows
39//! - File permissions (Unix)
40//! - Error handling for missing files
41
42use crate::crypto::CryptoKey;
43use crate::error::{GitCryptError, Result};
44use std::fs::{self, File};
45use std::io::{Read, Write};
46use std::path::{Path, PathBuf};
47
48/// Key storage and management
49pub struct KeyManager {
50    git_dir: PathBuf,
51}
52
53impl KeyManager {
54    pub fn new(git_dir: impl AsRef<Path>) -> Self {
55        Self {
56            git_dir: git_dir.as_ref().to_path_buf(),
57        }
58    }
59
60    /// Get the path to the git-crypt directory
61    pub fn git_crypt_dir(&self) -> PathBuf {
62        self.git_dir.join("git-crypt")
63    }
64
65    /// Get the path to the default key file
66    pub fn default_key_path(&self) -> PathBuf {
67        self.git_crypt_dir().join("keys").join("default")
68    }
69
70    /// Initialize the git-crypt directory structure
71    pub fn init_dirs(&self) -> Result<()> {
72        let git_crypt_dir = self.git_crypt_dir();
73        if git_crypt_dir.exists() {
74            return Err(GitCryptError::AlreadyInitialized);
75        }
76
77        fs::create_dir_all(&git_crypt_dir)?;
78        fs::create_dir(git_crypt_dir.join("keys"))?;
79
80        Ok(())
81    }
82
83    /// Check if repository is initialized
84    pub fn is_initialized(&self) -> bool {
85        self.git_crypt_dir().exists()
86    }
87
88    /// Generate and save a new key
89    pub fn generate_key(&self) -> Result<CryptoKey> {
90        let key = CryptoKey::generate();
91        self.save_key(&key)?;
92        Ok(key)
93    }
94
95    /// Save a key to disk
96    pub fn save_key(&self, key: &CryptoKey) -> Result<()> {
97        let key_path = self.default_key_path();
98        fs::create_dir_all(key_path.parent().unwrap())?;
99
100        let mut file = File::create(&key_path)?;
101        file.write_all(key.as_bytes())?;
102
103        // Set restrictive permissions (Unix only)
104        #[cfg(unix)]
105        {
106            use std::os::unix::fs::PermissionsExt;
107            let mut perms = fs::metadata(&key_path)?.permissions();
108            perms.set_mode(0o600);
109            fs::set_permissions(&key_path, perms)?;
110        }
111
112        Ok(())
113    }
114
115    /// Load the key from disk
116    pub fn load_key(&self) -> Result<CryptoKey> {
117        let key_path = self.default_key_path();
118
119        if !key_path.exists() {
120            return Err(GitCryptError::KeyNotFound("default".into()));
121        }
122
123        let mut file = File::open(&key_path)?;
124        let mut key_bytes = Vec::new();
125        file.read_to_end(&mut key_bytes)?;
126
127        CryptoKey::from_bytes(&key_bytes)
128    }
129
130    /// Export key to a file
131    pub fn export_key(&self, output_path: impl AsRef<Path>) -> Result<()> {
132        let key = self.load_key()?;
133        let mut file = File::create(output_path.as_ref())?;
134        file.write_all(key.as_bytes())?;
135
136        #[cfg(unix)]
137        {
138            use std::os::unix::fs::PermissionsExt;
139            let mut perms = fs::metadata(output_path.as_ref())?.permissions();
140            perms.set_mode(0o600);
141            fs::set_permissions(output_path.as_ref(), perms)?;
142        }
143
144        Ok(())
145    }
146
147    /// Import key from a file
148    pub fn import_key(&self, input_path: impl AsRef<Path>) -> Result<()> {
149        let mut file = File::open(input_path)?;
150        let mut key_bytes = Vec::new();
151        file.read_to_end(&mut key_bytes)?;
152
153        let key = CryptoKey::from_bytes(&key_bytes)?;
154        self.save_key(&key)?;
155
156        Ok(())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use tempfile::TempDir;
164
165    fn create_test_git_dir() -> TempDir {
166        TempDir::new().unwrap()
167    }
168
169    #[test]
170    fn test_git_crypt_dir_path() {
171        let temp = create_test_git_dir();
172        let key_manager = KeyManager::new(temp.path());
173
174        let expected = temp.path().join("git-crypt");
175        assert_eq!(key_manager.git_crypt_dir(), expected);
176    }
177
178    #[test]
179    fn test_default_key_path() {
180        let temp = create_test_git_dir();
181        let key_manager = KeyManager::new(temp.path());
182
183        let expected = temp.path().join("git-crypt").join("keys").join("default");
184        assert_eq!(key_manager.default_key_path(), expected);
185    }
186
187    #[test]
188    fn test_is_initialized_false() {
189        let temp = create_test_git_dir();
190        let key_manager = KeyManager::new(temp.path());
191
192        assert!(!key_manager.is_initialized());
193    }
194
195    #[test]
196    fn test_init_dirs() {
197        let temp = create_test_git_dir();
198        let key_manager = KeyManager::new(temp.path());
199
200        key_manager.init_dirs().unwrap();
201
202        assert!(key_manager.git_crypt_dir().exists());
203        assert!(key_manager.git_crypt_dir().join("keys").exists());
204        assert!(key_manager.is_initialized());
205    }
206
207    #[test]
208    fn test_init_dirs_twice_fails() {
209        let temp = create_test_git_dir();
210        let key_manager = KeyManager::new(temp.path());
211
212        key_manager.init_dirs().unwrap();
213        let result = key_manager.init_dirs();
214
215        assert!(result.is_err());
216        assert!(matches!(
217            result.unwrap_err(),
218            GitCryptError::AlreadyInitialized
219        ));
220    }
221
222    #[test]
223    fn test_generate_and_load_key() {
224        let temp = create_test_git_dir();
225        let key_manager = KeyManager::new(temp.path());
226
227        key_manager.init_dirs().unwrap();
228        let key1 = key_manager.generate_key().unwrap();
229        let key2 = key_manager.load_key().unwrap();
230
231        // Keys should be the same
232        assert_eq!(key1.as_bytes(), key2.as_bytes());
233    }
234
235    #[test]
236    fn test_load_key_before_init_fails() {
237        let temp = create_test_git_dir();
238        let key_manager = KeyManager::new(temp.path());
239
240        let result = key_manager.load_key();
241        assert!(result.is_err());
242    }
243
244    #[test]
245    fn test_save_and_load_key() {
246        let temp = create_test_git_dir();
247        let key_manager = KeyManager::new(temp.path());
248
249        key_manager.init_dirs().unwrap();
250
251        let original_key = CryptoKey::generate();
252        key_manager.save_key(&original_key).unwrap();
253
254        let loaded_key = key_manager.load_key().unwrap();
255        assert_eq!(original_key.as_bytes(), loaded_key.as_bytes());
256    }
257
258    #[test]
259    fn test_export_and_import_key() {
260        let temp = create_test_git_dir();
261        let key_manager = KeyManager::new(temp.path());
262
263        key_manager.init_dirs().unwrap();
264        let original_key = key_manager.generate_key().unwrap();
265
266        let export_path = temp.path().join("exported.key");
267        key_manager.export_key(&export_path).unwrap();
268
269        // Verify export file exists
270        assert!(export_path.exists());
271
272        // Create new key manager for import test
273        let temp2 = create_test_git_dir();
274        let key_manager2 = KeyManager::new(temp2.path());
275        key_manager2.init_dirs().unwrap();
276
277        // Import the key
278        key_manager2.import_key(&export_path).unwrap();
279        let imported_key = key_manager2.load_key().unwrap();
280
281        // Keys should match
282        assert_eq!(original_key.as_bytes(), imported_key.as_bytes());
283    }
284
285    #[test]
286    fn test_export_key_without_init_fails() {
287        let temp = create_test_git_dir();
288        let key_manager = KeyManager::new(temp.path());
289
290        let export_path = temp.path().join("exported.key");
291        let result = key_manager.export_key(&export_path);
292
293        assert!(result.is_err());
294    }
295
296    #[test]
297    fn test_import_invalid_key_file() {
298        let temp = create_test_git_dir();
299        let key_manager = KeyManager::new(temp.path());
300        key_manager.init_dirs().unwrap();
301
302        // Create invalid key file (wrong size)
303        let invalid_key_path = temp.path().join("invalid.key");
304        fs::write(&invalid_key_path, b"too short").unwrap();
305
306        let result = key_manager.import_key(&invalid_key_path);
307        assert!(result.is_err());
308    }
309
310    #[test]
311    fn test_import_nonexistent_file() {
312        let temp = create_test_git_dir();
313        let key_manager = KeyManager::new(temp.path());
314        key_manager.init_dirs().unwrap();
315
316        let result = key_manager.import_key("/nonexistent/path.key");
317        assert!(result.is_err());
318    }
319
320    #[test]
321    fn test_key_file_permissions_unix() {
322        #[cfg(unix)]
323        {
324            use std::os::unix::fs::PermissionsExt;
325
326            let temp = create_test_git_dir();
327            let key_manager = KeyManager::new(temp.path());
328            key_manager.init_dirs().unwrap();
329
330            key_manager.generate_key().unwrap();
331
332            let key_path = key_manager.default_key_path();
333            let metadata = fs::metadata(&key_path).unwrap();
334            let permissions = metadata.permissions();
335
336            // Should be 0600 (owner read/write only)
337            assert_eq!(permissions.mode() & 0o777, 0o600);
338        }
339    }
340
341    #[test]
342    fn test_multiple_save_overwrites() {
343        let temp = create_test_git_dir();
344        let key_manager = KeyManager::new(temp.path());
345        key_manager.init_dirs().unwrap();
346
347        let key1 = CryptoKey::generate();
348        key_manager.save_key(&key1).unwrap();
349
350        let key2 = CryptoKey::generate();
351        key_manager.save_key(&key2).unwrap();
352
353        let loaded = key_manager.load_key().unwrap();
354
355        // Should have the second key
356        assert_eq!(key2.as_bytes(), loaded.as_bytes());
357        assert_ne!(key1.as_bytes(), loaded.as_bytes());
358    }
359
360    #[test]
361    fn test_key_survives_encrypt_decrypt() {
362        let temp = create_test_git_dir();
363        let key_manager = KeyManager::new(temp.path());
364        key_manager.init_dirs().unwrap();
365
366        let key = key_manager.generate_key().unwrap();
367        let plaintext = b"Secret data";
368
369        let ciphertext = key.encrypt(plaintext).unwrap();
370
371        // Load key again and decrypt
372        let loaded_key = key_manager.load_key().unwrap();
373        let decrypted = loaded_key.decrypt(&ciphertext).unwrap();
374
375        assert_eq!(plaintext.as_slice(), &decrypted[..]);
376    }
377}