From 1957081c122fe231eb6120192489dd979d214317 Mon Sep 17 00:00:00 2001 From: mediocregopher Date: Sun, 13 Aug 2023 21:34:14 +0200 Subject: Update branch with all changes which could be brought in from private branches For a while I was keeping a private branch where there were a lot of non-public things included, and that became the de-facto branch while this one lagged. This one is now up-to-date, all private stuff is dealt with via config files which are not committed. --- bin/dual-monitor | 16 + bin/git-remote-gcrypt | 921 -------------------------------------------------- bin/go-playground | 2 +- bin/quick-reboot | 31 ++ bin/quick-shutdown | 31 ++ bin/rotcheck | 353 +++++++++++++++++++ 6 files changed, 432 insertions(+), 922 deletions(-) create mode 100755 bin/dual-monitor delete mode 100755 bin/git-remote-gcrypt create mode 100755 bin/quick-reboot create mode 100755 bin/quick-shutdown create mode 100755 bin/rotcheck (limited to 'bin') diff --git a/bin/dual-monitor b/bin/dual-monitor new file mode 100755 index 0000000..9ed4d10 --- /dev/null +++ b/bin/dual-monitor @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +xrandr \ + --output eDP-1 \ + --primary \ + --mode 1920x1080 \ + --pos 0x0 \ + --rotate normal \ + --output DP-1 --off \ + --output HDMI-1 --off \ + --output DP-2 --off \ + --output HDMI-2 \ + --mode 1920x1080 \ + --pos 0x0 \ + --rotate normal \ + --brightness 0.9 diff --git a/bin/git-remote-gcrypt b/bin/git-remote-gcrypt deleted file mode 100755 index 8b66f2f..0000000 --- a/bin/git-remote-gcrypt +++ /dev/null @@ -1,921 +0,0 @@ -#!/bin/sh - -# git-remote-gcrypt -# -# Copyright (c) 2013 engla -# Copyright (c) 2013, 2014 Joey Hess -# Copyright (c) 2016 Sean Whitton and contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) version 2 or any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# See README.rst for usage instructions - -set -e # errexit -set -f # noglob -set -C # noclobber - -export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked -Gref="refs/gcrypt/gitception$GITCEPTION" -Gref_rbranch="refs/heads/master" -Packkey_bytes=63 # nbr random bytes for packfile keys, any >= 256 bit is ok -Hashtype=SHA256 # SHA512 SHA384 SHA256 SHA224 supported. -Manifestfile=91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a -Hex40="[a-f0-9]" -Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40 -Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40 # Match SHA-1 hexdigest -GPG="$(git config --get "gpg.program" '.+' || echo gpg)" - -Did_find_repo= # yes for connected, no for no repo -Localdir="${GIT_DIR:=.git}/remote-gcrypt" -Tempdir= - -Repoid= -Refslist= -Packlist= -Keeplist= -Extnlist= -Repack_limit=25 - -Recipients= - -# compat/utility functions -# xfeed: The most basic output function puts $1 into the stdin of $2..$# -xfeed() -{ - local input_= - input_=$1; shift - "$@" <&2; } -echo_die() { echo_info "$@" ; exit 1; } - -isnull() { case "$1" in "") return 0;; *) return 1;; esac; } -isnonnull() { ! isnull "$1"; } -iseq() { case "$1" in "$2") return 0;; *) return 1;; esac; } -isnoteq() { ! iseq "$1" "$2"; } -negate() { ! "$@"; } - -# Execute $@ or die -pipefail() -{ - "$@" || { echo_info "'$1' failed!"; kill $$; exit 1; } -} - -isurl() { isnull "${2%%$1://*}"; } -islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; } - -xgrep() { command grep "$@" || : ; } - -# setvar is used for named return variables -# $1 *must* be a valid variable name, $2 is any value -# -# Conventions -# return variable names are passed with a @ prefix -# return variable functions use f_ prefix local vars -# return var consumers use r_ prefix vars (or Titlecase globals) -setvar() -{ - isnull "${1##@*}" || echo_die "Missing @ for return variable: $1" - eval ${1#@}=\$2 -} - -Newline=" -" - -# $1 is return var, $2 is value appended with newline separator -append_to() -{ - local f_append_tmp_= - eval f_append_tmp_=\$${1#@} - isnull "$f_append_tmp_" || f_append_tmp_=$f_append_tmp_$Newline - setvar "$1" "$f_append_tmp_$2" -} - -# Pick words from each line -# $1 return variable name -# $2 input value -pick_fields_1_2() -{ - local f_ret= f_one= f_two= - while read f_one f_two _ # from << here-document - do - f_ret="$f_ret$f_one $f_two$Newline" - done </dev/null && - obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" && - isnonnull "$obj_id" && git cat-file blob "$obj_id" && ret_=: || - { ret_=false && : ; } - [ -e "$fet_head.$$~" ] && command mv -f "$fet_head.$$~" "$fet_head" || : - $ret_ -} - -anon_commit() -{ - GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \ - GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \ - GIT_COMMITTER_EMAIL="root@localhost" \ - GIT_COMMITTER_DATE="1356994801 -0400" \ - git commit-tree "$@" </dev/null >&2 || : - git rev-parse -q --verify "$Gref" >/dev/null && return 0 || - commit_id=$(anon_commit "$empty_tree") && - git update-ref "$Gref" "$commit_id" -} -## end gitception - -# Fetch repo $1, file $2, tmpfile in $3 -GET() -{ - if isurl sftp "$1" - then - (exec 0>&-; curl -s -S -k "$1/$2") > "$3" - elif isurl rsync "$1" - then - (exec 0>&-; rsync -I -W "${1#rsync://}"/"$2" "$3" >&2) - elif islocalrepo "$1" - then - cat "$1/$2" > "$3" - else - gitception_get "${1#gitception://}" "$2" > "$3" - fi -} - -# Put repo $1, file $2 or fail, tmpfile in $3 -PUT() -{ - if isurl sftp "$1" - then - curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2" - elif isurl rsync "$1" - then - rsync -I -W "$3" "${1#rsync://}"/"$2" >&2 - elif islocalrepo "$1" - then - cat >| "$1/$2" < "$3" - else - gitception_put "${1#gitception://}" "$2" < "$3" - fi -} - -# Put all PUT changes for repo $1 at once -PUT_FINAL() -{ - if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1" - then - : - else - git push --quiet -f "${1#gitception://}" "$Gref:$Gref_rbranch" - fi -} - -# Put directory for repo $1 -PUTREPO() -{ - if isurl sftp "$1" - then - : - elif isurl rsync "$1" - then - rsync -q -r --exclude='*' "$Localdir/" "${1#rsync://}" >&2 - elif islocalrepo "$1" - then - mkdir -p "$1" - else - gitception_new_repo "${1#gitception://}" - fi -} - -# For repo $1, delete all newline-separated files in $2 -REMOVE() -{ - local fn_= - if isurl sftp "$1" - then - # FIXME - echo_info "sftp: Ignore remove request $1/$2" - elif isurl rsync "$1" - then - xfeed "$2" rsync -I -W -v -r --delete --include-from=- \ - --exclude='*' "$Localdir"/ "${1#rsync://}/" >&2 - elif islocalrepo "$1" - then - for fn_ in $2; do - rm -f "$1"/"$fn_" - done - else - for fn_ in $2; do - gitception_remove "${1#gitception://}" "$fn_" - done - fi -} - -CLEAN_FINAL() -{ - if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1" - then - : - else - git update-ref -d "$Gref" || : - fi -} - -ENCRYPT() -{ - rungpg --batch --force-mdc --compress-algo none --trust-model=always --passphrase-fd 3 -c 3<&1 && - status_=$(rungpg --status-fd 3 -q -d 3>&1 1>&4) && - xfeed "$status_" grep "^\[GNUPG:\] ENC_TO " >/dev/null && - (xfeed "$status_" grep -e "$1" >/dev/null || { - echo_info "Failed to verify manifest signature!" && - echo_info "Only accepting signatories: ${2:-(none)}" && - return 1 - }) -} - -# Generate $1 random bytes -genkey() -{ - rungpg --armor --gen-rand 1 "$1" -} - -gpg_hash() -{ - local hash_= - hash_=$(rungpg --with-colons --print-md "$1" | tr A-F a-f) - hash_=${hash_#:*:} - xecho "${hash_%:}" -} - -rungpg() -{ - if isnonnull "$Conf_gpg_args"; then - set -- "$Conf_gpg_args" "$@" - fi - # gpg will fail to run when there is no controlling tty, - # due to trying to print messages to it, even if a gpg agent is set - # up. --no-tty fixes this. - if [ "x$GPG_AGENT_INFO" != "x" ]; then - ${GPG} --no-tty $@ - else - ${GPG} $@ - fi -} - -# Pass the branch/ref by pipe to git -safe_git_rev_parse() -{ - git cat-file --batch-check 2>/dev/null | - xgrep -v "missing" | cut -f 1 -d ' ' -} - -make_new_repo() -{ - echo_info "Setting up new repository" - PUTREPO "$URL" - - # Needed assumption: the same user should have no duplicate Repoid - Repoid=":id:$(genkey 15)" - iseq "${NAME#gcrypt::}" "$URL" || - git config "remote.$NAME.gcrypt-id" "$Repoid" - echo_info "Remote ID is $Repoid" - Extnlist="extn comment" -} - - -# $1 return var for goodsig match, $2 return var for signers text -read_config() -{ - local recp_= r_tail= r_keyinfo= r_keyfpr= gpg_list= cap_= conf_part= good_sig= signers_= - Conf_signkey=$(git config --get "remote.$NAME.gcrypt-signingkey" '.+' || - git config --path user.signingkey || :) - conf_part=$(git config --get "remote.$NAME.gcrypt-participants" '.+' || - git config --get gcrypt.participants '.+' || :) - Conf_pubish_participants=$(git config --get --bool "remote.$NAME.gcrypt-publish-participants" '.+' || - git config --get --bool gcrypt.publish-participants || :) - Conf_gpg_args=$(git config --get gcrypt.gpg-args '.+' || :) - - # Figure out which keys we should encrypt to or accept signatures from - if isnull "$conf_part" || iseq "$conf_part" simple - then - signers_="(default keyring)" - Recipients="--throw-keyids --default-recipient-self" - good_sig="^\[GNUPG:\] GOODSIG " - setvar "$1" "$good_sig" - setvar "$2" "$signers_" - return 0 - fi - - for recp_ in $conf_part - do - gpg_list=$(rungpg --with-colons --fingerprint -k "$recp_") - r_tail_=$(echo "$recp_" | sed -e 's/^0x//') - filter_to @r_keyinfo "pub*" "$gpg_list" - if echo "$recp_" | grep -E -q '^[xA-F0-9]+$'; then # is $recp_ a keyid? - filter_to @r_keyfpr "fpr*$r_tail_*" "$gpg_list" - else - filter_to @r_keyfpr "fpr*" "$gpg_list" - fi - isnull "$r_keyinfo" || isnonnull "${r_keyinfo##*"$Newline"*}" || - echo_info "WARNING: '$recp_' matches multiple keys, using one" - isnull "$r_keyfpr" || isnonnull "${r_keyfpr##*"$Newline"*}" || - echo_info "WARNING: '$recp_' matches multiple fingerprints, using one" - r_keyinfo=${r_keyinfo%%"$Newline"*} - r_keyfpr=${r_keyfpr%%"$Newline"*} - keyid_=$(xfeed "$r_keyinfo" cut -f 5 -d :) - fprid_=$(xfeed "$r_keyfpr" cut -f 10 -d :) - - isnonnull "$fprid_" && - signers_="$signers_ $keyid_" && - append_to @good_sig "^\[GNUPG:\] VALIDSIG .*$fprid_$" || { - echo_info "WARNING: Skipping missing key $recp_" - continue - } - # Check 'E'ncrypt capability - cap_=$(xfeed "$r_keyinfo" cut -f 12 -d :) - if ! iseq "${cap_#*E}" "$cap_"; then - if [ "$Conf_pubish_participants" = true ]; then - Recipients="$Recipients -r $keyid_" - else - Recipients="$Recipients -R $keyid_" - fi - fi - done - - if isnull "$Recipients" - then - echo_info "You have not configured any keys you can encrypt to" \ - "for this repository" - echo_info "Use ::" - echo_info " git config gcrypt.participants YOURKEYID" - exit 1 - fi - setvar "$1" "$good_sig" - setvar "$2" "$signers_" -} - -ensure_connected() -{ - local manifest_= r_repoid= r_name= url_frag= r_sigmatch= r_signers= \ - tmp_manifest= - - if isnonnull "$Did_find_repo" - then - return - fi - Did_find_repo=no - read_config @r_sigmatch @r_signers - - iseq "${NAME#gcrypt::}" "$URL" || r_name=$NAME - - if isurl gitception "$URL" && isnonnull "$r_name"; then - git config "remote.$r_name.url" "gcrypt::${URL#gitception://}" - echo_info "Updated URL for $r_name, gitception:// -> ()" - fi - - # Find the URL fragment - url_frag=${URL##*"#"} - isnoteq "$url_frag" "$URL" || url_frag= - URL=${URL%"#$url_frag"} - - # manifestfile -- sha224 hash if we can, else the default location - if isurl sftp "$URL" || islocalrepo "$URL" || isurl rsync "$URL" - then - # not for gitception - isnull "$url_frag" || - Manifestfile=$(xecho_n "$url_frag" | gpg_hash SHA224) - else - isnull "$url_frag" || Gref_rbranch="refs/heads/$url_frag" - fi - - Repoid= - isnull "$r_name" || - Repoid=$(git config "remote.$r_name.gcrypt-id" || :) - - - tmp_manifest="$Tempdir/maniF" - GET "$URL" "$Manifestfile" "$tmp_manifest" 2>/dev/null || { - echo_info "Repository not found: $URL" - if ! isnull "$Repoid"; then - echo_info "..but repository ID is set. Aborting." - return 1 - else - return 0 - fi - } - - Did_find_repo=yes - echo_info "Decrypting manifest" - manifest_=$(PRIVDECRYPT "$r_sigmatch" "$r_signers" < "$tmp_manifest") && - isnonnull "$manifest_" || - echo_die "Failed to decrypt manifest!" - rm -f "$tmp_manifest" - - filter_to @Refslist "$Hex40 *" "$manifest_" - filter_to @Packlist "pack :*:* *" "$manifest_" - filter_to @Keeplist "keep :*:*" "$manifest_" - filter_to @Extnlist "extn *" "$manifest_" - filter_to @r_repoid "repo *" "$manifest_" - - r_repoid=${r_repoid#repo } - r_repoid=${r_repoid% *} - if isnull "$Repoid" - then - echo_info "Remote ID is $r_repoid" - Repoid=$r_repoid - elif isnoteq "$r_repoid" "$Repoid" - then - echo_info "WARNING:" - echo_info "WARNING: Remote ID has changed!" - echo_info "WARNING: from $Repoid" - echo_info "WARNING: to $r_repoid" - echo_info "WARNING:" - Repoid=$r_repoid - else - return 0 - fi - - isnull "$r_name" || git config "remote.$r_name.gcrypt-id" "$r_repoid" -} - -# $1 is the hash type (SHA256 etc) -# $2 the pack id -# $3 the key -get_verify_decrypt_pack() -{ - local rcv_id= tmp_encrypted= - tmp_encrypted="$Tempdir/packF" - GET "$URL" "$2" "$tmp_encrypted" && - rcv_id=$(gpg_hash "$1" < "$tmp_encrypted") && - iseq "$rcv_id" "$2" || echo_die "Packfile $2 does not match digest!" - DECRYPT "$3" < "$tmp_encrypted" - rm -f "$tmp_encrypted" -} - -# download all packlines (pack :SHA256:a32abc1231) from stdin (or die) -# $1 destdir (when repack, else "") -get_pack_files() -{ - local pack_id= r_pack_key_line= htype_= pack_= key_= - while IFS=': ' read -r _ htype_ pack_ # </dev/null - xecho "pack $pack_id" >> "$Localdir/have_packs$GITCEPTION" - else - git index-pack -v --stdin "$1/${pack_}.pack" >/dev/null - fi - done -} - -# Download and unpack remote packfiles -# $1 return var for list of packfiles to delete -repack_if_needed() -{ - local n_= m_= kline_= r_line= r_keep_packlist= r_del_list= - - isnonnull "$Packlist" || return 0 - - if isnonnull "${GCRYPT_FULL_REPACK:-}" - then - Keeplist= - Repack_limit=0 - fi - - pick_fields_1_2 @r_del_list "$Packlist" - - n_=$(line_count "$Packlist") - m_=$(line_count "$Keeplist") - if iseq 0 "$(( $Repack_limit < ($n_ - $m_) ))"; then - return - fi - echo_info "Repacking remote $NAME, ..." - - mkdir "$Tempdir/pack" - - # Split packages to keep and to repack - if isnonnull "$Keeplist"; then - while read -r _ kline_ _ # < (if sha-1 exists locally) - r_revlist=$(xfeed "$Refslist" cut -f 1 -d ' ' | - safe_git_rev_parse | sed -e 's/^\(.\)/^&/') - fi - - while IFS=: read -r src_ dst_ # << +src:dst - do - src_=${src_#+} - filter_to ! @Refslist "$Hex40 $dst_" "$Refslist" - - if isnonnull "$src_" - then - append_to @r_revlist "$src_" - obj_=$(xfeed "$src_" safe_git_rev_parse) - append_to @Refslist "$obj_ $dst_" - fi - done < "$tmp_objlist" - - # Only send pack if we have any objects to send - if [ -s "$tmp_objlist" ] - then - key_=$(genkey "$Packkey_bytes") - pack_id=$(export GIT_ALTERNATE_OBJECT_DIRECTORIES=$Tempdir; - pipefail git pack-objects --stdout < "$tmp_objlist" | - pipefail ENCRYPT "$key_" | - tee "$tmp_encrypted" | gpg_hash "$Hashtype") - - append_to @Packlist "pack :${Hashtype}:$pack_id $key_" - if isnonnull "$r_pack_delete" - then - append_to @Keeplist "keep :${Hashtype}:$pack_id 1" - fi - fi - - # Generate manifest - echo_info "Encrypting to: $Recipients" - echo_info "Requesting manifest signature" - - tmp_manifest="$Tempdir/maniP" - PRIVENCRYPT "$Recipients" > "$tmp_manifest" <&2 -} - -setup() -{ - mkdir -p "$Localdir" - - # Set up a subdirectory in /tmp - temp_key=$(genkey 9 | tr '/' _) - Tempdir="${TMPDIR:-/tmp}/git-remote-gcrypt-${temp_key}.$$" - case "${MSYSTEM:-unknown}" in - MSYS*|MINGW*) - mkdir "${Tempdir}" - echo_info "Warning: Not securing tempdir ${Tempdir} because we are on mingw/msys" - ;; - unknown|*) - mkdir -m 700 "${Tempdir}" - ;; - esac - - trap cleanup_tmpfiles EXIT - trap 'exit 1' 1 2 3 15 -} - -# handle git-remote-helpers protocol -gcrypt_main_loop() -{ - local input_= input_inner= r_args= temp_key= - - NAME=$1 # Remote name - URL=$2 # Remote URL - - setup - - while read input_ - do - case "$input_" in - capabilities) - do_capabilities - ;; - list|list\ for-push) - do_list - ;; - fetch\ *) - r_args=${input_##fetch } - while read input_inner - do - case "$input_inner" in - fetch*) - r_args= #ignored - ;; - *) - break - ;; - esac - done - do_fetch "$r_args" - ;; - push\ *) - r_args=${input_##push } - while read input_inner - do - case "$input_inner" in - push\ *) - append_to @r_args "${input_inner#push }" - ;; - *) - break - ;; - esac - done - do_push "$r_args" - ;; - ?*) - echo_die "Unknown input!" - ;; - *) - CLEAN_FINAL "$URL" - exit 0 - ;; - esac - done -} - -if [ "x$1" = x--check ] -then - NAME=dummy-gcrypt-check - URL=$2 - setup - ensure_connected - git remote remove $NAME 2>/dev/null || true - if iseq "$Did_find_repo" "no" - then - exit 100 - fi -else - gcrypt_main_loop "$@" -fi diff --git a/bin/go-playground b/bin/go-playground index 37675d0..64633a9 100755 --- a/bin/go-playground +++ b/bin/go-playground @@ -1,5 +1,5 @@ #!/bin/sh cd "$(mktemp -d)"; go mod init local-playground; -echo -e 'package main\n\nimport (\n\t"fmt"\n)\n\nfunc main() {\n\tfmt.Println("aloha")\n}\n' > main.go; +echo 'package main\n\nimport (\n\t"fmt"\n)\n\nfunc main() {\n\tfmt.Println("aloha")\n}\n' > main.go; $EDITOR main.go; diff --git a/bin/quick-reboot b/bin/quick-reboot new file mode 100755 index 0000000..9f7b751 --- /dev/null +++ b/bin/quick-reboot @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +# This assumes that /proc/cmdline contains a cryptdevice with a UUID identifier, +# like: +# +# cryptdevice=UUID=1ff1d6f7-7540-4500-8011-1abe1e9ac00d:cryptroot +uuid=$(cat /proc/cmdline | \ + tr ' ' '\n' | \ + grep cryptdevice | \ + cut -d= -f3 | \ + cut -d: -f1) + +device=$(lsblk -o PATH,UUID | grep "$uuid" | awk '{print $1}') +echo "Root device is $device" + +echo -n "Enter root key: " +read -s pw +echo "" + +# This will check if the key is right, and cause the process to exit if not due +# to the "set -e" +echo "Checking key..." +echo "$pw" | sudo cryptsetup open --test-passphrase "$device" + +echo "Good job, writing /boot/keyfile..." +echo -n "$pw" | sudo tee /boot/keyfile >/dev/null + +echo "Rebooting..." +sudo systemctl reboot diff --git a/bin/quick-shutdown b/bin/quick-shutdown new file mode 100755 index 0000000..f5a5259 --- /dev/null +++ b/bin/quick-shutdown @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +# This assumes that /proc/cmdline contains a cryptdevice with a UUID identifier, +# like: +# +# cryptdevice=UUID=1ff1d6f7-7540-4500-8011-1abe1e9ac00d:cryptroot +uuid=$(cat /proc/cmdline | \ + tr ' ' '\n' | \ + grep cryptdevice | \ + cut -d= -f3 | \ + cut -d: -f1) + +device=$(lsblk -o PATH,UUID | grep "$uuid" | awk '{print $1}') +echo "Root device is $device" + +echo -n "Enter root key: " +read -s pw +echo "" + +# This will check if the key is right, and cause the process to exit if not due +# to the "set -e" +echo "Checking key..." +echo "$pw" | sudo cryptsetup open --test-passphrase "$device" + +echo "Good job, writing /boot/keyfile..." +echo -n "$pw" | sudo tee /boot/keyfile >/dev/null + +echo "Shutting down..." +sudo systemctl poweroff diff --git a/bin/rotcheck b/bin/rotcheck new file mode 100755 index 0000000..c8a59fe --- /dev/null +++ b/bin/rotcheck @@ -0,0 +1,353 @@ +#!/bin/sh +set -uf +IFS="$(printf '\n\t')" +LC_ALL="C" + +# Copyright (C) 2019 Jamie Nguyen +# +# A simple shell script to recursively generate, update and verify checksums +# for files you care about. It's useful for detecting bit rot. +# +# It's written in POSIX shell, but requires GNU coreutils, BusyBox or some +# other collection that includes similar checksum tools. + +VERSION=1.1.2 +COMMAND="sha512sum" +CHECKFILE="./.rotcheck" + +APPEND_MODE=0 +CHECK_MODE=0 +DELETE_MODE=0 +UPDATE_MODE=0 + +IGNORE_MISSING=0 +FOLLOW_SYMLINKS=1 +VERBOSE=0 +WARN_FORMATTING=0 +EXCLUDE_HIDDEN=0 +FORCE_UPDATE=0 + +usage() { + cat << EOF +rotcheck $VERSION +Usage: rotcheck MODE [OPTIONS] + or: rotcheck MODE [OPTIONS] -- [DIRECTORY]... [ARBITRARY FIND OPTION]... +Recursively generate, update and verify checksums. + +MODES: + -a APPEND mode: Record checksums for any files without a checksum + already. Never modify existing checksums. + -c CHECK mode: Check that files checksums are the same. + -d DELETE mode: Remove checksums for files that don't exist. + -u APPEND-AND-UPDATE mode: Like append-only mode, but also update + checksums for files with a modification date newer than the + the checksum file. (NB: Also see \`-M\`.) + +OPTIONS: + -b COMMAND Checksum command to use. Default: sha512sum + -f FILE File to store checksums. For relative paths, prefix with "./" + or the checksum file will be checksummed. Default: ./.rotcheck + -h Display this help. + -n Don't follow symlinks. The default is to follow symlinks. + -v Be more verbose when adding, deleting, changing or verifying + checksums. + -w Warn about improperly formatted checksum lines. + -x Exclude all hidden files and directories when generating + checksums. The default is to include them. + -M Use with \`-u\` to update checksums regardless of modification + time. This is very slow so avoid if possible; try \`touch\` + instead to bump the modification time of specific files. + WARNING: The checksums might have changed due to bit rot so + use this option with care! + + (specific to GNU coreutils >= 8.25) + -i Ignore missing files when verifying checksums. + + +Supported commands: + GNU coreutils: + md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, b2sum + + BusyBox (applets must be symlinked): + md5sum, sha1sum, sha256sum, sha512sum, sha3sum + + BSD & macOS (install GNU coreutils): + gmd5sum, gsha1sum, gsha224sum, gsha256sum, gsha384sum, gsha512sum, gb2sum + + +Examples: + # Create checksum file (located at "./.rotcheck"): + rotcheck -a + + # You've added some new files and need to append some checksums: + rotcheck -va + + # You've edited some files and need to update the checksums (for files with + # a modification time newer than the checksum file): + rotcheck -vu + + # Verify checksums: + rotcheck -c + + # Search other directories instead of the current directory. + # WARNING: checksums might get duplicated if mixing relative and absolute + # paths, or if you change the way you specify directory paths! + rotcheck -a -- /mnt/archive-2018/ /mnt/archive-2019/ + + # Exclude .git folders (these arguments are passed directly to find): + rotcheck -a -- ! -path '*/\\.git/*' + +EOF + exit 0 +} + +fail() { + printf '%s\n' "$@"; exit 1 +} + +# Curiously, I stumbled across a bug in bash-3.0.16 (c. 2004) or older +# where \0177 (DEL) isn't handled properly. See the `find_safe` function below. +# bash-3.1 (c. 2005), dash-0.5.2 (c. 2005), and zsh-3.1 (c. 2000) all work +# and probably others too. +if [ -n ${BASH+x} ] && [ -n ${BASH_VERSION+x} ]; then + if printf '%s' "${BASH_VERSION:-x}" | grep -qE '^[0-2]+|^3\.0'; then + fail "bash-3.0.16 and older are broken." \ + "Try bash>=3.1, dash, zsh, or another POSIX shell." + fi +fi + +# Command-line arguments. `getopts` is POSIX, while `getopt` is not. +[ $# -gt 0 ] && [ "$1" = "--help" ] && usage +while getopts ":acdub:f:hinvwxM" opt; do + case "$opt" in + a) APPEND_MODE=1;; + c) CHECK_MODE=1;; + d) DELETE_MODE=1;; + u) UPDATE_MODE=1;; + b) COMMAND="$OPTARG";; + f) CHECKFILE="$OPTARG";; + h) usage;; + i) IGNORE_MISSING=1;; + n) FOLLOW_SYMLINKS=0;; + v) VERBOSE=1;; + w) WARN_FORMATTING=1;; + x) EXCLUDE_HIDDEN=1;; + M) FORCE_UPDATE=1;; + \?) fail "-$OPTARG: Invalid argument";; + :) fail "-$OPTARG requires an argument";; + esac +done; shift $(($OPTIND - 1)) + + + +# A few sanity checks. +MODE=$(($APPEND_MODE + $CHECK_MODE + $DELETE_MODE + $UPDATE_MODE)) +if [ $MODE -eq 0 ]; then + fail "Please specify one of -a, -c, -d, or -u." \ + "See \`rotcheck -h\` for help with usage." +elif [ $MODE -gt 1 ]; then + fail "You can only use one of -a, -c, -d, or -u options." \ + "See \`rotcheck -h\` for help with usage." +elif [ $CHECK_MODE -eq 1 ] || [ $DELETE_MODE -eq 1 ]; then + if [ ! -f "$CHECKFILE" ]; then + fail "$CHECKFILE: No such file." \ + "Try running \`rotcheck -a\` first, or see \`rotcheck -h\`." + fi +elif ! command -v "$COMMAND" >/dev/null 2>/dev/null; then + fail "$COMMAND: command not found" \ + "Try specifying a supported command using \`rotcheck -b COMMAND\`." \ + "You may need to install GNU coreutils or BusyBox." \ + "On *BSD, GNU coreutils commands begin with 'g', like 'gsha512sum'." \ + "See \`rotcheck -h\` for help with usage." +fi + +# When printing text to terminal, make sure it won't do anything unexpected. +printf_sanitized() { + printf '%s' "$@" | tr -d '[:cntrl:]' | iconv -cs -f UTF-8 -t UTF-8 + printf '\n' +} + +verify_checksums() { + IGNORE="" ; [ $IGNORE_MISSING -eq 1 ] && IGNORE="--ignore-missing" + WARN="" ; [ $WARN_FORMATTING -eq 1 ] && WARN="-w" + $COMMAND -c $WARN $IGNORE -- "$CHECKFILE" +} + +# Just verify checksums. +if [ $CHECK_MODE -eq 1 ]; then + # Only GNU coreutils supports `--quiet`, so use `grep -v` instead. + # Unfortunately, pipefail isn't POSIX so to return the exit status from the + # checksum command, we have to be clever (aka crazy) with file descriptors + # and subshells instead. + if [ $VERBOSE -eq 1 ]; then + verify_checksums + exit $? + else + exec 4>&1 + ( + exec 3>&1 + ( + # 2>&1 preserves order of stdout/stderr. + verify_checksums 2>&1; printf '%d' $? 1>&3 + ) | grep -Ev ': OK$' 1>&4 + exec 3>&- + ) | ( read -r retval; exit $retval ); retval=$? + exec 4>&- + exit $retval + fi +fi + +# Delete checksums for files that no longer exist. +if [ $DELETE_MODE -eq 1 ]; then + i=1 + for file in $(cut -d ' ' -f 3- -- "$CHECKFILE"); do + # `sed -i` isn't POSIX (nor is `mktemp`), so use `ex` instead. + if [ ! -f "$file" ]; then + cat << EOF | ex -s -- "$CHECKFILE" +${i}d +x +EOF + # Print what checksums were deleted. + if [ $VERBOSE -eq 1 ]; then + printf '%s' "DELETED: " + printf_sanitized "$file" + fi + else + # Only increment the line number if we didn't delete a line. + i=$(($i + 1)) + fi + done + exit $? +fi + +# For safety and sanity, ignore all filenames that have control characters +# like newline, tab, delete etc. +find_safe() { + FIND_L="" + FIND_FOLLOW="" + if [ $FOLLOW_SYMLINKS -eq 1 ]; then + # Old versions of findutils don't have -L. Use it if available. + if find -L / -maxdepth 0 -type d >/dev/null 2>/dev/null; then + FIND_L="-L" + else + FIND_FOLLOW="-follow" + fi + fi + + # POSIX find requires that you specify the search path either first + # or immediately after -H/-L. Use current directory by default unless + # user has specified a path. + FIND_DOT="./" + if [ $# -gt 0 ]; then + first_char="$(printf '%s' "$1" | cut -c 1)" + # Replace search path unless first arg is a non-path `find` option. + if [ "$first_char" != "-" ] \ + && [ "$first_char" != "!" ] && [ "$first_char" != "(" ]; then + FIND_DOT="" + fi + fi + + HIDDEN="" + [ $EXCLUDE_HIDDEN -eq 1 ] && HIDDEN='*/\.*' + + find $FIND_L $FIND_DOT "$@" $FIND_FOLLOW \ + -type f ! -path "$CHECKFILE" ! -path "$HIDDEN" \ + ! -name "$(printf '*%b*' '\0001')" ! -name "$(printf '*%b*' '\0002')" \ + ! -name "$(printf '*%b*' '\0003')" ! -name "$(printf '*%b*' '\0004')" \ + ! -name "$(printf '*%b*' '\0005')" ! -name "$(printf '*%b*' '\0006')" \ + ! -name "$(printf '*%b*' '\0007')" ! -name "$(printf '*%b*' '\0010')" \ + ! -name "$(printf '*%b*' '\0011')" ! -name "$(printf '*%b*' '\0012')" \ + ! -name "$(printf '*%b*' '\0013')" ! -name "$(printf '*%b*' '\0014')" \ + ! -name "$(printf '*%b*' '\0015')" ! -name "$(printf '*%b*' '\0016')" \ + ! -name "$(printf '*%b*' '\0017')" ! -name "$(printf '*%b*' '\0020')" \ + ! -name "$(printf '*%b*' '\0021')" ! -name "$(printf '*%b*' '\0022')" \ + ! -name "$(printf '*%b*' '\0023')" ! -name "$(printf '*%b*' '\0024')" \ + ! -name "$(printf '*%b*' '\0025')" ! -name "$(printf '*%b*' '\0026')" \ + ! -name "$(printf '*%b*' '\0027')" ! -name "$(printf '*%b*' '\0030')" \ + ! -name "$(printf '*%b*' '\0031')" ! -name "$(printf '*%b*' '\0032')" \ + ! -name "$(printf '*%b*' '\0033')" ! -name "$(printf '*%b*' '\0034')" \ + ! -name "$(printf '*%b*' '\0035')" ! -name "$(printf '*%b*' '\0036')" \ + ! -name "$(printf '*%b*' '\0037')" ! -name "$(printf '*%b*' '\0177')" +} + +find_updated_files() { + if [ $FORCE_UPDATE -eq 1 ]; then + find_safe "$@" + else + find_safe "$@" -newer "$CHECKFILE" + fi +} + +# This function could be replaced entirely with the much simpler: +# cut -d ' ' -f 3- "$CHECKFILE" | grep -Fxn -- "$file" | cut -d ':' -f 1 +# But this function is slightly faster as it avoids passing huge chunks of text +# (ie, the whole checksum file minus the first column) through a pipe. +get_line_number() { + # Avoid `grep -E` as filename characters might get interpreted (eg, $). + for l in $(grep -Fn -- "$file" "$CHECKFILE" | cut -d ':' -f 1); do + if sed -n -e "${l}p" -- "$CHECKFILE" \ + | cut -d ' ' -f 3- | grep -Fxq -- "$file" >/dev/null; then + printf '%d' "$l" + return 0 + fi + done + printf '%d' "0" +} + +umask 077 +# For files with a modification date newer than the checksum file, if there's +# an existing checksum then update it. Otherwise append a new checksum. +if [ $UPDATE_MODE -eq 1 ] && [ -f "$CHECKFILE" ]; then + for file in $(find_updated_files "$@"); do + line_num="$(get_line_number)" + if [ ${line_num:-0} -eq 0 ]; then + # No checksum yet, so append one. + $COMMAND -- "$file" >> "$CHECKFILE" + else + old="$(sed -n -e "${line_num}p" -- "$CHECKFILE" | cut -d ' ' -f 1)" + new="$($COMMAND -- "$file")" + # Should never happen, but double check these aren't empty: + if [ -z ${old:+x} ] || [ -z ${new:+x} ]; then + continue + fi + # `sed -i` isn't POSIX (nor is `mktemp`), so use `ex` instead. + if [ "$old" != "${new%% *}" ]; then + cat << EOF | ex -s -- "$CHECKFILE" +${line_num}c +$new +. +x +EOF + # Bail immediately if something went wrong. + [ $? -ne 0 ] && fail "Failed to update checksum file." + + # Print what checksums were changed. + if [ $VERBOSE -eq 1 ]; then + printf '%s' "CHANGED: " + printf_sanitized "$file" + fi + fi + fi + done +fi + +# Append checksums for files that have no checksum yet. +if [ $APPEND_MODE -eq 1 ] || [ $UPDATE_MODE -eq 1 ]; then + for file in $(find_safe "$@"); do + # Avoid `grep -E` as filename characters might get interpreted (eg, $). + # The first grep isn't strictly needed, but grep+cut+grep is faster + # than just cut+grep here. + if [ ! -f "$CHECKFILE" ] || ! grep -- "$file" "$CHECKFILE" \ + | cut -d ' ' -f 3- | grep -Fxq -- "$file"; then + if ! $COMMAND -- "$file" >> "$CHECKFILE"; then + fail "Failed to write to checksum file." + fi + + # Print what checksums were appended. + if [ $VERBOSE -eq 1 ]; then + printf '%s' "ADDED: " + printf_sanitized "$file" + fi + fi + done +fi -- cgit v1.2.3