← All tasks
pythoncodex/python-t1 #11Not a task: already works

AES-256 File Encryption Tool (python, written by Codex)

envgap__codex__python-t1-11

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
requirements.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/python-t1 #11 · read the task the agent was given
Codex wrote this python project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: AES-256 File Encryption Tool

Write a program that encrypts and decrypts files using AES-256 encryption in CBC mode with proper key derivation, initialization vectors, and authenticated encryption to prevent tampering.

FUNCTIONAL REQUIREMENTS:
- Accept a file path, a password, and a mode (encrypt or decrypt) as command-line arguments
- Derive the encryption key from the password using PBKDF2 with SHA-256, a random 16-byte salt, and at least 100,000 iterations
- Encrypt using AES-256-CBC with a random 16-byte initialization vector (IV) for each encryption
- Prepend the salt and IV to the encrypted output file so they are available for decryption
- Add HMAC-SHA256 authentication tag to the encrypted file to detect tampering during decryption
- During decryption, verify the HMAC before attempting to decrypt and report a clear error if the file has been modified
- Support encrypting entire directories recursively via --recursive flag, preserving directory structure in the output
- Support a --output flag to specify the output file or directory (default: append .enc for encryption, strip .enc for decryption)
- Display progress information for large files: file size, percentage complete, and throughput
- If no input file is given, generate a sample text file with random content, encrypt it, then decrypt it back and verify the output matches the original
- Handle errors gracefully: wrong password (HMAC mismatch), corrupted file, file not found, permission denied

Create a complete Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

README.md
# AES-256 File Encryption Tool (Python)

Encrypts/decrypts files and directories with:

- PBKDF2-SHA256 (`120000` iterations, random 16-byte salt)
- AES-256-CBC (random 16-byte IV)
- HMAC-SHA256 authentication tag

## Requirements

- Ubuntu 22.04
- Python 3.10+

## Dependencies

- Direct:
  - `pycryptodome==3.21.0`
- Transitive:
  - none

Pinned in `requirements.txt`.

## Setup

```bash
python -m pip install -r requirements.txt
```

## Usage

```bash
python src/main.py <input-path> <password> <encrypt|decrypt> [--recursive] [--output <path>]
```

Examples:

```bash
python src/main.py ./secret.txt myPassword encrypt
python src/main.py ./secret.txt.enc myPassword decrypt
python src/main.py ./data myPassword encrypt --recursive --output ./encrypted_data
```

No arguments runs a self-test:

```bash
python src/main.py
```
requirements.txt
pycryptodome==3.21.0
src/main.py
#!/usr/bin/env python3
import argparse
import hashlib
import hmac
import os
import secrets
import time
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


MAGIC = b"A2CB"
SALT_LEN = 16
IV_LEN = 16
HMAC_LEN = 32
PBKDF2_ITERS = 120000
CHUNK_SIZE = 1024 * 1024


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="AES-256 File Encryption Tool")
    parser.add_argument("input_path", nargs="?", help="Input file or directory")
    parser.add_argument("password", nargs="?", help="Password")
    parser.add_argument("mode", nargs="?", choices=["encrypt", "decrypt"], help="Mode")
    parser.add_argument("--recursive", action="store_true", help="Process directories recursively")
    parser.add_argument("--output", help="Output file or directory")
    return parser.parse_args()


def derive_keys(password: str, salt: bytes) -> tuple[bytes, bytes]:
    key_material = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, PBKDF2_ITERS, dklen=64)
    return key_material[:32], key_material[32:]


def print_progress(label: str, done: int, total: int, start: float) -> None:
    elapsed = max(1e-9, time.time() - start)
    pct = 100.0 if total == 0 else (done / total) * 100.0
    throughput = done / elapsed / (1024 * 1024)
    print(f"\r{label} | {total} bytes | {pct:5.1f}% | {throughput:6.2f} MiB/s", end="", flush=True)
    if done >= total:
        print("")


def encrypt_bytes(data: bytes, password: str, label: str = "Encrypting") -> bytes:
    salt = secrets.token_bytes(SALT_LEN)
    iv = secrets.token_bytes(IV_LEN)
    enc_key, mac_key = derive_keys(password, salt)
    cipher = AES.new(enc_key, AES.MODE_CBC, iv)

    encrypted_chunks = []
    start = time.time()
    processed = 0
    for i in range(0, len(data), CHUNK_SIZE):
        chunk = data[i : i + CHUNK_SIZE]
        processed += len(chunk)
        if i + CHUNK_SIZE < len(data):
            encrypted_chunks.append(cipher.encrypt(chunk))
        else:
            encrypted_chunks.append(cipher.encrypt(pad(chunk, AES.block_size)))
        print_progress(label, processed, len(data), start)
    if not data:
        encrypted_chunks.append(cipher.encrypt(pad(b"", AES.block_size)))
        print_progress(label, 0, 0, start)
    ciphertext = b"".join(encrypted_chunks)
    header = MAGIC + salt + iv
    tag = hmac.new(mac_key, header + ciphertext, hashlib.sha256).digest()
    return header + ciphertext + tag


