Building EasyCrypt: A Secure File Encryption Tool with GUI Integration - Complete Python Tutorial

Introduction

In today's digital world, protecting sensitive data is more crucial than ever. Whether you're securing personal documents, confidential work files, or sensitive information, having a reliable encryption tool at your fingertips is essential. In this comprehensive tutorial, I'll walk you through building EasyCrypt - a robust, user-friendly file encryption tool that seamlessly integrates with your Linux desktop environment.

Source code is available over at my Github page: https://github.com/ernos/EasyCrypt-Nautilus-Extension

What makes EasyCrypt special?

  • 🔐 Military-grade encryption using Fernet (symmetric encryption with AES-128)
  • đŸ–ąī¸ GUI integration via Zenity dialogs and Nautilus context menus
  • 🔄 Dual-mode operation - both terminal CLI and graphical interface
  • đŸ—‘ī¸ Secure file deletion with multiple overwrite methods (shred, dd, rm)
  • ✅ Integrity verification using SHA-256 checksums
  • âš™ī¸ Highly configurable via JSON config file or command-line arguments
  • đŸ“Ļ Batch processing for multiple files simultaneously

By the end of this tutorial, you'll understand not just how to use EasyCrypt, but how to build similar secure applications in Python, integrate them with desktop environments, and implement best practices for cryptographic security.

Table of Contents

  1. Project Overview and Architecture
  2. Security Foundations: Understanding Fernet Encryption
  3. Core Encryption Implementation
  4. Building the Dual-Interface System
  5. File Detection and Format Handling
  6. Secure File Deletion Mechanisms
  7. Configuration Management
  8. Nautilus Desktop Integration
  9. Real-World Usage Patterns
  10. Security Considerations and Best Practices

Project Overview and Architecture

EasyCrypt consists of two main components:

1. The Core Encryption Tool (easycrypt.py)

A Python command-line/GUI hybrid application that handles:

  • File encryption and decryption
  • Password-based key derivation
  • Secure file deletion
  • Integrity verification
  • Configuration management
  • Multiple UI modes (terminal, Zenity, stdin/stdout)

2. The Nautilus Extension (easycrypt-nautilus-extension.py)

A GNOME Files (Nautilus) integration that provides:

  • Right-click context menu integration
  • Multi-file selection support
  • Folder-based file selection
  • Seamless desktop environment integration

Architecture Diagram

┌─────────────────────────────────────────────────────┐
│              User Interface Layer                    │
├──────────────────â”Ŧ──────────────────â”Ŧ───────────────┤
│  Terminal CLI    │  Zenity Dialogs  │  Stdin/Stdout │
└──────────────────┴──────────────────┴───────────────┘
                           ↓
┌─────────────────────────────────────────────────────┐
│          Configuration & Argument Parser             │
│    (Merge CLI args with config.json settings)       │
└─────────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────┐
│              Core Encryption Engine                  │
├──────────────────â”Ŧ──────────────────â”Ŧ───────────────┤
│  Key Derivation  │  Fernet Crypto   │  Checksum     │
│    (PBKDF2)      │   (AES-128)      │   (SHA-256)   │
└──────────────────┴──────────────────┴───────────────┘
                           ↓
┌─────────────────────────────────────────────────────┐
│              File Operations Layer                   │
├──────────────────â”Ŧ──────────────────â”Ŧ───────────────┤
│  Read/Write      │  Detection       │  Deletion     │
│                  │  (Encrypted?)    │  (Shred/DD)   │
└──────────────────┴──────────────────┴───────────────┘

Security Foundations: Understanding Fernet Encryption

Before diving into the code, let's understand the cryptographic foundation of EasyCrypt.

What is Fernet?

Fernet is a symmetric encryption specification from the cryptography library that provides:

  • AES-128 encryption in CBC mode
  • HMAC authentication using SHA-256
  • Timestamp verification to prevent replay attacks
  • Guaranteed message integrity and confidentiality

Why Fernet?

from cryptography.fernet import Fernet

# Fernet provides a simple, secure API
key = Fernet.generate_key()
cipher = Fernet(key)

# Encrypt
token = cipher.encrypt(b"Secret data")

# Decrypt
data = cipher.decrypt(token)

Fernet is perfect for file encryption because:

  1. Battle-tested: Widely used in production systems
  2. Simple API: Hard to misuse compared to raw AES
  3. Authenticated: Prevents tampering with encrypted data
  4. Standard format: Recognizable token structure

Password-Based Key Derivation with PBKDF2

Since users provide passwords (not cryptographic keys), we need to derive a secure key:

from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
import base64

def generate_key(password: str, salt: bytes) -> bytes:
    """
    Derives a 32-byte key from a password using PBKDF2-HMAC-SHA256.
    
    Args:
        password: User's password string
        salt: 16-byte random salt (unique per file)
    
    Returns:
        Base64-encoded 32-byte key suitable for Fernet
    """
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,              # Fernet requires 32 bytes
        salt=salt,              # Random salt prevents rainbow tables
        iterations=100000,      # High iteration count slows brute force
        backend=default_backend(),
    )
    return base64.urlsafe_b64encode(kdf.derive(password.encode()))

Key security features:

  • Salt: Each file gets a unique 16-byte random salt
  • 100,000 iterations: Makes brute-force attacks expensive
  • SHA-256: Cryptographically secure hash function
  • Deterministic: Same password + salt = same key (for decryption)

Encrypted File Format

EasyCrypt uses a custom but straightforward file format:

