I built SwatCrypt because I wanted to encrypt files and I didn’t trust myself to bolt crypto onto something else without making a mistake. The goal was simple: a file goes in, a .swc container comes out, and without the passphrase or keyfile it’s unrecoverable. The implementation required me to make a sequence of choices — cipher, KDF, file format, streaming strategy — and every one of them had a non-obvious wrong answer waiting.
Why not AES-GCM
AES-GCM is the obvious starting point. It’s a NIST standard, hardware-accelerated on every modern CPU, and used everywhere from TLS to full-disk encryption. The problem is nonce management.
GCM uses a 96-bit nonce. That’s twelve bytes. If you generate nonces randomly, the birthday bound means you have meaningful collision probability after about 2^32 encryptions — roughly four billion. That sounds like a lot until you remember that a tool which encrypts individual files will generate a new nonce per chunk, and chunks are 64 KB by default. A 64 GB file is a million chunks, which is still fine, but the math gets uncomfortable faster than you’d think.
XChaCha20-Poly1305 uses a 192-bit nonce. You can generate that randomly for every encryption operation and the collision probability over the entire lifetime of the tool is negligible. For a desktop encryption utility, that removes a whole category of nonce-reuse risk that you’d otherwise have to actively manage.
The performance difference is real but small. AES-NI makes GCM fast. XChaCha20 is still fast enough — on a modern machine you’re nowhere near a throughput bottleneck encrypting files from disk.
Key derivation with Argon2id
Passwords are not keys. A password has maybe 40 bits of entropy if you’re lucky. AES-256 needs 256 bits. The bridge between them is a key derivation function, and the choice matters a great deal.
Argon2id won the Password Hashing Competition in 2015. It’s memory-hard (expensive to parallelize on GPUs), time-hard (can’t be shortcut with ASICs), and parameterizable. SwatCrypt uses 64 MiB of memory and 3 iterations by default, with single-thread parallelism.
Why those numbers? The OWASP recommendation for Argon2id is at minimum 64 MiB / 3 iterations / 1 thread. That’s where I landed. It’s enough to make offline brute-force attacks genuinely painful while keeping the UX acceptable — the KDF takes a visible but non-annoying fraction of a second on mid-range hardware.
The parameters are stored in the file header alongside the salt, so future versions can increase them without breaking old files.
Keyfile support
Passwords can be stolen by keyloggers or guessed. Keyfiles add a second factor that is completely orthogonal to the passphrase. SwatCrypt generates keyfiles up to 4096 bytes of cryptographically random data. The passphrase and keyfile are combined during key derivation — you need both to decrypt.
The implementation uses a 32-byte salt derived from the combination of passphrase and keyfile inputs fed into Argon2id. Losing the keyfile means losing access to the encrypted data. That’s a feature, not a bug, but it’s one worth being explicit about in the UI, which SwatCrypt is.
You can use passphrase-only, keyfile-only, or both. The combination is handled in the KDF step rather than as separate encryption layers, which keeps the crypto surface minimal.
Streaming encryption and chunk boundaries
Loading the entire file into memory to encrypt it is a bad idea for two reasons. First, memory is finite. A 50 GB archive doesn’t fit. Second, you don’t want the decrypted plaintext to exist in memory longer than necessary.
SwatCrypt uses chunked streaming. The default chunk size is 64 KiB, configurable between 4 KiB and 2 MiB. Each chunk is independently encrypted with XChaCha20-Poly1305 and gets its own 16-byte AEAD authentication tag. This means:
- Decryption can start on chunk 1 before chunk 2 is read
- A corrupted chunk is detected immediately rather than at the end of the stream
- Memory pressure is bounded regardless of file size
The nonce is derived per-chunk from the file-level key material. The chunk counter is included in the nonce construction, which means reordering or truncating the encrypted stream is detectable — you can’t strip the last few chunks off a .swc file and have the recipient successfully decrypt the rest.
The file format design is where encryption tools most often go wrong. You need to store enough metadata to decrypt without revealing anything that weakens the encryption.
A .swc file has two sections:
Header (authenticated plaintext): Magic bytes, version number, cipher suite identifier, KDF parameters (memory, iterations, parallelism), salt length, nonce length, chunk size, and feature flags (compression enabled, bundle mode). This is plaintext because the decryptor needs these values before it has a key, but it’s included in the AEAD authentication — so tampering with it is detected during decryption.
Body (encrypted): A manifest (JSON with relative file paths and sizes) followed by a tar-stream payload, optionally gzip-compressed before encryption. The manifest lets you list the contents of a .swc bundle without decrypting the actual files.
The choice to use tar internally was deliberate. Tar is boring and well-understood. It handles directories, preserves relative paths, and is supported everywhere. There’s no value in inventing a custom archive format when the encryption layer already provides the security.
Secure deletion before you think to ask
When SwatCrypt encrypts a file, it can optionally wipe the plaintext source before deletion. This does what BitBurn does for standalone wiping — random overwriting before unlink. The underlying issue with SSDs and wear levelling means you can’t guarantee true erasure on flash storage, but the overwrite is still better than leaving the plaintext intact.
The zeroize crate is used throughout to zero sensitive data — passphrases, key material, derived keys — when it goes out of scope. This is enforced by types that implement Zeroize rather than left to manual cleanup calls that could be missed. The entire codebase has #![forbid(unsafe_code)] in the lint configuration. No unsafe blocks anywhere. The cryptography is handled by audited crates (chacha20poly1305, argon2) that have their own unsafe budgets, but the SwatCrypt code itself has none.
The most-used path for SwatCrypt is drag-and-drop in the GUI. But a right-click context menu in Windows Explorer is more ergonomic for day-to-day use.
SwatCrypt installs its context menu entry into HKCU\Software\Classes — the per-user registry hive — rather than HKLM. This means the context menu works without requiring administrator elevation at install time. The caveat is that it only shows up for the current user, which is fine for a personal encryption tool.
The entry adds “Decrypt with SwatCrypt” to the .swc file type. Encryption is done through the GUI or CLI — the context menu only handles the decryption case, which is the one you most often want to do without opening the full application.
CLI for scripting
The GUI is for interactive use. The CLI is for everything else.
swatcrypt encrypt --output archive.swc file1.txt dir/
swatcrypt decrypt --output ./recovered/ archive.swc
swatcrypt info archive.swc
info reads the header without decryption — it’ll tell you the cipher suite, KDF parameters, whether compression is enabled, and the list of files in the bundle. Useful for verifying what you have before you decrypt it.
The CLI uses rpassword for passphrase input, which reads from the terminal without echoing and without writing to stdin history. If you’re piping the passphrase in from a file in a script, that’s your call, but the default path handles it correctly.
What it doesn’t do
SwatCrypt doesn’t do public-key encryption. There’s no asymmetric key pair. This is a deliberate scope decision — symmetric encryption from a passphrase is simpler to understand, simpler to audit, and covers the “encrypt a file for myself” case, which is 90% of what I need.
It also doesn’t do network transfer, cloud upload, or anything that requires a server. It’s a local tool. If you want to send an encrypted file, you encrypt it, then send it through whatever channel you’re already using. Keeping the encryption layer separate from the transport layer is the right architecture.
The tool is on GitHub, MIT licensed.