mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-02-19 10:54:53 +00:00
feat(a): improve encryption script with better error handling
- Add dependency check for age and curl with install instructions - Add --delete flag to remove originals after encryption - Add -f/--force flag to control overwrite behavior - Skip already-encrypted .age files during encryption - Include hidden files (dotglob) when encrypting directories - Handle empty directories gracefully with nullglob - Allow flags in any position (proper option parsing) - Add set -euo pipefail for better error handling - Update documentation with all features and examples - Bump version to 1.1.0
This commit is contained in:
274
local/bin/a
274
local/bin/a
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# A script for encrypting and decrypting files or directories with age and SSH keys
|
# A script for encrypting and decrypting files or directories with age and SSH keys
|
||||||
|
|
||||||
VERSION="1.0.0"
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="1.1.0"
|
||||||
|
|
||||||
# Default ENV values
|
# Default ENV values
|
||||||
KEYS_FILE="${AGE_KEYSFILE:-$HOME/.ssh/keys.txt}"
|
KEYS_FILE="${AGE_KEYSFILE:-$HOME/.ssh/keys.txt}"
|
||||||
@@ -9,14 +11,49 @@ KEYS_SOURCE="${AGE_KEYSSOURCE:-https://github.com/ivuorinen.keys}"
|
|||||||
LOG_FILE="${AGE_LOGFILE:-$HOME/.cache/a.log}"
|
LOG_FILE="${AGE_LOGFILE:-$HOME/.cache/a.log}"
|
||||||
|
|
||||||
VERBOSE=false
|
VERBOSE=false
|
||||||
|
DELETE_ORIGINAL=false
|
||||||
|
FORCE=false
|
||||||
|
|
||||||
# Parse flags for verbosity
|
# Check for required dependencies
|
||||||
for arg in "$@"; do
|
check_dependencies()
|
||||||
if [[ "$arg" == "-v" || "$arg" == "--verbose" ]]; then
|
{
|
||||||
VERBOSE=true
|
if ! command -v age &> /dev/null; then
|
||||||
break
|
echo "Error: 'age' is not installed. Please install it first." >&2
|
||||||
|
echo " brew install age # macOS" >&2
|
||||||
|
echo " apt install age # Debian/Ubuntu" >&2
|
||||||
|
echo " dnf install age # Fedora" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
echo "Error: 'curl' is not installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse flags
|
||||||
|
parse_flags()
|
||||||
|
{
|
||||||
|
local args=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-v | --verbose)
|
||||||
|
VERBOSE=true
|
||||||
|
;;
|
||||||
|
--delete)
|
||||||
|
DELETE_ORIGINAL=true
|
||||||
|
;;
|
||||||
|
-f | --force)
|
||||||
|
FORCE=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
args+=("$arg")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
# Return remaining arguments
|
||||||
|
printf '%s\n' "${args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
# Ensure log directory and file exist with correct permissions
|
# Ensure log directory and file exist with correct permissions
|
||||||
prepare_log_file()
|
prepare_log_file()
|
||||||
@@ -38,8 +75,6 @@ prepare_log_file()
|
|||||||
chmod 0600 "$LOG_FILE"
|
chmod 0600 "$LOG_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_log_file
|
|
||||||
|
|
||||||
# Logging function
|
# Logging function
|
||||||
log_message()
|
log_message()
|
||||||
{
|
{
|
||||||
@@ -56,7 +91,7 @@ log_message()
|
|||||||
print_help()
|
print_help()
|
||||||
{
|
{
|
||||||
cat << EOF
|
cat << EOF
|
||||||
Usage: a [command] [file_or_directory] [options]
|
Usage: a [options] [command] [file_or_directory]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
e, enc, encrypt Encrypt the specified file or directory
|
e, enc, encrypt Encrypt the specified file or directory
|
||||||
@@ -65,12 +100,14 @@ Commands:
|
|||||||
version, --version Show version information
|
version, --version Show version information
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-v, --verbose Print log messages to console in addition to writing to log file
|
-v, --verbose Print log messages to console
|
||||||
|
--delete Delete original files after successful encryption
|
||||||
|
-f, --force Overwrite existing output files without prompting
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
AGE_KEYSFILE Path to the SSH keys file (default: $HOME/.ssh/keys.txt)
|
AGE_KEYSFILE Path to the SSH keys file (default: \$HOME/.ssh/keys.txt)
|
||||||
AGE_KEYSSOURCE URL to fetch SSH keys if keys file does not exist
|
AGE_KEYSSOURCE URL to fetch SSH keys if keys file does not exist
|
||||||
AGE_LOGFILE Path to the log file (default: $HOME/.cache/a.log)
|
AGE_LOGFILE Path to the log file (default: \$HOME/.cache/a.log)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
Encrypt a file:
|
Encrypt a file:
|
||||||
@@ -79,14 +116,21 @@ Examples:
|
|||||||
Encrypt a directory:
|
Encrypt a directory:
|
||||||
a e /path/to/directory
|
a e /path/to/directory
|
||||||
|
|
||||||
|
Encrypt and delete originals:
|
||||||
|
a --delete e file.txt
|
||||||
|
|
||||||
Decrypt a file:
|
Decrypt a file:
|
||||||
a d file.txt.age
|
a d file.txt.age
|
||||||
|
|
||||||
|
Force overwrite existing files:
|
||||||
|
a -f e file.txt
|
||||||
|
|
||||||
Specify a custom keys file:
|
Specify a custom keys file:
|
||||||
AGE_KEYSFILE=/path/to/keys.txt a e file.txt
|
AGE_KEYSFILE=/path/to/keys.txt a e file.txt
|
||||||
|
|
||||||
Specify a custom keys source and log file:
|
Requirements:
|
||||||
AGE_KEYSSOURCE=https://example.com/keys.txt AGE_LOGFILE=/tmp/a.log a d file.txt.age
|
- age (encryption tool): https://github.com/FiloSottile/age
|
||||||
|
- curl (for fetching keys)
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,26 +159,104 @@ fetch_keys_if_missing()
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to encrypt a single file
|
||||||
|
encrypt_single_file()
|
||||||
|
{
|
||||||
|
local file="$1"
|
||||||
|
|
||||||
|
# Skip already encrypted files
|
||||||
|
if [[ "$file" == *.age ]]; then
|
||||||
|
log_message "Skipping already encrypted file: $file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output_file="${file}.age"
|
||||||
|
|
||||||
|
# Check if output file exists
|
||||||
|
if [[ -f "$output_file" && "$FORCE" != true ]]; then
|
||||||
|
log_message "Error: Output file '$output_file' already exists. Use --force to overwrite."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
fetch_keys_if_missing
|
||||||
|
|
||||||
|
local temp_file
|
||||||
|
temp_file="$(mktemp -p "$(dirname "$file")")"
|
||||||
|
|
||||||
|
if age -R "$KEYS_FILE" "$file" > "$temp_file" && mv "$temp_file" "$output_file"; then
|
||||||
|
log_message "File encrypted successfully: $output_file"
|
||||||
|
|
||||||
|
if [[ "$DELETE_ORIGINAL" == true ]]; then
|
||||||
|
rm -f "$file"
|
||||||
|
log_message "Original file deleted: $file"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$temp_file"
|
||||||
|
log_message "Error: Failed to encrypt file '$file'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Function to encrypt files or directories
|
# Function to encrypt files or directories
|
||||||
encrypt_file_or_directory()
|
encrypt_file_or_directory()
|
||||||
{
|
{
|
||||||
local file="$1"
|
local file="$1"
|
||||||
|
|
||||||
if [[ -d "$file" ]]; then
|
if [[ -d "$file" ]]; then
|
||||||
for f in "$file"/*; do
|
# Enable dotglob to include hidden files
|
||||||
|
shopt -s dotglob nullglob
|
||||||
|
local files=("$file"/*)
|
||||||
|
shopt -u dotglob nullglob
|
||||||
|
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
log_message "Warning: Directory '$file' is empty."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
encrypt_file_or_directory "$f"
|
encrypt_file_or_directory "$f"
|
||||||
done
|
done
|
||||||
elif [[ -f "$file" ]]; then
|
elif [[ -f "$file" ]]; then
|
||||||
fetch_keys_if_missing
|
encrypt_single_file "$file"
|
||||||
local output_file="${file}.age"
|
else
|
||||||
local temp_file
|
log_message "Warning: '$file' is not a file or directory, skipping."
|
||||||
temp_file="$(mktemp -p "$(dirname "$file")")"
|
fi
|
||||||
if age -R "$KEYS_FILE" "$file" > "$temp_file" && mv "$temp_file" "$output_file"; then
|
}
|
||||||
log_message "File encrypted successfully: $output_file"
|
|
||||||
else
|
# Function to decrypt a single file
|
||||||
rm -f "$temp_file"
|
decrypt_single_file()
|
||||||
log_message "Error: Failed to encrypt file '$file'."
|
{
|
||||||
exit 1
|
local file="$1"
|
||||||
|
|
||||||
|
if [[ ! "$file" == *.age ]]; then
|
||||||
|
log_message "Skipping non-.age file: $file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output_file="${file%.age}"
|
||||||
|
|
||||||
|
# Check if output file exists
|
||||||
|
if [[ -f "$output_file" && "$FORCE" != true ]]; then
|
||||||
|
log_message "Error: Output file '$output_file' already exists. Use --force to overwrite."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
fetch_keys_if_missing
|
||||||
|
|
||||||
|
local temp_file
|
||||||
|
temp_file="$(mktemp -p "$(dirname "$file")")"
|
||||||
|
|
||||||
|
if age -d -i "$KEYS_FILE" "$file" > "$temp_file" && mv "$temp_file" "$output_file"; then
|
||||||
|
log_message "File decrypted successfully: $output_file"
|
||||||
|
|
||||||
|
if [[ "$DELETE_ORIGINAL" == true ]]; then
|
||||||
|
rm -f "$file"
|
||||||
|
log_message "Encrypted file deleted: $file"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$temp_file"
|
||||||
|
log_message "Error: Failed to decrypt file '$file'."
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,54 +264,76 @@ encrypt_file_or_directory()
|
|||||||
decrypt_file_or_directory()
|
decrypt_file_or_directory()
|
||||||
{
|
{
|
||||||
local file="$1"
|
local file="$1"
|
||||||
|
|
||||||
if [[ -d "$file" ]]; then
|
if [[ -d "$file" ]]; then
|
||||||
for f in "$file"/*.age; do
|
# Enable nullglob to handle no matches gracefully
|
||||||
decrypt_file_or_directory "$f"
|
shopt -s nullglob
|
||||||
|
local files=("$file"/*.age)
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
log_message "Warning: No .age files found in directory '$file'."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
decrypt_single_file "$f"
|
||||||
done
|
done
|
||||||
elif [[ -f "$file" ]]; then
|
elif [[ -f "$file" ]]; then
|
||||||
fetch_keys_if_missing
|
decrypt_single_file "$file"
|
||||||
local output_file="${file%.age}"
|
else
|
||||||
local temp_file
|
log_message "Warning: '$file' is not a file or directory, skipping."
|
||||||
temp_file="$(mktemp -p "$(dirname "$file")")"
|
|
||||||
if age -d -i "$KEYS_FILE" "$file" > "$temp_file" && mv "$temp_file" "$output_file"; then
|
|
||||||
log_message "File decrypted successfully: $output_file"
|
|
||||||
else
|
|
||||||
rm -f "$temp_file"
|
|
||||||
log_message "Error: Failed to decrypt file '$file'."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main logic
|
# Main entry point
|
||||||
case "$1" in
|
main()
|
||||||
e | enc | encrypt)
|
{
|
||||||
if [[ $# -lt 2 ]]; then
|
check_dependencies
|
||||||
log_message "Error: No file or directory specified for encryption."
|
|
||||||
|
# Parse flags and get remaining arguments
|
||||||
|
mapfile -t ARGS < <(parse_flags "$@")
|
||||||
|
|
||||||
|
prepare_log_file
|
||||||
|
|
||||||
|
local command="${ARGS[0]:-}"
|
||||||
|
local target="${ARGS[1]:-}"
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
e | enc | encrypt)
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
log_message "Error: No file or directory specified for encryption."
|
||||||
|
print_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
encrypt_file_or_directory "$target"
|
||||||
|
;;
|
||||||
|
d | dec | decrypt)
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
log_message "Error: No file or directory specified for decryption."
|
||||||
|
print_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
decrypt_file_or_directory "$target"
|
||||||
|
;;
|
||||||
|
help | --help | -h)
|
||||||
|
print_help
|
||||||
|
;;
|
||||||
|
version | --version)
|
||||||
|
print_version
|
||||||
|
;;
|
||||||
|
"")
|
||||||
print_help
|
print_help
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
;;
|
||||||
encrypt_file_or_directory "$2"
|
*)
|
||||||
;;
|
log_message "Error: Unknown command '$command'"
|
||||||
d | dec | decrypt)
|
|
||||||
if [[ $# -lt 2 ]]; then
|
|
||||||
log_message "Error: No file or directory specified for decryption."
|
|
||||||
print_help
|
print_help
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
;;
|
||||||
decrypt_file_or_directory "$2"
|
esac
|
||||||
;;
|
}
|
||||||
help | --help)
|
|
||||||
print_help
|
main "$@"
|
||||||
;;
|
|
||||||
version | --version)
|
|
||||||
print_version
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_message "Error: Unknown command '$1'"
|
|
||||||
print_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# vim: ft=bash:syn=sh:ts=2:sw=2:et:ai:nowrap
|
# vim: ft=bash:syn=sh:ts=2:sw=2:et:ai:nowrap
|
||||||
|
|||||||
@@ -2,28 +2,76 @@
|
|||||||
|
|
||||||
Encrypt or decrypt files and directories using `age` and your GitHub SSH keys.
|
Encrypt or decrypt files and directories using `age` and your GitHub SSH keys.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [age](https://github.com/FiloSottile/age) - encryption tool
|
||||||
|
- curl - for fetching SSH keys
|
||||||
|
|
||||||
|
Install age:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install age # macOS
|
||||||
|
apt install age # Debian/Ubuntu
|
||||||
|
dnf install age # Fedora
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
a encrypt <file|dir>
|
a [options] <command> <file|directory>
|
||||||
a decrypt <file.age|dir>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
- `e`, `enc`, `encrypt` - encrypt files
|
||||||
|
- `d`, `dec`, `decrypt` - decrypt files
|
||||||
|
- `help`, `--help`, `-h` - show help
|
||||||
|
- `version`, `--version` - show version
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
- `-v`, `--verbose` – show log output
|
- `-v`, `--verbose` - show log output
|
||||||
|
- `--delete` - delete original files after successful operation
|
||||||
|
- `-f`, `--force` - overwrite existing output files
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
|
|
||||||
- `AGE_KEYSFILE` – location of the keys file
|
- `AGE_KEYSFILE` - location of the keys file (default: `~/.ssh/keys.txt`)
|
||||||
- `AGE_KEYSSOURCE` – URL to fetch keys if missing
|
- `AGE_KEYSSOURCE` - URL to fetch keys if missing (default: GitHub keys)
|
||||||
- `AGE_LOGFILE` – log file path
|
- `AGE_LOGFILE` - log file path (default: `~/.cache/a.log`)
|
||||||
|
|
||||||
## Example
|
## Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Encrypt a file
|
||||||
a encrypt secret.txt
|
a encrypt secret.txt
|
||||||
|
|
||||||
|
# Encrypt with short command
|
||||||
|
a e secret.txt
|
||||||
|
|
||||||
|
# Decrypt a file
|
||||||
a decrypt secret.txt.age
|
a decrypt secret.txt.age
|
||||||
|
a d secret.txt.age
|
||||||
|
|
||||||
|
# Encrypt a directory (includes hidden files)
|
||||||
|
a e /path/to/secrets/
|
||||||
|
|
||||||
|
# Encrypt and delete originals
|
||||||
|
a --delete e secret.txt
|
||||||
|
|
||||||
|
# Force overwrite existing .age file
|
||||||
|
a -f e secret.txt
|
||||||
|
|
||||||
|
# Verbose output
|
||||||
|
a -v e secret.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Encrypting a directory processes all files recursively, including hidden files
|
||||||
|
- Already encrypted files (`.age`) are skipped during encryption
|
||||||
|
- Only `.age` files are processed during directory decryption
|
||||||
|
- Original files are preserved by default (use `--delete` to remove them)
|
||||||
|
- Output files are not overwritten by default (use `--force` to overwrite)
|
||||||
|
|
||||||
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->
|
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->
|
||||||
|
|||||||
Reference in New Issue
Block a user