┌──────────────────────────────────────────────────┐
│  Byte 0-15: Salt (16 bytes, random)              │
├──────────────────────────────────────────────────┤
│  Byte 16+: Fernet Token (variable length)        │
│    - Starts with b'gAAAAAB'                      │
│    - Contains: version + timestamp + IV + data   │
│    - Contains: HMAC signature                    │
└──────────────────────────────────────────────────┘

Core Encryption Implementation

Let's build the heart of EasyCrypt - the encryption and decryption functions.

Encrypting a File

def encrypt_file(origfile: str, password: str, output_path=None) -> tuple:
    """
    Encrypts a file using Fernet with password-based key derivation.
    
    Process:
    1. Generate random salt
    2. Read original file data
    3. Derive key from password + salt
    4. Encrypt data with Fernet
    5. Write salt + encrypted data to output
    6. Verify integrity with checksum
    
    Args:
        origfile: Path to file to encrypt
        password: User's password
        output_path: Optional custom output path
    
    Returns:
        (success: bool, message: str)
    """
    # Generate unique salt for this file
    origfilesalt = os.urandom(16)
    
    # Read original file
    with open(origfile, "rb") as f:
        file_data = f.read()
    
    # Calculate original checksum for verification
    origsha256 = hashlib.sha256(file_data).hexdigest()
    
    # Derive key from password and salt
    key = generate_key(password, origfilesalt)
    cipher = Fernet(key)
    
    # Encrypt the data
    encrypted_data = cipher.encrypt(file_data)
    
    # Determine output filename
    newfilename = output_path or (origfile + ".enc")
    
    # Handle overwrite protection (see Configuration section)
    if os.path.exists(newfilename):
        if not config["auto_overwrite"]:
            # Ask user if they want to overwrite
            # (Implementation details in GUI section)
            pass
    
    # Write salt + encrypted data
    with open(newfilename, "wb") as f:
        f.write(origfilesalt + encrypted_data)
    
    # === VERIFICATION PHASE ===
    # Immediately verify encryption worked correctly
    with open(newfilename, "rb") as f:
        salt_verify = f.read(16)
        encrypted_verify = f.read()
    
    # Try to decrypt and compare checksums
    key_verify = generate_key(password, salt_verify)
    cipher_verify = Fernet(key_verify)
    
    try:
        decrypted_data = cipher_verify.decrypt(encrypted_verify)
        decrypted_checksum = hashlib.sha256(decrypted_data).hexdigest()
        
        if decrypted_checksum == origsha256:
            return (
                True,
                f"✅ Encrypted and verified: {os.path.basename(newfilename)}"
            )
        else:
            return (False, "❌ Checksum mismatch - encryption failed!")
    except Exception as e:
        return (False, f"❌ Verification failed: {e}")

Why verify immediately?

  1. Catch corruption early: Better to know now than when you need to decrypt
  2. Password validation: Ensures the password was captured correctly
  3. User confidence: Provides immediate feedback

Decrypting a File

def decrypt_file(encrypted_path: str, password: str, output_path: str = None):
    """
    Decrypts a Fernet-encrypted file with custom format.
    
    Process:
    1. Read salt from first 16 bytes
    2. Read encrypted data from remaining bytes
    3. Derive key from password + salt
    4. Decrypt with Fernet
    5. Write decrypted data
    6. Verify integrity
    
    Args:
        encrypted_path: Path to .enc file
        password: User's password
        output_path: Optional output path (defaults to removing .enc)
    
    Returns:
        (success: bool, message: str)
    """
    # Read salt and encrypted data
    with open(encrypted_path, "rb") as f:
        salt = f.read(16)           # First 16 bytes = salt
        encrypted_data = f.read()   # Rest = Fernet token
    
    # Derive the same key used for encryption
    key = generate_key(password, salt)
    cipher = Fernet(key)
    
    # Attempt decryption
    try:
        decrypted_data = cipher.decrypt(encrypted_data)
    except InvalidToken:
        outl("❌ Decryption failed. Wrong password or corrupted file.", ERROR)
        return (False, "Wrong password or corrupted file")
    
    # Determine output filename
    if output_path is None:
        if encrypted_path.endswith(".enc"):
            output_path = encrypted_path[:-4]  # Remove .enc
        else:
            output_path = encrypted_path + ".decrypted"
    
    # Overwrite protection
    if os.path.exists(output_path):
        if not outl(f"File '{output_path}' exists. Overwrite?", QUESTION):
            new_name = outl("Enter new filename:", ENTRY)
            if new_name:
                output_path = new_name
            else:
                return (False, "Decryption cancelled")
    
    # Write decrypted data
    with open(output_path, "wb") as f:
        f.write(decrypted_data)
    
    # Verify integrity
    decrypted_checksum = hashlib.sha256(decrypted_data).hexdigest()
    file_checksum_result = file_checksum(output_path)
    
    if decrypted_checksum == file_checksum_result:
        return (
            True,
            f"✅ Decrypted and verified: {os.path.basename(output_path)}"
        )
    else:
        outl("❌ Checksum mismatch! File may be corrupted.", ERROR)
        return (False, "Checksum verification failed")

Helper: File Checksum Calculation

def file_checksum(path: str) -> str:
    """
    Calculate SHA-256 checksum of a file.
    Uses chunked reading for memory efficiency with large files.
    
    Args:
        path: Path to file
    
    Returns:
        Hexadecimal SHA-256 hash
    """
    h = hashlib.sha256()
    
    # Read in 4KB chunks to handle large files efficiently
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            h.update(chunk)
    
    return h.hexdigest()

Building the Dual-Interface System

