feat: Add git-crypt key rotation functionality
This commit is contained in:
parent
5e252412d4
commit
f59fff3a8b
1 changed files with 117 additions and 3 deletions
120
lib/git-crypt.sh
120
lib/git-crypt.sh
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# lib/git-crypt.sh
|
# lib/git-crypt.sh
|
||||||
# git-crypt lifecycle management (init, export, verify).
|
# git-crypt lifecycle management (init, export, verify, rotate).
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
gcrypt_init() {
|
gcrypt_init() {
|
||||||
|
|
@ -26,16 +26,130 @@ gcrypt_verify() {
|
||||||
info "Verifying git-crypt status for ${genome_name}..."
|
info "Verifying git-crypt status for ${genome_name}..."
|
||||||
git-crypt lock
|
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
|
if file "raw/private/.gitkeep" 2>/dev/null | grep -q "data"; then
|
||||||
success "Encryption verified: private/ directory is protected."
|
success "Encryption verified: private/ directory is protected."
|
||||||
else
|
else
|
||||||
warn "Encryption check inconclusive. Please run 'git-crypt status' manually."
|
warn "Encryption check inconclusive. Run 'git-crypt status' manually."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -f "$key_path" ]] && git-crypt unlock "$key_path"
|
[[ -f "$key_path" ]] && git-crypt unlock "$key_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# gcrypt_rotate_key <genome_name>
|
||||||
|
# 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/<genome_name>.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() {
|
gcrypt_print_key_instructions() {
|
||||||
local genome_name="$1"
|
local genome_name="$1"
|
||||||
local v_url="${VAULTWARDEN_URL:-https://your-vaultwarden.com}"
|
local v_url="${VAULTWARDEN_URL:-https://your-vaultwarden.com}"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue