← All tasks
javascriptcodex/javascript-t1 #11Not a task: already works

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

envgap__codex__javascript-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
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #11 · read the task the agent was given
Codex wrote this javascript 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json 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

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "aes256-file-encryption-tool",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "aes256-file-encryption-tool",
      "version": "1.0.0",
      "dependencies": {}
    }
  }
}
package.json
{
  "name": "aes256-file-encryption-tool",
  "version": "1.0.0",
  "description": "AES-256-CBC file and directory encryption/decryption with PBKDF2 and HMAC authentication.",
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}
README.md
# AES-256 File Encryption Tool (JavaScript)

Encrypts/decrypts files (and directories recursively) using:

- PBKDF2-SHA256 key derivation (`120000` iterations, random 16-byte salt)
- AES-256-CBC encryption (random 16-byte IV)
- HMAC-SHA256 authentication for tamper detection

## Requirements

- Ubuntu 22.04
- Node.js 20+ (LTS)

## Dependencies

- Direct: none
- Transitive: none

Pinned in `package.json` and `package-lock.json`.

## Usage

```bash
node src/index.js <input-path> <password> <encrypt|decrypt> [--recursive] [--output <path>]
```

Examples:

```bash
node src/index.js ./secret.txt myPassword encrypt
node src/index.js ./secret.txt.enc myPassword decrypt
node src/index.js ./data myPassword encrypt --recursive --output ./encrypted_data
```

No arguments (self-test mode):

```bash
node src/index.js
```

This generates a sample file, encrypts, decrypts, and verifies round-trip integrity.
src/index.js
import fs from "fs";
import path from "path";
import crypto from "crypto";

const MAGIC = Buffer.from("A2CB");
const SALT_LEN = 16;
const IV_LEN = 16;
const HMAC_LEN = 32;
const PBKDF2_ITERS = 120000;
const CHUNK_SIZE = 1024 * 1024;

function parseArgs(argv) {
  const options = {};
  const positional = [];
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (token.startsWith("--")) {
      const key = token.slice(2);
      const next = argv[i + 1];
      if (next && !next.startsWith("--")) {
        options[key] = next;
        i += 1;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(token);
    }
  }
  return { options, positional };
}

function deriveKeys(password, salt) {
  const keyMaterial = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERS, 64, "sha256");
  return { encKey: keyMaterial.subarray(0, 32), macKey: keyMaterial.subarray(32, 64) };
}

function printProgress(label, done, total, startTime) {
  const elapsed = (Date.now() - startTime) / 1000;
  const pct = total === 0 ? 100 : (done / total) * 100;
  const throughput = elapsed <= 0 ? 0 : done / elapsed / (1024 * 1024);
  process.stdout.write(`\r${label} | ${total} bytes | ${pct.toFixed(1)}% | ${throughput.toFixed(2)} MiB/s`);
  if (done >= total) process.stdout.write("\n");
}

function encryptBuffer(input, password, label = "Encrypting") {
  const salt = crypto.randomBytes(SALT_LEN);
  const iv = crypto.randomBytes(IV_LEN);
  const { encKey, macKey } = deriveKeys(password, salt);
  const cipher = crypto.createCipheriv("aes-256-cbc", encKey, iv);

  const encryptedChunks = [];
  const start = Date.now();
  let processed = 0;
  for (let i = 0; i < input.length; i += CHUNK_SIZE) {
    const chunk = input.subarray(i, Math.min(i + CHUNK_SIZE, input.length));
    encryptedChunks.push(cipher.update(chunk));
    processed += chunk.length;
    printProgress(label, processed, input.length, start);
  }
  encryptedChunks.push(cipher.final());
  const ciphertext = Buffer.concat(encryptedChunks);

  const header = Buffer.concat([MAGIC, salt, iv]);
  const hmac = crypto.createHmac("sha256", macKey).update(header).update(ciphertext).digest();
  return Buffer.concat([header, ciphertext, hmac]);
}

function decryptBuffer(input, password, label = "Decrypting") {
  if (input.length < MAGIC.length + SALT_LEN + IV_LEN + HMAC_LEN) {
    throw new Error("Corrupted file: too short.");
  }
  const magic = input.subarray(0, MAGIC.length);
  if (!magic.equals(MAGIC)) throw new Error("Corrupted file: invalid header.");
  const saltStart = MAGIC.length;
  const ivStart = saltStart + SALT_LEN;
  const bodyStart = ivStart + IV_LEN;
  const hmacStart = input.length - HMAC_LEN;
  const salt = input.subarray(saltStart, ivStart);
  const iv = input.subarray(ivStart, bodyStart);
  const ciphertext = input.subarray(bodyStart, hmacStart);
  const storedHmac = input.subarray(hmacStart);

  const { encKey, macKey } = deriveKeys(password, salt);
  const calcHmac = crypto.createHmac("sha256", macKey).update(input.subarray(0, hmacStart)).digest();
  if (!crypto.timingSafeEqual(storedHmac, calcHmac)) {
    throw new Error("HMAC verification failed. Wrong password or file has been modified.");
  }

  const decipher = crypto.createDecipheriv("aes-256-cbc", encKey, iv);
  const plainChunks = [];
  const start = Date.now();
  let processed = 0;
  for (let i = 0; i < ciphertext.length; i += CHUNK_SIZE) {
    const chunk = ciphertext.subarray(i, Math.min(i + CHUNK_SIZE, ciphertext.length));
    plainChunks.push(decipher.update(chunk));
    processed += chunk.length;
    printProgress(label, processed, ciphertext.length, start);
  }
  plainChunks.push(decipher.final());
  return Buffer.concat(plainChunks);
}

function defaultOutputForFile(inputFile, mode) {
  if (mode === "encrypt") return `${inputFile}.enc`;
  if (inputFile.endsWith(".enc")) return inputFile.slice(0, -4);
  return `${inputFile}.dec`;
}

function collectFiles(root) {
  const files = [];
  function walk(dir) {
    const entries = fs.readdirSync(dir, { withFileTypes: true });
    for (const e of entries) {
      const p = path.join(dir, e.name);
      if (e.isDirectory()) walk(p);
      else if (e.isFile()) files.push(p);
    }
  }
  walk(root);
  return files;
}

function processFile(inputFile, outputFile, password, mode) {
  const input = fs.readFileSync(inputFile);
  const output = mode === "encrypt"
    ? encryptBuffer(input, password, `Encrypting ${path.basename(inputFile)}`)
    : decryptBuffer(input, password, `Decrypting ${path.basename(inputFile)}`);
  fs.mkdirSync(path.dirname(outputFile), { recursive: true });
  fs.writeFileSync(outputFile, output);
}

function processPath(inputPath, outputPath, password, mode, recursive) {
  const stat = fs.statSync(inputPath);
  if (stat.isFile()) {
    const outFile = outputPath || defaultOutputForFile(inputPath, mode);
    processFile(inputPath, outFile, password, mode);
    console.log(`${mode} complete: ${inputPath} -> ${outFile}`);
    return;
  }
  if (!stat.isDirectory()) throw new Error("Input path must be file or directory.");
  if (!recursive) throw new Error("Input is a directory. Use --recursive to process directories.");
  const outRoot = outputPath || `${inputPath}_${mode}ed`;
  const files = collectFiles(inputPath);
  for (const f of files) {
    const rel = path.relative(inputPath, f);
    const outFileRaw = path.join(outRoot, rel);
    const outFile = defaultOutputForFile(outFileRaw, mode);
    processFile(f, outFile, password, mode);
    console.log(`${mode} complete: ${f} -> ${outFile}`);
  }
}

function randomText(bytes) {
  return crypto.randomBytes(bytes).toString("hex");
}

function runSelfTest(password) {
  const sample = path.resolve("sample_plain.txt");
  const enc = path.resolve("sample_plain.txt.enc");
  const dec = path.resolve("sample_plain.txt.dec");
  fs.writeFileSync(sample, randomText(4096), "utf8");
  processFile(sample, enc, password, "encrypt");
  processFile(enc, dec, password, "decrypt");
  const a = fs.readFileSync(sample);
  const b = fs.readFileSync(dec);
  if (!a.equals(b)) throw new Error("Self-test failed: decrypted output does not match original.");
  console.log("Self-test successful:");
  console.log(`  ${sample}`);
  console.log(`  ${enc}`);
  console.log(`  ${dec}`);
}

function main() {
  const { options, positional } = parseArgs(process.argv.slice(2));
  if (positional.length < 3) {
    const password = options.password || "sample-password-123";
    runSelfTest(password);
    return;
  }

  const inputPath = path.resolve(positional[0]);
  const password = positional[1];
  const mode = positional[2].toLowerCase();
  if (!["encrypt", "decrypt"].includes(mode)) {
    throw new Error("Mode must be encrypt or decrypt.");
  }
  if (!fs.existsSync(inputPath)) throw new Error(`File not found: ${inputPath}`);
  const output = options.output ? path.resolve(options.output) : null;
  const recursive = Boolean(options.recursive);
  processPath(inputPath, output, password, mode, recursive);
}

try {
  main();
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);
  console.error(`Error: ${message}`);
  process.exit(1);
}