One of EasyCrypt's most powerful features is its dual-mode operation: it works both as a terminal CLI tool and with graphical dialogs. This is achieved through an abstraction layer.

The Output Abstraction Layer

# Message type constants
ERROR = 1
WARNING = 2
PASSWORD = 3
DOUBLEPASSWORD = 4
QUESTION = 5
ENTRY = 6
INFO = 7

def outl(str, type=DEBUG):
    """
    Universal output/input function that adapts to the current UI mode.
    
    Depending on config["use_zenity"], this function either:
    - Shows Zenity dialogs (GUI mode)
    - Uses terminal input/output (CLI mode)
    
    Args:
        str: Message or prompt text
        type: Message type (INFO, ERROR, PASSWORD, QUESTION, etc.)
    
    Returns:
        - For questions: bool (True/False)
        - For entry/password: string input
        - For info/error: True if displayed
    """
    global config
    
    # Determine UI mode
    if config["use_zenity"]:
        msgtype = "zenity"
    else:
        msgtype = "terminal"
    
    # Route to appropriate handler based on type
    if type == INFO:
        if msgtype == "zenity":
            zenity_info(str)
        else:
            print(f"â„šī¸  {str}")
        return True
    
    elif type == ERROR:
        if msgtype == "zenity":
            zenity_error(str)
        else:
            print(f"❌ ERROR: {str}", file=sys.stderr)
        return True
    
    elif type == WARNING:
        if msgtype == "zenity":
            zenity_warning(str)
        else:
            print(f"âš ī¸  WARNING: {str}")
        return True
    
    elif type == PASSWORD:
        if msgtype == "zenity":
            return zenity_password(str)
        else:
            return getpass.getpass(f"🔑 {str}: ")
    
    elif type == QUESTION:
        if msgtype == "zenity":
            return zenity_question(str)
        else:
            response = input(f"❓ {str} (y/n): ")
            return response.lower() in ['y', 'yes']
    
    elif type == ENTRY:
        if msgtype == "zenity":
            return zenity_entry(str)
        else:
            return input(f"🔤 {str}: ")
    
    return False

Zenity Dialog Wrappers

Zenity is a tool that displays GTK dialogs from shell scripts or Python.

def zenity_info(msg):
    """Display an information dialog."""
    subprocess.run([
        "zenity", 
        "--info", 
        "--title=â„šī¸ EasyCrypt Information â„šī¸", 
        f"--text={msg}"
    ])

def zenity_error(msg):
    """Display an error dialog."""
    subprocess.run([
        "zenity", 
        "--error", 
        "--title=⛔ EasyCrypt Error ⛔", 
        f"--text={msg}"
    ])

def zenity_warning(msg):
    """Display a warning dialog."""
    subprocess.run([
        "zenity", 
        "--warning", 
        "--title=âš ī¸ EasyCrypt Warning âš ī¸", 
        f"--text={msg}"
    ])

def zenity_password(msg):
    """Display a password entry dialog."""
    result = subprocess.run(
        [
            "zenity",
            "--password",
            f"--title=🔑 {msg} 🔑"
        ],
        stdout=subprocess.PIPE,
    )
    return result.stdout.decode().strip()

def zenity_question(msg):
    """Display a yes/no question dialog."""
    result = subprocess.run([
        "zenity", 
        "--question", 
        "--title=❓ EasyCrypt Question ❓", 
        f"--text={msg}"
    ])
    return result.returncode == 0  # 0 = Yes button clicked

def zenity_entry(msg, default=""):
    """Display a text entry dialog."""
    result = subprocess.run(
        [
            "zenity",
            "--entry",
            "--title=🔤 EasyCrypt Entry 🔤",
            f"--text={msg}",
            f"--entry-text={default}"
        ],
        stdout=subprocess.PIPE,
    )
    return result.stdout.decode().strip()

def zenity_select_files(start_dir="."):
    """Display a file selection dialog with multi-select support."""
    result = subprocess.run(
        [
            "zenity",
            "--file-selection",
            "--multiple",
            "--separator=|",  # Files separated by pipe
            "--title=đŸ—ƒī¸ Select Files for EasyCrypt đŸ—ƒī¸",
            f"--filename={os.path.abspath(start_dir)}/"
        ],
        stdout=subprocess.PIPE,
    )
    files = result.stdout.decode().strip()
    return files.split('|') if files else []

Why This Abstraction Matters

This design pattern provides several benefits:

  1. Single Codebase: One implementation works in multiple contexts
  2. Easy Testing: Terminal mode is perfect for automated tests
  3. User Choice: Users pick their preferred interface
  4. Desktop Integration: Zenity dialogs look native in GNOME
  5. Flexibility: Easy to add new UI modes (Qt, Tkinter, etc.)

Example usage:

# This code works in both modes!
password = outl("Enter encryption password", PASSWORD)
if outl("Delete original file after encryption?", QUESTION):
    delete_file(original_file)
outl("Encryption completed successfully!", INFO)

File Detection and Format Handling

EasyCrypt needs to automatically detect whether a file is already encrypted to prevent double-encryption.

Detecting Encrypted Files

