Skip to content
ashishsaud2025Public

About

CLI for encrypting and decrypting single files with authenticated encryption (AES-GCM + PBKDF2) and optional secure deletion.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

cryptfile

A small command-line tool for encrypting and decrypting individual files. It has two commands and no plugin system, sync feature, or cloud component. The point of this project is to demonstrate correct use of authenticated encryption on files of arbitrary size, not to be a full-featured tool.

cryptfile encrypt myphotos.tar
cryptfile decrypt myphotos.tar.enc

Install

pip install -e .

This installs a cryptfile command backed by the cryptography library.

Usage

cryptfile encrypt <file> [-o output] [--iterations N] [--shred]
cryptfile decrypt <file> [-o output]

Both commands prompt for the passphrase interactively (via getpass). The passphrase is never accepted as a command-line argument, because arguments are visible in shell history and in ps output on shared machines.

  • encrypt writes <file>.enc by default, or a path given with -o.
  • decrypt writes <file> with .enc stripped by default, or a path given with -o.
  • --shred overwrites and deletes the original plaintext file after a successful encrypt. See "Shred limitations" below before relying on this for anything sensitive.

Crypto design

Key derivation: PBKDF2-HMAC-SHA256, 310,000 iterations

The encryption key is derived from the user's passphrase with PBKDF2-HMAC-SHA256. PBKDF2 is not the newest password-hashing construction available (Argon2id would be a reasonable alternative), but it is well understood, has an implementation directly available in cryptography's hazmat layer without pulling in another dependency, and is still an accepted choice under current OWASP guidance provided the iteration count is high enough.

310,000 iterations is OWASP's published baseline for PBKDF2-HMAC-SHA256 as of this writing. It is not hardcoded as an unchangeable constant: the iteration count actually used is stored in the file's header, so a file encrypted today can still be decrypted correctly even if a future version of this tool raises the default for new files. Old files keep working; they just remain protected at whatever iteration count they were written with, which is a case for re-encrypting old sensitive files periodically rather than assuming they get stronger for free.

The salt is 16 random bytes generated fresh for every file with os.urandom and stored alongside the ciphertext. It is never fixed or reused across files. Without a per-file salt, an attacker who precomputes a table of derived keys for common passphrases could apply that table to every file encrypted with this tool at once; a fresh salt forces the attacker to redo the expensive derivation work per file.

Encryption: AES-256-GCM, chunked

AES-GCM was chosen over AES-CBC because GCM is an authenticated mode: decryption fails loudly if the ciphertext or the associated data has been modified, instead of silently producing corrupted plaintext that looks superficially valid. CBC on its own gives no integrity guarantee at all; using it safely means bolting on a separate MAC and getting the encrypt-then-MAC ordering right, which is exactly the kind of detail that is easy to get wrong by hand. GCM avoids that whole category of mistake by folding authentication into the mode itself.

The one hard rule for GCM is that a (key, nonce) pair must never be reused. Reusing a nonce with the same key breaks GCM's confidentiality and, in some cases, allows an attacker to forge ciphertexts. This tool handles that as follows:

  • Every file gets a fresh, unique 256-bit key, because every file gets a fresh random salt and the key is derived from (passphrase, salt).
  • Within a file, the nonce for chunk i is a random 4-byte per-file prefix concatenated with an 8-byte big-endian counter starting at 0. Since the key is already unique to the file, a monotonically increasing counter is sufficient on its own to guarantee no nonce is reused within that file. The random prefix is added on top as a second layer of protection, so that even a bug which reset the counter to 0 across two different files would still need the same key to matter, and the key already differs because the salt does.
  • The counter is 8 bytes, so a single file would need over 2^64 chunks before it could wrap around. At a 64 KiB chunk size that is far beyond any file this tool will ever see in practice.

Why chunked encryption instead of one big nonce over the whole file

Encrypting a whole file as a single AES-GCM operation would require holding the entire plaintext (or the entire ciphertext) in memory at once, which does not scale to large files. It also concentrates all of a file's data behind a single nonce and a single 128-bit authentication tag, so any single bit error anywhere in a multi-gigabyte file fails the whole file at once with no ability to process it incrementally.

Splitting the file into fixed-size chunks (64 KiB by default) and encrypting each chunk as its own AES-GCM operation solves the memory problem: the tool only ever needs one chunk resident in memory during encryption or decryption, so file size is effectively unbounded on memory grounds. Each chunk gets a unique nonce as described above, and its own tag, so tampering can be localized to a specific chunk index instead of merely "somewhere in the file."

