use crate::crypto::CryptoKey;
use crate::error::{GitCryptError, Result};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
pub struct KeyManager {
git_dir: PathBuf,
}
impl KeyManager {
pub fn new(git_dir: impl AsRef<Path>) -> Self {
Self {
git_dir: git_dir.as_ref().to_path_buf(),
}
}
pub fn git_crypt_dir(&self) -> PathBuf {
self.git_dir.join("git-crypt")
}
pub fn default_key_path(&self) -> PathBuf {
self.git_crypt_dir().join("keys").join("default")
}
pub fn init_dirs(&self) -> Result<()> {
let git_crypt_dir = self.git_crypt_dir();
if git_crypt_dir.exists() {
return Err(GitCryptError::AlreadyInitialized);
}
fs::create_dir_all(&git_crypt_dir)?;
fs::create_dir(git_crypt_dir.join("keys"))?;
Ok(())
}
pub fn is_initialized(&self) -> bool {
self.git_crypt_dir().exists()
}
pub fn generate_key(&self) -> Result<CryptoKey> {
let key = CryptoKey::generate();
self.save_key(&key)?;
Ok(key)
}
pub fn save_key(&self, key: &CryptoKey) -> Result<()> {
let key_path = self.default_key_path();
fs::create_dir_all(key_path.parent().unwrap())?;
let mut file = File::create(&key_path)?;
file.write_all(key.as_bytes())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&key_path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(&key_path, perms)?;
}
Ok(())
}
pub fn load_key(&self) -> Result<CryptoKey> {
let key_path = self.default_key_path();
if !key_path.exists() {
return Err(GitCryptError::KeyNotFound("default".into()));
}
let mut file = File::open(&key_path)?;
let mut key_bytes = Vec::new();
file.read_to_end(&mut key_bytes)?;
CryptoKey::from_bytes(&key_bytes)
}
pub fn export_key(&self, output_path: impl AsRef<Path>) -> Result<()> {
let key = self.load_key()?;
let mut file = File::create(output_path.as_ref())?;
file.write_all(key.as_bytes())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(output_path.as_ref())?.permissions();
perms.set_mode(0o600);
fs::set_permissions(output_path.as_ref(), perms)?;
}
Ok(())
}
pub fn import_key(&self, input_path: impl AsRef<Path>) -> Result<()> {
let mut file = File::open(input_path)?;
let mut key_bytes = Vec::new();
file.read_to_end(&mut key_bytes)?;
let key = CryptoKey::from_bytes(&key_bytes)?;
self.save_key(&key)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_git_dir() -> TempDir {
TempDir::new().unwrap()
}
#[test]
fn test_git_crypt_dir_path() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
let expected = temp.path().join("git-crypt");
assert_eq!(key_manager.git_crypt_dir(), expected);
}
#[test]
fn test_default_key_path() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
let expected = temp.path().join("git-crypt").join("keys").join("default");
assert_eq!(key_manager.default_key_path(), expected);
}
#[test]
fn test_is_initialized_false() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
assert!(!key_manager.is_initialized());
}
#[test]
fn test_init_dirs() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
assert!(key_manager.git_crypt_dir().exists());
assert!(key_manager.git_crypt_dir().join("keys").exists());
assert!(key_manager.is_initialized());
}
#[test]
fn test_init_dirs_twice_fails() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let result = key_manager.init_dirs();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
GitCryptError::AlreadyInitialized
));
}
#[test]
fn test_generate_and_load_key() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let key1 = key_manager.generate_key().unwrap();
let key2 = key_manager.load_key().unwrap();
assert_eq!(key1.as_bytes(), key2.as_bytes());
}
#[test]
fn test_load_key_before_init_fails() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
let result = key_manager.load_key();
assert!(result.is_err());
}
#[test]
fn test_save_and_load_key() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let original_key = CryptoKey::generate();
key_manager.save_key(&original_key).unwrap();
let loaded_key = key_manager.load_key().unwrap();
assert_eq!(original_key.as_bytes(), loaded_key.as_bytes());
}
#[test]
fn test_export_and_import_key() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let original_key = key_manager.generate_key().unwrap();
let export_path = temp.path().join("exported.key");
key_manager.export_key(&export_path).unwrap();
assert!(export_path.exists());
let temp2 = create_test_git_dir();
let key_manager2 = KeyManager::new(temp2.path());
key_manager2.init_dirs().unwrap();
key_manager2.import_key(&export_path).unwrap();
let imported_key = key_manager2.load_key().unwrap();
assert_eq!(original_key.as_bytes(), imported_key.as_bytes());
}
#[test]
fn test_export_key_without_init_fails() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
let export_path = temp.path().join("exported.key");
let result = key_manager.export_key(&export_path);
assert!(result.is_err());
}
#[test]
fn test_import_invalid_key_file() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let invalid_key_path = temp.path().join("invalid.key");
fs::write(&invalid_key_path, b"too short").unwrap();
let result = key_manager.import_key(&invalid_key_path);
assert!(result.is_err());
}
#[test]
fn test_import_nonexistent_file() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let result = key_manager.import_key("/nonexistent/path.key");
assert!(result.is_err());
}
#[test]
fn test_key_file_permissions_unix() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
key_manager.generate_key().unwrap();
let key_path = key_manager.default_key_path();
let metadata = fs::metadata(&key_path).unwrap();
let permissions = metadata.permissions();
assert_eq!(permissions.mode() & 0o777, 0o600);
}
}
#[test]
fn test_multiple_save_overwrites() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let key1 = CryptoKey::generate();
key_manager.save_key(&key1).unwrap();
let key2 = CryptoKey::generate();
key_manager.save_key(&key2).unwrap();
let loaded = key_manager.load_key().unwrap();
assert_eq!(key2.as_bytes(), loaded.as_bytes());
assert_ne!(key1.as_bytes(), loaded.as_bytes());
}
#[test]
fn test_key_survives_encrypt_decrypt() {
let temp = create_test_git_dir();
let key_manager = KeyManager::new(temp.path());
key_manager.init_dirs().unwrap();
let key = key_manager.generate_key().unwrap();
let plaintext = b"Secret data";
let ciphertext = key.encrypt(plaintext).unwrap();
let loaded_key = key_manager.load_key().unwrap();
let decrypted = loaded_key.decrypt(&ciphertext).unwrap();
assert_eq!(plaintext.as_slice(), &decrypted[..]);
}
}