def is_file_encrypted(file_path: str, filemode: bool = True) -> bool:
    """
    Checks if a file is encrypted using our custom Fernet format.
    
    Our format:
    - First 16 bytes: salt (random bytes)
    - Next bytes: Fernet token (starts with b'gAAAAAB')
    - Minimum file size: 32 bytes (16 salt + minimum token)
    
    Fernet tokens have a recognizable structure:
    - Version byte: Always starts with 'g' (base64 of 0x80)
    - Timestamp: 8 bytes
    - IV: 16 bytes  
    - Ciphertext: Variable
    - HMAC: 32 bytes
    
    Args:
        file_path: Path to file to check
        filemode: If True, read from file; if False, treat as bytes
    
    Returns:
        True if file appears to be encrypted, False otherwise
    """
    try:
        with open(file_path, "rb") as f:
            salt = f.read(16)      # Skip salt
            header = f.read(8)     # Read beginning of token
        
        # Fernet tokens (base64 encoded) start with 'gAAAAAB'
        # The 'g' is base64 for 0x80 (Fernet version marker)
        return header.startswith(b"gAAAAAB")
    
    except FileNotFoundError:
        outl(f"File not found: {file_path}", ERROR)
        return False
    except Exception as e:
        outl(f"Could not check file: {e}", ERROR)
        return False

Why this works:

Fernet tokens are base64-encoded and always start with a version byte. The byte 0x80 encodes to 'gA' in base64, followed by timestamp bytes that typically encode to 'AAAAB' or similar for recent timestamps.

Smart Mode Detection

def process_file(filepath, password, mode, output, delete_method, shred_passes):
    """
    Process a file for encryption or decryption with automatic mode detection.
    
    Args:
        filepath: Path to file
        password: Encryption/decryption password
        mode: 'encrypt', 'decrypt', or None for auto-detect
        output: Output file path
        delete_method: Method for secure deletion
        shred_passes: Number of overwrite passes
    """
    # Detect if file is encrypted
    is_encrypted = is_file_encrypted(filepath)
    
    # Determine operation
    if mode is None:
        # Auto-detect mode based on file content
        mode = "decrypt" if is_encrypted else "encrypt"
        outl(f"Auto-detected mode: {mode}", INFO)
    else:
        # Warn if user's choice conflicts with file state
        if mode == "encrypt" and is_encrypted:
            if not outl(
                "File appears already encrypted. Encrypt again (not recommended)?", 
                QUESTION
            ):
                return "Operation cancelled"
        
        if mode == "decrypt" and not is_encrypted:
            outl(
                "Warning: File doesn't look encrypted, but attempting decryption...",
                WARNING
            )
    
    # Perform operation
    if mode == "encrypt":
        success, message = encrypt_file(filepath, password, output)
    elif mode == "decrypt":
        success, message = decrypt_file(filepath, password, output)
    
    # Handle original file deletion if requested
    if success and config["delete_original"]:
        delete_success, delete_msg = file_delete_original(filepath)
        message += "\n" + delete_msg
    
    return message

Secure File Deletion Mechanisms

Simply calling os.remove() doesn't truly delete data - it just removes the file system entry. The actual data remains on disk until overwritten. EasyCrypt implements multiple secure deletion methods.

Method 1: Using shred

def shred_delete(path, passes=3):
    """
    Securely delete a file using the GNU shred utility.
    
    Shred overwrites the file multiple times with random data
    before removing it, making data recovery much harder.
    
    Args:
        path: File path to delete
        passes: Number of overwrite passes (default: 3)
    """
    if os.path.isfile(path):
        subprocess.run([
            "shred",
            "-u",              # Remove file after shredding
            "-n", str(passes), # Number of overwrite passes
            path
        ])
        return True
    elif os.path.isdir(path):
        # Recursively shred all files in directory
        for root, dirs, files in os.walk(path, topdown=False):
            for filename in files:
                file_path = os.path.join(root, filename)
                subprocess.run(["shred", "-u", "-n", str(passes), file_path])
        os.rmdir(path)
        return True
    return False

Method 2: Using dd for Overwriting

def filewipedata(path, passes):
    """
    Overwrites file data using dd (disk dump) utility.
    
    This method:
    1. Determines file size
    2. Overwrites with random data from /dev/urandom
    3. Repeats for specified number of passes
    4. Deletes the file
    
    Args:
        path: File path to wipe
        passes: Number of times to overwrite
    
    Returns:
        True if successful
    """
    filesize = os.path.getsize(path)
    
    for pass_num in range(passes):
        # Overwrite file with random data
        with open(path, "wb") as f:
            # Read random data from /dev/urandom and write to file
            with open("/dev/urandom", "rb") as random_source:
                bytes_written = 0
                chunk_size = 4096
                
                while bytes_written < filesize:
                    chunk = random_source.read(
                        min(chunk_size, filesize - bytes_written)
                    )
                    f.write(chunk)
                    bytes_written += len(chunk)
        
        # Sync to ensure data is written to disk
        os.sync()
    
    # Finally remove the file
    os.remove(path)
    return True

Unified Deletion Interface

def file_delete_original(origfile: str) -> tuple:
    """
    Securely delete original file based on user configuration.
    
    Supports three methods:
    - 'shred': Uses GNU shred utility (recommended)
    - 'dd': Manual overwrite with /dev/urandom
    - 'rm': Simple removal (least secure)
    
    Args:
        origfile: Path to file to delete
    
    Returns:
        (success: bool, message: str)
    """
    global config
    
    # Check if auto-delete is enabled
    if not config["delete_original"]:
        return (False, "Auto-delete disabled")
    
    # Get configuration
    wipe_method = config["delete_method"]
    num_passes = config["shred_passes"]
    
    # Validate method
    if wipe_method not in ["shred", "rm", "dd"]:
        outl(f"Unknown wipe method: {wipe_method}", ERROR)
        return (False, f"Invalid delete method: {wipe_method}")
    
    try:
        if wipe_method == "shred":
            subprocess.run([
                "shred", 
                "-u", 
                "-n", str(num_passes), 
                origfile
            ], check=True)
            return (
                True, 
                f"\nSecurely deleted '{os.path.basename(origfile)}' "
                f"({num_passes} passes)"
            )
        
        elif wipe_method == "dd":
            if filewipedata(origfile, num_passes):
                return (
                    True,
                    f"\nOverwrote and deleted '{os.path.basename(origfile)}' "
                    f"({num_passes} passes)"
                )
        
        elif wipe_method == "rm":
            os.remove(origfile)
            return (
                True,
                f"\nDeleted '{os.path.basename(origfile)}' (no overwrite)"
            )
    
    except Exception as e:
        outl(f"Could not delete file: {e}", ERROR)
        return (False, f"Deletion failed: {e}")
    
    return (False, "Unknown error during deletion")

