1use std::fs::{self, File};
43use std::io::{Read, Write};
44use std::path::{Path, PathBuf};
45use crate::crypto::CryptoKey;
46use crate::error::{GitCryptError, Result};
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!(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 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 assert!(export_path.exists());
268
269 let temp2 = create_test_git_dir();
271 let key_manager2 = KeyManager::new(temp2.path());
272 key_manager2.init_dirs().unwrap();
273
274 key_manager2.import_key(&export_path).unwrap();
276 let imported_key = key_manager2.load_key().unwrap();
277
278 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 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 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 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 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}