From 07ab3a77b44944a1ac234fbd717391f36370416e Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Tue, 17 Oct 2017 18:12:57 -0600 Subject: initial public commit --- bin/git-remote-gcrypt | 921 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 921 insertions(+) create mode 100755 bin/git-remote-gcrypt (limited to 'bin/git-remote-gcrypt') diff --git a/bin/git-remote-gcrypt b/bin/git-remote-gcrypt new file mode 100755 index 0000000..8b66f2f --- /dev/null +++ b/bin/git-remote-gcrypt @@ -0,0 +1,921 @@ +#!/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 -- cgit v1.2.3