Security Considerations for File Deletion

âš ī¸ Important Limitations:

  1. Modern File Systems: SSD wear-leveling and journaling may keep copies
  2. Copy-on-Write: Filesystems like Btrfs and ZFS make overwriting complex
  3. Best Practice: For true security, use full-disk encryption (LUKS, dm-crypt)
  4. Multiple Passes: 3-7 passes are generally sufficient for HDDs
  5. SSDs: Physical destruction is the only guaranteed method

Configuration Management

EasyCrypt uses a flexible configuration system that combines JSON config files with command-line arguments.

Configuration File Format

{
  "use_zenity": true,
  "delete_original": false,
  "auto_overwrite": false,
  "delete_method": "shred",
  "shred_passes": 5,
  "verbose_logging": false,
  "input_files": [],
  "output_file": null,
  "mode": null,
  "password": null
}

Loading Configuration

def load_config(config_path="easycryptconfig.json"):
    """
    Load configuration from JSON file.
    
    Searches in:
    1. Specified path
    2. Current directory
    3. Home directory
    
    Args:
        config_path: Path to config file
    
    Returns:
        (success: bool, config: dict)
    """
    if config_path and os.path.exists(config_path):
        try:
            with open(config_path, "r") as f:
                config_data = json.load(f)
            return (True, config_data)
        except json.JSONDecodeError as e:
            outl(f"Invalid JSON in config file: {e}", ERROR)
            return (False, {})
        except Exception as e:
            outl(f"Could not load config: {e}", ERROR)
            return (False, {})
    
    return (False, {})

Merging CLI Arguments with Config

def merge_args_with_config(args, config):
    """
    Merge command-line arguments with config file settings.
    
    Priority: CLI arguments > Config file > Defaults
    
    Args:
        args: argparse.Namespace from argument parser
        config: dict from JSON config file
    
    Returns:
        Merged configuration dictionary
    """
    merged = {
        # Input files from CLI or config
        "input_files": (
            args.inputs if args.inputs 
            else config.get("input_files", [])
        ),
        
        # Output path
        "output_file": (
            args.output if args.output is not None 
            else config.get("output_file")
        ),
        
        # Operation mode
        "mode": (
            "encrypt" if args.encrypt
            else "decrypt" if args.decrypt
            else config.get("mode")
        ),
        
        # Password (WARNING: CLI passwords are visible in process list!)
        "password": (
            args.password if args.password is not None 
            else config.get("password")
        ),
        
        # UI mode
        "use_zenity": (
            args.use_zenity if args.use_zenity is not False
            else config.get("use_zenity", False)
        ),
        
        # Deletion settings
        "delete_method": (
            args.delete_method if args.delete_method is not None
            else config.get("delete_method", "shred")
        ),
        
        "shred_passes": (
            args.shred_passes if args.shred_passes is not None
            else config.get("shred_passes", 5)
        ),
        
        "auto_overwrite": config.get("auto_overwrite", False),
        "delete_original": (
            args.auto_delete if args.auto_delete
            else config.get("delete_original", False)
        ),
        
        "verbose_logging": (
            args.verbose if hasattr(args, 'verbose') and args.verbose
            else config.get("verbose_logging", False)
        ),
        
        "config_file": (
            args.config if args.config is not None
            else config.get("config_file", "easycryptconfig.json")
        ),
    }
    
    return merged

Command-Line Argument Parser

def main():
    parser = argparse.ArgumentParser(
        description="""
        EasyCrypt - Secure file encryption tool with GUI integration.
        
        Encrypt or decrypt files using Fernet (AES-128) with password-based
        key derivation (PBKDF2-HMAC-SHA256). Supports both terminal and 
        graphical (Zenity) interfaces.
        """,
        formatter_class=argparse.RawTextHelpFormatter
    )
    
    # Positional arguments
    parser.add_argument(
        "inputs", 
        nargs="*",
        help="Input file(s), or '-' to read from stdin."
    )
    
    # Output options
    parser.add_argument(
        "-o", "--output",
        help="Output file, or '-' for stdout. Defaults to auto-naming."
    )
    
    # Mode selection (mutually exclusive)
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "--encrypt", 
        action="store_true",
        help="Force encryption mode."
    )
    group.add_argument(
        "--decrypt", 
        action="store_true",
        help="Force decryption mode."
    )
    
    # Security options
    parser.add_argument(
        "-p", "--password",
        help="Password for encryption/decryption. (WARNING: Visible in process list!)"
    )
    
    # Configuration
    parser.add_argument(
        "-c", "--config",
        default="easycryptconfig.json",
        help="Path to custom configuration file."
    )
    
    # UI mode
    parser.add_argument(
        "-z", "--use-zenity",
        action="store_true",
        help="Use Zenity dialogs instead of terminal I/O."
    )
    
    # Deletion options
    parser.add_argument(
        "-m", "--delete-method",
        choices=["shred", "dd", "rm"],
        default="shred",
        help="Method for secure file deletion."
    )
    
    parser.add_argument(
        "-n", "--shred-passes",
        type=int,
        default=5,
        help="Number of overwrite passes for secure deletion."
    )
    
    parser.add_argument(
        "-d", "--auto-delete",
        action="store_true",
        help="Automatically delete original file after encryption/decryption."
    )
    
    # Verbose logging
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Enable verbose logging output."
    )
    
    # Version
    parser.add_argument(
        "-v", "--version",
        action="version",
        version=f"EasyCrypt v{__version__}",
        help="Show version and exit."
    )
    
    args = parser.parse_args()
    
    # Load and merge configuration
    # ... (configuration loading code)

