Add utility modules: encryption, file_utils, git_utils, path_utils
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-04 20:07:51 +00:00
parent 1a7e2dd948
commit 365259f895

View File

@@ -0,0 +1,169 @@
"""Encryption utilities for secure storage and sync operations."""
import os
import base64
import getpass
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
@dataclass
class EncryptedData:
"""Represents encrypted data."""
ciphertext: bytes
salt: bytes
nonce: Optional[bytes] = None
class EncryptionManager:
"""Manages encryption and decryption of configuration files."""
def __init__(self, key_file: Optional[str] = None, passphrase: Optional[str] = None):
self.key_file = key_file
self.passphrase = passphrase
self._fernet: Optional[Fernet] = None
self._key: Optional[bytes] = None
def _derive_key(self, passphrase: str, salt: bytes) -> bytes:
"""Derive encryption key from passphrase using PBKDF2."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=480000,
backend=default_backend()
)
return base64.urlsafe_b64encode(kdf.derive(passphrase.encode()))
def _get_fernet(self) -> Fernet:
"""Get or create Fernet instance."""
if self._fernet is None:
salt, passphrase = self._load_or_create_key()
self._key = self._derive_key(passphrase, salt)
self._fernet = Fernet(self._key)
return self._fernet
def _load_or_create_key(self) -> Tuple[bytes, str]:
"""Load existing key or create new one."""
passphrase = self.passphrase
if passphrase is None:
passphrase = self._get_passphrase()
if self.key_file and Path(self.key_file).exists():
with open(self.key_file, 'rb') as f:
salt = f.read(16)
return salt, passphrase
salt = os.urandom(16)
if self.key_file:
Path(self.key_file).parent.mkdir(parents=True, exist_ok=True)
with open(self.key_file, 'wb') as f:
f.write(salt)
return salt, passphrase
def _get_passphrase(self) -> str:
"""Get passphrase from user input."""
if self.passphrase:
return self.passphrase
return getpass.getpass("Enter encryption passphrase: ")
def set_passphrase(self, passphrase: str) -> None:
"""Set the encryption passphrase."""
self.passphrase = passphrase
self._fernet = None
self._key = None
def encrypt(self, data: str) -> EncryptedData:
"""Encrypt string data."""
salt, passphrase = self._load_or_create_key()
key = self._derive_key(passphrase, salt)
fernet = Fernet(key)
ciphertext = fernet.encrypt(data.encode())
return EncryptedData(ciphertext=ciphertext, salt=salt)
def decrypt(self, encrypted_data: EncryptedData, passphrase: Optional[str] = None) -> str:
"""Decrypt encrypted data."""
if passphrase is None:
passphrase = self._get_passphrase()
else:
self.passphrase = passphrase
key = self._derive_key(passphrase, encrypted_data.salt)
fernet = Fernet(key)
plaintext = fernet.decrypt(encrypted_data.ciphertext)
return plaintext.decode()
def encrypt_file(self, input_path: str, output_path: Optional[str] = None) -> str:
"""Encrypt a file and return the path to encrypted file."""
with open(input_path, 'r', encoding='utf-8') as f:
content = f.read()
encrypted = self.encrypt(content)
output = output_path or f"{input_path}.enc"
with open(output, 'wb') as f:
f.write(encrypted.salt + encrypted.ciphertext)
return output
def decrypt_file(self, input_path: str, output_path: Optional[str] = None) -> str:
"""Decrypt a file and return the path to decrypted file."""
with open(input_path, 'rb') as f:
file_content = f.read()
salt = file_content[:16]
ciphertext = file_content[16:]
encrypted_data = EncryptedData(ciphertext=ciphertext, salt=salt)
decrypted = self.decrypt(encrypted_data)
output = output_path or input_path.rsplit('.', 1)[0] if input_path.endswith('.enc') else input_path
with open(output, 'w', encoding='utf-8') as f:
f.write(decrypted)
return output
def encrypt_manifest(self, manifest_data: str) -> str:
"""Encrypt manifest data."""
encrypted = self.encrypt(manifest_data)
return base64.b64encode(encrypted.salt + encrypted.ciphertext).decode()
def decrypt_manifest(self, encrypted_data: str) -> str:
"""Decrypt manifest data."""
raw_data = base64.b64decode(encrypted_data.encode())
salt = raw_data[:16]
ciphertext = raw_data[16:]
encrypted = EncryptedData(ciphertext=ciphertext, salt=salt)
return self.decrypt(encrypted)
def generate_key_file(self, key_path: str, passphrase: Optional[str] = None) -> None:
"""Generate a new key file."""
salt = os.urandom(16)
passwd = passphrase or self._get_passphrase()
key = self._derive_key(passwd, salt)
Path(key_path).parent.mkdir(parents=True, exist_ok=True)
with open(key_path, 'wb') as f:
f.write(salt + key)
def verify_key(self, key_path: str, passphrase: str) -> bool:
"""Verify if a passphrase matches a key file."""
if not Path(key_path).exists():
return False
with open(key_path, 'rb') as f:
file_content = f.read()
salt = file_content[:16]
stored_key = file_content[16:]
computed_key = self._derive_key(passphrase, salt)
return computed_key == stored_key