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 std::fs::{self, File};
43use std::io::{Read, Write};
44use std::path::{Path, PathBuf};
45use crate::crypto::CryptoKey;
46use crate::error::{GitCryptError, Result};
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!(result.unwrap_err(), GitCryptError::AlreadyInitialized));
217    }
218
219    #[test]
220    fn test_generate_and_load_key() {
221        let temp = create_test_git_dir();
222        let key_manager = KeyManager::new(temp.path());
223
224        key_manager.init_dirs().unwrap();
225        let key1 = key_manager.generate_key().unwrap();
226        let key2 = key_manager.load_key().unwrap();
227
228        // Keys should be the same
229        assert_eq!(key1.as_bytes(), key2.as_bytes());
230    }
231
232    #[test]
233    fn test_load_key_before_init_fails() {
234        let temp = create_test_git_dir();
235        let key_manager = KeyManager::new(temp.path());
236
237        let result = key_manager.load_key();
238        assert!(result.is_err());
239    }
240
241    #[test]
242    fn test_save_and_load_key() {
243        let temp = create_test_git_dir();
244        let key_manager = KeyManager::new(temp.path());
245
246        key_manager.init_dirs().unwrap();
247
248        let original_key = CryptoKey::generate();
249        key_manager.save_key(&original_key).unwrap();
250
251        let loaded_key = key_manager.load_key().unwrap();
252        assert_eq!(original_key.as_bytes(), loaded_key.as_bytes());
253    }
254
255    #[test]
256    fn test_export_and_import_key() {
257        let temp = create_test_git_dir();
258        let key_manager = KeyManager::new(temp.path());
259
260        key_manager.init_dirs().unwrap();
261        let original_key = key_manager.generate_key().unwrap();
262
263        let export_path = temp.path().join("exported.key");
264        key_manager.export_key(&export_path).unwrap();
265
266        // Verify export file exists
267        assert!(export_path.exists());
268
269        // Create new key manager for import test
270        let temp2 = create_test_git_dir();
271        let key_manager2 = KeyManager::new(temp2.path());
272        key_manager2.init_dirs().unwrap();
273
274        // Import the key
275        key_manager2.import_key(&export_path).unwrap();
276        let imported_key = key_manager2.load_key().unwrap();
277
278        // Keys should match
279        assert_eq!(original_key.as_bytes(), imported_key.as_bytes());
280    }
281
282    #[test]
283    fn test_export_key_without_init_fails() {
284        let temp = create_test_git_dir();
285        let key_manager = KeyManager::new(temp.path());
286
287        let export_path = temp.path().join("exported.key");
288        let result = key_manager.export_key(&export_path);
289
290        assert!(result.is_err());
291    }
292
293    #[test]
294    fn test_import_invalid_key_file() {
295        let temp = create_test_git_dir();
296        let key_manager = KeyManager::new(temp.path());
297        key_manager.init_dirs().unwrap();
298
299        // Create invalid key file (wrong size)
300        let invalid_key_path = temp.path().join("invalid.key");
301        fs::write(&invalid_key_path, b"too short").unwrap();
302
303        let result = key_manager.import_key(&invalid_key_path);
304        assert!(result.is_err());
305    }
306
307    #[test]
308    fn test_import_nonexistent_file() {
309        let temp = create_test_git_dir();
310        let key_manager = KeyManager::new(temp.path());
311        key_manager.init_dirs().unwrap();
312
313        let result = key_manager.import_key("/nonexistent/path.key");
314        assert!(result.is_err());
315    }
316
317    #[test]
318    fn test_key_file_permissions_unix() {
319        #[cfg(unix)]
320        {
321            use std::os::unix::fs::PermissionsExt;
322
323            let temp = create_test_git_dir();
324            let key_manager = KeyManager::new(temp.path());
325            key_manager.init_dirs().unwrap();
326
327            key_manager.generate_key().unwrap();
328
329            let key_path = key_manager.default_key_path();
330            let metadata = fs::metadata(&key_path).unwrap();
331            let permissions = metadata.permissions();
332
333            // Should be 0600 (owner read/write only)
334            assert_eq!(permissions.mode() & 0o777, 0o600);
335        }
336    }
337
338    #[test]
339    fn test_multiple_save_overwrites() {
340        let temp = create_test_git_dir();
341        let key_manager = KeyManager::new(temp.path());
342        key_manager.init_dirs().unwrap();
343
344        let key1 = CryptoKey::generate();
345        key_manager.save_key(&key1).unwrap();
346
347        let key2 = CryptoKey::generate();
348        key_manager.save_key(&key2).unwrap();
349
350        let loaded = key_manager.load_key().unwrap();
351
352        // Should have the second key
353        assert_eq!(key2.as_bytes(), loaded.as_bytes());
354        assert_ne!(key1.as_bytes(), loaded.as_bytes());
355    }
356
357    #[test]
358    fn test_key_survives_encrypt_decrypt() {
359        let temp = create_test_git_dir();
360        let key_manager = KeyManager::new(temp.path());
361        key_manager.init_dirs().unwrap();
362
363        let key = key_manager.generate_key().unwrap();
364        let plaintext = b"Secret data";
365
366        let ciphertext = key.encrypt(plaintext).unwrap();
367
368        // Load key again and decrypt
369        let loaded_key = key_manager.load_key().unwrap();
370        let decrypted = loaded_key.decrypt(&ciphertext).unwrap();
371
372        assert_eq!(plaintext.as_slice(), &decrypted[..]);
373    }
374}