1use crate::crypto::CryptoKey;
43use crate::error::{GitCryptError, Result};
44use std::fs::{self, File};
45use std::io::{Read, Write};
46use std::path::{Path, PathBuf};
47
48pub 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 pub fn git_crypt_dir(&self) -> PathBuf {
62 self.git_dir.join("git-crypt")
63 }
64
65 pub fn default_key_path(&self) -> PathBuf {
67 self.git_crypt_dir().join("keys").join("default")
68 }
69
70 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 pub fn is_initialized(&self) -> bool {
85 self.git_crypt_dir().exists()
86 }
87
88 pub fn generate_key(&self) -> Result<CryptoKey> {
90 let key = CryptoKey::generate();
91 self.save_key(&key)?;
92 Ok(key)
93 }
94
95 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 #[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 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 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 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 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 assert!(export_path.exists());
271
272 let temp2 = create_test_git_dir();
274 let key_manager2 = KeyManager::new(temp2.path());
275 key_manager2.init_dirs().unwrap();
276
277 key_manager2.import_key(&export_path).unwrap();
279 let imported_key = key_manager2.load_key().unwrap();
280
281 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 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 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 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 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}