Wenn ich lokale Dateien in einen Cloud-Speicher sichere, möchte ich vermeiden, alles unverschlüsselt hochzuladen.
Deshalb habe ich ein age-tar-Skript geschrieben, das age und tar kombiniert, um verschlüsselte Archive pro Verzeichnis zu erstellen.
Welches Problem ich lösen wollte
Bei Backup-Abläufen werden meist zwei Dinge schnell lästig:
- Für jedes Verzeichnis manuell erst
tarund dannageauszuführen, ist umständlich - Verzeichnisnamen mit japanischen Zeichen oder Leerzeichen sind in der CLI unbequem zu handhaben
age-tar löst beide Punkte zusammen.
Was dieses Skript kann
- Jedes Unterverzeichnis direkt unter einem Zielverzeichnis mit
tar + ageverschlüsseln - Ausgabedateinamen per base64URL codieren, damit sie CLI-freundlich bleiben
- Beim Entschlüsseln den ursprünglichen Verzeichnisnamen wiederherstellen und extrahieren
- Mit
--dryrunnur die auszuführenden Kommandos anzeigen
Voraussetzungen
age installieren
# macOSbrew install age
# Ubuntu/Debianapt install age
# Oder die offiziellen Releases verwenden# https://github.com/FiloSottile/age/releasesSchlüssel erzeugen
# Geheimen Schlüssel erstellenage-keygen -o ~/.age/key.txt
# Öffentlichen Schlüssel ableitenage-keygen -y ~/.age/key.txt > ~/.age/key.pubVerwendung
Verschlüsseln
age-tar -R ~/.age/key.pub -i /path/to/backup/targetJedes Verzeichnis direkt unter /path/to/backup/target wird zu einer .tar.age-Datei.
target/├── 日本語フォルダ/├── my documents/└── projects/Nach der Verschlüsselung bleiben die Originalverzeichnisse erhalten:
target/├── 5pel5pys6Kqe44OV44Kp44Or44OA.tar.age├── bXkgZG9jdW1lbnRz.tar.age├── cHJvamVjdHM.tar.age├── 日本語フォルダ/├── my documents/└── projects/Entschlüsseln
age-tar -d -I ~/.age/key.txt -i /path/to/encrypted/targetDie .tar.age-Dateien werden entschlüsselt und unter ihren ursprünglichen Verzeichnisnamen entpackt.
Dry Run
# Encryptage-tar -R ~/.age/key.pub -i /path/to/target --dryrun
# Decryptage-tar -d -I ~/.age/key.txt -i /path/to/target --dryrunDabei werden nur die Befehle angezeigt, echte Dateivorgänge finden nicht statt.
Beispielablauf
# 1. Lokal verschlüsselnage-tar -R ~/.age/key.pub -i ~/important-data
# 2. Verschlüsselte Dateien hochladenrclone copy ~/important-data/*.tar.age remote:backup/
# 3. Wiederherstellenrclone copy remote:backup/ ~/restore/age-tar -d -I ~/.age/key.txt -i ~/restoreVollständiges Skript
#!/bin/bash
set -euo pipefail
usage() { cat <<EOFUsage: Encrypt: $(basename "$0") -R <public_key_path> -i <target_directory> [--dryrun] Decrypt: $(basename "$0") -d -I <identity_path> -i <target_directory> [--dryrun]
Options: -R <path> Path to the age public key file (for encryption) -I <path> Path to the age identity/secret key file (for decryption) -i <path> Path to the target directory (required) -d Decrypt mode --dryrun Simulation mode - show commands without executing
Description: Encrypt mode: Archives each subdirectory under the target directory into a tar file, then encrypts it using age with the specified public key. The output filename is the directory name encoded in base64URL format.
Decrypt mode: Decrypts each .tar.age file in the target directory, extracts the tar archive, and restores the original directory name from the base64URL encoded filename.EOF exit 1}
# Encode string to base64URL formatto_base64url() { local str="$1" # base64URL: replace + with -, / with _, remove = padding echo -n "$str" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '='}
# Decode base64URL format to stringfrom_base64url() { local str="$1" # Restore standard base64: replace - with +, _ with / local base64_str base64_str=$(echo -n "$str" | tr '_' '/' | tr '\-' '+')
# Add padding if necessary local padding=$((4 - ${#base64_str} % 4)) if [[ $padding -ne 4 ]]; then base64_str="${base64_str}$(printf '=%.0s' $(seq 1 $padding))" fi
echo -n "$base64_str" | base64 -d}
# Parse argumentsPUBLIC_KEY=""IDENTITY=""TARGET_DIR=""DRYRUN=falseDECRYPT_MODE=false
while [[ $# -gt 0 ]]; do case "$1" in -R) if [[ -z "${2:-}" ]]; then echo "Error: -R requires a path argument" >&2 exit 1 fi PUBLIC_KEY="$2" shift 2 ;; -I) if [[ -z "${2:-}" ]]; then echo "Error: -I requires a path argument" >&2 exit 1 fi IDENTITY="$2" shift 2 ;; -i) if [[ -z "${2:-}" ]]; then echo "Error: -i requires a path argument" >&2 exit 1 fi TARGET_DIR="$2" shift 2 ;; -d) DECRYPT_MODE=true shift ;; --dryrun) DRYRUN=true shift ;; -h|--help) usage ;; *) echo "Error: Unknown option: $1" >&2 usage ;; esacdone
# Validate required argumentsif [[ -z "$TARGET_DIR" ]]; then echo "Error: -i <target_directory> is required" >&2 usagefi
if $DECRYPT_MODE; then if [[ -z "$IDENTITY" ]]; then echo "Error: -I <identity_path> is required for decryption" >&2 usage fi if [[ ! -f "$IDENTITY" ]]; then echo "Error: Identity file not found: $IDENTITY" >&2 exit 1 fielse if [[ -z "$PUBLIC_KEY" ]]; then echo "Error: -R <public_key_path> is required for encryption" >&2 usage fi if [[ ! -f "$PUBLIC_KEY" ]]; then echo "Error: Public key file not found: $PUBLIC_KEY" >&2 exit 1 fifi
if [[ ! -d "$TARGET_DIR" ]]; then echo "Error: Target directory not found: $TARGET_DIR" >&2 exit 1fi
# Check if age command existsif ! command -v age &> /dev/null; then echo "Error: 'age' command not found. Please install age first." >&2 exit 1fi
TARGET_DIR="${TARGET_DIR%/}" # Remove trailing slash if present
# Decrypt modeif $DECRYPT_MODE; then found_files=false for encrypted_file in "$TARGET_DIR"/*.tar.age; do # Skip if no files found (glob didn't match) [[ -f "$encrypted_file" ]] || continue
found_files=true filename=$(basename "$encrypted_file") encoded_name="${filename%.tar.age}" original_dirname=$(from_base64url "$encoded_name") tarfile="${TARGET_DIR}/${encoded_name}.tar"
echo "Processing: $filename" echo " Decoded name: $original_dirname"
if $DRYRUN; then echo " [dryrun] age -d -i \"$IDENTITY\" -o \"$tarfile\" \"$encrypted_file\"" echo " [dryrun] tar -xf \"$tarfile\" -C \"$TARGET_DIR\"" echo " [dryrun] rm \"$tarfile\"" else # Decrypt with age echo " Decrypting: $tarfile" age -d -i "$IDENTITY" -o "$tarfile" "$encrypted_file"
# Extract tar archive echo " Extracting: $original_dirname" tar -xf "$tarfile" -C "$TARGET_DIR"
# Remove the tar file echo " Removing tar: $tarfile" rm "$tarfile" fi
echo " Done: $original_dirname" echo done
if ! $found_files; then echo "Warning: No .tar.age files found in $TARGET_DIR" >&2 exit 0 fi
echo "All files decrypted successfully." exit 0fi
# Encrypt modefound_dirs=falsefor dir in "$TARGET_DIR"/*/; do # Skip if no directories found (glob didn't match) [[ -d "$dir" ]] || continue
found_dirs=true dir="${dir%/}" # Remove trailing slash dirname=$(basename "$dir") encoded_name=$(to_base64url "$dirname") tarfile="${TARGET_DIR}/${encoded_name}.tar" encrypted_file="${TARGET_DIR}/${encoded_name}.tar.age"
echo "Processing: $dirname" echo " Encoded name: $encoded_name"
if $DRYRUN; then echo " [dryrun] tar -cf \"$tarfile\" -C \"$TARGET_DIR\" \"$dirname\"" echo " [dryrun] age --armor -R \"$PUBLIC_KEY\" -o \"$encrypted_file\" \"$tarfile\"" echo " [dryrun] rm \"$tarfile\"" else # Create tar archive echo " Creating tar archive: $tarfile" tar -cf "$tarfile" -C "$TARGET_DIR" "$dirname"
# Encrypt with age echo " Encrypting: $encrypted_file" age --armor -R "$PUBLIC_KEY" -o "$encrypted_file" "$tarfile"
# Remove the unencrypted tar file echo " Removing unencrypted tar: $tarfile" rm "$tarfile" fi
echo " Done: $encrypted_file" echodone
if ! $found_dirs; then echo "Warning: No subdirectories found in $TARGET_DIR" >&2 exit 0fi
echo "All directories processed successfully."Installation
curl -o ~/.local/bin/age-tar <YOUR_GIST_URL>chmod +x ~/.local/bin/age-tar
# ~/.bashrc or ~/.zshrcexport PATH="$HOME/.local/bin:$PATH"Hinweise
- Diese Implementierung verarbeitet nur die unmittelbaren Unterverzeichnisse des Zielverzeichnisses
- Die Originalverzeichnisse bleiben nach der Verschlüsselung erhalten, daher sollte separat entschieden werden, ob sie gelöscht werden sollen
- Beim Entschlüsseln wird aus jeder
.tar.agezunächst eine temporäre.tarerzeugt, entpackt und anschließend wieder gelöscht
Zusammenfassung
Mit age-tar wird ein verschlüsseltes Backup auf Verzeichnisebene zu einem wiederholbaren Ablauf.
Durch die Kombination aus age-Verschlüsselung und base64URL-Normalisierung von Dateinamen lassen sich auch Daten mit japanischen Verzeichnisnamen deutlich besser über die CLI handhaben.
hsb.horse