diff --git a/lib/git-crypt.sh b/lib/git-crypt.sh index 3877c0c..ce64ad1 100644 --- a/lib/git-crypt.sh +++ b/lib/git-crypt.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ============================================================================= # lib/git-crypt.sh -# git-crypt lifecycle management (init, export, verify). +# git-crypt lifecycle management (init, export, verify, rotate). # ============================================================================= gcrypt_init() { @@ -26,16 +26,130 @@ gcrypt_verify() { info "Verifying git-crypt status for ${genome_name}..." git-crypt lock - # Checking if the private marker is still encrypted (binary check) if file "raw/private/.gitkeep" 2>/dev/null | grep -q "data"; then success "Encryption verified: private/ directory is protected." else - warn "Encryption check inconclusive. Please run 'git-crypt status' manually." + warn "Encryption check inconclusive. Run 'git-crypt status' manually." fi [[ -f "$key_path" ]] && git-crypt unlock "$key_path" } +# --------------------------------------------------------------------------- +# gcrypt_rotate_key +# Rotates the git-crypt symmetric key for the current genome directory. +# +# WHAT THIS DOES: +# 1. Unlocks the repo with the existing key (working tree is decrypted). +# 2. Removes the old key material from .git/git-crypt/keys/. +# 3. Runs git-crypt init to generate a new symmetric key. +# 4. Stages and commits private files — they are re-encrypted with the new key. +# 5. Exports the new key to KEYS_DIR for Vaultwarden upload. +# +# WHAT THIS DOES NOT DO (limitation): +# Git history still contains blobs encrypted with the OLD key. Anyone who +# has the old key and access to the git history can still decrypt those blobs. +# To purge old encrypted blobs from history entirely, run git-filter-repo +# separately after this function completes (manual step — not automated here +# because it rewrites all commit hashes and requires force-pushing). +# +# USAGE: +# source lib/git-crypt.sh +# cd ~/knowledge-genome-setup/genome-dev +# gcrypt_rotate_key "genome-dev" +# +# REQUIRES: +# - The old key file at KEYS_DIR/.key OR the repo is already unlocked. +# - Clean working tree (no uncommitted changes outside private/). +# --------------------------------------------------------------------------- +gcrypt_rotate_key() { + local genome_name="$1" + local old_key_path="${KEYS_DIR}/${genome_name}.key" + local new_key_name="${genome_name}-rotated-$(date +%Y%m%d)" + + step "Key rotation: ${genome_name}" + + warn "SCOPE: this rotates the key for future commits only." + warn " Old git history retains blobs encrypted with the previous key." + warn " See function header in git-crypt.sh for full purge instructions." + echo "" + + # 1. Unlock with old key (if not already unlocked) + if git-crypt status 2>/dev/null | grep -q "encrypted"; then + info "Repository appears to be locked. Attempting unlock..." + if [[ -f "$old_key_path" ]]; then + git-crypt unlock "$old_key_path" + success "Unlocked with existing key." + else + error "Old key not found at: ${old_key_path}" + error "Unlock manually before rotating: git-crypt unlock /path/to/${genome_name}.key" + exit 1 + fi + else + info "Repository is already unlocked — proceeding." + fi + + # 2. Ensure working tree is clean (private files excluded — they will be re-staged) + if ! git diff --quiet -- ':!raw/private' ':!wiki/private' 2>/dev/null; then + error "Working tree has uncommitted changes outside private/. Commit or stash them first." + exit 1 + fi + + # 3. Remove old key material only (preserves .git/git-crypt/ structure) + info "Removing old key material..." + rm -rf .git/git-crypt/keys + success "Old key material removed." + + # 4. Re-initialize git-crypt (generates a new symmetric key) + info "Initializing new symmetric key..." + git-crypt init + success "New key generated." + + # 5. Re-stage private files so they are committed encrypted with the new key + local staged=0 + if compgen -G "raw/private/*" > /dev/null 2>&1; then + git add raw/private/ + staged=1 + fi + if compgen -G "wiki/private/*" > /dev/null 2>&1; then + git add wiki/private/ + staged=1 + fi + + if [[ $staged -eq 1 ]]; then + # Exclude .gitkeep-only commits — only commit if real content exists + if ! git diff --cached --quiet; then + git commit -m "security: rotate git-crypt key for ${genome_name}" + success "Private files re-committed with new key." + else + info "Only .gitkeep files in private/ — no content commit needed." + fi + else + info "No private files found to re-encrypt." + fi + + # 6. Export new key + gcrypt_export_key "$new_key_name" + + echo "" + success "Key rotation complete for: ${genome_name}" + echo "" + warn "NEXT STEPS:" + echo " 1. Push the new commit: git push origin main" + echo " 2. Upload the new key to Vaultwarden:" + echo " base64 < ${KEYS_DIR}/${new_key_name}.key" + echo " → Secure Note name: \"${genome_name} key\" (replace existing)" + echo " 3. Delete both key files from disk:" + echo " rm ${KEYS_DIR}/${genome_name}.key" + echo " rm ${KEYS_DIR}/${new_key_name}.key" + echo " 4. Revoke access from any previous key holders." + echo " 5. For full history purge (removes old encrypted blobs from git history):" + echo " git filter-repo --invert-paths --path raw/private --path wiki/private" + echo " git push --force origin main" + echo " (⚠ rewrites all commit hashes — coordinate with any collaborators)" + echo "" +} + gcrypt_print_key_instructions() { local genome_name="$1" local v_url="${VAULTWARDEN_URL:-https://your-vaultwarden.com}"