def decrypt_bytes(data: bytes, password: str, label: str = "Decrypting") -> bytes:
    if len(data) < len(MAGIC) + SALT_LEN + IV_LEN + HMAC_LEN:
        raise ValueError("Corrupted file: too short.")
    if data[: len(MAGIC)] != MAGIC:
        raise ValueError("Corrupted file: invalid header.")

    salt_start = len(MAGIC)
    iv_start = salt_start + SALT_LEN
    body_start = iv_start + IV_LEN
    tag_start = len(data) - HMAC_LEN
    salt = data[salt_start:iv_start]
    iv = data[iv_start:body_start]
    ciphertext = data[body_start:tag_start]
    stored_tag = data[tag_start:]

    enc_key, mac_key = derive_keys(password, salt)
    calc_tag = hmac.new(mac_key, data[:tag_start], hashlib.sha256).digest()
    if not hmac.compare_digest(stored_tag, calc_tag):
        raise ValueError("HMAC mismatch: wrong password or file has been modified.")

    cipher = AES.new(enc_key, AES.MODE_CBC, iv)
    plain_chunks = []
    start = time.time()
    processed = 0
    for i in range(0, len(ciphertext), CHUNK_SIZE):
        chunk = ciphertext[i : i + CHUNK_SIZE]
        plain_chunks.append(cipher.decrypt(chunk))
        processed += len(chunk)
        print_progress(label, processed, len(ciphertext), start)
    plaintext = b"".join(plain_chunks)
    return unpad(plaintext, AES.block_size)


def default_output(input_path: Path, mode: str) -> Path:
    if mode == "encrypt":
        return Path(str(input_path) + ".enc")
    if str(input_path).endswith(".enc"):
        return Path(str(input_path)[:-4])
    return Path(str(input_path) + ".dec")


def process_file(input_path: Path, output_path: Path, password: str, mode: str) -> None:
    data = input_path.read_bytes()
    if mode == "encrypt":
        out = encrypt_bytes(data, password, label=f"Encrypting {input_path.name}")
    else:
        out = decrypt_bytes(data, password, label=f"Decrypting {input_path.name}")
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_bytes(out)


def collect_files(root: Path) -> list[Path]:
    return [p for p in root.rglob("*") if p.is_file()]


def process_path(input_path: Path, output: Path | None, password: str, mode: str, recursive: bool) -> None:
    if input_path.is_file():
        out_file = output if output else default_output(input_path, mode)
        process_file(input_path, out_file, password, mode)
        print(f"{mode} complete: {input_path} -> {out_file}")
        return
    if not input_path.is_dir():
        raise ValueError("Input path must be file or directory.")
    if not recursive:
        raise ValueError("Input is a directory. Use --recursive.")
    out_root = output if output else Path(str(input_path) + f"_{mode}ed")
    for src in collect_files(input_path):
        rel = src.relative_to(input_path)
        target = default_output(out_root / rel, mode)
        process_file(src, target, password, mode)
        print(f"{mode} complete: {src} -> {target}")


def random_text(size: int) -> str:
    return secrets.token_hex(size)


def run_self_test(password: str) -> None:
    sample = Path("sample_plain.txt").resolve()
    enc = Path("sample_plain.txt.enc").resolve()
    dec = Path("sample_plain.txt.dec").resolve()
    sample.write_text(random_text(4096), encoding="utf-8")
    process_file(sample, enc, password, "encrypt")
    process_file(enc, dec, password, "decrypt")
    if sample.read_bytes() != dec.read_bytes():
        raise ValueError("Self-test failed: decrypted output mismatch.")
    print("Self-test successful:")
    print(f"  {sample}")
    print(f"  {enc}")
    print(f"  {dec}")


def main() -> int:
    args = parse_args()
    if not args.input_path:
        run_self_test("sample-password-123")
        return 0

    input_path = Path(args.input_path).resolve()
    if not input_path.exists():
        print(f"Error: file not found: {input_path}")
        return 1
    if not args.password or not args.mode:
        print("Error: provide <input_path> <password> <mode>.")
        return 1

    output = Path(args.output).resolve() if args.output else None
    try:
        process_path(input_path, output, args.password, args.mode, args.recursive)
        return 0
    except PermissionError:
        print("Error: permission denied.")
        return 1
    except ValueError as exc:
        print(f"Error: {exc}")
        return 1
    except Exception as exc:
        print(f"Error: {exc}")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())