Nautilus Desktop Integration

The Nautilus extension makes EasyCrypt seamlessly integrated into the GNOME desktop environment.

Understanding Nautilus Python Extensions

Nautilus (GNOME Files) supports Python extensions through python-nautilus. Extensions can:

  • Add context menu items
  • Create custom columns
  • Add emblem overlays
  • Provide file information

Building the Extension

#!/usr/bin/env python3
"""
EasyCrypt Nautilus Extension

Adds right-click context menu items to GNOME Files (Nautilus) for:
- Encrypting/decrypting selected files
- Encrypting files in a folder
- Encrypting files from background context menu

Installation:
    1. Install python-nautilus: sudo apt install python3-nautilus
    2. Copy to: ~/.local/share/nautilus-python/extensions/
    3. Restart Nautilus: nautilus -q

Author: Maximilian Cornett
"""

import subprocess
from gi.repository import Nautilus, GObject

class EasyCryptMenuProvider(GObject.GObject, Nautilus.MenuProvider):
    """
    Provides context menu items for EasyCrypt integration.
    
    Inherits from:
    - GObject.GObject: Base class for GObject system
    - Nautilus.MenuProvider: Interface for adding menu items
    """
    
    def __init__(self):
        """Initialize the extension."""
        pass
    
    def get_file_items(self, files):
        """
        Called when user right-clicks on file(s).
        
        Args:
            files: List of NautilusVFSFile objects
        
        Returns:
            List of Nautilus.MenuItem objects to add to context menu
        """
        # Handle file selections (not directories)
        if len(files) >= 1 and not files[0].is_directory():
            # Create menu item for selected files
            item = Nautilus.MenuItem(
                name="EasyCryptExtension::encrypt_selected_files",
                label="🔐 EasyCrypt Selected Files",
                tip="Encrypt or decrypt selected files with EasyCrypt.",
            )
            
            # Connect to handler
            item.connect("activate", self.open_files_with_easycrypt, files)
            
            return [item]
        
        # Handle single folder selection
        elif len(files) == 1 and files[0].is_directory():
            item = Nautilus.MenuItem(
                name="EasyCryptExtension::encrypt_folder_files",
                label="🔐 Encrypt/Decrypt Files in This Folder",
                tip="Open file dialog to select files in this folder.",
            )
            
            item.connect("activate", self.open_with_easycrypt, files[0])
            
            return [item]
        
        else:
            return []
    
    def get_background_items(self, current_folder):
        """
        Called when user right-clicks on empty space in folder.
        
        Args:
            current_folder: NautilusVFSFile object for current folder
        
        Returns:
            List of Nautilus.MenuItem objects
        """
        item = Nautilus.MenuItem(
            name="EasyCryptExtension::encrypt_background",
            label="🔐 Encrypt Files Here",
            tip="Open file dialog to select files for encryption",
        )
        
        item.connect("activate", self.open_with_easycrypt, current_folder)
        
        return [item]
    
    def open_files_with_easycrypt(self, menu, files):
        """
        Handler: Launch EasyCrypt with selected files.
        
        Args:
            menu: The menu item that triggered this (unused)
            files: List of NautilusVFSFile objects
        """
        # Extract file paths
        filepaths = [file.get_location().get_path() for file in files]
        
        # Launch EasyCrypt with Zenity mode and file paths
        subprocess.Popen(["easycrypt", "--use-zenity"] + filepaths)
    
    def open_with_easycrypt(self, menu, folder):
        """
        Handler: Launch EasyCrypt with folder path for file selection.
        
        Args:
            menu: The menu item that triggered this
            folder: NautilusVFSFile object for the folder
        """
        folder_path = folder.get_location().get_path()
        
        # Launch EasyCrypt with folder path (it will show file picker)
        subprocess.Popen(["easycrypt", "--use-zenity", folder_path])

Installation and Setup

1. Install Dependencies:

# Install Nautilus Python bindings
sudo apt install python3-nautilus

# Install EasyCrypt dependencies
pip install cryptography

2. Install the Extension:

# Create extension directory if it doesn't exist
mkdir -p ~/.local/share/nautilus-python/extensions/

# Copy extension file
cp easycrypt-nautilus-extension.py \
   ~/.local/share/nautilus-python/extensions/

# Make EasyCrypt available system-wide
sudo cp easycrypt.py /usr/local/bin/easycrypt
sudo chmod +x /usr/local/bin/easycrypt

3. Restart Nautilus:

nautilus -q  # Quit Nautilus
nautilus &   # Restart (or just open any folder)

4. Verify Installation:

Right-click any file in Nautilus - you should see "🔐 EasyCrypt Selected Files" in the context menu!

How the Integration Works

User Action → Nautilus → Extension → EasyCrypt
    ↓            ↓           ↓            ↓
