Quand je sauvegarde des fichiers locaux vers un stockage cloud, je préfère éviter de les téléverser en clair.
J’ai donc écrit un script age-tar qui combine age et tar pour créer des archives chiffrées répertoire par répertoire.
Ce que je voulais résoudre
Deux points deviennent vite pénibles dans un flux de sauvegarde :
- Exécuter manuellement
tarpuisagepour chaque répertoire est fastidieux - Les noms de répertoires contenant du japonais ou des espaces sont peu pratiques à manipuler en CLI
age-tar traite ces deux problèmes en même temps.
Ce que fait ce script
- Chiffre chaque sous-répertoire situé directement sous un dossier cible avec
tar + age - Encode les noms de fichiers de sortie en base64URL pour garder des noms compatibles CLI
- Restaure le nom de répertoire d’origine pendant le déchiffrement
- Permet de vérifier uniquement les commandes via
--dryrun
Pré-requis
Installer age
# macOSbrew install age
# Ubuntu/Debianapt install age
# Ou utiliser les releases officielles# https://github.com/FiloSottile/age/releasesGénérer les clés
# Créer une clé secrèteage-keygen -o ~/.age/key.txt
# Extraire la clé publiqueage-keygen -y ~/.age/key.txt > ~/.age/key.pubUtilisation
Chiffrer
age-tar -R ~/.age/key.pub -i /path/to/backup/targetChaque dossier situé directement sous /path/to/backup/target devient un fichier .tar.age.
target/├── 日本語フォルダ/├── my documents/└── projects/Après chiffrement, les répertoires d’origine restent en place :
target/├── 5pel5pys6Kqe44OV44Kp44Or44OA.tar.age├── bXkgZG9jdW1lbnRz.tar.age├── cHJvamVjdHM.tar.age├── 日本語フォルダ/├── my documents/└── projects/Déchiffrer
age-tar -d -I ~/.age/key.txt -i /path/to/encrypted/targetLe script déchiffre les fichiers .tar.age puis les extrait avec le nom de répertoire d’origine.
Dry run
# Encryptage-tar -R ~/.age/key.pub -i /path/to/target --dryrun
# Decryptage-tar -d -I ~/.age/key.txt -i /path/to/target --dryrunSeules les commandes qui seraient exécutées sont affichées, sans opération réelle sur les fichiers.
Exemple de workflow
# 1. Chiffrer en localage-tar -R ~/.age/key.pub -i ~/important-data
# 2. Téléverser les fichiers chiffrésrclone copy ~/important-data/*.tar.age remote:backup/
# 3. Restaurerrclone copy remote:backup/ ~/restore/age-tar -d -I ~/.age/key.txt -i ~/restoreScript complet
#!/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"Remarques
- Cette implémentation traite uniquement les sous-répertoires immédiats du dossier cible
- Les répertoires d’origine restent présents après chiffrement, il faut donc décider séparément d’une politique de suppression
- Au déchiffrement, un fichier
.tartemporaire est créé à partir de.tar.age, extrait, puis supprimé
Résumé
age-tar permet de transformer une sauvegarde chiffrée par répertoire en procédure répétable.
En combinant le chiffrement avec age et une normalisation des noms de fichiers en base64URL, même des données avec des noms de dossiers japonais deviennent plus simples à manipuler en CLI.
hsb.horse