mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-02-06 10:59:22 +00:00
- 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
340 lines
7.6 KiB
Bash
Executable File
340 lines
7.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# A script for encrypting and decrypting files or directories with age and SSH keys
|
|
|
|
set -euo pipefail
|
|
|
|
VERSION="1.1.0"
|
|
|
|
# Default ENV values
|
|
KEYS_FILE="${AGE_KEYSFILE:-$HOME/.ssh/keys.txt}"
|
|
KEYS_SOURCE="${AGE_KEYSSOURCE:-https://github.com/ivuorinen.keys}"
|
|
LOG_FILE="${AGE_LOGFILE:-$HOME/.cache/a.log}"
|
|
|
|
VERBOSE=false
|
|
DELETE_ORIGINAL=false
|
|
FORCE=false
|
|
|
|
# Check for required dependencies
|
|
check_dependencies()
|
|
{
|
|
if ! command -v age &> /dev/null; then
|
|
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
|
|
|
|
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
|
|
prepare_log_file()
|
|
{
|
|
local log_dir
|
|
log_dir=$(dirname "$LOG_FILE")
|
|
|
|
# Create log directory if it does not exist
|
|
if [[ ! -d "$log_dir" ]]; then
|
|
mkdir -p "$log_dir"
|
|
fi
|
|
|
|
# Create log file if it does not exist
|
|
if [[ ! -f "$LOG_FILE" ]]; then
|
|
touch "$LOG_FILE"
|
|
fi
|
|
|
|
# Set permissions to 0600
|
|
chmod 0600 "$LOG_FILE"
|
|
}
|
|
|
|
# Logging function
|
|
log_message()
|
|
{
|
|
local message="$1"
|
|
echo "$(date +'%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE"
|
|
|
|
# Print to user if verbose flag is set
|
|
if [[ "$VERBOSE" == true ]]; then
|
|
echo "$message"
|
|
fi
|
|
}
|
|
|
|
# Function to print usage
|
|
print_help()
|
|
{
|
|
cat << EOF
|
|
Usage: a [options] [command] [file_or_directory]
|
|
|
|
Commands:
|
|
e, enc, encrypt Encrypt the specified file or directory
|
|
d, dec, decrypt Decrypt the specified file or directory
|
|
help, --help Show this help message
|
|
version, --version Show version information
|
|
|
|
Options:
|
|
-v, --verbose Print log messages to console
|
|
--delete Delete original files after successful encryption
|
|
-f, --force Overwrite existing output files without prompting
|
|
|
|
Environment Variables:
|
|
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_LOGFILE Path to the log file (default: \$HOME/.cache/a.log)
|
|
|
|
Examples:
|
|
Encrypt a file:
|
|
a e file.txt
|
|
|
|
Encrypt a directory:
|
|
a e /path/to/directory
|
|
|
|
Encrypt and delete originals:
|
|
a --delete e file.txt
|
|
|
|
Decrypt a file:
|
|
a d file.txt.age
|
|
|
|
Force overwrite existing files:
|
|
a -f e file.txt
|
|
|
|
Specify a custom keys file:
|
|
AGE_KEYSFILE=/path/to/keys.txt a e file.txt
|
|
|
|
Requirements:
|
|
- age (encryption tool): https://github.com/FiloSottile/age
|
|
- curl (for fetching keys)
|
|
EOF
|
|
}
|
|
|
|
# Function to print version
|
|
print_version()
|
|
{
|
|
echo "a version $VERSION"
|
|
echo "Created by Ismo Vuorinen <https://github.com/ivuorinen>"
|
|
}
|
|
|
|
# Function to fetch keys if missing
|
|
fetch_keys_if_missing()
|
|
{
|
|
if [[ ! -f "$KEYS_FILE" ]]; then
|
|
log_message "Keys file '$KEYS_FILE' not found. Attempting to fetch from $KEYS_SOURCE..."
|
|
mkdir -p "$(dirname "$KEYS_FILE")"
|
|
|
|
if ! curl -s "$KEYS_SOURCE" -o "$KEYS_FILE" || [[ ! -s "$KEYS_FILE" ]]; then
|
|
rm -f "$KEYS_FILE" 2> /dev/null || true
|
|
log_message "Error: Failed to fetch keys from $KEYS_SOURCE"
|
|
exit 1
|
|
fi
|
|
|
|
chmod 0400 "$KEYS_FILE"
|
|
log_message "Keys file fetched and permissions set to 0400."
|
|
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
|
|
encrypt_file_or_directory()
|
|
{
|
|
local file="$1"
|
|
|
|
if [[ -d "$file" ]]; then
|
|
# 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"
|
|
done
|
|
elif [[ -f "$file" ]]; then
|
|
encrypt_single_file "$file"
|
|
else
|
|
log_message "Warning: '$file' is not a file or directory, skipping."
|
|
fi
|
|
}
|
|
|
|
# Function to decrypt a single file
|
|
decrypt_single_file()
|
|
{
|
|
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
|
|
else
|
|
rm -f "$temp_file"
|
|
log_message "Error: Failed to decrypt file '$file'."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to decrypt files or directories
|
|
decrypt_file_or_directory()
|
|
{
|
|
local file="$1"
|
|
|
|
if [[ -d "$file" ]]; then
|
|
# Enable nullglob to handle no matches gracefully
|
|
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
|
|
elif [[ -f "$file" ]]; then
|
|
decrypt_single_file "$file"
|
|
else
|
|
log_message "Warning: '$file' is not a file or directory, skipping."
|
|
fi
|
|
}
|
|
|
|
# Main entry point
|
|
main()
|
|
{
|
|
check_dependencies
|
|
|
|
# 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
|
|
exit 1
|
|
;;
|
|
*)
|
|
log_message "Error: Unknown command '$command'"
|
|
print_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|
|
|
|
# vim: ft=bash:syn=sh:ts=2:sw=2:et:ai:nowrap
|