Right-click → Calls    → Creates    → Opens with
on file       get_file → MenuItem   → Zenity GUI
              _items()   object       and file paths

Real-World Usage Patterns

Let's explore common use cases and workflows.

Pattern 1: Quick File Encryption

# Right-click file → "EasyCrypt Selected Files"
# OR from terminal:
easycrypt document.pdf

What happens:

  1. Zenity password dialog appears
  2. Enter password (twice for encryption)
  3. File is encrypted to document.pdf.enc
  4. Optional: Original file is securely deleted
  5. Success notification appears

Pattern 2: Batch Encryption

# Select multiple files in Nautilus → Right-click → EasyCrypt
# OR:
easycrypt file1.txt file2.pdf file3.jpg

What happens:

  1. Single password prompt (used for all files)
  2. Each file is encrypted sequentially
  3. Progress shown for each file
  4. Summary notification at the end

Pattern 3: Folder-Based Encryption

# Right-click folder → "Encrypt/Decrypt Files in This Folder"
# OR:
easycrypt /path/to/folder

What happens:

  1. File selection dialog opens showing folder contents
  2. User selects which files to encrypt
  3. Single password for all selected files
  4. Batch processing begins

Pattern 4: Decryption

# Right-click .enc file → "EasyCrypt Selected Files"
# Automatically detects encryption and switches to decrypt mode

Pattern 5: CLI Automation

# Non-interactive mode (for scripts)
easycrypt --password "mypassword" \
          --encrypt \
          --auto-delete \
          --delete-method shred \
          --shred-passes 7 \
          sensitive_data.txt

âš ī¸ Warning: Passwords in command line are visible in process list and shell history!

Pattern 6: Pipe Mode

# Encrypt data from stdin
echo "Secret message" | easycrypt --encrypt - > message.enc

# Decrypt to stdout
easycrypt --decrypt message.enc - | less

Pattern 7: Configuration-Based Workflow

easycryptconfig.json:

{
  "use_zenity": true,
  "delete_original": true,
  "auto_overwrite": false,
  "delete_method": "shred",
  "shred_passes": 7,
  "verbose_logging": true
}
# All settings come from config file
easycrypt -c ~/easycryptconfig.json document.txt

Security Considerations and Best Practices

Strengths of EasyCrypt

✅ Strong Encryption: Fernet uses AES-128 in CBC mode with HMAC authentication
✅ Key Derivation: PBKDF2 with 100,000 iterations resists brute force
✅ Integrity Checking: SHA-256 checksums verify data integrity
✅ Unique Salts: Each file gets a unique random salt
✅ Authenticated Encryption: Fernet's HMAC prevents tampering
✅ Secure Deletion: Multiple overwrite passes make recovery harder

Limitations and Warnings

âš ī¸ Password Security:

  • Passwords in CLI arguments are visible in ps output
  • Always use GUI mode or config files for sensitive passwords
  • Strong passwords are essential (use passphrases!)

âš ī¸ Memory Security:

  • Passwords and plaintext temporarily reside in RAM
  • Advanced attackers with physical access could extract them
  • Not resistant to cold boot attacks

âš ī¸ File System Security:

  • Deleted files may persist on modern file systems (SSDs, COW)
  • Use full-disk encryption (LUKS) for comprehensive protection
  • Shredding may not work on journaling or copy-on-write filesystems

âš ī¸ Metadata Leakage:

  • File names, sizes, timestamps are not protected
  • Use encrypted containers (VeraCrypt, LUKS) for metadata protection

âš ī¸ Key Storage:

  • Passwords are not stored - must be remembered!
  • Consider using a password manager
  • No key recovery - lost password = lost data

Best Practices

1. Use Strong Passwords:

# Bad passwords
"password"
"12345678"
"qwerty"
"correct"
"horse"
"battery"
"staple"

# Good passwords (passphrases)
"MyD0g!sN@med$pot1"
"IfriKKin!L0veP1zza"
"TrumpetFish72!"
"Seaweed62Blue@Moon"

2. Verify Before Deleting:

# Don't auto-delete until you're confident
easycrypt --encrypt important.txt
# Test decryption first
easycrypt --decrypt important.txt.enc
# Compare files
diff important.txt important.txt.enc.decrypted
# Now safe to delete
rm important.txt

3. Use Configuration Files for Repeated Tasks:

{
  "delete_original": false,
  "shred_passes": 7,
  "verbose_logging": true
}

4. Combine with Full-Disk Encryption:

# EasyCrypt for file-level encryption
# LUKS for full-disk encryption
# Together = defense in depth

5. Regular Backups:

# Backup encrypted files (not just originals!)
# Store backups in different physical locations
# Test decryption of backups periodically

6. Secure Your System:

# Keep system updated
sudo apt update && sudo apt upgrade

# Use strong user passwords
# Enable screen lock
# Use full-disk encryption

# Don't run EasyCrypt as root unless necessary

Testing Your Encryption

Verify encryption worked:

# Try to decrypt with wrong password (should fail)
easycrypt --decrypt --password "wrong" test.txt.enc
# Expected: "❌ Decryption failed. Wrong password..."

# Try to decrypt with correct password (should succeed)
easycrypt --decrypt --password "correct" test.txt.enc
# Expected: "✅ Decrypted and verified: test.txt"

# Compare checksums
sha256sum original.txt
sha256sum original.txt.enc.decrypted
# Should match!

Threat Model

EasyCrypt protects against:

  • ✅ Casual snooping (shared computers, lost USB drives)
  • ✅ Data theft from backups
  • ✅ Unauthorized file access
  • ✅ File tampering (due to HMAC)