Chunking does introduce a new problem that a single whole-file nonce does not have: an attacker can drop, duplicate, or reorder chunks in a way that AES-GCM's per-chunk authentication alone does not catch, since each chunk is authenticated in isolation. This is addressed by binding each chunk's position and finality into its associated data (AAD):

AAD = chunk_index (8 bytes, big-endian) || is_last (1 byte)

Decryption is done in file order and checks the chunk index it expects against the index baked into the ciphertext's AAD implicitly, by only succeeding when it decrypts with the AAD for that exact index. An attacker who reorders chunks changes which AAD is needed to decrypt each one, so decryption of the misplaced chunk fails. An attacker who truncates the file after a non-final chunk causes decryption to finish without ever reaching a chunk whose AAD says is_last = true, which this tool treats as an error rather than a successful decrypt of a shorter file.

File format

MAGIC            8 bytes    b"CRYPTFIL"
VERSION          1 byte     currently 1
KDF_ITERATIONS   4 bytes    big-endian uint32
SALT             16 bytes   PBKDF2 salt
NONCE_PREFIX     4 bytes    random per-file nonce prefix
CHUNK_SIZE       4 bytes    big-endian uint32, plaintext bytes per chunk
---- header ends, one or more chunks follow ----
IS_LAST          1 byte     0x00 or 0x01, plaintext framing hint
CHUNK_LEN        4 bytes    big-endian uint32, length of ciphertext+tag
CHUNK_DATA       CHUNK_LEN  ciphertext followed by the 16-byte GCM tag

IS_LAST is stored in plaintext so a reader does not need to guess which AAD to try, but it is not trusted on its own: it is also part of the AAD used for that chunk's GCM tag, so if it is altered on disk the chunk simply fails authentication instead of being silently misinterpreted.

The format is versioned from the first release. A future version that changes the header layout, the KDF, or the chunking scheme can bump VERSION and branch on it during reads, without losing the ability to decrypt files written by this version.

Why hazmat primitives directly, not a hand-rolled cipher

This tool uses cryptography's hazmat AESGCM and PBKDF2HMAC classes directly rather than implementing AES or PBKDF2 from scratch. Implementing AES-GCM correctly by hand, including getting the nonce construction, tag verification, and constant-time comparisons right, is a well-known source of real vulnerabilities even for careful engineers, and there is no benefit to redoing that work here. The part of this project that is actually interesting is the key management, nonce handling, and chunking design described above, not a reimplementation of primitives that a well-vetted library already provides.

Threat model and limitations

This tool protects file contents at rest against someone who obtains the .enc file without the passphrase, assuming the passphrase itself is strong and kept secret. It does not protect against everything, and some of the gaps are worth stating plainly rather than glossing over:

  • No protection against a compromised machine while the tool is running. If the OS, another process, or a keylogger can observe what you type or read process memory while cryptfile is running, the passphrase and plaintext are exposed regardless of what the crypto does. This tool has no defense against that; it assumes the machine it runs on is not already compromised at the time of use.

  • No secure memory wiping. Python does not give a program reliable control over when memory is copied, moved by the garbage collector, or paged to disk. This tool does not attempt to zero out passphrases or plaintext buffers in memory, and doing so in CPython would be partial at best. Treat anything decrypted during a run as potentially recoverable from memory or swap after the fact, and don't consider this suitable for adversaries who can capture memory images of the machine.

  • Passphrase strength is the user's responsibility. PBKDF2 at 310,000 iterations raises the cost of brute-forcing a weak passphrase, but it does not turn a weak passphrase into a strong one. A short or common passphrase is still crackable, just more slowly.

  • --shred is best-effort, not a guarantee. It overwrites the original file's bytes in place and deletes it. On a traditional spinning disk with a filesystem that overwrites data in place, this is reasonably effective. On modern SSDs, wear leveling means the physical cells holding the old data are frequently not the ones that get overwritten by a logical "overwrite the same file" operation, so a copy of the original data can persist on the drive regardless of what this tool does at the filesystem level. Journaling and copy-on-write filesystems (and filesystem snapshots, and backups) have similar problems. If a file absolutely must not be recoverable, full-disk encryption from the start is a much stronger guarantee than after-the-fact shredding of a plaintext copy.

  • No protection for metadata. File size (rounded up to the nearest chunk boundary) is visible from the size of the .enc file. This tool does not pad files to hide their approximate size, and it does not encrypt filenames.

  • Not a substitute for transport security. This tool encrypts files at rest. It has no opinion on how the resulting .enc file gets from one machine to another; use a secure channel for that separately.

About

CLI for encrypting and decrypting single files with authenticated encryption (AES-GCM + PBKDF2) and optional secure deletion.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages