#!/usr/bin/env python3 import argparse import subprocess import sys import os import shutil from datetime import datetime import time import json import urllib.request import tempfile # Global variables DEBUG = False FAST_MODE = False PARANOID_MODE = False TIMESTAMP = datetime.now().strftime('%Y%m%d_%H%M%S') LOG_FILE = f"covert_sd_setup_{TIMESTAMP}.log" CREATE_KALI = False CREATE_DOCS = False CREATE_TAILS = False CREATE_CUSTOM = False KALI_ISO = "" TAILS_ISO = "" CUSTOM_ISO = "" DRIVE = "" STATE_FILE = "/tmp/covert_sd_state.json" # Checkpoint states CHECKPOINTS = { "VERACRYPT_DOWNLOADED": False, "DRIVE_SELECTED": False, "DRIVE_WIPED": False, "ISO_WRITTEN": False, "PARTITIONS_CREATED": False, "PERSISTENCE_SETUP": False, "DOCS_ENCRYPTED": False, "TOOLS_SETUP": False, "COMPLETE": False } def log(message): """Logs a message to both the log file and the terminal.""" with open(LOG_FILE, "a") as log_file: log_file.write(message + "\n") print(message) def save_state(checkpoint_name, data=None): """Save current progress to state file.""" global CHECKPOINTS CHECKPOINTS[checkpoint_name] = True state = { "checkpoints": CHECKPOINTS, "drive": DRIVE, "timestamp": TIMESTAMP, "log_file": LOG_FILE, "create_kali": CREATE_KALI, "create_docs": CREATE_DOCS, "create_tails": CREATE_TAILS, "create_custom": CREATE_CUSTOM, "kali_iso": KALI_ISO, "tails_iso": TAILS_ISO, "custom_iso": CUSTOM_ISO, "fast_mode": FAST_MODE, "paranoid_mode": PARANOID_MODE } if data: state.update(data) try: with open(STATE_FILE, 'w') as f: json.dump(state, f, indent=2) log(f"✓ Checkpoint saved: {checkpoint_name}") except Exception as e: log(f"⚠ Warning: Could not save checkpoint: {e}") def load_state(): """Load previous progress from state file.""" global CHECKPOINTS, DRIVE, TIMESTAMP, LOG_FILE global CREATE_KALI, CREATE_DOCS, CREATE_TAILS, CREATE_CUSTOM global KALI_ISO, TAILS_ISO, CUSTOM_ISO, FAST_MODE, PARANOID_MODE if not os.path.exists(STATE_FILE): return None try: with open(STATE_FILE, 'r') as f: state = json.load(f) CHECKPOINTS = state.get("checkpoints", CHECKPOINTS) DRIVE = state.get("drive", "") TIMESTAMP = state.get("timestamp", TIMESTAMP) LOG_FILE = state.get("log_file", LOG_FILE) CREATE_KALI = state.get("create_kali", False) CREATE_DOCS = state.get("create_docs", False) CREATE_TAILS = state.get("create_tails", False) CREATE_CUSTOM = state.get("create_custom", False) KALI_ISO = state.get("kali_iso", "") TAILS_ISO = state.get("tails_iso", "") CUSTOM_ISO = state.get("custom_iso", "") FAST_MODE = state.get("fast_mode", False) PARANOID_MODE = state.get("paranoid_mode", False) return state except Exception as e: log(f"⚠ Warning: Could not load state: {e}") return None def clear_state(): """Remove state file when setup is complete.""" if os.path.exists(STATE_FILE): os.remove(STATE_FILE) log("✓ State file cleared") def run_command(command, shell=False, interactive=False, ignore_enospc=False): """ Runs a system command. Args: command (list or str): The command to execute. shell (bool): Whether to execute the command through the shell. interactive (bool): If True, streams the command's output live. ignore_enospc (bool): If True, don't exit on "No space left" error (for dd filling disk). """ if DEBUG: log(f"Running command: {command}") try: if interactive: # Use subprocess.run with no stdout/stderr capture for truly interactive commands # This allows the command to directly interact with the terminal # Don't use check=True if we want to ignore ENOSPC result = subprocess.run(command, shell=shell, check=not ignore_enospc) if ignore_enospc and result.returncode != 0: # For dd, exit code 1 with ENOSPC is success (filled the disk) return elif result.returncode != 0: raise subprocess.CalledProcessError(result.returncode, command) return else: # Handle non-interactive commands if isinstance(command, list): result = subprocess.run( command, shell=shell, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True ) else: result = subprocess.run( command, shell=shell, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True ) if result.stdout: log(result.stdout.strip()) if result.stderr: log(result.stderr.strip()) except subprocess.CalledProcessError as e: log(f"Command failed: {e}\nOutput: {e.stdout}\nError: {e.stderr}") sys.exit(1) def download_file(url, destination, description="file"): """ Downloads a file with progress indication. Args: url (str): URL to download from destination (str): Local path to save file description (str): Description for user feedback """ try: log(f"Downloading {description}...") log(f" From: {url}") log(f" To: {destination}") def progress_hook(block_num, block_size, total_size): if total_size > 0: downloaded = block_num * block_size percent = min(100, (downloaded * 100) // total_size) mb_downloaded = downloaded / (1024 * 1024) mb_total = total_size / (1024 * 1024) # Use \r to overwrite the same line print(f"\r Progress: {percent}% ({mb_downloaded:.1f} MB / {mb_total:.1f} MB)", end='', flush=True) urllib.request.urlretrieve(url, destination, reporthook=progress_hook) print() # New line after progress log(f"✓ {description} downloaded successfully") return True except Exception as e: log(f"✗ Failed to download {description}: {e}") return False def download_portable_veracrypt(): """ Downloads portable VeraCrypt binaries for Windows, Linux, and macOS. Returns the path to a temporary directory containing the downloads. """ log("\n" + "="*70) log("DOWNLOADING PORTABLE VERACRYPT") log("="*70) log("\nTo make this device work on ANY computer without software installation,") log("we need to download portable VeraCrypt for Windows, Linux, and macOS.") log("") log("Total download size: ~86-90 MB") log(" • Windows Portable: ~39 MB") log(" • Linux AppImage: ~13 MB") log(" • Linux .deb Installer: ~13 MB") log(" • macOS DMG: ~22 MB") log("") log("This is a one-time download that will be stored on your device.") log("") # Use consistent temp directory for caching across runs temp_dir = "/tmp/veracrypt_download" # Check if we already have cached downloads if os.path.exists(temp_dir): log(f"✓ Found existing VeraCrypt cache at: {temp_dir}") log("Checking if all required files are present...") # Check if all files exist and are recent enough cache_valid = True cached_files = [] for platform, info in { "Windows": "VeraCrypt-Portable-Windows.exe", "Linux AppImage": "VeraCrypt-Portable-Linux.AppImage", "Linux DEB": "veracrypt-Ubuntu-22.04-amd64.deb", "macOS": "VeraCrypt-MacOS.dmg" }.items(): file_path = os.path.join(temp_dir, info) if os.path.exists(file_path): size_mb = os.path.getsize(file_path) / (1024 * 1024) cached_files.append(f" ✓ {platform}: {size_mb:.1f} MB") else: cache_valid = False break if cache_valid: log("✓ All VeraCrypt files found in cache!") for cf in cached_files: log(cf) log("") use_cache = input("Use cached files? (y/n) [Default: y]: ") or "y" if use_cache.lower() == "y": log("✓ Using cached VeraCrypt downloads") return temp_dir else: log("Removing old cache and downloading fresh copies...") shutil.rmtree(temp_dir) else: log("⚠ Cache incomplete, will re-download") shutil.rmtree(temp_dir) # Create temp directory for downloads os.makedirs(temp_dir, exist_ok=True) log(f"Downloading to: {temp_dir}") log("") # Get latest VeraCrypt version from GitHub log("Fetching latest VeraCrypt version from GitHub...") try: import urllib.request import json with urllib.request.urlopen("https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest") as response: release_data = json.loads(response.read().decode()) VC_TAG = release_data["tag_name"] # e.g., "VeraCrypt_1.26.24" VC_VERSION = VC_TAG.replace("VeraCrypt_", "") # e.g., "1.26.24" log(f"✓ Latest version: {VC_VERSION}") except Exception as e: log(f"⚠ Warning: Could not fetch latest version ({e})") log(" Falling back to version 1.26.24") VC_TAG = "VeraCrypt_1.26.24" VC_VERSION = "1.26.24" log("") downloads = { "Windows Portable": { "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/VeraCrypt.Portable.{VC_VERSION}.exe", "filename": "VeraCrypt-Portable-Windows.exe", "required": True }, "Linux Portable AppImage": { "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/VeraCrypt-{VC_VERSION}-x86_64.AppImage", "filename": "VeraCrypt-Portable-Linux.AppImage", "required": True }, "Linux DEB Installer": { "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/veracrypt-{VC_VERSION}-Ubuntu-22.04-amd64.deb", "filename": "veracrypt-Ubuntu-22.04-amd64.deb", "required": False }, "macOS": { "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/VeraCrypt_{VC_VERSION}.dmg", "filename": "VeraCrypt-MacOS.dmg", "required": False } } success_count = 0 required_count = 0 for platform, info in downloads.items(): log(f"\n[{platform}]") dest_path = os.path.join(temp_dir, info["filename"]) if info.get("required"): required_count += 1 if download_file(info["url"], dest_path, f"{platform} VeraCrypt"): success_count += 1 # Make Linux AppImage executable if "AppImage" in info["filename"]: try: os.chmod(dest_path, 0o755) log(f" ✓ Made executable") except Exception as e: log(f" ⚠ Warning: Could not make executable: {e}") else: if info.get("required"): log(f" ✗ ERROR: {platform} is required but download failed!") log("\n" + "="*70) if success_count >= required_count: log("✓ PORTABLE VERACRYPT DOWNLOAD COMPLETE") log("="*70) log(f"\nDownloaded {success_count} of {len(downloads)} platform(s)") log(f"Files saved to: {temp_dir}") return temp_dir else: log("✗ DOWNLOAD FAILED") log("="*70) log(f"\nOnly {success_count} of {required_count} required downloads succeeded.") log("Cannot proceed without portable VeraCrypt binaries.") log("\nTroubleshooting:") log(" • Check your internet connection") log(" • Verify firewall settings") log(" • Try again later (download servers may be busy)") sys.exit(1) def check_dependencies(): """Checks and installs missing dependencies.""" dependencies = [ "parted", "cryptsetup", "lsblk", "dd", "sgdisk", "wipefs", "bc", "fdisk", "veracrypt", "lsof", "fuser", "mountpoint", "udevadm" ] missing = [] for dep in dependencies: if not shutil.which(dep): missing.append(dep) if missing: log(f"Missing dependencies: {', '.join(missing)}") install = input(f"Do you want to install the missing dependencies? (y/n) [Default: y]: ") or "y" if install.lower() == "y": run_command(["sudo", "apt", "update"]) run_command(["sudo", "apt", "install", "-y"] + missing) else: log("Cannot proceed without installing dependencies. Exiting.") sys.exit(1) def list_drives(): """Lists all available drives.""" log("Available drives:") result = subprocess.run(["lsblk", "-J", "-o", "NAME,SIZE,TYPE"], capture_output=True, text=True) try: lsblk_output = json.loads(result.stdout) for device in lsblk_output['blockdevices']: if device['type'] == 'disk': name = device['name'] size = device['size'] drive = f"/dev/{name} {size}" log(drive) except json.JSONDecodeError: log("Error: Unable to parse lsblk output.") sys.exit(1) def get_partition_name(drive, partition_number): """ Generates the partition name based on the drive and partition number. Args: drive (str): The drive path (e.g., /dev/sda). partition_number (int): The partition number. Returns: str: The full partition path (e.g., /dev/sda1 or /dev/nvme0n1p1). """ if 'nvme' in drive or 'mmcblk' in drive: return f"{drive}p{partition_number}" else: return f"{drive}{partition_number}" def prepare_drive(drive): """ Unmounts any mounted partitions, disables swap, and kills processes using the drive. Args: drive (str): The drive to prepare (e.g., /dev/sda). """ # Unmount all mounted partitions result = subprocess.run(["lsblk", "-lnp", drive], capture_output=True, text=True) for line in result.stdout.strip().splitlines(): parts = line.strip().split() if len(parts) >= 7 and parts[6]: # If mountpoint is not empty part = parts[0] log(f"Unmounting {part}...") run_command(["sudo", "umount", "-l", part]) # Disable swap if it's on the drive with open("/proc/swaps") as swaps_file: for line in swaps_file: if drive in line: swap_part = line.strip().split()[0] log(f"Disabling swap on {swap_part}...") run_command(["sudo", "swapoff", swap_part]) # Kill any processes using the drive log(f"Checking for processes using {drive}...") result = subprocess.run(["sudo", "lsof", drive], capture_output=True, text=True) if result.stdout.strip(): log(f"Processes using {drive}:\n{result.stdout}") kill = input(f"Do you want to kill these processes? (y/n) [Default: y]: ") or "y" if kill.lower() == "y": run_command(["sudo", "fuser", "-k", drive]) log(f"Killed processes using {drive}.") else: log("Cannot proceed while processes are using the drive. Exiting.") sys.exit(1) else: log(f"No processes are using {drive}.") def setup_usb(): """ Sets up the bootable USB (Tails or Kali) and creates additional partitions if required. """ global DRIVE # Download portable VeraCrypt first (so it's ready to copy later) veracrypt_dir = None if CREATE_DOCS and not CHECKPOINTS["VERACRYPT_DOWNLOADED"]: veracrypt_dir = download_portable_veracrypt() save_state("VERACRYPT_DOWNLOADED", {"veracrypt_dir": veracrypt_dir}) elif CREATE_DOCS: # Already downloaded, use cached version veracrypt_dir = "/tmp/veracrypt_download" log("✓ Skipping VeraCrypt download (already completed)") if not CHECKPOINTS["DRIVE_SELECTED"]: log("\n" + "="*70) log("STEP 1: DRIVE SELECTION") log("="*70) log("Available storage devices on your system:") list_drives() log("\n⚠️ WARNING: This will repartition and format the selected drive.") log("Make sure you select the correct device!") while True: DRIVE = input("\nEnter the drive to use (e.g., /dev/sdb): ") if not DRIVE: log("Error: No drive specified. Please enter a drive path.") continue # Check if drive exists if not os.path.exists(DRIVE): log(f"Error: Drive {DRIVE} does not exist!") log("Please check the available drives listed above and try again.") retry = input("Try again? (y/n) [Default: y]: ") or "y" if retry.lower() != "y": log("Drive selection canceled. Exiting.") sys.exit(1) continue # Check if it's a block device if not os.path.isdir(DRIVE) and not os.path.exists(DRIVE): log(f"Error: {DRIVE} is not a valid block device!") continue # Confirm selection - more accurate warning log(f"\nYou selected: {DRIVE}") log("The script will:") log(" • Clear partition table") log(" • Create new partitions") log(" • Optionally wipe (you'll be asked)") log(" • Format with new filesystems") confirm = input(f"\nProceed with {DRIVE}? (yes/no) [Default: no]: ") if confirm.lower() == "yes": break else: log("Drive selection canceled.") retry = input("Select a different drive? (y/n) [Default: y]: ") or "y" if retry.lower() != "y": log("Exiting.") sys.exit(1) log("\n" + "="*70) log("STEP 2: PREPARING DRIVE") log("="*70) prepare_drive(DRIVE) save_state("DRIVE_SELECTED") else: log(f"✓ Skipping drive selection (already selected: {DRIVE})") if CREATE_KALI or CREATE_TAILS or CREATE_DOCS: log("\n" + "="*70) log("STEP 3: DRIVE WIPING (OPTIONAL)") log("="*70) log("Wiping destroys all existing data on the drive.") log("This prevents recovery of old files and ensures a clean setup.") log("") if PARANOID_MODE: log("PARANOID MODE: 3-pass overwrite (very slow, ~2-3 hours for 64GB)") else: log("STANDARD MODE: 1-pass zero overwrite (~10-20 min for 64GB)") log("Skip wiping if this is already a blank/new drive.") log("") wipe = input(f"Do you want to wipe {DRIVE} before starting? (y/n) [Default: n]: ") or "n" if wipe.lower() == "y": if PARANOID_MODE: log("\n" + "-"*70) log("PARANOID WIPE: 3 passes with random data + final zero pass") log("WARNING: This will take HOURS on large drives!") log("Estimated time: 2-3 hours for 64GB drive") log("-"*70) confirm_wipe = input(f"Type 'WIPE' to confirm complete wipe of {DRIVE}: ") if confirm_wipe == "WIPE": log("\nClearing partition table and filesystem signatures...") run_command(["sudo", "wipefs", "--all", DRIVE]) run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) log("\nStarting 3-pass shred (this will take a long time)...") log("Pass 1/3: Random data") log("Pass 2/3: Random data") log("Pass 3/3: Random data") log("Final pass: Zeros") log("You can monitor progress below:") # Paranoid: 3 passes with random data, then zeros run_command(["sudo", "shred", "-vfz", "-n", "3", DRIVE], interactive=True) log(f"\n✓ {DRIVE} securely wiped successfully (paranoid mode).") else: log("\nFull wipe canceled. Clearing partition table only...") run_command(["sudo", "wipefs", "--all", DRIVE]) run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) log("✓ Partition table cleared.") else: log("\n" + "-"*70) log("STANDARD WIPE: Single pass with zeros") log("Estimated time: ~10-20 minutes for 64GB drive") log("-"*70) confirm_wipe = input(f"Type 'WIPE' to confirm wipe of {DRIVE}: ") if confirm_wipe == "WIPE": log("\nClearing partition table and filesystem signatures...") run_command(["sudo", "wipefs", "--all", DRIVE]) run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) log("\nOverwriting entire drive with zeros...") log("Progress will be shown below (this may take 10-20 minutes):") log("(Note: dd will show 'No space left on device' when complete - this is normal)") log("") # Standard: Single pass with zeros (much faster) # Note: dd will report "No space left on device" when it fills the drive - this is expected and means success run_command(["sudo", "dd", "if=/dev/zero", f"of={DRIVE}", "bs=1M", "status=progress", "conv=fdatasync"], interactive=True, ignore_enospc=True) log(f"\n✓ {DRIVE} wiped successfully (standard mode).") else: log("\nFull wipe canceled. Clearing partition table only...") run_command(["sudo", "wipefs", "--all", DRIVE]) run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) log("✓ Partition table cleared.") else: log("\nSkipping full drive wipe. Clearing partition table only...") run_command(["sudo", "wipefs", "--all", DRIVE]) run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) log("✓ Partition table cleared.") # Initialize variables to track if ISO has been written iso_written = False if CREATE_KALI or CREATE_TAILS or CREATE_CUSTOM: log("\n" + "="*70) log("STEP 4: WRITING BOOTABLE ISO") log("="*70) global KALI_ISO, TAILS_ISO, CUSTOM_ISO if CREATE_KALI: if not KALI_ISO: KALI_ISO = input("Enter the path to the Kali ISO file: ") if not os.path.isfile(KALI_ISO): log(f"Error: Kali ISO file not found at {KALI_ISO}") sys.exit(1) ISO_PATH = KALI_ISO iso_name = "Kali Linux" elif CREATE_TAILS: if not TAILS_ISO: TAILS_ISO = input("Enter the path to the Tails ISO file: ") if not os.path.isfile(TAILS_ISO): log(f"Error: Tails ISO file not found at {TAILS_ISO}") sys.exit(1) ISO_PATH = TAILS_ISO iso_name = "Tails" elif CREATE_CUSTOM: if not CUSTOM_ISO: CUSTOM_ISO = input("Enter the path to your custom ISO file: ") if not os.path.isfile(CUSTOM_ISO): log(f"Error: Custom ISO file not found at {CUSTOM_ISO}") sys.exit(1) ISO_PATH = CUSTOM_ISO iso_name = "Custom" # Get ISO size for time estimate iso_size_bytes = os.path.getsize(ISO_PATH) iso_size_gb = iso_size_bytes / (1024**3) estimated_time = int((iso_size_gb / 0.5) * 60) # Rough estimate: 30 seconds per GB at 50MB/s log(f"ISO file: {ISO_PATH}") log(f"ISO size: {iso_size_gb:.2f} GB") log(f"Estimated write time: ~{estimated_time} seconds ({estimated_time//60} min)") log("") if CREATE_TAILS: # For Tails, flash the ISO to the entire drive without any partitioning log(f"Writing {iso_name} ISO to {DRIVE}...") log("Progress will be shown below:") log("") run_command(f"sudo dd if='{ISO_PATH}' of='{DRIVE}' bs=64M status=progress conv=fdatasync", shell=True, interactive=True) log(f"\n✓ {iso_name} ISO written to {DRIVE} successfully.") iso_written = True else: # For Kali or Custom ISO, flash and proceed with partition setup log(f"Writing {iso_name} ISO to {DRIVE}...") log("Progress will be shown below:") log("") run_command(f"sudo dd if='{ISO_PATH}' of='{DRIVE}' bs=64M status=progress conv=fdatasync", shell=True, interactive=True) log(f"\n✓ {iso_name} ISO written to {DRIVE} successfully.") iso_written = True # After writing ISO, set up partitions accordingly if iso_written: if CREATE_KALI or CREATE_CUSTOM: # Both Kali and custom ISOs use the same partitioning approach fix_partition_table(veracrypt_dir) elif CREATE_TAILS: if CREATE_DOCS: # For Tails with docs, add partitions after Tails fix_partition_table_tails(veracrypt_dir) else: # Tails only, no additional partitions log("Tails installation complete. No additional partitions requested.") elif CREATE_DOCS: # If no ISO was written, just set up documents partition fix_partition_table_docs_only(veracrypt_dir) def fix_partition_table_docs_only(veracrypt_dir=None): """ Sets up partitions solely for encrypted documents without altering existing OS partitions. """ log("\n" + "="*70) log("STEP 5: CREATING PARTITION LAYOUT") log("="*70) log("Creating 2 partitions:") log(" 1. Encrypted documents partition (VeraCrypt)") log(" 2. Tools partition (1GB, unencrypted, contains mount scripts)") log("") # Clear existing GPT label to start fresh log("Creating fresh GPT partition table...") run_command(f"sudo parted -a optimal -s {DRIVE} mklabel gpt", shell=True) run_command(f"sudo partprobe {DRIVE}", shell=True) run_command("sudo udevadm settle", shell=True) time.sleep(5) # Increased sleep to ensure partitions are recognized log("✓ GPT partition table created") # Get the total size of the drive in bytes log("\nCalculating partition sizes...") result = subprocess.run(["lsblk", "-b", "-d", "-n", "-o", "SIZE", DRIVE], capture_output=True, text=True) try: total_size_bytes = int(result.stdout.strip()) except ValueError: log(f"Error: Unable to determine drive size. lsblk output: {result.stdout}") sys.exit(1) total_size_mib = total_size_bytes / (1024 * 1024) # Convert to MiB total_size_gb = total_size_mib / 1024 # Convert to GiB available_gb = (total_size_mib - 1024) / 1024 # Minus 1GB reserved for scripts log(f" Total drive size: {total_size_gb:.2f} GB ({total_size_mib:.0f} MiB)") log(f" Available for documents: {available_gb:.2f} GB (after reserving 1GB for tools)") log("") # Ask for document partition size size_docs = input("Enter size for documents partition in GB (leave blank to use remaining space minus 1GB): ") start_docs_mib = 1 # Starting immediately after the first MiB if size_docs: try: size_docs_gb = float(size_docs) end_docs_mib = start_docs_mib + (size_docs_gb * 1024) if end_docs_mib > (total_size_mib - 1024): log("Error: Documents partition size exceeds available space when reserving 1GB for unencrypted partition.") sys.exit(1) except ValueError: log("Invalid size entered for documents partition. Exiting.") sys.exit(1) else: end_docs_mib = total_size_mib - 1024 # Reserve 1GB for unencrypted partition # Create documents partition log(f"\nCreating partition 1 (documents): {(end_docs_mib - start_docs_mib)/1024:.2f} GB...") run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_docs_mib}MiB {end_docs_mib}MiB", shell=True) log("✓ Documents partition created") # Create unencrypted partition start_unencrypted_mib = end_docs_mib log(f"Creating partition 2 (tools): ~1 GB...") run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_unencrypted_mib}MiB 100%", shell=True) log("✓ Tools partition created") # Refresh partition table to recognize new partitions log("\nRefreshing partition table...") run_command(f"sudo partprobe {DRIVE}", shell=True) run_command("sudo udevadm settle", shell=True) time.sleep(5) # Increased sleep to ensure partitions are recognized log("✓ Partition table refreshed") setup_unencrypted_partition(veracrypt_dir) setup_docs_partition() def fix_partition_table_tails(veracrypt_dir=None): """ Adds encrypted documents partition after Tails installation without breaking boot. Tails creates its own partition table, so we extend it carefully. """ log("Adding encrypted documents partition after Tails installation...") # Wait for Tails partitions to settle run_command(f"sudo partprobe {DRIVE}", shell=True) run_command("sudo udevadm settle", shell=True) time.sleep(5) # Get current partition information result = subprocess.run(["sudo", "parted", "-s", DRIVE, "unit", "MiB", "print"], capture_output=True, text=True) log("Current partition table after Tails installation:") log(result.stdout) # Find the end of the last Tails partition last_partition_end = None partition_lines = [line for line in result.stdout.strip().splitlines() if line.strip() and line.strip()[0].isdigit()] if partition_lines: # Get the last partition's end position last_line = partition_lines[-1] parts = last_line.strip().split() if len(parts) >= 3: last_partition_end = parts[2].replace('MiB', '') if last_partition_end is None: log("Error: Could not determine end of Tails partitions.") sys.exit(1) log(f"Last Tails partition ends at: {last_partition_end}MiB") # Get the total size of the drive result = subprocess.run(["lsblk", "-b", "-d", "-n", "-o", "SIZE", DRIVE], capture_output=True, text=True) try: total_size_bytes = int(result.stdout.strip()) except ValueError: log(f"Error: Unable to determine drive size. lsblk output: {result.stdout}") sys.exit(1) total_size_mib = total_size_bytes / (1024 * 1024) total_size_gb = total_size_mib / 1024 available_after_tails_mib = total_size_mib - float(last_partition_end) available_after_tails_gb = available_after_tails_mib / 1024 log(f"Total drive size: {total_size_gb:.2f} GB ({total_size_mib:.0f} MiB)") log(f"Available space after Tails: {available_after_tails_gb:.2f} GB") if available_after_tails_gb < 2: log(f"Warning: Only {available_after_tails_gb:.2f} GB available after Tails. Need at least 2GB for docs + tools partitions.") proceed = input("Continue anyway? (y/n) [Default: n]: ") or "n" if proceed.lower() != "y": log("Partition setup canceled.") return # Set up documents partition start_docs_mib = float(last_partition_end) remaining_after_tails_gb = (total_size_mib - start_docs_mib) / 1024 log(f"Remaining space for documents: {remaining_after_tails_gb:.2f} GB (will reserve 1GB for tools partition)") size_docs = input("Enter size for documents partition in GB (leave blank to use remaining space minus 1GB): ") if size_docs: try: size_docs_gb = float(size_docs) end_docs_mib = start_docs_mib + (size_docs_gb * 1024) if end_docs_mib > (total_size_mib - 1024): log("Error: Documents partition size exceeds available space when reserving 1GB for tools partition.") sys.exit(1) except ValueError: log("Invalid size entered for documents partition. Exiting.") sys.exit(1) else: end_docs_mib = total_size_mib - 1024 # Reserve 1GB for tools partition run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_docs_mib}MiB {end_docs_mib}MiB", shell=True) log("Created documents partition after Tails.") # Create unencrypted tools partition start_unencrypted_mib = end_docs_mib run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_unencrypted_mib}MiB 100%", shell=True) log("Created tools partition for scripts/instructions.") # Refresh partition table run_command(f"sudo partprobe {DRIVE}", shell=True) run_command("sudo udevadm settle", shell=True) time.sleep(5) setup_docs_partition() setup_unencrypted_partition(veracrypt_dir) log("Tails + encrypted documents partition setup complete!") def fix_partition_table(veracrypt_dir=None): """ Fixes the partition table for Kali Linux by adding persistence and documents partitions. """ log("Fixing partition table to reclaim remaining space...") # Attempt to delete partition 2 if it exists try: run_command(f"sudo parted -a optimal -s {DRIVE} rm 2", shell=True) log("Deleted partition 2.") except SystemExit: log("No partition 2 to delete.") # Get the end of partition 1 result = subprocess.run(["sudo", "parted", "-s", DRIVE, "unit", "MiB", "print"], capture_output=True, text=True) end_of_p1 = None for line in result.stdout.strip().splitlines(): if line.strip().startswith("1"): parts = line.strip().split() if len(parts) >= 3: end_of_p1 = parts[2].replace('MiB', '') break if end_of_p1 is None: log("Error: Could not find end of partition 1.") sys.exit(1) log(f"End of partition 1: {end_of_p1}MiB") # Get the total size of the drive in bytes result = subprocess.run(["lsblk", "-b", "-d", "-n", "-o", "SIZE", DRIVE], capture_output=True, text=True) try: total_size_bytes = int(result.stdout.strip()) except ValueError: log(f"Error: Unable to determine drive size. lsblk output: {result.stdout}") sys.exit(1) total_size_mib = total_size_bytes / (1024 * 1024) # Convert to MiB total_size_gb = total_size_mib / 1024 # Convert to GiB available_after_p1_gb = (total_size_mib - float(end_of_p1)) / 1024 log(f"Total drive size: {total_size_gb:.2f} GB ({total_size_mib:.0f} MiB)") log(f"Available space after partition 1: {available_after_p1_gb:.2f} GB") # Ask for persistence partition size size_persistence = input("Enter size for persistence partition in GB (e.g., 4): ") or "4" try: size_persistence_gb = float(size_persistence) except ValueError: log("Invalid size entered for persistence partition. Exiting.") sys.exit(1) start_persistence_mib = float(end_of_p1) end_persistence_mib = start_persistence_mib + (size_persistence_gb * 1024) if end_persistence_mib > total_size_mib: log("Error: Persistence partition size exceeds available space.") sys.exit(1) # Create persistence partition run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary ext4 {start_persistence_mib}MiB {end_persistence_mib}MiB", shell=True) log("Created persistence partition.") # Set up documents partition start_docs_mib = end_persistence_mib remaining_after_persistence_gb = (total_size_mib - end_persistence_mib) / 1024 log(f"Remaining space after persistence partition: {remaining_after_persistence_gb:.2f} GB (will reserve 1GB for scripts)") size_docs = input("Enter size for documents partition in GB (leave blank to use remaining space minus 1GB): ") if size_docs: try: size_docs_gb = float(size_docs) end_docs_mib = start_docs_mib + (size_docs_gb * 1024) if end_docs_mib > (total_size_mib - 1024): log("Error: Documents partition size exceeds available space when reserving 1GB for unencrypted partition.") sys.exit(1) except ValueError: log("Invalid size entered for documents partition. Exiting.") sys.exit(1) else: end_docs_mib = total_size_mib - 1024 # Reserve 1GB for unencrypted partition run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_docs_mib}MiB {end_docs_mib}MiB", shell=True) log("Created documents partition.") # Create unencrypted partition start_unencrypted_mib = end_docs_mib run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_unencrypted_mib}MiB 100%", shell=True) log("Created unencrypted partition for scripts/instructions.") # Refresh partition table to recognize new partitions run_command(f"sudo partprobe {DRIVE}", shell=True) run_command("sudo udevadm settle", shell=True) time.sleep(5) # Increased sleep to ensure partitions are recognized setup_kali_partition() if CREATE_DOCS: setup_docs_partition() setup_unencrypted_partition(veracrypt_dir) def setup_kali_partition(): """ Configures the persistence partition for Kali Linux. """ global DRIVE PERSIST_PART = get_partition_name(DRIVE, 2) # Check if partition exists if not os.path.exists(PERSIST_PART): log(f"Error: Partition {PERSIST_PART} does not exist.") sys.exit(1) run_command(["sudo", "wipefs", "--all", PERSIST_PART]) log("Configuring encrypted persistence partition...") log("You will be prompted to enter a passphrase for the persistence partition.") log("Please choose a strong passphrase and remember it!") log("When asked 'Are you sure? (Type YES in capital letters):', type: YES") log("Then enter your passphrase twice when prompted.") if FAST_MODE: luks_format_cmd = ( f"sudo cryptsetup luksFormat '{PERSIST_PART}' " f"--type luks1 " f"--cipher aes-cbc-essiv:sha256 " f"--key-size 256 " f"--hash sha256 " f"--iter-time 1000 " f"--verify-passphrase" ) mkfs_cmd = f"sudo mkfs.ext3 -L persistence /dev/mapper/kali_USB" elif PARANOID_MODE: luks_format_cmd = ( f"sudo cryptsetup luksFormat '{PERSIST_PART}' " f"--type luks2 " f"--cipher aes-xts-plain64 " f"--key-size 512 " f"--hash sha512 " f"--pbkdf argon2id " f"--iter-time 10000 " f"--pbkdf-memory 1048576 " f"--pbkdf-parallel 4 " f"--verify-passphrase" ) mkfs_cmd = f"sudo mkfs.ext4 -L persistence /dev/mapper/kali_USB" else: luks_format_cmd = ( f"sudo cryptsetup luksFormat '{PERSIST_PART}' " f"--type luks2 " f"--cipher aes-xts-plain64 " f"--key-size 512 " f"--hash sha512 " f"--iter-time 5000 " f"--verify-passphrase" ) mkfs_cmd = f"sudo mkfs.ext4 -L persistence /dev/mapper/kali_USB" run_command(luks_format_cmd, shell=True, interactive=True) time.sleep(2) run_command(f"sudo cryptsetup luksOpen '{PERSIST_PART}' kali_USB", shell=True, interactive=True) run_command(mkfs_cmd, shell=True) run_command("sudo mkdir -p /mnt/kali_USB", shell=True) run_command("sudo mount /dev/mapper/kali_USB /mnt/kali_USB", shell=True) run_command('echo "/ union" | sudo tee /mnt/kali_USB/persistence.conf', shell=True) run_command("sudo umount /mnt/kali_USB", shell=True) run_command("sudo cryptsetup luksClose kali_USB", shell=True) log("Kali persistence setup complete.") def setup_docs_partition(): """ Configures the VeraCrypt encrypted documents partition. """ global DRIVE DOCS_PART = get_partition_name(DRIVE, get_last_partition_number() - 1) log("\n" + "="*70) log("STEP 6: ENCRYPTING DOCUMENTS PARTITION") log("="*70) # Check if the partition exists if not os.path.exists(DOCS_PART): log(f"Error: Partition {DOCS_PART} does not exist.") sys.exit(1) # Force wipe existing filesystem signatures log(f"Preparing partition {DOCS_PART}...") run_command(["sudo", "wipefs", "--all", "--force", DOCS_PART]) log(f"✓ Partition prepared") log("\n" + "="*70) log("ENCRYPTED VOLUME SETUP") log("="*70) log("\nYou will now create an encrypted volume for sensitive documents.") log("") log("SECURITY SETTINGS:") log(" • Encryption: AES-Twofish-Serpent (triple cascade)") log(" • Hash: SHA-512") if PARANOID_MODE: log(" • PIM: 5000 (PARANOID mode - maximum security)") log(" • Unlock time: ~5-7 seconds") else: log(" • PIM: 2000 (standard high security)") log(" • Unlock time: ~2-3 seconds") log("") log("PASSWORD REQUIREMENTS:") log(" • Minimum 20 characters (longer is better)") log(" • Mix uppercase, lowercase, numbers, and symbols") log(" • Avoid dictionary words or personal information") log(" • Example: My$ecur3Tr@v3lD0cs!2025#Paris") log("") log("⚠ CRITICAL: There is NO password recovery!") log(" If you forget your password, your data is GONE FOREVER.") log("") log("="*70) input("Press ENTER when you're ready to create your password...") log("\nConfiguring VeraCrypt encryption for documents partition...") # Always use maximum security for documents partition (ignore FAST_MODE) # Triple cascade encryption with strongest hash pim_value = 5000 if PARANOID_MODE else 2000 veracrypt_create_cmd = ( f"veracrypt --text --create '{DOCS_PART}' " f"--encryption AES-Twofish-Serpent " f"--hash SHA-512 " f"--filesystem ext4 " f"--volume-type normal " f"--pim {pim_value} " # 2000 standard, 5000 paranoid ) log("\nStarting VeraCrypt encryption (this may take a few minutes)...") log("="*70) log("VERACRYPT PROMPTS - FOLLOW THESE STEPS:") log("="*70) log("") log("1. 'Enter password:' → Type your strong password (20+ characters)") log(" Then press ENTER") log("") log("2. 'Re-enter password:' → Type the SAME password again") log(" Then press ENTER") log("") log("3. 'Enter keyfile path [none]:' → Just press ENTER") log(" (Keyfiles not needed for most users)") log("") log("4. 'Please type at least 320 randomly chosen characters'") log(" → Smash random keys on your keyboard") log(" → Keep typing until it shows 'Characters remaining: 0'") log(" → Then press ENTER") log(" (This creates random entropy for encryption)") log("") log(f"ℹ️ NOTE: PIM is set to {pim_value} automatically (not prompted)") log("") log("="*70) log("Creating volume now...") log("="*70) log("") run_command(veracrypt_create_cmd, shell=True, interactive=True) log("\n✓ Documents partition encrypted successfully!") log(f" Encryption: AES-Twofish-Serpent (triple cascade)") log(f" Hash: SHA-512") log(f" PIM: {pim_value}") log(f" Filesystem: ext4") save_state("DOCS_ENCRYPTED") def setup_unencrypted_partition(veracrypt_dir=None): """ Sets up the unencrypted partition for scripts and instructions. Args: veracrypt_dir (str): Path to directory containing portable VeraCrypt files """ global DRIVE UNENCRYPTED_PART = get_partition_name(DRIVE, get_last_partition_number()) log("\n" + "="*70) log("STEP 7: SETTING UP TOOLS PARTITION") log("="*70) # Check if the partition exists if not os.path.exists(UNENCRYPTED_PART): log(f"Error: Partition {UNENCRYPTED_PART} does not exist.") sys.exit(1) log(f"Formatting {UNENCRYPTED_PART} as FAT32...") run_command(f"sudo mkfs.vfat -n 'TOOLS' {UNENCRYPTED_PART}", shell=True) log("✓ FAT32 filesystem created") log("Mounting tools partition...") run_command("sudo mkdir -p /mnt/unencrypted", shell=True) run_command(f"sudo mount {UNENCRYPTED_PART} /mnt/unencrypted", shell=True) log("✓ Tools partition mounted") # Get the directory where this script is located script_dir = os.path.dirname(os.path.abspath(__file__)) tools_dir = os.path.join(script_dir, "tools") log("\nCopying helper files from tools/ directory...") # Copy README.txt readme_path = os.path.join(tools_dir, "README.txt") if os.path.exists(readme_path): run_command(f"sudo cp '{readme_path}' /mnt/unencrypted/README.txt", shell=True) log(" ✓ README.txt copied") else: log(f" ⚠ Warning: {readme_path} not found, skipping README") # Copy WINDOWS_INSTRUCTIONS.txt windows_inst_path = os.path.join(tools_dir, "WINDOWS_INSTRUCTIONS.txt") if os.path.exists(windows_inst_path): run_command(f"sudo cp '{windows_inst_path}' /mnt/unencrypted/WINDOWS_INSTRUCTIONS.txt", shell=True) log(" ✓ WINDOWS_INSTRUCTIONS.txt copied") else: log(f" ⚠ Warning: {windows_inst_path} not found") # Copy MOBILE_INSTRUCTIONS.txt mobile_inst_path = os.path.join(tools_dir, "MOBILE_INSTRUCTIONS.txt") if os.path.exists(mobile_inst_path): run_command(f"sudo cp '{mobile_inst_path}' /mnt/unencrypted/MOBILE_INSTRUCTIONS.txt", shell=True) log(" ✓ MOBILE_INSTRUCTIONS.txt copied") else: log(f" ⚠ Warning: {mobile_inst_path} not found") # Copy mount_storage.sh mount_script_path = os.path.join(tools_dir, "mount_storage.sh") if os.path.exists(mount_script_path): run_command(f"sudo cp '{mount_script_path}' /mnt/unencrypted/mount_storage.sh", shell=True) run_command("sudo chmod +x /mnt/unencrypted/mount_storage.sh", shell=True) log(" ✓ mount_storage.sh copied") else: log(f" ⚠ Warning: {mount_script_path} not found, skipping mount script") # Copy lock_storage.sh lock_script_path = os.path.join(tools_dir, "lock_storage.sh") if os.path.exists(lock_script_path): run_command(f"sudo cp '{lock_script_path}' /mnt/unencrypted/lock_storage.sh", shell=True) run_command("sudo chmod +x /mnt/unencrypted/lock_storage.sh", shell=True) log(" ✓ lock_storage.sh copied") else: log(f" ⚠ Warning: {lock_script_path} not found, skipping lock script") # Copy portable VeraCrypt files if veracrypt_dir and os.path.exists(veracrypt_dir): log("\nCopying portable VeraCrypt binaries...") run_command("sudo mkdir -p /mnt/unencrypted/VeraCrypt", shell=True) veracrypt_files = [ ("VeraCrypt-Portable-Windows.exe", "Windows portable"), ("VeraCrypt-Portable-Linux.AppImage", "Linux portable AppImage"), ("veracrypt-Ubuntu-22.04-amd64.deb", "Linux DEB installer"), ("VeraCrypt-MacOS.dmg", "macOS installer") ] for filename, description in veracrypt_files: src = os.path.join(veracrypt_dir, filename) if os.path.exists(src): run_command(f"sudo cp '{src}' /mnt/unencrypted/VeraCrypt/{filename}", shell=True) if "Linux" in filename: run_command(f"sudo chmod +x /mnt/unencrypted/VeraCrypt/{filename}", shell=True) log(f" ✓ {description} copied") else: log(f" ⚠ {description} not found (optional)") log(" ✓ Portable VeraCrypt binaries installed") log("\n 📌 IMPORTANT: These binaries allow accessing encrypted files") log(" on ANY Windows, Linux, or Mac computer WITHOUT installing software!") else: log("\n ⚠ No portable VeraCrypt binaries provided - skipping") log("\nUnmounting tools partition...") run_command("sudo umount /mnt/unencrypted", shell=True) log("✓ Tools partition setup complete") save_state("TOOLS_SETUP") def get_last_partition_number(): """ Returns the highest partition number on the DRIVE. Returns: int: The highest partition number. """ result = subprocess.run(["lsblk", "-ln", "-o", "NAME", DRIVE], capture_output=True, text=True) partitions = [line.strip() for line in result.stdout.strip().splitlines() if line.strip()] partition_numbers = [] for part in partitions: if 'p' in part: # Handle /dev/nvme0n1p1 format if part.startswith(os.path.basename(DRIVE)): num = part.replace(os.path.basename(DRIVE), '').replace('p', '') if num.isdigit(): partition_numbers.append(int(num)) else: # Handle /dev/sda1 format if part.startswith(os.path.basename(DRIVE)): num = part.replace(os.path.basename(DRIVE), '') if num.isdigit(): partition_numbers.append(int(num)) if not partition_numbers: log(f"No partitions found on {DRIVE}.") sys.exit(1) return max(partition_numbers) def main(): """ The main function that parses arguments and orchestrates the setup process. """ global DEBUG, FAST_MODE, CREATE_KALI, CREATE_DOCS, CREATE_TAILS, KALI_ISO, TAILS_ISO, DRIVE, CREATE_CUSTOM, CUSTOM_ISO CREATE_CUSTOM = False CUSTOM_ISO = "" parser = argparse.ArgumentParser(description="Covert SD Card Tool") # Creating a mutually exclusive group for -a and -t to prevent their simultaneous use group = parser.add_mutually_exclusive_group() group.add_argument("-a", "--all", action="store_true", help="Set up both OS bootable USB and documents partition") group.add_argument("-t", "--tails", action="store_true", help="Create Tails bootable USB (no persistence)") # Other arguments remain outside the mutually exclusive group parser.add_argument("-k", "--kali", action="store_true", help="Create Kali bootable USB and persistence partition") parser.add_argument("-d", "--docs", action="store_true", help="Create encrypted documents partition") parser.add_argument("-c", "--custom", action="store_true", help="Create custom ISO bootable USB (like Kali, with persistence)") parser.add_argument("-i", "--iso", help="Path to the ISO file (Kali, Tails, or custom)") parser.add_argument("--fast", action="store_true", help="Enable fast setup with less secure encryption") parser.add_argument("--paranoid", action="store_true", help="Enable paranoid mode: maximum security, 3-pass shred, highest encryption") parser.add_argument("--debug", action="store_true", help="Enable debug mode") parser.add_argument("-r", "--resume", action="store_true", help="Resume from previous incomplete setup") args = parser.parse_args() # Prevent using -a with -t; argparse handles this due to mutually exclusive group # However, if you want to provide a custom error message or additional handling, you can add it here # Handle resume flag if args.resume: previous_state = load_state() if previous_state: log("\n" + "="*70) log("🔄 RESUMING PREVIOUS SETUP") log("="*70) log("") log("Found incomplete setup from previous run:") log(f" Drive: {previous_state.get('drive', 'Unknown')}") log(f" Started: {previous_state.get('timestamp', 'Unknown')}") log("") log("Completed steps:") for checkpoint, completed in previous_state.get('checkpoints', {}).items(): status = "✓" if completed else "✗" log(f" {status} {checkpoint}") log("") log("✓ Resuming from last checkpoint...") log(f"Continuing log in: {LOG_FILE}") log("") # State already loaded by load_state() # Skip to setup_usb() - it will check checkpoints check_dependencies() setup_usb() return else: log("Error: No previous setup found to resume.") log("State file not found at: /tmp/covert_sd_state.json") log("") log("To start a new setup, run without --resume flag:") log(" sudo ./covert_sd_card_tool.py -d -a -t /path/to/tails.iso") sys.exit(1) # Check if state exists but user didn't use --resume flag previous_state = load_state() if previous_state: log("\n" + "="*70) log("⚠️ PREVIOUS INCOMPLETE SETUP FOUND") log("="*70) log("") log(f"Drive: {previous_state.get('drive', 'Unknown')}") log(f"Started: {previous_state.get('timestamp', 'Unknown')}") log("") log("You have two options:") log(" 1. Resume previous setup: sudo ./covert_sd_card_tool.py --resume") log(" 2. Start fresh (WARNING: will overwrite any existing setup)") log("") choice = input("Continue with fresh setup? (yes/no) [Default: no]: ") or "no" if choice.lower() != "yes": log("Exiting. Use --resume flag to continue previous setup.") sys.exit(0) else: log("Starting fresh setup (old state will be cleared)...") clear_state() if not any([args.all, args.kali, args.docs, args.tails, args.custom]): parser.print_help() sys.exit(1) DEBUG = args.debug if DEBUG: log("Debug mode enabled") FAST_MODE = args.fast PARANOID_MODE = args.paranoid # Paranoid and Fast are mutually exclusive if FAST_MODE and PARANOID_MODE: log("Error: Cannot use --fast and --paranoid modes together.") sys.exit(1) if FAST_MODE: log("Fast mode enabled: Using less secure encryption for quicker setup.") elif PARANOID_MODE: log("PARANOID MODE enabled: Maximum security - 3-pass wipe, strongest encryption, highest PIM.") if args.all: CREATE_DOCS = True # When -a is used without -t, default to creating Kali CREATE_KALI = True else: CREATE_KALI = args.kali CREATE_DOCS = args.docs CREATE_TAILS = args.tails CREATE_CUSTOM = args.custom if args.iso: if CREATE_KALI: KALI_ISO = args.iso elif CREATE_TAILS: TAILS_ISO = args.iso elif CREATE_CUSTOM: CUSTOM_ISO = args.iso check_dependencies() setup_usb() # Final summary log("\n" + "="*70) log("SETUP COMPLETE!") log("="*70) log("") log("Your secure storage device is ready!") log("") log("What was created:") if CREATE_KALI: log(" ✓ Kali Linux bootable drive") log(" ✓ LUKS encrypted persistence partition") elif CREATE_TAILS: log(" ✓ Tails bootable drive") elif CREATE_CUSTOM: log(" ✓ Custom bootable drive") log(" ✓ LUKS encrypted persistence partition") if CREATE_DOCS: log(" ✓ VeraCrypt encrypted documents partition") if PARANOID_MODE: log(" - Triple cascade encryption (AES-Twofish-Serpent)") log(" - PIM 5000 (PARANOID mode)") else: log(" - Triple cascade encryption (AES-Twofish-Serpent)") log(" - PIM 2000 (standard security)") log(" ✓ Tools partition with helper scripts") log("") log("Next steps:") log(" 1. Safely eject the device") log(" 2. On your target system, mount the TOOLS partition") log(" 3. Read README.txt for instructions") log(" 4. Run: sudo ./mount_storage.sh") log("") log(f"Log file saved: {LOG_FILE}") log("") if CREATE_DOCS and os.path.exists("/tmp/veracrypt_download"): cache_size = sum(os.path.getsize(os.path.join("/tmp/veracrypt_download", f)) for f in os.listdir("/tmp/veracrypt_download") if os.path.isfile(os.path.join("/tmp/veracrypt_download", f))) / (1024 * 1024) log("💡 TIP: VeraCrypt downloads cached at /tmp/veracrypt_download") log(f" Size: {cache_size:.1f} MB") log(" Next SD card creation will skip downloads!") log(" To clean up: rm -rf /tmp/veracrypt_download") log("") log("="*70) # Mark as complete and clear state file save_state("COMPLETE") clear_state() if __name__ == "__main__": main()