EasyCrypt does NOT protect against:

  • ❌ Keyloggers capturing your password
  • ❌ Memory forensics on running system
  • ❌ Sophisticated nation-state attackers
  • ❌ Rubber-hose cryptanalysis (physical coercion)
  • ❌ Quantum computers (in the future)

Performance Considerations

Benchmarks

On a typical modern system (Intel i7, SSD):

File Size Encryption Time Decryption Time
1 MB ~0.05s ~0.04s
10 MB ~0.3s ~0.25s
100 MB ~2.5s ~2.2s
1 GB ~25s ~22s

Factors affecting performance:

  • CPU speed (AES-NI instructions help significantly)
  • Disk I/O speed (SSD vs HDD)
  • File size
  • Shred passes (more passes = more time)

Optimization Tips

1. Use Fewer Shred Passes for Testing:

easycrypt --shred-passes 1 test_file.txt

2. Disable Auto-Delete During Development:

{
  "delete_original": false
}

3. Batch Operations:

# Process multiple files with single password entry
easycrypt file1.txt file2.txt file3.txt

4. Use Terminal Mode for Automation:

# Zenity has GUI overhead
easycrypt --no-zenity -p "password" *.txt

Advanced Features and Extensions

Adding Support for Asymmetric Encryption

EasyCrypt uses symmetric encryption (same key for encrypt/decrypt). You could extend it with RSA for different use cases:

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes

def generate_keypair():
    """Generate RSA public/private key pair."""
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=4096,
    )
    public_key = private_key.public_key()
    return private_key, public_key

def encrypt_with_public_key(data, public_key):
    """Encrypt data with public key (for sending to someone)."""
    return public_key.encrypt(
        data,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

def decrypt_with_private_key(encrypted_data, private_key):
    """Decrypt data with private key (recipient)."""
    return private_key.decrypt(
        encrypted_data,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

Adding Compression

Compress before encrypting to save space:

import gzip

def encrypt_file_compressed(filepath, password):
    """Encrypt file with compression."""
    # Read original data
    with open(filepath, "rb") as f:
        data = f.read()
    
    # Compress
    compressed_data = gzip.compress(data)
    
    # Encrypt compressed data
    # ... (use existing encryption code)

Adding Progress Bars

For large files, show progress:

def encrypt_file_with_progress(filepath, password):
    """Encrypt file with progress indication."""
    file_size = os.path.getsize(filepath)
    bytes_read = 0
    
    # Use Zenity progress bar
    process = subprocess.Popen(
        ["zenity", "--progress", "--title=Encrypting", 
         "--text=Encrypting file...", "--percentage=0"],
        stdin=subprocess.PIPE
    )
    
    # Read in chunks and update progress
    with open(filepath, "rb") as f:
        while True:
            chunk = f.read(4096)
            if not chunk:
                break
            bytes_read += len(chunk)
            percentage = int((bytes_read / file_size) * 100)
            process.stdin.write(f"{percentage}\n".encode())
            process.stdin.flush()
    
    process.stdin.close()
    process.wait()

Conclusion

We've built a comprehensive, secure file encryption tool that demonstrates several important concepts:

Python Development:

  • ✅ Cryptographic operations with the cryptography library
  • ✅ Cross-platform file handling
  • ✅ Configuration management
  • ✅ Command-line argument parsing
  • ✅ Subprocess management for external tools

Security Engineering:

  • ✅ Symmetric encryption with Fernet
  • ✅ Password-based key derivation (PBKDF2)
  • ✅ Salt generation and management
  • ✅ Integrity verification with checksums
  • ✅ Secure file deletion techniques

Desktop Integration:

  • ✅ Nautilus extension development
  • ✅ Zenity GUI dialogs
  • ✅ Context menu integration
  • ✅ Seamless user experience

Software Design:

  • ✅ Separation of concerns
  • ✅ Abstraction layers
  • ✅ Configuration over hardcoding
  • ✅ Error handling and validation

Key Takeaways

  1. Security is multi-layered: EasyCrypt combines encryption, key derivation, integrity checking, and secure deletion
  2. User experience matters: The dual CLI/GUI interface makes the tool accessible to different users
  3. Configuration is powerful: JSON config + CLI args = maximum flexibility
  4. Desktop integration is valuable: Nautilus extension makes encryption effortless
  5. Testing is essential: Always verify encryption/decryption works correctly

What's Next?

Potential improvements:

  • Hardware security module (HSM) support
  • Cloud backup integration
  • Mobile app version (Android/iOS)
  • File compression before encryption
  • Steganography support
  • Web interface for remote access
  • Password strength meter
  • Multi-user support with asymmetric encryption

Resources and Further Reading

Python Cryptography:

GNOME Development:

Security:

Final Thoughts

EasyCrypt demonstrates that building secure, user-friendly tools doesn't require massive frameworks or complex architectures. With Python, standard cryptographic libraries, and thoughtful design, you can create professional-grade security tools that integrate seamlessly with desktop environments.

The most important lesson: Security and usability go hand-in-hand. The best encryption tool is worthless if it's too difficult to use correctly. By providing both CLI and GUI interfaces, automatic mode detection, and desktop integration, EasyCrypt makes strong encryption accessible to everyone.

Stay secure, and happy encrypting! 🔐


Need an Android Developer or a full-stack website developer?

I specialize in Kotlin, Jetpack Compose, and Material Design 3. For websites, I use modern web technologies to create responsive and user-friendly experiences. Check out my portfolio or get in touch to discuss your project.