first version
Python Code Quality / python-code-quality (push) Successful in 11s
Python Test / python-test (push) Successful in 12s

This commit is contained in:
Brian Bjarke Jensen
2026-01-07 17:59:16 +01:00
parent f8e512e663
commit 422bad3fb8
45 changed files with 3682 additions and 12 deletions
@@ -0,0 +1,5 @@
"""File encrypter module."""
from .file_encrypter import FileEncrypter
__all__ = ["FileEncrypter"]
@@ -0,0 +1,174 @@
"""Definiition of FileEncrypter class."""
from io import BytesIO
import os
import base64
import json
from pathlib import Path
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from .file_encrypter_interface import FileEncrypterInterface
class FileEncrypter(FileEncrypterInterface):
"""Class to handle file encryption and decryption."""
_encoding: str = "utf-8"
_salt_length: int = 16 # Length of the salt in bytes
_nonce_length: int = 12 # Length of the nonce in bytes
@classmethod
def encrypt_file(
cls,
input_file_path: Path,
output_file_path: Path,
password: str,
additional_data: dict[str, str] | None = None,
) -> None:
"""Encrypt a file using the provided password.
Args:
input_file_path (str): The path to the input file to be encrypted.
output_file_path (str): The path where the encrypted file will be saved.
password (str): The password used for encryption.
additional_data (dict[str, str] | None): Optional additional authenticated data (AAD).
"""
# Check inputs
if not isinstance(input_file_path, Path):
raise TypeError("input_file_path must be a Path object")
if not input_file_path.exists():
raise FileNotFoundError(f"input_file_path {input_file_path} does not exist")
if not input_file_path.is_file():
raise ValueError("input_file_path must be a file")
if not isinstance(output_file_path, Path):
raise TypeError("output_file_path must be a Path object")
if not isinstance(password, str):
raise TypeError("password must be a string")
if len(password) == 0:
raise ValueError("password cannot be empty")
password_bytes = base64.urlsafe_b64decode(password.encode(cls._encoding))
if len(password_bytes) != 32:
raise ValueError("Password must decode to 32 bytes for AES-256")
if additional_data is not None and not isinstance(additional_data, dict):
raise TypeError("additional_data must be a dict if provided")
# Prepare additional authenticated data (AAD) if provided
additional_data_bytes = None
if additional_data is not None:
additional_data_bytes = json.dumps(additional_data).encode(cls._encoding)
# Generate a random Number Used Once (nonce)
nonce = os.urandom(cls._nonce_length)
# Initialize AESGCM with derived key
aesgcm = AESGCM(password_bytes)
# Read the input file
with open(input_file_path, "rb") as file:
original_data = file.read()
# Encrypt the data
encrypted_data = aesgcm.encrypt(nonce, original_data, additional_data_bytes)
# Write the salt, nonce, and encrypted data to the output file
with open(output_file_path, "wb") as file:
file.write(nonce + encrypted_data)
@classmethod
def decrypt_file(
cls,
input_file_path: Path,
password: str,
additional_data: dict[str, str] | None = None,
) -> BytesIO:
"""Decrypt a file using the provided password.
Args:
input_file_path (str): The path to the input file to be decrypted.
password (str): The password used for decryption.
additional_data (dict[str, str] | None): Optional additional authenticated data (AAD).
Returns:
BytesIO: The decrypted file data in memory.
"""
# Check inputs
if not isinstance(input_file_path, Path):
raise TypeError("input_file_path must be a Path object")
if not input_file_path.exists():
raise FileNotFoundError(f"input_file_path {input_file_path} does not exist")
if not input_file_path.is_file():
raise ValueError("input_file_path must be a file")
if not isinstance(password, str):
raise TypeError("password must be a string")
if len(password) == 0:
raise ValueError("password cannot be empty")
password_bytes = base64.urlsafe_b64decode(password.encode(cls._encoding))
if len(password_bytes) != 32:
raise ValueError("Password must decode to 32 bytes for AES-256")
if additional_data is not None and not isinstance(additional_data, dict):
raise TypeError("additional_data must be a dict if provided")
# Prepare additional authenticated data (AAD) if provided
additional_data_bytes = None
if additional_data is not None:
additional_data_bytes = json.dumps(additional_data).encode(cls._encoding)
# Initialize AESGCM with derived key
aesgcm = AESGCM(password_bytes)
# Read the input file
with open(input_file_path, "rb") as file:
file_data = file.read()
# Split the nonce and encrypted data
nonce = file_data[: cls._nonce_length]
encrypted_data = file_data[cls._nonce_length :]
# Decrypt the data
decrypted_data = aesgcm.decrypt(nonce, encrypted_data, additional_data_bytes)
# # Extract the salt and encrypted data
# salt = file_data[:16]
# encrypted_data = file_data[16:]
# # Derive the key from the password using PBKDF2
# kdf = PBKDF2HMAC(
# algorithm=hashes.SHA256(),
# length=32,
# salt=salt,
# iterations=480000, # OWASP recommended minimum for 2023+
# )
# key = base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
# fernet = Fernet(key)
# # Decrypt the data
# decrypted_data = fernet.decrypt(encrypted_data)
return BytesIO(decrypted_data)
@classmethod
def decrypt_to_disk(
cls,
input_file_path: Path,
output_file_path: Path,
password: str,
additional_data: dict[str, str] | None = None,
) -> None:
"""Decrypt a file using the provided password and save to disk.
Args:
input_file_path (str): The path to the input file to be decrypted.
output_file_path (str): The path where the decrypted file will be saved.
password (str): The password used for decryption.
additional_data (dict[str, str] | None): Optional additional authenticated data (AAD).
"""
decrypted_data_io = FileEncrypter.decrypt_file(
input_file_path, password, additional_data
)
# Write the decrypted data to the output file
with open(output_file_path, "wb") as file:
file.write(decrypted_data_io.getbuffer())
@@ -0,0 +1,64 @@
"""Definition of FileEncrypterInterface."""
from io import BytesIO
from abc import ABC, abstractmethod
from pathlib import Path
class FileEncrypterInterface(ABC):
"""Interface for file encrypters."""
@classmethod
@abstractmethod
def encrypt_file(
cls,
input_file_path: Path,
output_file_path: Path,
password: str,
additional_data: dict[str, str] | None = None,
) -> None:
"""Encrypt a file using the provided password.
Args:
input_file_path (str): The path to the input file to be encrypted.
output_file_path (str): The path where the encrypted file will be saved.
password (str): The password used for encryption.
additional_data (dict[str, str] | None): Optional additional authenticated data (AAD).
"""
@classmethod
@abstractmethod
def decrypt_file(
cls,
input_file_path: Path,
password: str,
additional_data: dict[str, str] | None = None,
) -> BytesIO:
"""Decrypt a file using the provided password.
Args:
input_file_path (str): The path to the input file to be decrypted.
password (str): The password used for decryption.
additional_data (dict[str, str] | None): Optional additional authenticated data (AAD).
Returns:
BytesIO: The decrypted file data in memory.
"""
@classmethod
@abstractmethod
def decrypt_to_disk(
cls,
input_file_path: Path,
output_file_path: Path,
password: str,
additional_data: dict[str, str] | None = None,
) -> None:
"""Decrypt a file and save it to disk.
Args:
input_file_path (str): The path to the input file to be decrypted.
output_file_path (str): The path where the decrypted file will be saved.
password (str): The password used for decryption.
additional_data (dict[str, str] | None): Optional additional authenticated data (AAD).
"""