PYTHONPython

encryption

real world projects / file sync / sync / utils

PYTHON
encryption.py🐍
"""
File encryption utilities using AES.
"""

import os
import base64
from pathlib import Path
from typing import Optional
import logging

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend

logger = logging.getLogger(__name__)

# AES block size
BLOCK_SIZE = 128  # bits
IV_SIZE = 16  # bytes


def _get_key_bytes(key: str) -> bytes:
    """Convert string key to 32 bytes for AES-256."""
    if len(key) == 32:
        return key.encode('utf-8')
    
    # If key is base64 encoded
    try:
        decoded = base64.b64decode(key)
        if len(decoded) == 32:
            return decoded
    except:
        pass
    
    # Pad or truncate key
    key_bytes = key.encode('utf-8')
    if len(key_bytes) < 32:
        key_bytes = key_bytes.ljust(32, b'\0')
    return key_bytes[:32]


def encrypt_file(
    input_path: Path,
    key: str,
    output_path: Optional[Path] = None,
    chunk_size: int = 8192
) -> Path:
    """
    Encrypt a file using AES-256-CBC.
    
    Args:
        input_path: Path to input file
        key: Encryption key (string)
        output_path: Path for encrypted file (default: input + .enc)
        chunk_size: Size of chunks to read
    
    Returns:
        Path to encrypted file
    """
    if output_path is None:
        output_path = input_path.with_suffix(input_path.suffix + ".enc")
    
    key_bytes = _get_key_bytes(key)
    iv = os.urandom(IV_SIZE)
    
    cipher = Cipher(
        algorithms.AES(key_bytes),
        modes.CBC(iv),
        backend=default_backend()
    )
    encryptor = cipher.encryptor()
    padder = padding.PKCS7(BLOCK_SIZE).padder()
    
    with open(input_path, 'rb') as f_in:
        with open(output_path, 'wb') as f_out:
            # Write IV first (needed for decryption)
            f_out.write(iv)
            
            while True:
                chunk = f_in.read(chunk_size)
                
                if chunk:
                    padded_chunk = padder.update(chunk)
                    encrypted_chunk = encryptor.update(padded_chunk)
                    f_out.write(encrypted_chunk)
                else:
                    # Final block
                    padded_final = padder.finalize()
                    encrypted_final = encryptor.update(padded_final) + encryptor.finalize()
                    f_out.write(encrypted_final)
                    break
    
    logger.debug(f"Encrypted: {input_path} -> {output_path}")
    return output_path


def decrypt_file(
    input_path: Path,
    key: str,
    output_path: Optional[Path] = None,
    chunk_size: int = 8192
) -> Path:
    """
    Decrypt a file encrypted with AES-256-CBC.
    
    Args:
        input_path: Path to encrypted file
        key: Encryption key (string)
        output_path: Path for decrypted file (default: remove .enc)
        chunk_size: Size of chunks to read
    
    Returns:
        Path to decrypted file
    """
    if output_path is None:
        if input_path.suffix == ".enc":
            output_path = input_path.with_suffix("")
        else:
            output_path = input_path.with_suffix(".decrypted")
    
    key_bytes = _get_key_bytes(key)
    
    with open(input_path, 'rb') as f_in:
        # Read IV from file
        iv = f_in.read(IV_SIZE)
        
        cipher = Cipher(
            algorithms.AES(key_bytes),
            modes.CBC(iv),
            backend=default_backend()
        )
        decryptor = cipher.decryptor()
        unpadder = padding.PKCS7(BLOCK_SIZE).unpadder()
        
        with open(output_path, 'wb') as f_out:
            encrypted_data = f_in.read()
            decrypted_padded = decryptor.update(encrypted_data) + decryptor.finalize()
            decrypted_data = unpadder.update(decrypted_padded) + unpadder.finalize()
            f_out.write(decrypted_data)
    
    logger.debug(f"Decrypted: {input_path} -> {output_path}")
    return output_path


def generate_key() -> str:
    """
    Generate a random 32-byte key for AES-256.
    
    Returns:
        Base64-encoded key string
    """
    key_bytes = os.urandom(32)
    return base64.b64encode(key_bytes).decode('utf-8')


class FileEncryptor:
    """Convenience class for file encryption."""
    
    def __init__(self, key: str):
        self.key = key
    
    def encrypt(self, input_path: Path, output_path: Optional[Path] = None) -> Path:
        """Encrypt a file."""
        return encrypt_file(input_path, self.key, output_path)
    
    def decrypt(self, input_path: Path, output_path: Optional[Path] = None) -> Path:
        """Decrypt a file."""
        return decrypt_file(input_path, self.key, output_path)
PreviousNext