After a scare where a sensitive backup archive of mine sat unencrypted on a cloud drive for longer than I was comfortable with, I decided to build a proper encryption and decryption workflow using nothing but Bash and OpenSSL. What started as a single command grew into a small toolset I now use for every sensitive file I need to store or transfer. This article covers the decryption side in depth, along with the encryption context needed to understand it fully.
Why Build This in Bash
Encryption and decryption logic itself should never be hand-rolled — that’s a job for well-audited, standard cryptographic libraries. Bash’s role here isn’t to implement cryptography, but to orchestrate trusted tools like openssl or gpg reliably:
- It wraps well-tested cryptographic primitives instead of reinventing them.
- It integrates decryption into automated pipelines (restoring backups, reading encrypted config files at deploy time).
- It’s universally available without needing to install a separate application.
The Tool Behind It: OpenSSL
OpenSSL ships with virtually every Linux distribution and macOS by default, and its command-line interface supports strong symmetric encryption suitable for file-level protection.
Check that it’s available:
openssl version
Step 1: Encrypting a File (Context for Decryption)
Since decryption only makes sense with matching encryption, here’s the encryption side first:
openssl enc -aes-256-cbc -salt -pbkdf2 -in secret.txt -out secret.txt.enc
You’ll be prompted to enter a passphrase, which is used to derive the actual encryption key.
Breaking Down the Flags
-aes-256-cbcselects AES with a 256-bit key in CBC mode, a strong, widely trusted symmetric cipher.-saltadds a random salt to the key derivation process, which prevents identical passphrases from always producing identical ciphertext (protecting against precomputed “rainbow table” style attacks).-pbkdf2uses the PBKDF2 key derivation function with many iterations, making brute-force passphrase guessing significantly slower than older, weaker derivation methods.-in/-outspecify the input plaintext file and the output encrypted file.
Step 2: Decrypting a File
openssl enc -d -aes-256-cbc -pbkdf2 -in secret.txt.enc -out secret.txt
-dtells OpenSSL to decrypt rather than encrypt.- The cipher (
-aes-256-cbc) and key derivation flag (-pbkdf2) must exactly match what was used during encryption, or decryption will fail.
A Basic Bash Decryption Script
#!/bin/bash
set -euo pipefail
usage() {
echo "Usage: $0 <encrypted_file> <output_file>"
exit 1
}
if [ "$#" -ne 2 ]; then
usage
fi
encrypted_file="$1"
output_file="$2"
if [ ! -f "$encrypted_file" ]; then
echo "Error: encrypted file '$encrypted_file' not found." >&2
exit 1
fi
openssl enc -d -aes-256-cbc -pbkdf2 -in "$encrypted_file" -out "$output_file"
echo "Decrypted successfully: $output_file"
How This Works Internally
- The script validates argument count and file existence before attempting anything.
openssl enc -d ...performs the actual decryption, prompting interactively for the passphrase (since none was hardcoded or piped in).- On success, it confirms the output file was written.
Decrypting Without an Interactive Prompt (Using an Environment Variable)
For automated pipelines, prompting for a passphrase interactively isn’t practical. You can pass it via an environment variable instead:
#!/bin/bash
set -euo pipefail
encrypted_file="$1"
output_file="$2"
if [ -z "${DECRYPT_PASSPHRASE:-}" ]; then
echo "Error: DECRYPT_PASSPHRASE environment variable is not set." >&2
exit 1
fi
openssl enc -d -aes-256-cbc -pbkdf2 -pass env:DECRYPT_PASSPHRASE -in "$encrypted_file" -out "$output_file"
echo "Decrypted successfully: $output_file"
Usage:
export DECRYPT_PASSPHRASE="my-secure-passphrase"
./decrypt.sh secret.txt.enc secret.txt
unset DECRYPT_PASSPHRASE
Why -pass env:DECRYPT_PASSPHRASE
The -pass flag tells OpenSSL where to source the passphrase from instead of prompting interactively. env:VARNAME reads it from the named environment variable. This is far safer than passing a passphrase directly as a command-line argument, since command-line arguments are often visible to other users on the system via ps aux.
Batch-Decrypting Multiple Files
#!/bin/bash
set -euo pipefail
if [ -z "${DECRYPT_PASSPHRASE:-}" ]; then
echo "Error: DECRYPT_PASSPHRASE environment variable is not set." >&2
exit 1
fi
output_dir="decrypted"
mkdir -p "$output_dir"
for enc_file in *.enc; do
[ -e "$enc_file" ] || continue
base_name="${enc_file%.enc}"
openssl enc -d -aes-256-cbc -pbkdf2 -pass env:DECRYPT_PASSPHRASE -in "$enc_file" -out "$output_dir/$base_name"
echo "Decrypted: $enc_file -> $output_dir/$base_name"
done
echo "Batch decryption complete."
This decrypts every .enc file in the current directory using a single shared passphrase, writing outputs into a dedicated decrypted/ folder.
Verifying Integrity After Decryption
Encryption alone doesn’t guarantee the decrypted file is intact — pairing it with a checksum, generated before encryption, gives you confidence the round trip succeeded:
#!/bin/bash
set -euo pipefail
encrypted_file="$1"
expected_checksum_file="$2"
output_file="${encrypted_file%.enc}"
openssl enc -d -aes-256-cbc -pbkdf2 -pass env:DECRYPT_PASSPHRASE -in "$encrypted_file" -out "$output_file"
if sha256sum -c "$expected_checksum_file"; then
echo "Integrity check passed."
else
echo "Warning: integrity check failed!" >&2
exit 1
fi
This assumes a checksum file was generated at encryption time (sha256sum original_file > checksum.sha256) and shipped alongside the encrypted file for later verification.
Using GPG as an Alternative
For asymmetric encryption (public/private key pairs) rather than a shared passphrase, GPG is often a better fit:
# Decrypting a GPG-encrypted file
gpg --output secret.txt --decrypt secret.txt.gpg
GPG will prompt for your private key’s passphrase (if it’s protected by one) and use your locally stored private key to decrypt the file — no shared secret needs to be communicated at all, which is a meaningful advantage over symmetric approaches like the OpenSSL example above.
Real-World Use Cases
- Encrypted backups: Storing sensitive backup archives in encrypted form on cloud storage, decrypting only when a restore is actually needed.
- Secure configuration management: Storing encrypted secrets or config files in a repository, decrypted at deploy time using a securely injected passphrase.
- Secure file transfer: Encrypting a file before sending it over an untrusted channel, then decrypting it on the receiving end.
- Compliance requirements: Meeting data-at-rest encryption requirements for sensitive files stored on shared or less-trusted infrastructure.
- Personal data protection: Encrypting personal documents (tax records, ID scans) before storing them in general-purpose cloud storage.
Automation Example
Here’s a deployment script that decrypts a secrets file just before starting an application, then cleans it up immediately after:
#!/bin/bash
set -euo pipefail
encrypted_secrets="/opt/app/secrets.env.enc"
decrypted_secrets="/opt/app/secrets.env"
if [ -z "${DEPLOY_PASSPHRASE:-}" ]; then
echo "Error: DEPLOY_PASSPHRASE not set." >&2
exit 1
fi
openssl enc -d -aes-256-cbc -pbkdf2 -pass env:DEPLOY_PASSPHRASE -in "$encrypted_secrets" -out "$decrypted_secrets"
chmod 600 "$decrypted_secrets"
# shellcheck disable=SC1090
source "$decrypted_secrets"
shred -u "$decrypted_secrets"
echo "Application starting with decrypted secrets loaded into environment..."
# exec ./start_app.sh
This decrypts secrets just-in-time, restricts file permissions immediately, sources the values into the current shell environment, and then securely deletes the plaintext file using shred -u rather than leaving it sitting on disk.
Best Practices
- Never hardcode passphrases directly inside scripts — use environment variables, a secrets manager, or interactive prompts instead.
- Always match the exact cipher and key derivation flags (
-aes-256-cbc -pbkdf2) between encryption and decryption commands. - Use
shred -u(or equivalent secure deletion) to remove decrypted plaintext files as soon as they’re no longer needed, rather than relying on a normalrm. - Restrict file permissions (
chmod 600) on any decrypted output containing sensitive data. - Pair encryption with a checksum recorded before encryption, so you can verify successful, uncorrupted decryption afterward.
Security Considerations
- Never pass passphrases as plain command-line arguments: Arguments are visible to other local users via
ps auxor/proc; always use-pass env:VARNAME,-pass file:path, or an interactive prompt instead. - Weak passphrases undermine everything: AES-256 is only as strong as the passphrase protecting it — a short or common passphrase makes brute-forcing feasible regardless of cipher strength.
- Clean up plaintext immediately: Decrypted files sitting on disk defeat the purpose of encryption; automate their secure deletion once used.
- Prefer asymmetric encryption (GPG) for multi-party scenarios: Shared passphrases must be distributed securely to every party that needs decryption access, which itself becomes a security liability; public-key cryptography avoids this by only ever sharing public keys.
- Keep OpenSSL updated: Older versions have had vulnerabilities; always run a current, patched version of the library and CLI tool.
Optimization Tips
- For large files, OpenSSL’s streaming design means memory usage stays low regardless of file size — there’s rarely a need for special handling of huge files.
- Combine compression and encryption for large text-heavy files: compress first with
gzip, then encrypt, since compressing already-encrypted data yields negligible size reduction (encrypted data looks like random noise, which doesn’t compress well). - For decrypting many files in automation, avoid prompting for a passphrase per file — use the environment-variable approach once and loop through all files within the same script execution.
Troubleshooting Common Issues
Problem: bad decrypt error during decryption. This almost always means either the wrong passphrase was supplied, or the cipher/KDF flags don’t match what was used during encryption (e.g., forgetting -pbkdf2 on one side but not the other).
Problem: Decrypted file is empty or corrupted. Verify the encrypted file wasn’t truncated during transfer (check its size or checksum against the original encrypted output), and confirm the exact same OpenSSL flags were used on both ends.
Problem: Script hangs waiting for input. This happens when no passphrase source is provided and OpenSSL is waiting on an interactive prompt in a non-interactive (e.g., cron or CI) context — use -pass env:VARNAME or -pass file:path instead.
Problem: “unknown option” errors from OpenSSL. This typically indicates a version mismatch — very old OpenSSL versions may not support -pbkdf2; check your installed version with openssl version and consider upgrading.
Common Mistakes to Avoid
- Passing a passphrase directly on the command line (
-pass pass:mypassword), exposing it to any other user able to view running processes. - Forgetting to match encryption and decryption flags exactly, leading to confusing “bad decrypt” failures.
- Leaving decrypted plaintext files lying around on disk indefinitely instead of cleaning them up immediately after use.
- Using a weak or reused passphrase, undermining otherwise strong AES-256 encryption.
- Not testing the full encrypt-then-decrypt round trip before relying on it for something important, like a real backup.
Frequently Asked Questions
Is AES-256-CBC still considered secure in 2026? Yes, AES-256 remains a strong, widely trusted cipher when combined with proper key derivation (-pbkdf2) and a strong passphrase; the security relies more on passphrase strength and correct usage than the cipher choice itself.
What’s the difference between using OpenSSL and GPG for this? OpenSSL’s enc command uses symmetric encryption with a shared passphrase, while GPG typically uses asymmetric public/private key pairs (though it also supports symmetric mode) — GPG is generally preferable when multiple parties need independent decryption access without sharing a single secret.
Can I decrypt a file without knowing which cipher was originally used? Not directly — you need to know (or record) which cipher and KDF flags were used at encryption time; without that information, decryption will fail even with the correct passphrase.
How do I securely share the passphrase with someone else who needs to decrypt the file? Never send it over the same channel as the encrypted file itself; use a separate, trusted channel (in person, a password manager’s sharing feature, or an encrypted messaging app) to communicate it.
What happens if I forget the passphrase? The file is effectively unrecoverable — there is no “reset” mechanism for properly implemented symmetric encryption, which is precisely the point of strong encryption, but also why passphrase management matters so much.
Summary
A Bash-based file decryption tool built around OpenSSL wraps proven, standard cryptographic primitives rather than reinventing them, while Bash itself handles the orchestration: argument parsing, batch processing, integrity verification, and secure cleanup of plaintext afterward. The details that matter most: never expose passphrases as plain command-line arguments, always match your cipher and KDF flags exactly between encryption and decryption, and treat any decrypted plaintext file as a temporary artifact that should be securely removed the moment it’s no longer needed.
References
- OpenSSL
enccommand documentation: https://docs.openssl.org/master/man1/openssl-enc/ - GNU Privacy Guard (GPG) documentation: https://www.gnupg.org/documentation/
- NIST guidance on AES: https://csrc.nist.gov/publications/detail/fips/197/final
- Bash Reference Manual: https://www.gnu.org/software/bash/manual/bash.html