#!/bin/sh # git-remote-gcrypt # # Copyright (c) 2013 engla # Copyright (c) 2013, 2014 Joey Hess # Copyright (c) 2016, 2018 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 isurl rclone "$1" then (exec 0>&-; rclone copyto "${1#rclone://}"/"$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 $Conf_rsync_put_flags -I -W "$3" "${1#rsync://}"/"$2" >&2 elif isurl rclone "$1" then rclone copyto "$3" "${1#rclone://}"/"$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" || isurl rclone "$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 $Conf_rsync_put_flags -q -r --exclude='*' \ "$Localdir/" "${1#rsync://}" >&2 elif isurl rclone "$1" then rclone mkdir "${1#rclone://}" >&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 isurl rclone "$1" then xfeed "$2" rclone delete -v --include-from=/dev/stdin "${1#rclone://}/" >&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" || isurl rclone "$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 '.+' || :) Conf_rsync_put_flags=$(git config --get "remote.$NAME.gcrypt-rsync-put-flags" '.+' || git config --get "gcrypt.rsync-put-flags" '.+' || :) Conf_force_required=$(git config --get --bool "remote.$NAME.gcrypt-require-explicit-force-push" '.+' || git config --get --bool gcrypt.require-explicit-force-push '.+' || :) # 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= tmp_stderr= 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" || isurl rclone "$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" tmp_stderr="$Tempdir/stderr" GET "$URL" "$Manifestfile" "$tmp_manifest" 2>| "$tmp_stderr" || { if ! isnull "$Repoid"; then cat >&2 "$tmp_stderr" echo_info "Repository not found: $URL" echo_info "..but repository ID is set. Aborting." return 1 else echo_info "Repository not found: $URL" 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 if [ $(echo "$src_" | cut -c1) != + ] then force_passed=false fi 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 if isurl rclone "$URL"; then echo_info "WARNING: rclone support is experimental." echo_info "WARNING: Early adoptors only. Keep backups." fi } # 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