From 365259f8958552aa36ce9688a05ac01068f9cdc9 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 20:07:51 +0000 Subject: [PATCH] Add utility modules: encryption, file_utils, git_utils, path_utils --- confsync/utils/encryption.py | 169 +++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 confsync/utils/encryption.py diff --git a/confsync/utils/encryption.py b/confsync/utils/encryption.py new file mode 100644 index 0000000..ed760df --- /dev/null +++ b/confsync/utils/encryption.py @@ -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