Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 60 additions & 61 deletions .mise/tasks/setup
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ set -euo pipefail

source "$MISE_CONFIG_ROOT/lib/common.sh"
source "$MISE_CONFIG_ROOT/lib/hooks.sh"
source "$MISE_CONFIG_ROOT/lib/encryption.sh"
source "$MISE_CONFIG_ROOT/lib/setup.sh"
require_git
require_rudi

Expand Down Expand Up @@ -50,19 +52,56 @@ if [ ${#patterns[@]} -eq 0 ]; then
patterns=("$NOTES_DIR/**")
fi

# Inspect the pre-setup index before attributes or the manifest can change how
# tracked notes are interpreted.
tracked_readable_snapshot=$(mktemp) || {
echo "Error: failed to create tracked note snapshot" >&2
exit 1
}
if ! write_tracked_readable_notes \
"$TARGET_DIR" "$NOTES_DIR" "$tracked_readable_snapshot"; then
rm -f "$tracked_readable_snapshot"
echo "Error: failed to inspect tracked notes before setup." >&2
exit 1
fi
tracked_readable_count=$(tracked_readable_note_count "$tracked_readable_snapshot")
rm -f "$tracked_readable_snapshot"

repo_was_initialized=false
is_initialized && repo_was_initialized=true
unlock_requested=${usage_unlock:-false}
require_tracked_plaintext_setup_ready \
"$TARGET_DIR" "$tracked_readable_count" \
"$repo_was_initialized" "$unlock_requested"

setup_confirmation_message="notes setup will initialize/update git-crypt, .gitattributes, $NOTES_DIR/.manifest, and git hooks for $NOTES_DIR/ in this repo."
if [ "$tracked_readable_count" -gt 0 ]; then
setup_confirmation_message="$setup_confirmation_message It will prepare $tracked_readable_count tracked plaintext note(s) for forward encryption."
fi
if [ "${usage_unlock:-false}" = "true" ]; then
setup_confirmation_message="$setup_confirmation_message It will also unlock/decrypt all git-crypt files after setup."
fi
confirm_destructive "$setup_confirmation_message Continue?"

# --- Initialize via rudi ---
if is_initialized || [ -d "$TARGET_DIR/.git/git-crypt" ]; then
if $repo_was_initialized; then
echo "git-crypt already initialized — updating auxiliary files..."
else
echo "Initializing git-crypt..."
rudi init --no-user
fi
if [ "$tracked_readable_count" -gt 0 ]; then
echo "Preparing $tracked_readable_count tracked plaintext note(s) for forward encryption..."
fi

# Existing repositories must unlock before setup dirties .gitattributes.
unlocked_before_mutation=false
if [ "${usage_unlock:-false}" = "true" ] && $repo_was_initialized; then
echo ""
echo "Unlocking..."
cd "$MISE_CONFIG_ROOT" && NOTES_CALLER_PWD="$TARGET_DIR" mise run -q unlock
unlocked_before_mutation=true
fi

# --- Add GPG keys via rudi ---
if [ ${#keys[@]} -gt 0 ]; then
Expand Down Expand Up @@ -90,46 +129,7 @@ if [ ${#keys[@]} -gt 0 ]; then
fi

# --- Configure .gitattributes via rudi ---
GITATTRIBUTES="$TARGET_DIR/.gitattributes"

gitattributes_has_encrypted_pattern() {
local pattern="$1"
awk -v pattern="$pattern" '
$1 == pattern {
for (i = 2; i <= NF; i++) {
if ($i == "filter=git-crypt") found = 1
}
}
END { exit(found ? 0 : 1) }
' "$GITATTRIBUTES" 2>/dev/null
}

missing_patterns=()
for p in "${patterns[@]}"; do
[ -z "$p" ] && continue
if ! gitattributes_has_encrypted_pattern "$p"; then
missing_patterns+=("$p")
fi
done

if [ ${#missing_patterns[@]} -gt 0 ]; then
echo ""
echo "Configuring encrypted patterns..."
for p in "${missing_patterns[@]}"; do
rudi assign "$p"
if ! gitattributes_has_encrypted_pattern "$p"; then
# rudi treats an existing pattern with disabled attributes (for example
# "notes/** -filter=git-crypt") as already assigned. Append an explicit
# positive assignment so the requested pattern is actually encrypted.
# Fixed width here serializes .gitattributes; it is not a rendered table.
printf '%-40s filter=git-crypt diff=git-crypt\n' "$p" >> "$GITATTRIBUTES" # codebase:ignore
fi
done
echo " Updated .gitattributes"
else
echo ""
echo " Requested encrypted patterns already configured"
fi
configure_setup_encrypted_patterns "$TARGET_DIR" "${patterns[@]}"

# --- Bootstrap obfuscation manifest ---
if [ ! -f "$TARGET_DIR/$NOTES_DIR/.manifest" ]; then
Expand All @@ -148,31 +148,23 @@ echo " Installed hooks"

# --- Unlock if requested ---
if [ "${usage_unlock:-false}" = "true" ]; then
echo ""
echo "Unlocking..."
cd "$MISE_CONFIG_ROOT" && NOTES_CALLER_PWD="$TARGET_DIR" mise run -q unlock
if ! $unlocked_before_mutation; then
echo ""
echo "Unlocking..."
cd "$MISE_CONFIG_ROOT" && NOTES_CALLER_PWD="$TARGET_DIR" mise run -q unlock
fi
echo ""
echo "Done! Repo is set up and unlocked."
else
# Detect if this is an existing repo with encrypted notes
# (i.e., joining an existing repo vs creating a new one).
has_encrypted_notes=false
if [ -d "$TARGET_DIR/$NOTES_DIR" ]; then
encrypted_notes_snapshot=$(mktemp) || { echo "Error: failed to create encrypted note snapshot" >&2; exit 1; }
encrypted_notes_status=0
find "$TARGET_DIR/$NOTES_DIR" -type f ! -name .manifest -print0 > "$encrypted_notes_snapshot" || encrypted_notes_status=$?
if [ "$encrypted_notes_status" -ne 0 ]; then
rm -f "$encrypted_notes_snapshot"
echo "Error: failed to inspect existing encrypted notes." >&2
exit "$encrypted_notes_status"
fi
while IFS= read -r -d '' f; do
if head -c 10 "$f" 2>/dev/null | grep -q "GITCRYPT"; then
has_encrypted_notes=true
break
fi
done < "$encrypted_notes_snapshot"
rm -f "$encrypted_notes_snapshot"
has_encrypted_notes=""
encrypted_notes_status=0
has_encrypted_notes=$(notes_tree_encryption_state "$TARGET_DIR/$NOTES_DIR") \
|| encrypted_notes_status=$?
if [ "$encrypted_notes_status" -ne 0 ]; then
echo "Error: failed to inspect existing encrypted notes." >&2
exit "$encrypted_notes_status"
fi

echo ""
Expand All @@ -184,6 +176,13 @@ else
echo " This repo already has encrypted notes."
echo " Run 'notes unlock' to decrypt them (requires your GPG key)."
echo " Or re-run setup with --unlock: notes setup --unlock"
elif [ "$tracked_readable_count" -gt 0 ]; then
echo " 1. Stage attributes: git add .gitattributes"
echo " 2. Stage notes: notes stage --all"
echo " 3. Commit setup and encrypted notes"
echo ""
echo "Existing tracked notes need a one-time Notes stage so Git reapplies"
echo "the new encryption filter and the commit hook obfuscates their names."
else
echo " 1. Check status: notes status"
echo " 2. Commit the setup: git add .gitattributes .git-crypt && git commit"
Expand Down
31 changes: 4 additions & 27 deletions .mise/tasks/stage
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ source "$MISE_CONFIG_ROOT/lib/obfuscate.sh"
source "$MISE_CONFIG_ROOT/lib/suppress.sh"
source "$MISE_CONFIG_ROOT/lib/changes.sh"
source "$MISE_CONFIG_ROOT/lib/hooks.sh"
source "$MISE_CONFIG_ROOT/lib/encryption.sh"
source "$MISE_CONFIG_ROOT/lib/stage.sh"
require_git

notes_dir="${usage_dir:-notes}"
Expand Down Expand Up @@ -228,34 +230,9 @@ for relpath in ${to_stage[@]+"${to_stage[@]}"}; do
local_file="$abs_notes_dir/$relpath"

if [ -f "$local_file" ]; then
# Modified or new: force-add the readable file.
git -C "$TARGET_DIR" add -f "$notes_dir/$relpath"
echo " staged: $relpath"
stage_readable_note "$TARGET_DIR" "$notes_dir" "$relpath"
else
# Deleted: remove the obfuscated ID from the index.
id=$(manifest_id_for_name "$manifest" "$relpath")
if [ -n "$id" ]; then
# First remove the tracked blob when it is still present in the index.
# If the index cannot be updated (for example, an index lock exists),
# fail before mutating the working manifest. If a previous interrupted
# run already staged/removed the blob, keep going and only stage the
# manifest repair.
if git -C "$TARGET_DIR" ls-files --error-unmatch -- "$notes_dir/$id" >/dev/null 2>&1; then
git -C "$TARGET_DIR" rm --cached --quiet "$notes_dir/$id"
fi

tmp_manifest=$(mktemp) || exit 1
manifest_backup=$(mktemp) || exit 1
cp "$manifest" "$manifest_backup"
awk -F '\t' -v path="$relpath" '$2 != path { print $0 }' "$manifest" > "$tmp_manifest"
mv -f "$tmp_manifest" "$manifest"
if ! git -C "$TARGET_DIR" add -f "$notes_dir/.manifest"; then
mv -f "$manifest_backup" "$manifest"
exit 1
fi
rm -f "$manifest_backup"
echo " staged (delete): $relpath"
fi
stage_deleted_note "$TARGET_DIR" "$notes_dir" "$manifest" "$relpath"
fi
staged=$((staged + 1))
done
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

**Collective memory, encrypted.**

[![tests: 467](https://img.shields.io/badge/tests-467-brightgreen?style=flat)](test/)
[![tests: 474](https://img.shields.io/badge/tests-474-brightgreen?style=flat)](test/)
![lints: 8](https://img.shields.io/badge/lints-8-blue?style=flat)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)

Expand Down
26 changes: 25 additions & 1 deletion lib/encryption.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
#!/usr/bin/env bash
# encryption.sh — staged encrypted-path validation
# encryption.sh — Git blob and staged encrypted-path validation

GITCRYPT_HEADER_HEX="00474954435259505400"

# Print encrypted or plaintext for a raw Git blob. Backend failures preserve
# their exit status. The producer is fully drained so large blobs do not cause
# SIGPIPE under pipefail.
# Usage: git_blob_encryption_state <repo> <object>
git_blob_encryption_state() {
local repo="$1" object="$2" header_hex blob_status=0

header_hex=$(
git -C "$repo" cat-file blob "$object" 2>/dev/null |
{ dd bs=1 count=10 2>/dev/null; cat >/dev/null; } |
od -An -tx1 |
tr -d ' \n'
) || blob_status=$?
[ "$blob_status" -eq 0 ] || return "$blob_status"

if [ "$header_hex" = "$GITCRYPT_HEADER_HEX" ]; then
printf 'encrypted\n'
else
printf 'plaintext\n'
fi
}

# Verify that indexed blobs for encrypted paths are encrypted.
# The common path checks all paths in one git-crypt call. If that call reports
Expand Down
Loading
Loading