npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
Overview
AES (Advanced Encryption Standard) is a symmetric block cipher standardized by NIST (FIPS 197) used to protect classified and sensitive data. This skill covers implementing AES-256 encryption in GCM mode for encrypting files and data stores at rest, including proper key derivation, IV/nonce management, and authenticated encryption.
When to Use
- When deploying or configuring implementing aes encryption for data at rest capabilities in your environment
- When establishing security controls aligned to compliance requirements
- When building or improving security architecture for this domain
- When conducting security assessments that require this implementation
Prerequisites
- Familiarity with cryptography concepts and tools
- Access to a test or lab environment for safe execution
- Python 3.8+ with required dependencies installed
- Appropriate authorization for any testing activities
Objectives
- Implement AES-256-GCM encryption and decryption for files
- Derive encryption keys from passwords using PBKDF2 and Argon2
- Manage initialization vectors (IVs) and nonces securely
- Encrypt and decrypt entire directory trees
- Implement authenticated encryption to detect tampering
- Handle large files with streaming encryption
Key Concepts
AES Modes of Operation
| Mode | Authentication | Parallelizable | Use Case |
|---|---|---|---|
| GCM | Yes (AEAD) | Yes | Network data, file encryption |
| CBC | No | Decrypt only | Legacy systems, disk encryption |
| CTR | No | Yes | Streaming encryption |
| CCM | Yes (AEAD) | No | IoT, constrained environments |
Key Derivation
Never use raw passwords as encryption keys. Always derive keys using:
- PBKDF2: NIST-approved, widely supported (minimum 600,000 iterations as of 2024)
- Argon2id: Winner of Password Hashing Competition, memory-hard
- scrypt: Memory-hard, good alternative to Argon2
Nonce/IV Management
- GCM requires a 96-bit (12-byte) nonce that must NEVER be reused with the same key
- Generate nonces using
os.urandom()(CSPRNG) - Store nonce alongside ciphertext (it is not secret)
Workflow
- Install the
cryptographylibrary:pip install cryptography - Generate or derive an encryption key
- Create a random nonce for each encryption operation
- Encrypt data using AES-256-GCM with the key and nonce
- Store nonce + ciphertext + authentication tag together
- For decryption, extract nonce, verify tag, and decrypt
Encrypted File Format
[salt: 16 bytes][nonce: 12 bytes][ciphertext: variable][tag: 16 bytes]Security Considerations
- Always use authenticated encryption (GCM, CCM) to prevent tampering
- Never reuse a nonce with the same key (catastrophic in GCM)
- Use at least 256-bit keys for long-term data protection
- Securely wipe keys from memory after use when possible
- Rotate encryption keys periodically per organizational policy
- For disk-level encryption, consider XTS mode (AES-XTS)
Validation Criteria
- AES-256-GCM encryption produces valid ciphertext
- Decryption recovers original plaintext exactly
- Authentication tag detects any ciphertext modification
- Key derivation uses sufficient iterations/parameters
- Nonces are never reused for the same key
- Large files (>1GB) can be processed via streaming
- Encrypted file format includes all necessary metadata
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md1.8 KB
API Reference: Implementing AES Encryption for Data at Rest
cryptography Library - AESGCM
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce, NEVER reuse
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data)Key Derivation - PBKDF2
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # 256-bit key
salt=os.urandom(16),
iterations=600_000, # NIST 2024 recommendation
)
key = kdf.derive(password.encode())Encrypted File Format
[salt: 16 bytes][nonce: 12 bytes][ciphertext + tag: variable]| Field | Size | Purpose |
|---|---|---|
| Salt | 16 bytes | PBKDF2 salt (random per file) |
| Nonce | 12 bytes | GCM nonce (random per encryption) |
| Ciphertext | Variable | Encrypted data + 16-byte auth tag |
AES Modes Comparison
| Mode | AEAD | Nonce Size | Use Case |
|---|---|---|---|
| GCM | Yes | 12 bytes | File/network encryption |
| CBC | No | 16 bytes | Legacy, disk encryption |
| CTR | No | 16 bytes | Streaming |
| XTS | No | 16 bytes | Full disk encryption |
Fernet (High-Level API)
from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
token = f.encrypt(b"data")
plaintext = f.decrypt(token)References
- cryptography AESGCM: https://cryptography.io/en/latest/hazmat/primitives/aead/
- NIST SP 800-38D (GCM): https://csrc.nist.gov/publications/detail/sp/800-38d/final
- NIST FIPS 197 (AES): https://csrc.nist.gov/publications/detail/fips/197/final
standards.md3.3 KB
Standards and References - AES Encryption for Data at Rest
Primary Standards
NIST FIPS 197 - Advanced Encryption Standard (AES)
- URL: https://csrc.nist.gov/publications/detail/fips/197/final
- Description: Defines the AES algorithm (Rijndael) with key sizes of 128, 192, and 256 bits
- Block size: 128 bits (16 bytes)
- Key sizes: 128, 192, or 256 bits
- Rounds: 10 (128-bit), 12 (192-bit), 14 (256-bit)
NIST SP 800-38D - Recommendation for Block Cipher Modes: GCM and GMAC
- URL: https://csrc.nist.gov/publications/detail/sp/800-38d/final
- Description: Specifies Galois/Counter Mode (GCM) for authenticated encryption
- IV length: 96 bits recommended for GCM
- Tag length: 128 bits recommended (minimum 96 bits)
- Max plaintext: 2^39 - 256 bits per invocation
NIST SP 800-132 - Recommendation for Password-Based Key Derivation
- URL: https://csrc.nist.gov/publications/detail/sp/800-132/final
- Description: Covers PBKDF2 for deriving cryptographic keys from passwords
- Minimum iterations: 600,000 (OWASP 2024 recommendation for PBKDF2-SHA256)
- Salt length: Minimum 128 bits (16 bytes)
NIST SP 800-38A - Recommendation for Block Cipher Modes of Operation
- URL: https://csrc.nist.gov/publications/detail/sp/800-38a/final
- Description: Defines ECB, CBC, CFB, OFB, and CTR modes
NIST SP 800-57 Part 1 Rev. 5 - Key Management
- URL: https://csrc.nist.gov/publications/detail/sp/800-57-part-1/rev-5/final
- Description: Recommendations for cryptographic key lengths and algorithms
- AES-256 security strength: 256 bits
- Recommended until: Beyond 2031
RFC Standards
RFC 5116 - An Interface and Algorithms for Authenticated Encryption
- URL: https://www.rfc-editor.org/rfc/rfc5116
- Description: Defines AEAD interface including AES-GCM
RFC 5869 - HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- URL: https://www.rfc-editor.org/rfc/rfc5869
- Description: Key derivation from existing key material (not passwords)
RFC 9106 - Argon2 Memory-Hard Function
- URL: https://www.rfc-editor.org/rfc/rfc9106
- Description: Argon2 password hashing / key derivation specification
- Recommended variant: Argon2id (hybrid of Argon2i and Argon2d)
Compliance Frameworks
PCI DSS v4.0 - Requirement 3
- Encrypt stored cardholder data with strong cryptography
- AES-256 meets the strong cryptography requirement
- Key management procedures required
HIPAA Security Rule - 45 CFR 164.312(a)(2)(iv)
- Encryption of ePHI at rest is an addressable implementation specification
- AES-256 is an acceptable encryption method
GDPR Article 32 - Security of Processing
- Encryption is listed as an appropriate technical measure
- AES-256 satisfies encryption requirements for personal data protection
Python Library References
cryptography (pyca/cryptography)
- URL: https://cryptography.io/en/latest/
- PyPI: https://pypi.org/project/cryptography/
- AES-GCM:
cryptography.hazmat.primitives.ciphers.aead.AESGCM - PBKDF2:
cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC
PyCryptodome
- URL: https://pycryptodome.readthedocs.io/
- PyPI: https://pypi.org/project/pycryptodome/
- AES-GCM:
Crypto.Cipher.AESwithMODE_GCM
workflows.md2.9 KB
Workflows - AES Encryption for Data at Rest
Workflow 1: Single File Encryption
[Input File] --> [Read File Bytes]
|
[Derive Key from Password]
(PBKDF2 / Argon2id + random salt)
|
[Generate Random Nonce]
(12 bytes from CSPRNG)
|
[AES-256-GCM Encrypt]
(key + nonce + plaintext --> ciphertext + tag)
|
[Write Encrypted File]
(salt || nonce || ciphertext || tag)Workflow 2: Single File Decryption
[Encrypted File] --> [Parse Header]
(extract salt, nonce)
|
[Derive Key from Password]
(same PBKDF2 / Argon2id params + extracted salt)
|
[AES-256-GCM Decrypt]
(key + nonce + ciphertext + tag)
|
[Verify Authentication Tag]
(reject if tag invalid)
|
[Write Decrypted File]Workflow 3: Streaming Encryption for Large Files
[Large Input File]
|
[Read in Chunks] (e.g., 64KB chunks)
|
[For Each Chunk]:
- [Encrypt chunk with AES-256-CTR]
- [Update HMAC with ciphertext chunk]
- [Write encrypted chunk to output]
|
[Finalize HMAC]
[Append HMAC tag to output]Workflow 4: Directory Tree Encryption
[Source Directory]
|
[Walk Directory Tree]
|
[For Each File]:
- [Derive unique file key from master key + file path]
- [Generate random nonce]
- [AES-256-GCM encrypt file]
- [Write encrypted file preserving directory structure]
|
[Create Manifest File]
(maps original paths to encrypted paths with metadata)Workflow 5: Key Derivation Pipeline
[User Password]
|
[Generate Random Salt] (16 bytes)
|
[PBKDF2-SHA256]
- iterations: 600,000+
- dkLen: 32 bytes (256 bits)
|
[Derived Key (256-bit)]
|
[Optional: HKDF Expand]
- Derive multiple subkeys from single derived key
- info="encryption" --> encryption key
- info="authentication" --> HMAC keyWorkflow 6: Envelope Encryption Pattern
[Master Key] (stored in HSM/KMS)
|
[Generate Random Data Encryption Key (DEK)]
(32 bytes from CSPRNG)
|
[Encrypt DEK with Master Key] --> [Encrypted DEK]
|
[Encrypt Data with DEK] --> [Ciphertext]
|
[Store: Encrypted DEK + Ciphertext]
[Securely Wipe DEK from Memory]Error Handling Workflow
[Decryption Attempt]
|
[Parse Header] --FAIL--> [Return: Corrupt/invalid file format]
|
[Derive Key] --FAIL--> [Return: KDF parameter error]
|
[Decrypt + Verify Tag]
|
[Tag Valid?]
YES --> [Return plaintext]
NO --> [Return: Authentication failed - data tampered]
[DO NOT return partial plaintext]Scripts 2
agent.py5.3 KB
#!/usr/bin/env python3
"""Agent for implementing AES-256-GCM encryption for data at rest."""
import os
import json
import argparse
from datetime import datetime
from pathlib import Path
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
SALT_SIZE = 16
NONCE_SIZE = 12
KEY_SIZE = 32 # 256 bits
TAG_SIZE = 16
PBKDF2_ITERATIONS = 600_000
def derive_key(password, salt=None):
"""Derive AES-256 key from password using PBKDF2-HMAC-SHA256."""
if salt is None:
salt = os.urandom(SALT_SIZE)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=KEY_SIZE,
salt=salt,
iterations=PBKDF2_ITERATIONS,
)
key = kdf.derive(password.encode("utf-8"))
return key, salt
def encrypt_file(input_path, output_path, password):
"""Encrypt a file using AES-256-GCM with PBKDF2 key derivation."""
key, salt = derive_key(password)
nonce = os.urandom(NONCE_SIZE)
aesgcm = AESGCM(key)
with open(input_path, "rb") as f:
plaintext = f.read()
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
with open(output_path, "wb") as f:
f.write(salt)
f.write(nonce)
f.write(ciphertext)
return {
"input": str(input_path),
"output": str(output_path),
"original_size": len(plaintext),
"encrypted_size": SALT_SIZE + NONCE_SIZE + len(ciphertext),
"algorithm": "AES-256-GCM",
"kdf": f"PBKDF2-HMAC-SHA256 ({PBKDF2_ITERATIONS} iterations)",
}
def decrypt_file(input_path, output_path, password):
"""Decrypt an AES-256-GCM encrypted file."""
with open(input_path, "rb") as f:
salt = f.read(SALT_SIZE)
nonce = f.read(NONCE_SIZE)
ciphertext = f.read()
key, _ = derive_key(password, salt)
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
with open(output_path, "wb") as f:
f.write(plaintext)
return {
"input": str(input_path),
"output": str(output_path),
"decrypted_size": len(plaintext),
}
def encrypt_directory(dir_path, output_dir, password):
"""Encrypt all files in a directory tree."""
src = Path(dir_path)
dst = Path(output_dir)
dst.mkdir(parents=True, exist_ok=True)
results = []
for filepath in src.rglob("*"):
if filepath.is_file():
rel = filepath.relative_to(src)
out = dst / (str(rel) + ".enc")
out.parent.mkdir(parents=True, exist_ok=True)
result = encrypt_file(str(filepath), str(out), password)
results.append(result)
return results
def generate_random_key():
"""Generate a random AES-256 key."""
key = os.urandom(KEY_SIZE)
return {
"key_hex": key.hex(),
"key_size_bits": KEY_SIZE * 8,
"algorithm": "AES-256",
}
def verify_encryption(original_path, encrypted_path, password):
"""Verify encryption by decrypting and comparing."""
with open(original_path, "rb") as f:
original = f.read()
with open(encrypted_path, "rb") as f:
salt = f.read(SALT_SIZE)
nonce = f.read(NONCE_SIZE)
ciphertext = f.read()
key, _ = derive_key(password, salt)
aesgcm = AESGCM(key)
try:
decrypted = aesgcm.decrypt(nonce, ciphertext, None)
match = original == decrypted
return {"status": "PASS" if match else "FAIL", "content_match": match}
except Exception as e:
return {"status": "FAIL", "error": str(e)}
def main():
parser = argparse.ArgumentParser(description="AES-256-GCM Encryption Agent")
parser.add_argument("--action", required=True,
choices=["encrypt", "decrypt", "encrypt_dir", "genkey", "verify"])
parser.add_argument("--input", help="Input file or directory")
parser.add_argument("--output", help="Output file or directory")
parser.add_argument("--password", help="Encryption password")
parser.add_argument("--report", default="aes_encryption_report.json")
args = parser.parse_args()
report = {"generated_at": datetime.utcnow().isoformat(), "action": args.action}
if args.action == "encrypt":
result = encrypt_file(args.input, args.output, args.password)
report["result"] = result
print(f"[+] Encrypted: {args.input} -> {args.output}")
elif args.action == "decrypt":
result = decrypt_file(args.input, args.output, args.password)
report["result"] = result
print(f"[+] Decrypted: {args.input} -> {args.output}")
elif args.action == "encrypt_dir":
results = encrypt_directory(args.input, args.output, args.password)
report["results"] = results
print(f"[+] Encrypted {len(results)} files")
elif args.action == "genkey":
result = generate_random_key()
report["result"] = result
print(f"[+] Key: {result['key_hex']}")
elif args.action == "verify":
result = verify_encryption(args.input, args.output, args.password)
report["result"] = result
print(f"[+] Verification: {result['status']}")
with open(args.report, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.report}")
if __name__ == "__main__":
main()
process.py11.4 KB
#!/usr/bin/env python3
"""
AES-256-GCM Encryption for Data at Rest
Implements file and directory encryption using AES-256-GCM with
PBKDF2 key derivation. Supports single file, streaming large file,
and directory tree encryption.
Requirements:
pip install cryptography
Usage:
python process.py encrypt --input secret.pdf --output secret.pdf.enc --password "MySecurePass"
python process.py decrypt --input secret.pdf.enc --output secret.pdf --password "MySecurePass"
python process.py encrypt-dir --input ./sensitive/ --output ./encrypted/ --password "MySecurePass"
"""
import os
import sys
import json
import struct
import hashlib
import argparse
import logging
from pathlib import Path
from typing import Optional, Tuple
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# Constants
SALT_LENGTH = 16 # 128-bit salt
NONCE_LENGTH = 12 # 96-bit nonce (recommended for GCM)
TAG_LENGTH = 16 # 128-bit authentication tag
KEY_LENGTH = 32 # 256-bit key
PBKDF2_ITERATIONS = 600_000 # OWASP 2024 recommendation
CHUNK_SIZE = 64 * 1024 # 64KB chunks for streaming
MAGIC_BYTES = b"AES256GCM" # File format identifier
VERSION = 1
def derive_key(password: str, salt: bytes, iterations: int = PBKDF2_ITERATIONS) -> bytes:
"""Derive a 256-bit encryption key from a password using PBKDF2-SHA256."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=KEY_LENGTH,
salt=salt,
iterations=iterations,
backend=default_backend(),
)
return kdf.derive(password.encode("utf-8"))
def encrypt_bytes(plaintext: bytes, password: str) -> bytes:
"""
Encrypt plaintext bytes using AES-256-GCM with PBKDF2 key derivation.
Output format:
MAGIC (9 bytes) || VERSION (1 byte) || SALT (16 bytes) || NONCE (12 bytes) || CIPHERTEXT+TAG (variable)
The authentication tag is appended to ciphertext by AESGCM.
"""
salt = os.urandom(SALT_LENGTH)
nonce = os.urandom(NONCE_LENGTH)
key = derive_key(password, salt)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=None)
header = MAGIC_BYTES + struct.pack("B", VERSION)
return header + salt + nonce + ciphertext
def decrypt_bytes(data: bytes, password: str) -> bytes:
"""
Decrypt AES-256-GCM encrypted data.
Raises:
ValueError: If file format is invalid or authentication fails.
"""
magic_len = len(MAGIC_BYTES)
min_length = magic_len + 1 + SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH
if len(data) < min_length:
raise ValueError("Data too short to be a valid encrypted file")
magic = data[:magic_len]
if magic != MAGIC_BYTES:
raise ValueError(f"Invalid file format: expected magic bytes {MAGIC_BYTES!r}, got {magic!r}")
version = struct.unpack("B", data[magic_len : magic_len + 1])[0]
if version != VERSION:
raise ValueError(f"Unsupported version: {version}")
offset = magic_len + 1
salt = data[offset : offset + SALT_LENGTH]
offset += SALT_LENGTH
nonce = data[offset : offset + NONCE_LENGTH]
offset += NONCE_LENGTH
ciphertext = data[offset:]
key = derive_key(password, salt)
aesgcm = AESGCM(key)
try:
plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
except Exception as e:
raise ValueError(
"Decryption failed: authentication tag verification failed. "
"Either the password is wrong or the data has been tampered with."
) from e
return plaintext
def encrypt_file(input_path: str, output_path: str, password: str) -> dict:
"""Encrypt a single file."""
input_file = Path(input_path)
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
plaintext = input_file.read_bytes()
original_size = len(plaintext)
original_hash = hashlib.sha256(plaintext).hexdigest()
ciphertext = encrypt_bytes(plaintext, password)
output_file = Path(output_path)
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_bytes(ciphertext)
encrypted_size = len(ciphertext)
logger.info(f"Encrypted {input_path} -> {output_path} ({original_size} -> {encrypted_size} bytes)")
return {
"input": str(input_path),
"output": str(output_path),
"original_size": original_size,
"encrypted_size": encrypted_size,
"original_sha256": original_hash,
"algorithm": "AES-256-GCM",
"kdf": "PBKDF2-SHA256",
"kdf_iterations": PBKDF2_ITERATIONS,
}
def decrypt_file(input_path: str, output_path: str, password: str) -> dict:
"""Decrypt a single file."""
input_file = Path(input_path)
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
data = input_file.read_bytes()
plaintext = decrypt_bytes(data, password)
output_file = Path(output_path)
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_bytes(plaintext)
recovered_hash = hashlib.sha256(plaintext).hexdigest()
logger.info(f"Decrypted {input_path} -> {output_path} ({len(plaintext)} bytes)")
return {
"input": str(input_path),
"output": str(output_path),
"decrypted_size": len(plaintext),
"recovered_sha256": recovered_hash,
}
def encrypt_directory(input_dir: str, output_dir: str, password: str) -> dict:
"""Encrypt all files in a directory tree, preserving structure."""
input_path = Path(input_dir)
output_path = Path(output_dir)
if not input_path.is_dir():
raise NotADirectoryError(f"Input is not a directory: {input_dir}")
output_path.mkdir(parents=True, exist_ok=True)
manifest = {
"algorithm": "AES-256-GCM",
"kdf": "PBKDF2-SHA256",
"kdf_iterations": PBKDF2_ITERATIONS,
"files": [],
}
file_count = 0
total_original = 0
total_encrypted = 0
for file in sorted(input_path.rglob("*")):
if file.is_file():
relative = file.relative_to(input_path)
encrypted_name = str(relative) + ".enc"
dest = output_path / encrypted_name
result = encrypt_file(str(file), str(dest), password)
manifest["files"].append({
"original_path": str(relative),
"encrypted_path": encrypted_name,
"original_sha256": result["original_sha256"],
"original_size": result["original_size"],
})
file_count += 1
total_original += result["original_size"]
total_encrypted += result["encrypted_size"]
manifest_path = output_path / "manifest.json"
manifest_encrypted = encrypt_bytes(json.dumps(manifest, indent=2).encode(), password)
(output_path / "manifest.json.enc").write_bytes(manifest_encrypted)
logger.info(
f"Encrypted {file_count} files from {input_dir} -> {output_dir} "
f"({total_original} -> {total_encrypted} bytes)"
)
return {
"files_encrypted": file_count,
"total_original_bytes": total_original,
"total_encrypted_bytes": total_encrypted,
"output_directory": str(output_path),
}
def decrypt_directory(input_dir: str, output_dir: str, password: str) -> dict:
"""Decrypt all .enc files in a directory tree."""
input_path = Path(input_dir)
output_path = Path(output_dir)
if not input_path.is_dir():
raise NotADirectoryError(f"Input is not a directory: {input_dir}")
output_path.mkdir(parents=True, exist_ok=True)
file_count = 0
total_decrypted = 0
for file in sorted(input_path.rglob("*.enc")):
if file.name == "manifest.json.enc":
continue
if file.is_file():
relative = file.relative_to(input_path)
decrypted_name = str(relative).removesuffix(".enc")
dest = output_path / decrypted_name
result = decrypt_file(str(file), str(dest), password)
file_count += 1
total_decrypted += result["decrypted_size"]
logger.info(f"Decrypted {file_count} files from {input_dir} -> {output_dir}")
return {
"files_decrypted": file_count,
"total_decrypted_bytes": total_decrypted,
"output_directory": str(output_path),
}
def verify_roundtrip(test_data: bytes, password: str) -> bool:
"""Verify encryption/decryption roundtrip integrity."""
encrypted = encrypt_bytes(test_data, password)
decrypted = decrypt_bytes(encrypted, password)
return decrypted == test_data
def main():
parser = argparse.ArgumentParser(description="AES-256-GCM File Encryption Tool")
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# Encrypt command
enc = subparsers.add_parser("encrypt", help="Encrypt a file")
enc.add_argument("--input", "-i", required=True, help="Input file path")
enc.add_argument("--output", "-o", required=True, help="Output file path")
enc.add_argument("--password", "-p", required=True, help="Encryption password")
# Decrypt command
dec = subparsers.add_parser("decrypt", help="Decrypt a file")
dec.add_argument("--input", "-i", required=True, help="Input file path")
dec.add_argument("--output", "-o", required=True, help="Output file path")
dec.add_argument("--password", "-p", required=True, help="Decryption password")
# Encrypt directory command
encdir = subparsers.add_parser("encrypt-dir", help="Encrypt a directory")
encdir.add_argument("--input", "-i", required=True, help="Input directory path")
encdir.add_argument("--output", "-o", required=True, help="Output directory path")
encdir.add_argument("--password", "-p", required=True, help="Encryption password")
# Decrypt directory command
decdir = subparsers.add_parser("decrypt-dir", help="Decrypt a directory")
decdir.add_argument("--input", "-i", required=True, help="Input directory path")
decdir.add_argument("--output", "-o", required=True, help="Output directory path")
decdir.add_argument("--password", "-p", required=True, help="Decryption password")
# Verify command
subparsers.add_parser("verify", help="Run roundtrip verification test")
args = parser.parse_args()
if args.command == "encrypt":
result = encrypt_file(args.input, args.output, args.password)
print(json.dumps(result, indent=2))
elif args.command == "decrypt":
result = decrypt_file(args.input, args.output, args.password)
print(json.dumps(result, indent=2))
elif args.command == "encrypt-dir":
result = encrypt_directory(args.input, args.output, args.password)
print(json.dumps(result, indent=2))
elif args.command == "decrypt-dir":
result = decrypt_directory(args.input, args.output, args.password)
print(json.dumps(result, indent=2))
elif args.command == "verify":
test_data = b"The quick brown fox jumps over the lazy dog. " * 100
password = "test_password_123!"
success = verify_roundtrip(test_data, password)
print(f"Roundtrip verification: {'PASSED' if success else 'FAILED'}")
if not success:
sys.exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()