I still remember the first time I lost a laptop with unencrypted client files on it. Nothing sensitive was ever misused, as far as I know, but the scare was enough to make me build a habit around encrypting anything that matters before it leaves my machine. Over the years I’ve settled on a simple, dependable approach: a small Bash script wrapped around OpenSSL that encrypts and decrypts files on demand. In this article I’m going to walk you through building that tool from scratch, explain exactly how it works under the hood, and share the security lessons I picked up the hard way.
This isn’t a theoretical exercise. By the end, you’ll have a working command-line utility you can drop into your ~/bin folder and use every day.
Why Build Your Own Encryption Tool in Bash
There are plenty of GUI encryption apps out there, but I prefer a script for a few reasons:
- It’s scriptable, so I can plug it into backup jobs, cron tasks, and CI pipelines.
- It has zero dependencies beyond OpenSSL, which ships with almost every Linux and macOS system.
- I can read every line of it, so I know exactly what it’s doing to my data.
- It’s portable — I can copy one file to a new machine and I’m back in business.
Prerequisites
Before I dive into the code, here’s what you need:
- A Linux, macOS, or WSL environment with Bash 4+.
- OpenSSL installed (
openssl versionto check). - Basic comfort with the terminal.
If OpenSSL isn’t installed, on Debian/Ubuntu you can grab it with:
sudo apt update && sudo apt install openssl -y
On macOS, it usually comes preinstalled, or you can get a newer version via Homebrew:
brew install openssl
Understanding the Core Concept: Symmetric Encryption
The tool I’m building uses symmetric encryption, meaning the same passphrase is used to both encrypt and decrypt the file. OpenSSL’s enc command supports this through the AES-256-CBC cipher, which is what I’ll use here. It’s not the newest cipher mode available, but it’s widely supported and battle-tested, which matters more to me than chasing the latest algorithm.
The basic OpenSSL syntax looks like this:
openssl enc -aes-256-cbc -salt -pbkdf2 -in secret.txt -out secret.txt.enc
And to reverse it:
openssl enc -aes-256-cbc -d -pbkdf2 -in secret.txt.enc -out secret.txt
That’s the engine. Now let’s build a proper script around it.
Step 1: Writing the Basic Script
I always start with a skeleton before adding bells and whistles. Create a file called cryptool.sh:
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [encrypt|decrypt] <file>"
exit 1
}
if [ "$#" -ne 2 ]; then
usage
fi
ACTION="$1"
FILE="$2"
if [ ! -f "$FILE" ]; then
echo "Error: File '$FILE' not found."
exit 1
fi
case "$ACTION" in
encrypt)
read -s -p "Enter passphrase: " PASSPHRASE
echo
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 \
-in "$FILE" -out "${FILE}.enc" -pass pass:"$PASSPHRASE"
echo "Encrypted: ${FILE}.enc"
;;
decrypt)
read -s -p "Enter passphrase: " PASSPHRASE
echo
openssl enc -aes-256-cbc -d -pbkdf2 -iter 100000 \
-in "$FILE" -out "${FILE%.enc}.dec" -pass pass:"$PASSPHRASE"
echo "Decrypted: ${FILE%.enc}.dec"
;;
*)
usage
;;
esac
Make it executable:
chmod +x cryptool.sh
Now test it:
./cryptool.sh encrypt notes.txt
./cryptool.sh decrypt notes.txt.enc
You should see a prompt for a passphrase, followed by confirmation that the encrypted or decrypted file was created.
Step 2: Explaining Each Part of the Script
I like to slow down here because understanding why each line exists is what lets you adapt the script later.
set -euo pipefail— This is my default safety net for almost every script I write.-eexits immediately if any command fails,-utreats unset variables as errors, and-o pipefailmakes a pipeline fail if any command inside it fails, not just the last one.usage()— A small function that prints instructions and exits with a non-zero status, which is a convention that tells other scripts (or humans) that something went wrong.read -s -p "Enter passphrase: " PASSPHRASE— The-sflag silences terminal echo so the passphrase isn’t shown as you type it, and-pdisplays a prompt inline.openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000— This is the heart of the operation.-saltadds randomness so identical files don’t produce identical ciphertext,-pbkdf2uses a proper key derivation function instead of OpenSSL’s older and weaker default, and-iter 100000slows down brute-force attacks by making key derivation computationally expensive.-pass pass:"$PASSPHRASE"— Passes the passphrase directly to OpenSSL. I’ll explain in the security section why this isn’t the safest option for shared systems.${FILE%.enc}— This is Bash parameter expansion; it strips the.encsuffix from the filename so the decrypted output has a sensible name.
Step 3: Adding File Integrity Checks
Encryption alone doesn’t tell you if a file was tampered with. I like adding a checksum step so I can verify integrity before and after transfer:
sha256sum "$FILE" > "${FILE}.sha256"
And to verify later:
sha256sum -c "${FILE}.sha256"
I usually generate this checksum right after encrypting, so I have a fingerprint of the encrypted file, not the original.
Step 4: Batch Encrypting a Directory
Once the single-file version worked reliably for me, I extended it to handle entire directories, since that’s closer to how I actually use it — encrypting a folder of invoices or backups in one go.
#!/usr/bin/env bash
set -euo pipefail
DIR="$1"
read -s -p "Enter passphrase: " PASSPHRASE
echo
find "$DIR" -type f ! -name "*.enc" | while read -r file; do
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 \
-in "$file" -out "${file}.enc" -pass pass:"$PASSPHRASE"
echo "Encrypted: $file"
done
The find ... | while read -r file pattern is one I use constantly. find lists every regular file that doesn’t already end in .enc, and the while loop processes them one at a time, which avoids the pitfalls of using for file in $(find ...) on filenames with spaces.
Real-World Use Cases
Here’s how I actually use this tool day to day:
- Encrypting backups before uploading to cloud storage. I never trust a third-party storage provider with plaintext data, even if they claim server-side encryption.
- Sending sensitive files over email. I encrypt, then share the passphrase over a separate channel like a phone call or Signal message.
- Protecting configuration files with secrets (API keys, database credentials) inside a git repository, so only encrypted versions get committed.
- Archiving old client project files before deleting the plaintext copies from a shared drive.
Automating It with Cron
If you want the encryption tool to run automatically, you can combine it with a cron job. Here’s an example that encrypts a nightly backup folder at 2 AM, using a passphrase stored securely in an environment variable rather than typed interactively:
0 2 * * * PASSPHRASE="$(cat /root/.backup_pass)" /home/user/scripts/cryptool.sh encrypt /home/user/backups/db_dump.sql
For this to work non-interactively, you’d modify the script to accept the passphrase from an environment variable when running in cron mode, since read won’t work without a terminal attached.
Security Considerations
This is the section I care about most, because a poorly built encryption script gives you false confidence.
- Never hardcode passphrases into the script. If you must automate it, store the passphrase in a file with strict permissions (
chmod 600) or use a secrets manager. - Avoid passing passphrases as command-line arguments (
-pass pass:mypassword), since these are visible to anyone runningps auxon the same machine. I used-pass pass:"$PASSPHRASE"above for simplicity, but in production I switch to-pass file:/path/to/passfilewith a restricted file, or pipe it through stdin using-pass stdin. - Use
-pbkdf2and a high iteration count. OpenSSL’s legacy key derivation is weak against modern hardware; PBKDF2 with 100,000+ iterations meaningfully raises the cost of brute-forcing. - Don’t reuse the same passphrase everywhere. If one encrypted archive is ever compromised, you don’t want that passphrase to unlock everything else.
- Securely delete the original file after encrypting, if that’s your goal. A plain
rmdoesn’t erase data from disk; considershred -uon Linux for sensitive files, understanding that on SSDs evenshredhas limitations due to wear leveling.
Optimization Tips
- For very large files, add the
-vflag temporarily during testing so you can see OpenSSL’s progress, though it doesn’t show a percentage. - If you’re encrypting many small files, batching them into a single tarball first (
tar czf archive.tar.gz files/) and encrypting that is faster than encrypting each file individually, since OpenSSL has per-invocation overhead. - On multi-core machines, you can parallelize batch encryption with
xargs -Pinstead of a sequentialwhileloop, for example:find . -type f | xargs -P 4 -I{} openssl enc -aes-256-cbc -salt -pbkdf2 -in {} -out {}.enc -pass pass:"$PASSPHRASE".
Troubleshooting Common Issues
“bad decrypt” error on decryption — This almost always means the passphrase was wrong, or the file was encrypted with a different OpenSSL version that used different defaults. Always encrypt and decrypt with the same flags.
Script hangs with no output — Check whether you’re running it inside a cron job or CI pipeline without a terminal; read -s -p requires an interactive shell.
Permission denied errors — Make sure the script is executable (chmod +x) and that you have write permissions in the target directory.
Decrypted file is empty or corrupted — Verify you used the exact same cipher, salt, and PBKDF2 settings on both ends; mismatched OpenSSL versions occasionally change default behaviors between major releases.
Common Mistakes to Avoid
- Forgetting
set -euo pipefail, which lets silent failures slip through unnoticed. - Using weak or predictable passphrases just because scripting feels “technical enough.”
- Storing the passphrase in the same location as the encrypted file — that defeats the purpose entirely.
- Not testing decryption immediately after encrypting, which means you might not discover a broken passphrase or corrupted file until it’s too late.
Frequently Asked Questions
Is AES-256-CBC still considered secure in 2026? Yes, when combined with a proper salt and PBKDF2 key derivation, AES-256-CBC remains cryptographically sound for personal and small business use. For very high-security needs, consider authenticated modes like AES-256-GCM, which OpenSSL also supports.
Can I use this script on Windows? Yes, through WSL (Windows Subsystem for Linux) or Git Bash, as long as OpenSSL is available in that environment.
What happens if I forget my passphrase? There’s no recovery. Symmetric encryption has no backdoor by design, so losing the passphrase means losing the data permanently.
Should I use GPG instead of OpenSSL? GPG is a great alternative, especially for asymmetric encryption (public/private key pairs) and signing. I stick with OpenSSL here because it’s simpler for quick symmetric use cases and requires no key management.
Summary
Building a Bash file encryption tool isn’t about reinventing cryptography — it’s about wrapping trustworthy, well-tested primitives like OpenSSL’s AES-256-CBC cipher in a script that’s convenient, repeatable, and safe to use daily. I’ve shown you how to build a single-file tool, extend it for batch directory encryption, automate it with cron, and avoid the security pitfalls that catch people off guard, like leaking passphrases through process lists. Start with the basic script, test decryption immediately, and gradually layer in the security practices as your needs grow.
