Files
Encrypt-Share-Attribution/create-attributable-archive.sh
T
2026-05-23 14:36:10 -06:00

356 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# packages: 7zip, shred, secure-delete, cracklib-runtime, openssl, curl
set -o errtrace
set -o nounset
set -o pipefail
IFS=$'\n\t'
unix_seconds=$(date +%s)
key_path="./private_ed25519_${unix_seconds}"
signature_tag="file-integrity"
out_dir="./out"
inner_dir="$out_dir/contents"
RED='\033[31m'
GREEN='\033[32m'
RESET='\033[0m'
num_of_args="$#"
all_args="$@"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing required command: $1" >&2
exit 1
fi
}
require_dependencies() {
local deps=(bash shred srm openssl curl ssh-keygen 7z sha512sum awk grep realpath)
for dep in "${deps[@]}"; do
require_command "$dep"
done
}
checkcode() {
local retcode="${1:-}"
if [[ -z "$retcode" ]]; then
echo -e "\n${RED}ERROR!${RESET} checkcode missing return code parameter\n" >&2
exit 1
fi
if [[ "$retcode" -ne 0 ]]; then
echo -e "${RED}ERROR!${RESET} Response code: $retcode" >&2
exit "$retcode"
fi
printf ' %bOK!%b\n' "$GREEN" "$RESET"
}
run_cmd() {
"$@"
checkcode $?
}
reset() {
printf 'Autoshredding known artifacts...\n'
find . -maxdepth 1 -type f \( -name 'private_*' -o -name 'attribution_passphrase_*' -o -name '*.sha512' -o -name 'checksums*' -o -name '*.sig' -o -name '*.7z' -o -name 'anonymous_signer' \) -exec shred -uz {} +
checkcode $?
if compgen -G 'private_*' >/dev/null 2>&1; then
printf 'Shredding errant private key files...\n'
shred -uz private_* || true
fi
if compgen -G 'attribution_passphrase_*' >/dev/null 2>&1; then
printf 'Shredding errant attribution passphrase files...\n'
shred -uz attribution_passphrase_* || true
fi
printf 'Removing previous output directory...\n'
rm -rf "$out_dir"
checkcode $?
printf 'Rebuilding output directory structure...\n'
mkdir -p "$inner_dir"
checkcode $?
printf 'Writing placeholder README files...\n'
echo 'put files to verifiably archive in here' > "$inner_dir/README.md"
checkcode $?
echo '# todo: make this nice' > "$out_dir/README.md"
checkcode $?
printf 'Copying verification helpers...\n'
cp test_validate_passphrase.txt "$out_dir/test_validate_passphrase.sh"
checkcode $?
chmod +x "$out_dir/test_validate_passphrase.sh"
checkcode $?
cp verify-everything.txt "$out_dir/verify-everything.sh"
checkcode $?
chmod +x "$out_dir/verify-everything.sh"
checkcode $?
local housekeeping_dirs=(archives keystore)
for dir in "${housekeeping_dirs[@]}"; do
if [[ -d "$dir" ]]; then
printf 'Hardening %s...\n' "$dir"
chmod 700 "$dir"
checkcode $?
find "$dir" -mindepth 1 -type d -exec srm -r -z -l '{}' + >/dev/null 2>&1 || true
find "$dir" -type f \( -name 'private_ed25519_*' -o -name 'attribution_passphrase_*' \) -exec shred -uz '{}' + >/dev/null 2>&1 || true
find "$dir" -type f -exec chmod 600 '{}' +
checkcode $?
fi
done
}
audit_passphrase() {
local raw_password="$1"
local check_password="$2"
if [[ -z "$raw_password" ]]; then
echo '[ERROR] No passphrase provided for validation.' >&2
return 2
fi
if [[ -z "$check_password" ]]; then
echo '[ERROR] No check passphrase provided for validation.' >&2
return 2
fi
if [[ "$raw_password" != "$check_password" ]]; then
echo '[ERROR] Passphrases do not match!' >&2
return 2
fi
unset check_password
local pass_len=${#raw_password}
if [[ "$pass_len" -lt 35 ]]; then
echo "❌ REJECTED: Passphrase is too short ($pass_len characters). Minimum length required is 35."
return 1
fi
echo "[PASS] Length verification satisfied ($pass_len characters)."
if command -v cracklib-check >/dev/null 2>&1; then
if ! printf '%s' "$raw_password" | cracklib-check | grep -q 'OK$'; then
echo '❌ REJECTED by cracklib-check.'
return 1
fi
echo '[PASS] Local dictionary and structural complexity audit clear.'
else
echo '[WARN] cracklib-check not found; skipping local dictionary audit.' >&2
fi
local full_hash prefix suffix response
full_hash=$(printf '%s' "$raw_password" | openssl dgst -sha1 | awk '{print toupper($2)}')
prefix=${full_hash:0:5}
suffix=${full_hash:5}
if ! response=$(curl -fsS -A 'Bash-Passphrase-Audit-Script' "https://api.pwnedpasswords.com/range/$prefix"); then
echo -e "${RED}[FATAL]${RESET} Failed to communicate with HIBP API." >&2
return 3
else
echo -e "connected to hibp...${GREEN}OK${RESET}"
fi
if printf '%s\n' "$response" | grep -qi "^$suffix:"; then
echo -e "${RED}[FATAL]${RESET} Passphrase has been leaked!" >&2
return 1
else
echo -e "not leaked! (via hibp)... ${GREEN}OK${RESET}"
fi
return 0
}
error_handle() {
local exit_code=$?
local script_path
if command -v realpath >/dev/null 2>&1; then
script_path=$(realpath "$0")
else
script_path="$PWD/$0"
fi
local hr='===================================================='
echo
echo "$hr"
echo -e "🚨 ${RED}FATAL ERROR DETECTED${RESET}"
echo "$hr"
echo "-> Script : $0"
echo "-> Num Script Args : $num_of_args"
echo "-> Script Args : $all_args"
echo "-> Shell : ${SHELL:-unknown}"
echo "-> Script Path : $script_path"
echo "-> Script (full) : $SHELL $script_path $all_args"
echo "-> User : ${USER:-unknown}"
echo "-> Working Directory : $PWD"
echo "-> Failed Command : $BASH_COMMAND"
echo "-> Line Number : $LINENO"
echo "-> Exit Status : $exit_code"
echo "-> Seconds Elapsed : $SECONDS"
echo "-> Date Failed : $(date)"
echo '-> Stack Trace'
local frame=0
while caller "$frame"; do
frame=$((frame + 1))
done
echo
echo "$hr"
echo
exit "$exit_code"
}
exit_cleanup() {
printf "cleaning up on exit"
reset > /dev/null 2>&1
checkcode $?
# todo: uset vars
}
trap error_handle ERR
trap exit_cleanup EXIT
audit_passphrase "yw0EKY6wbSxU6KJxWN3vFZDgnSjJ4F8wqFrAJMcDxfU" "yw0EKY6wbSxU6KJxWN3vFZDgnSjJ4F8wqFrAJMcDxfU"
require_dependencies
printf 'Setting up environment...\n'
reset
printf '\n\n'
read -n 1 -s -r -p "In another terminal/window, fill $inner_dir with whatever you please then press any key to continue..."
printf '\n'
printf 'ssh-keygen: creating new key: %s...\n' "$key_path"
ssh-keygen -t ed25519 -f "$key_path" -C 'anonymous' -N '' >/dev/null 2>&1
checkcode $?
printf 'ssh-keygen: fixing permissions on %s and %s...\n' "$key_path" "${key_path}.pub"
chmod 600 "$key_path" "${key_path}.pub"
checkcode $?
printf 'ssh-keygen: creating %s/anonymous_signer...\n' "$out_dir"
echo "anonymous namespaces=\"$signature_tag\" $(cat "${key_path}.pub")" > "$out_dir/anonymous_signer"
checkcode $?
printf 'Inject random data? (y/N): '
read -r random
if [[ -z "$random" || "$random" =~ ^[nN]$ ]]; then
echo -e "No random data added. ${GREEN}OK!${RESET}\n"
else
printf 'random: adding 1/2 random blocks of data (128 bytes) to outer archive...\n'
openssl rand -out "$out_dir/.$RANDOM" 128 >/dev/null 2>&1
checkcode $?
printf 'random: adding 2/2 random blocks of data (128 bytes) to inner archive...\n'
openssl rand -out "$inner_dir/.$RANDOM" 128 >/dev/null 2>&1
checkcode $?
fi
printf '7z: compressing inner volume...\n'
7z a "$out_dir/contents.7z" "$inner_dir" >/dev/null 2>&1
checkcode $?
printf 'Deleting %s...\n' "$inner_dir"
rm -rf "$inner_dir"
checkcode $?
printf 'ssh: signing %s...\n' "$out_dir/contents.7z"
ssh-keygen -Y sign -f "$key_path" -n "$signature_tag" "$out_dir/contents.7z" >/dev/null 2>&1
checkcode $?
printf 'Changing directory to %s...\n' "$out_dir"
cd "$out_dir"
checkcode $?
printf 'sha512: generating checksums...\n'
sha512sum * > checksums.sha512
checkcode $?
printf 'Changing directory back...\n'
cd ..
checkcode $?
printf 'Enter attribution passphrase:\n'
read -r -s attribution_passphrase
printf '\nEnter attribution passphrase again:\n'
read -r -s attribution_passphrase_check
printf '\n'
printf 'Auditing attribution passphrase...\n'
ret=$(audit_passphrase "$attribution_passphrase" "$attribution_passphrase_check")
echo "$ret"
printf 'Unsetting attribution_passphrase_check...\n'
unset attribution_passphrase_check
printf 'Calculating attribution checksum...\n'
{
printf '%s' "$attribution_passphrase"
cat "$out_dir/contents.7z"
} | sha512sum | awk '{print $1}' > "$out_dir/attribution-checksum.sha512"
checkcode $?
printf 'Sanity checking: changing working directory to %s...\n' "$out_dir"
cd "$out_dir"
checkcode $?
printf 'Sanity checking: verification...\n'
bash verify-everything.sh "$attribution_passphrase"
checkcode $?
printf 'Sanity checking: validate attribution passphrase...\n'
bash test_validate_passphrase.sh "$attribution_passphrase"
checkcode $?
printf 'Returning to project root...\n'
cd ..
checkcode $?
printf 'Unsetting attribution_passphrase...\n'
unset attribution_passphrase
printf '7z archiving outer dir...\n'
7z a ./out.7z "$out_dir" >/dev/null 2>&1
checkcode $?
printf 'Moving out.7z to archives...\n'
mv out.7z "archives/verifiable_archive_${unix_seconds}.7z"
checkcode $?
printf 'Enter keystore passphrase:\n'
read -r -s keystore_passphrase
printf '\nEnter keystore passphrase again:\n'
read -r -s keystore_passphrase_check
printf '\n'
printf 'Auditing keystore passphrase...\n'
ret="$(audit_passphrase \"$keystore_passphrase\" \"$keystore_passphrase_check\")"
echo -e "$ret"
printf 'Unsetting keystore_passphrase_check...\n'
unset keystore_passphrase_check
printf 'Archiving keys...\n'
set +u
shopt -s nullglob
private_files=(private_*)
passphrase_files=(attribution_passphrase_*)
shopt -u nullglob
set -u
if [[ ${#private_files[@]} -eq 0 && ${#passphrase_files[@]} -eq 0 ]]; then
echo 'No key or attribution passphrase files found to archive.' >&2
exit 1
fi
7z a "keystore/keystore_${unix_seconds}.7z" "${private_files[@]}" "${passphrase_files[@]}" -p"$keystore_passphrase" -mhe=on >/dev/null 2>&1
checkcode $?
printf 'Testing key archive...\n'
7z t "keystore/keystore_${unix_seconds}.7z" -p"$keystore_passphrase" >/dev/null 2>&1
checkcode $?