1#!/usr/bin/env bash
2
3# shellcheck disable=2034
4GITSECRET_VERSION='0.2.5'
5#!/usr/bin/env bash
6
7# Folders:
8_SECRETS_DIR=${SECRETS_DIR:-".gitsecret"}
9# if SECRETS_DIR env var is set, use that instead of .gitsecret
10# for full path to secrets dir, use _get_secrets_dir() from _git_secret_tools.sh
11_SECRETS_DIR_KEYS="${_SECRETS_DIR}/keys"
12_SECRETS_DIR_PATHS="${_SECRETS_DIR}/paths"
13
14# Files:
15_SECRETS_DIR_KEYS_MAPPING="${_SECRETS_DIR_KEYS}/mapping.cfg"
16_SECRETS_DIR_KEYS_TRUSTDB="${_SECRETS_DIR_KEYS}/trustdb.gpg"
17
18_SECRETS_DIR_PATHS_MAPPING="${_SECRETS_DIR_PATHS}/mapping.cfg"
19
20: "${SECRETS_EXTENSION:=".secret"}"
21
22# Commands:
23: "${SECRETS_GPG_COMMAND:="gpg"}"
24: "${SECRETS_CHECKSUM_COMMAND:="_os_based __sha256"}"
25: "${SECRETS_OCTAL_PERMS_COMMAND:="_os_based __get_octal_perms"}"
26: "${SECRETS_EPOCH_TO_DATE:="_os_based __epoch_to_date"}"
27
28
29# AWK scripts:
30# shellcheck disable=2016
31AWK_FSDB_HAS_RECORD='
32BEGIN { FS=":"; OFS=":"; cnt=0; }
33{
34  if ( key == $1 )
35  {
36    cnt++
37  }
38}
39END { if ( cnt > 0 ) print "0"; else print "1"; }
40'
41
42# shellcheck disable=2016
43AWK_FSDB_RM_RECORD='
44BEGIN { FS=":"; OFS=":"; }
45{
46  if ( key != $1 )
47  {
48    print $1,$2;
49  }
50}
51'
52
53# shellcheck disable=2016
54AWK_FSDB_CLEAR_HASHES='
55BEGIN { FS=":"; OFS=":"; }
56{
57  print $1,"";
58}
59'
60
61# shellcheck disable=2016
62AWK_GPG_VER_CHECK='
63/^gpg/{
64  version=$3
65  n=split(version,array,".")
66  if( n >= 2) {
67    if(array[1] >= 2)
68    {
69      if(array[2] >= 1)
70      {
71        print 1
72      }
73      else
74      {
75        print 0
76      }
77    }
78    else
79    {
80      print 0
81    }
82  }
83  else if(array[1] >= 2)
84  {
85    print 1
86  }
87  else
88  {
89    print 0
90  }
91}
92'
93
94# This is 1 for gpg version 2.1 or greater, otherwise 0
95GPG_VER_21="$(gpg --version | gawk "$AWK_GPG_VER_CHECK")"
96
97
98# Bash:
99
100function _function_exists {
101  local function_name="$1" # required
102
103  declare -f -F "$function_name" > /dev/null 2>&1
104  echo $?
105}
106
107
108# OS based:
109
110function _os_based {
111  # Pass function name as first parameter.
112  # It will be invoked as os-based function with the postfix.
113
114  case "$(uname -s)" in
115
116    Darwin)
117      "$1_osx" "${@:2}"
118    ;;
119
120    Linux)
121      "$1_linux" "${@:2}"
122    ;;
123
124    MINGW*)
125      "$1_linux" "${@:2}"
126    ;;
127
128    FreeBSD)
129      "$1_freebsd" "${@:2}"
130    ;;
131
132    # TODO: add MS Windows support.
133    # CYGWIN*|MINGW32*|MSYS*)
134    #   $1_ms ${@:2}
135    # ;;
136
137    *)
138      _abort 'unsupported OS.'
139    ;;
140  esac
141}
142
143
144# File System:
145
146function _set_config {
147  # This function creates a line in the config, or alters it.
148
149  local key="$1" # required
150  local value="$2" # required
151  local filename="$3" # required
152
153  # The exit status is 0 (true) if the name was found, 1 (false) if not:
154  local contains
155  contains=$(grep -Fq "$key" "$filename"; echo "$?")
156
157  # Append or alter?
158  if [[ "$contains" -eq 0 ]]; then
159    _os_based __replace_in_file "$@"
160  elif [[ "$contains" -eq 1 ]]; then
161    echo "${key} = ${value}" >> "$filename"
162  fi
163}
164
165
166function _file_has_line {
167  # First parameter is the key, second is the filename.
168
169  local key="$1" # required
170  local filename="$2" # required
171
172  local contains
173  contains=$(grep -qw "$key" "$filename"; echo $?)
174
175  # 0 on contains, 1 for error.
176  echo "$contains"
177}
178
179
180function _delete_line {
181  local escaped_path
182  # shellcheck disable=2001
183  escaped_path=$(echo "$1" | sed -e 's/[\/&]/\\&/g') # required
184
185  local line="$2" # required
186
187  sed -i.bak "/$escaped_path/d" "$line"
188}
189
190
191# this sets the global variable 'filename'
192# currently this function is only used by 'hide'
193function _temporary_file {
194  # This function creates temporary file
195  # which will be removed on system exit.
196  filename=$(_os_based __temp_file)  # is not `local` on purpose.
197
198  trap 'echo "cleaning up..."; rm -f "$filename";' EXIT
199}
200
201
202function _unique_filename {
203  # First parameter is base-path, second is filename,
204  # third is optional extension.
205  local n=0
206  local base_path="$1"
207  local result="$2"
208
209  while true; do
210    if [[ ! -f "$base_path/$result" ]]; then
211      break
212    fi
213
214    n=$(( n + 1 ))
215    result="${2}-${n}" # calling to the original "$2"
216  done
217  echo "$result"
218}
219
220# Helper function
221
222
223function _gawk_inplace {
224  local parms="$*"
225  local dest_file
226  dest_file="$(echo "$parms" | gawk -v RS="'" -v FS="'" 'END{ gsub(/^\s+/,""); print $1 }')"
227
228  _temporary_file
229
230  bash -c "gawk ${parms}" > "$filename"
231  mv "$filename" "$dest_file"
232}
233
234
235# File System Database (fsdb):
236
237
238function _get_record_filename {
239  # Returns 1st field from passed record
240  local record="$1"
241  local filename
242  filename=$(echo "$record" | awk -F: '{print $1}')
243
244  echo "$filename"
245}
246
247
248function _get_record_hash {
249  # Returns 2nd field from passed record
250  local record="$1"
251  local hash
252  hash=$(echo "$record" | awk -F: '{print $2}')
253
254  echo "$hash"
255}
256
257
258function _fsdb_has_record {
259  # First parameter is the key
260  # Second is the fsdb
261  local key="$1"  # required
262  local fsdb="$2" # required
263
264  # 0 on contains, 1 for error.
265  gawk -v key="$key" "$AWK_FSDB_HAS_RECORD" "$fsdb"
266}
267
268
269function _fsdb_rm_record {
270  # First parameter is the key (filename)
271  # Second is the path to fsdb
272  local key="$1"  # required
273  local fsdb="$2" # required
274
275  _gawk_inplace -v key="'$key'" "'$AWK_FSDB_RM_RECORD'" "$fsdb"
276}
277
278function _fsdb_clear_hashes {
279  # First parameter is the path to fsdb
280  local fsdb="$1" # required
281
282  _gawk_inplace "'$AWK_FSDB_CLEAR_HASHES'" "$fsdb"
283}
284
285
286# Manuals:
287
288function _show_manual_for {
289  local function_name="$1" # required
290
291  man "git-secret-${function_name}"
292  exit 0
293}
294
295
296# Invalid options
297
298function _invalid_option_for {
299  local function_name="$1" # required
300
301  man "git-secret-${function_name}"
302  exit 1
303}
304
305
306# VCS:
307
308function _check_ignore {
309  local filename="$1" # required
310
311  local result
312  result="$(git add -n "$filename" > /dev/null 2>&1; echo $?)"
313  # when ignored
314  if [[ "$result" -ne 0 ]]; then
315    result=0
316  else
317    result=1
318  fi
319  # returns 1 when not ignored, and 0 when ignored
320  echo "$result"
321}
322
323
324function _git_normalize_filename {
325  local filename="$1" # required
326
327  local result
328  result=$(git ls-files --full-name -o "$filename")
329  echo "$result"
330}
331
332
333function _maybe_create_gitignore {
334  # This function creates '.gitignore' if it was missing.
335
336  local full_path
337  full_path=$(_append_root_path '.gitignore')
338
339  if [[ ! -f "$full_path" ]]; then
340    touch "$full_path"
341  fi
342}
343
344
345function _add_ignored_file {
346  # This function adds a line with the filename into the '.gitignore' file.
347  # It also creates '.gitignore' if it's not there
348
349  local filename="$1" # required
350
351  _maybe_create_gitignore
352
353  local full_path
354  full_path=$(_append_root_path '.gitignore')
355
356  echo "$filename" >> "$full_path"
357}
358
359
360function _is_inside_git_tree {
361  # Checks if we are working inside the `git` tree.
362  local result
363  result=$(git rev-parse --is-inside-work-tree > /dev/null 2>&1; echo $?)
364
365  echo "$result"
366}
367
368function _is_tracked_in_git {
369  local filename="$1" # required
370  local result
371  result="$(git ls-files --error-unmatch "$filename" >/dev/null 2>&1; echo $?)"
372
373  if [[ "$result" -eq 0 ]]; then
374    echo "1"
375  else
376    echo "0"
377  fi
378}
379
380
381function _get_git_root_path {
382  # We need this function to get the location of the `.git` folder,
383  # since `.gitsecret` (or value set by SECRETS_DIR env var) must be on the same level.
384
385  local result
386  result=$(git rev-parse --show-toplevel)
387  echo "$result"
388}
389
390
391# Relative paths:
392
393function _append_root_path {
394  # This function adds root path to any other path.
395
396  local path="$1" # required
397
398  local root_path
399  root_path=$(_get_git_root_path)
400
401  echo "$root_path/$path"
402}
403
404
405function _get_secrets_dir {
406  _append_root_path "${_SECRETS_DIR}"
407}
408
409
410function _get_secrets_dir_keys {
411  _append_root_path "${_SECRETS_DIR_KEYS}"
412}
413
414
415function _get_secrets_dir_path {
416  _append_root_path "${_SECRETS_DIR_PATHS}"
417}
418
419
420function _get_secrets_dir_keys_mapping {
421  _append_root_path "${_SECRETS_DIR_KEYS_MAPPING}"
422}
423
424
425function _get_secrets_dir_keys_trustdb {
426  _append_root_path "${_SECRETS_DIR_KEYS_TRUSTDB}"
427}
428
429
430function _get_secrets_dir_paths_mapping {
431  _append_root_path "${_SECRETS_DIR_PATHS_MAPPING}"
432}
433
434
435# Logic:
436
437function _abort {
438  local message="$1" # required
439  local exit_code=${2:-"1"}     # defaults to 1
440
441  >&2 echo "git-secret: abort: $message"
442  exit "$exit_code"
443}
444
445# _warn() sends warnings to stdout so user sees them
446function _warn {
447  local message="$1" # required
448
449  >&2 echo "git-secret: warning: $message"
450}
451
452# _warn_or_abort "$error_message" "$exit_code" "$error_ok"
453function _warn_or_abort {
454  local message="$1"            # required
455  local exit_code=${2:-"1"}     # defaults to 1
456  local error_ok=${3:-0}        # can be 0 or 1
457
458  if [[ "$error_ok" -eq "0" ]]; then
459    if [[ "$exit_code" -eq "0" ]]; then
460      # if caller sends an exit_code of 0, we change it to 1 before aborting.
461      exit_code=1
462    fi
463    _abort "$message" "$exit_code"
464  else
465    _warn "$message" "$exit_code"
466  fi
467}
468
469function _find_and_clean {
470  # required:
471  local pattern="$1" # can be any string pattern
472
473  # optional:
474  local verbose=${2:-""} # can be empty or should be equal to "v"
475
476  local root
477  root=$(_get_git_root_path)
478
479  # shellcheck disable=2086
480  find "$root" -path "$pattern" -type f -print0 | xargs -0 rm -f$verbose
481}
482
483
484function _find_and_clean_formatted {
485  # required:
486  local pattern="$1" # can be any string pattern
487
488  # optional:
489  local verbose=${2:-""} # can be empty or should be equal to "v"
490  local message=${3:-"cleaning:"} # can be any string
491
492  if [[ -n "$verbose" ]]; then
493    echo && echo "$message"
494  fi
495
496  _find_and_clean "$pattern" "$verbose"
497
498  if [[ -n "$verbose" ]]; then
499    echo
500  fi
501}
502
503
504# this sets the global array variable 'filenames'
505function _list_all_added_files {
506  local path_mappings
507  path_mappings=$(_get_secrets_dir_paths_mapping)
508
509  if [[ ! -s "$path_mappings" ]]; then
510    _abort "$path_mappings is missing."
511  fi
512
513  local filename
514  filenames=()      # not local
515  while read -r line; do
516    filename=$(_get_record_filename "$line")
517    filenames+=("$filename")
518  done < "$path_mappings"
519
520  declare -a filenames     # so caller can get list from filenames array
521}
522
523
524function _secrets_dir_exists {
525  # This function checks if "$_SECRETS_DIR" exists and.
526
527  local full_path
528  full_path=$(_get_secrets_dir)
529
530  if [[ ! -d "$full_path" ]]; then
531    local name
532    name=$(basename "$full_path")
533    _abort "directory '$name' does not exist. Use 'git secret init' to initialize git-secret"
534  fi
535}
536
537
538function _secrets_dir_is_not_ignored {
539  # This function checks that "$_SECRETS_DIR" is not ignored.
540
541  local git_secret_dir
542  git_secret_dir=$(_get_secrets_dir)
543
544  # Create git_secret_dir required for check
545  local cleanup=0
546  if [[ ! -d "$git_secret_dir" ]]; then
547    mkdir "$git_secret_dir"
548    cleanup=1
549  fi
550  local ignores
551  ignores=$(_check_ignore "$git_secret_dir")
552  if [[ "$cleanup" == 1 ]]; then
553    rmdir "$git_secret_dir"
554  fi
555
556  if [[ ! $ignores -eq 1 ]]; then
557    _abort "'$git_secret_dir' is in .gitignore"
558  fi
559}
560
561
562function _user_required {
563  # This function does a bunch of validations:
564  # 1. It calls `_secrets_dir_exists` to verify that "$_SECRETS_DIR" exists.
565  # 2. It ensures that "$_SECRETS_DIR_KEYS_TRUSTDB" exists.
566  # 3. It ensures that there are added public keys.
567
568  _secrets_dir_exists
569
570  local trustdb
571  trustdb=$(_get_secrets_dir_keys_trustdb)
572
573  local error_message="no public keys for users found. run 'git secret tell email@address'."
574  if [[ ! -f "$trustdb" ]]; then
575    _abort "$error_message"
576  fi
577
578  local secrets_dir_keys
579  secrets_dir_keys=$(_get_secrets_dir_keys)
580
581  local keys_exist
582  keys_exist=$($SECRETS_GPG_COMMAND --homedir "$secrets_dir_keys" --no-permission-warning -n --list-keys)
583  local exit_code=$?
584  if [[ "$exit_code" -ne 0 ]]; then
585    # this might catch corner case where gpg --list-keys shows
586    # 'gpg: skipped packet of type 12 in keybox' warnings but succeeds?
587    # See #136
588    _abort "problem listing public keys with gpg: exit code $exit_code"
589  fi
590  if [[ -z "$keys_exist" ]]; then
591    _abort "$error_message"
592  fi
593}
594
595# note: this has the same 'username matching' issue described in
596# https://github.com/sobolevn/git-secret/issues/268
597# where it will match emails that have other emails as substrings.
598# we need to use fingerprints for a unique key id with gpg.
599function _get_user_key_expiry {
600  # This function returns the user's key's expiry, as an epoch.
601  # It will return the empty string if there is no expiry date for the user's key
602  local username="$1"
603  local line
604
605  local secrets_dir_keys
606  secrets_dir_keys=$(_get_secrets_dir_keys)
607
608  line=$($SECRETS_GPG_COMMAND --homedir "$secrets_dir_keys" --no-permission-warning --list-public-keys --with-colon --fixed-list-mode "$username" | grep ^pub:)
609
610  local expiry_epoch
611  expiry_epoch=$(echo "$line" | cut -d: -f7)
612  echo "$expiry_epoch"
613}
614
615
616function _assert_keychain_contains_emails {
617  local homedir=$1
618  local emails=$2
619
620  local gpg_uids
621  gpg_uids=$(_get_users_in_gpg_keyring "$homedir")
622  for email in "${emails[@]}"; do
623    local email_ok=0
624    for uid in $gpg_uids; do
625        if [[ "$uid" == "$email" ]]; then
626            email_ok=1
627        fi
628    done
629    if [[ $email_ok -eq 0 ]]; then
630      _abort "email not found in gpg keyring: $email"
631    fi
632  done
633}
634
635
636function _get_raw_filename {
637  echo "$(dirname "$1")/$(basename "$1" "$SECRETS_EXTENSION")" | sed -e 's#^\./##'
638}
639
640
641function _get_encrypted_filename {
642  local filename
643  filename="$(dirname "$1")/$(basename "$1" "$SECRETS_EXTENSION")"
644  echo "${filename}${SECRETS_EXTENSION}" | sed -e 's#^\./##'
645}
646
647
648function _get_users_in_gpg_keyring {
649  # show the users in the gpg keyring.
650  # `whoknows` command uses it internally.
651  # parses the `gpg` public keys
652  local homedir=$1
653  local result
654  local args=()
655  if [[ -n "$homedir" ]]; then
656    args+=( "--homedir" "$homedir" )
657  fi
658
659  # pluck out 'uid' lines, fetch 10th field, extract part in <> if it exists (else leave alone).
660  # we use --fixed-list-mode so older versions of gpg emit 'uid:' lines.
661  # sed at the end is to extract email from <>. (If there's no <>, then the line is just an email address anyway.)
662  result=$($SECRETS_GPG_COMMAND "${args[@]}" --no-permission-warning --list-public-keys --with-colon --fixed-list-mode | grep ^uid: | cut -d: -f10 | sed 's/.*<\(.*\)>.*/\1/')
663
664  echo "$result"
665}
666
667
668function _get_users_in_gitsecret_keyring {
669  # show the users in the gitsecret keyring.
670  local secrets_dir_keys
671  secrets_dir_keys=$(_get_secrets_dir_keys)
672
673  local result
674  result=$(_get_users_in_gpg_keyring "$secrets_dir_keys")
675
676  echo "$result"
677}
678
679
680function _get_recipients {
681  # This function is required to create an encrypted file for different users.
682  # These users are called 'recipients' in the `gpg` terms.
683  # It basically just parses the `gpg` public keys
684
685  local result
686  result=$(_get_users_in_gitsecret_keyring | sed 's/^/-r/')   # put -r before each user
687  echo "$result"
688}
689
690
691function _decrypt {
692  # required:
693  local filename="$1"
694
695  # optional:
696  local write_to_file=${2:-1} # can be 0 or 1
697  local force=${3:-0} # can be 0 or 1
698  local homedir=${4:-""}
699  local passphrase=${5:-""}
700  local error_ok=${6:-0} # can be 0 or 1
701
702  local encrypted_filename
703  encrypted_filename=$(_get_encrypted_filename "$filename")
704
705  local args=( "--use-agent" "--decrypt" "--no-permission-warning" )
706
707  if [[ "$write_to_file" -eq 1 ]]; then
708    args+=( "-o" "$filename" )
709  fi
710
711  if [[ "$force" -eq 1 ]]; then
712    args+=( "--yes" )
713  fi
714
715  if [[ -n "$homedir" ]]; then
716    args+=( "--homedir" "$homedir" )
717  fi
718
719  if [[ "$GPG_VER_21" -eq 1 ]]; then
720    args+=( "--pinentry-mode" "loopback" )
721  fi
722
723  set +e   # disable 'set -e' so we can capture exit_code
724
725  #echo "# gpg passphrase: $passphrase" >&3
726  local exit_code
727  if [[ -n "$passphrase" ]]; then
728    echo "$passphrase" | $SECRETS_GPG_COMMAND "${args[@]}" --quiet --batch --yes --no-tty --passphrase-fd 0 \
729      "$encrypted_filename"
730    exit_code=$?
731  else
732    $SECRETS_GPG_COMMAND "${args[@]}" "--quiet" "$encrypted_filename"
733    exit_code=$?
734  fi
735
736  set -e  # re-enable set -e
737
738  # note that according to https://github.com/sobolevn/git-secret/issues/238 ,
739  # it's possible for gpg to return a 0 exit code but not have decrypted the file
740  #echo "# gpg exit code: $exit_code, error_ok: $error_ok" >&3
741  if [[ "$exit_code" -ne "0" ]]; then
742    local msg="problem decrypting file with gpg: exit code $exit_code: $filename"
743    _warn_or_abort "$msg" "$exit_code" "$error_ok"
744  fi
745
746  # at this point the file should be written to disk or output to stdout
747}
748
749#!/usr/bin/env bash
750
751# support for freebsd. Mostly the same as OSX.
752
753
754# shellcheck disable=1117
755function __replace_in_file_freebsd {
756  sed -i.bak "s/^\($1[[:space:]]*=[[:space:]]*\).*\$/\1$2/" "$3"
757}
758
759
760function __temp_file_freebsd {
761  : "${TMPDIR:=/tmp}"
762  local filename
763  filename=$(mktemp -t _gitsecrets_XXX )
764  echo "$filename";
765}
766
767
768function __sha256_freebsd {
769  # this is in a different location than osx
770  /usr/local/bin/shasum -a256 "$1"
771}
772
773function __get_octal_perms_freebsd {
774  local filename
775  filename=$1
776  local perms
777  perms=$(stat -f "%04OLp" "$filename")
778  # perms is a string like '0644'.
779  # In the "%04OLp':
780  #   the '04' means 4 digits, 0 padded.  So we get 0644, not 644.
781  #   the 'O' means Octal.
782  #   the 'Lp' means 'low subfield of file type and permissions (st_mode).'
783  #     (without 'L' you get 6 digits like '100644'.)
784  echo "$perms"
785}
786
787function __epoch_to_date_freebsd {
788  local epoch=$1;
789  if [ -z "$epoch" ]; then
790    echo ''
791  else
792    local cmd="date +%F -r $epoch"
793    local datetime
794    datetime=$($cmd)
795    echo "$datetime"
796  fi
797}
798#!/usr/bin/env bash
799
800
801# shellcheck disable=1117
802function __replace_in_file_linux {
803  sed -i.bak "s/^\($1\s*=\s*\).*\$/\1$2/" "$3"
804}
805
806
807function __temp_file_linux {
808  local filename
809  filename=$(mktemp)
810  echo "$filename"
811}
812
813function __sha256_linux {
814  sha256sum "$1"
815}
816
817function __get_octal_perms_linux {
818  local filename
819  filename=$1
820  local perms
821  perms=$(stat --format '%a' "$filename")
822  # a string like '0644'
823  echo "$perms"
824}
825
826function __epoch_to_date_linux {
827  local epoch=$1;
828  if [ -z "$epoch" ]; then
829    echo ''
830  else
831    local cmd="date +%F -d @$epoch"
832    local datetime
833    datetime=$($cmd)
834    echo "$datetime"
835  fi
836}
837#!/usr/bin/env bash
838
839
840# shellcheck disable=1117
841function __replace_in_file_osx {
842  sed -i.bak "s/^\($1[[:space:]]*=[[:space:]]*\).*\$/\1$2/" "$3"
843}
844
845
846function __temp_file_osx {
847  : "${TMPDIR:=/tmp}"
848  local filename
849  filename=$(mktemp -t _gitsecrets_XXX )
850  echo "$filename";
851}
852
853
854function __sha256_osx {
855  /usr/bin/shasum -a256 "$1"
856}
857
858function __get_octal_perms_osx {
859  local filename
860  filename=$1
861  local perms
862  perms=$(stat -f "%04OLp" "$filename")
863  # see _git_secret_tools_freebsd.sh for more about stat's format string
864  echo "$perms"
865}
866
867function __epoch_to_date_osx {
868  local epoch=$1;
869  if [ -z "$epoch" ]; then
870    echo ''
871  else
872    #date -r 234234234 +"%Y-%m-%d"
873    local datetime
874    datetime=$(date -r "$epoch" +'%Y-%m-%d')
875    echo "$datetime"
876  fi
877}
878
879#!/usr/bin/env bash
880
881
882function add {
883  local auto_ignore=0
884  OPTIND=1
885
886  while getopts "ih" opt; do
887    case "$opt" in
888      i) auto_ignore=1;;
889
890      h) _show_manual_for "add";;
891
892      *) _invalid_option_for "add";;
893    esac
894  done
895
896  shift $((OPTIND-1))
897  [ "$1" = "--" ] && shift
898
899  _user_required
900
901  # Checking if all files are correct (ignored and inside the repo):
902
903  local not_ignored=()
904  local items=( "$@" )
905
906  # Checking if all files in options are ignored:
907  for item in "${items[@]}"; do
908    local path # absolute path
909    local normalized_path # relative to the .git dir
910    normalized_path=$(_git_normalize_filename "$item")
911    path=$(_append_root_path "$normalized_path")
912
913    # check that the file is not tracked
914    local in_git
915    in_git=$(_is_tracked_in_git "$item")
916    if [[ "$in_git" -ne 0  ]]; then
917       _abort "file tracked in git, consider using 'git rm --cached $item'"
918    fi
919
920    # Checking that file is valid:
921    if [[ ! -f "$path" ]]; then
922      _abort "file not found: $item"
923    fi
924
925    # Checking that it is ignored:
926    local ignored
927    ignored=$(_check_ignore "$path")
928
929    if [[ "$ignored" -ne 0 ]]; then
930      # Collect unignored files:
931      not_ignored+=("$normalized_path")
932    fi
933  done
934
935  # Are there any unignored files?
936
937  if [[ ! "${#not_ignored[@]}" -eq 0 ]]; then
938    # And show them all at once.
939    local message
940    message="these files are not in .gitignore: $*"
941
942    if [[ "$auto_ignore" -eq 0 ]]; then
943      # This file is not ignored. user don't want it to be added automatically.
944      # Raise the exception, since all files, which will be hidden, must be ignored.
945      _abort "$message"
946    else
947      # In this case these files should be added to the `.gitignore` automatically:
948      # see https://github.com/sobolevn/git-secret/issues/18 for more.
949      echo "$message"
950      echo "auto adding them to .gitignore"
951      for item in "${not_ignored[@]}"; do
952        _add_ignored_file "$item"
953      done
954    fi
955  fi
956
957  # Adding files to path mappings:
958
959  local fsdb
960  fsdb=$(_get_secrets_dir_paths_mapping)
961
962  for item in "${items[@]}"; do
963    local path
964    local key
965    path=$(_git_normalize_filename "$item")
966    key="$path"
967
968    # Adding files into system, skipping duplicates.
969    local already_in
970    already_in=$(_fsdb_has_record "$key" "$fsdb")
971    if [[ "$already_in" -eq 1 ]]; then
972      echo "$key" >> "$fsdb"
973    fi
974  done
975
976  echo "${#@} item(s) added."
977}
978#!/usr/bin/env bash
979
980
981function cat {
982  local homedir=''
983  local passphrase=''
984
985  OPTIND=1
986
987  while getopts 'hd:p:' opt; do
988    case "$opt" in
989      h) _show_manual_for 'cat';;
990
991      p) passphrase=$OPTARG;;
992
993      d) homedir=$OPTARG;;
994
995      *) _invalid_option_for 'cat';;
996    esac
997  done
998
999  shift $((OPTIND-1))
1000  [ "$1" = '--' ] && shift
1001
1002  _user_required
1003
1004  # Command logic:
1005
1006  for line in "$@"
1007  do
1008    local filename
1009    local path
1010
1011    filename=$(_get_record_filename "$line")
1012    path=$(_append_root_path "$filename")
1013
1014    # The parameters are: filename, write-to-file, force, homedir, passphrase
1015    _decrypt "$path" "0" "0" "$homedir" "$passphrase"
1016  done
1017}
1018#!/usr/bin/env bash
1019
1020function changes {
1021  local passphrase=""
1022
1023  OPTIND=1
1024
1025  while getopts 'hd:p:' opt; do
1026    case "$opt" in
1027      h) _show_manual_for 'changes';;
1028
1029      p) passphrase=$OPTARG;;
1030
1031      d) homedir=$OPTARG;;
1032
1033      *) _invalid_option_for 'changes';;
1034    esac
1035  done
1036
1037  shift $((OPTIND-1))
1038  [ "$1" = '--' ] && shift
1039
1040  _user_required
1041
1042  filenames=("$@")  # list of positional params. global.
1043  if [[ ${#filenames[@]} -eq 0 ]]; then
1044    # Checking if no filenames are passed, show diff for all files.
1045    _list_all_added_files    # this sets the array variable 'filenames'
1046  fi
1047
1048  IFS='
1049  '
1050
1051  for filename in "${filenames[@]}"; do
1052    local path # absolute path
1053    local normalized_path # relative to the .git dir
1054    local encrypted_filename
1055    normalized_path=$(_git_normalize_filename "$filename")
1056    encrypted_filename=$(_get_encrypted_filename "$filename")
1057
1058    if [[ ! -f "$encrypted_filename" ]]; then
1059        _abort "cannot find encrypted version of file: $filename"
1060    fi
1061    if [[ -n "$normalized_path" ]]; then
1062      path=$(_append_root_path "$normalized_path")
1063    else
1064      # Path was already normalized
1065      path=$(_append_root_path "$filename")
1066    fi
1067
1068    if [[ ! -f "$path" ]]; then
1069        _abort "file not found. Consider using 'git secret reveal': $filename"
1070    fi
1071
1072    # Now we have all the data required to do the last encryption and compare results:
1073    # now do a two-step to protect trailing newlines from the $() construct.
1074    local decrypted_x
1075    local decrypted
1076    decrypted_x=$(_decrypt "$path" "0" "0" "$homedir" "$passphrase"; echo x$?)
1077    decrypted="${decrypted_x%x*}"
1078    # we ignore the exit code because _decrypt will _abort if appropriate.
1079
1080
1081    echo "changes in ${path}:"
1082    # diff the result:
1083    # we have the '|| true' because `diff` returns error code if files differ.
1084    diff -u <(echo -n "$decrypted") "$path" || true
1085  done
1086}
1087#!/usr/bin/env bash
1088
1089
1090function clean {
1091  local verbose=''
1092
1093  OPTIND=1
1094
1095  while getopts 'vh' opt; do
1096    case "$opt" in
1097      v) verbose="v";;
1098
1099      h) _show_manual_for 'clean';;
1100
1101      *) _invalid_option_for 'clean';;
1102    esac
1103  done
1104
1105  shift $((OPTIND-1))
1106  [ "$1" = '--' ] && shift
1107
1108  _user_required
1109
1110  # User should see properly formatted output:
1111  _find_and_clean_formatted "*$SECRETS_EXTENSION" "$verbose"
1112}
1113#!/usr/bin/env bash
1114
1115# shellcheck disable=2016
1116AWK_FSDB_UPDATE_HASH='
1117BEGIN { FS=":"; OFS=":"; }
1118{
1119  if ( key == $1 )
1120  {
1121    print key,hash;
1122  }
1123  else
1124  {
1125    print $1,$2;
1126  }
1127}
1128'
1129
1130function _optional_clean {
1131  local clean="$1"
1132  local verbose=${2:-""}
1133
1134  if [[ $clean -eq 1 ]]; then
1135    _find_and_clean_formatted "*$SECRETS_EXTENSION" "$verbose"
1136  fi
1137}
1138
1139
1140function _optional_delete {
1141  local delete="$1"
1142  local verbose=${2:-""}
1143
1144  if [[ $delete -eq 1 ]]; then
1145    local path_mappings
1146    path_mappings=$(_get_secrets_dir_paths_mapping)
1147
1148    # We use custom formatting here:
1149    if [[ -n "$verbose" ]]; then
1150      echo && echo 'removing unencrypted files:'
1151    fi
1152
1153    while read -r line; do
1154      # So the formatting would not be repeated several times here:
1155      local filename
1156      filename=$(_get_record_filename "$line")
1157      _find_and_clean "*$filename" "$verbose"
1158    done < "$path_mappings"
1159
1160    if [[ -n "$verbose" ]]; then
1161      echo
1162    fi
1163  fi
1164}
1165
1166function _get_checksum_local {
1167  local checksum="$SECRETS_CHECKSUM_COMMAND"
1168  echo "$checksum"
1169}
1170
1171function _get_file_hash {
1172  local input_path="$1" # Required
1173  local checksum_local
1174  local file_hash
1175
1176  checksum_local="$(_get_checksum_local)"
1177  file_hash=$($checksum_local "$input_path" | gawk '{print $1}')
1178
1179  echo "$file_hash"
1180}
1181
1182function _optional_fsdb_update_hash {
1183  local key="$1"
1184  local hash="$2"
1185  local fsdb          # path_mappings
1186
1187  fsdb=$(_get_secrets_dir_paths_mapping)
1188
1189  _gawk_inplace -v key="'$key'" -v hash="$hash" "'$AWK_FSDB_UPDATE_HASH'" "$fsdb"
1190}
1191
1192
1193function hide {
1194  local clean=0
1195  local preserve=0
1196  local delete=0
1197  local fsdb_update_hash=0 # add checksum hashes to fsdb
1198  local verbose=''
1199  local force_continue=0
1200
1201  OPTIND=1
1202
1203  while getopts 'cFPdmvh' opt; do
1204    case "$opt" in
1205      c) clean=1;;
1206
1207      F) force_continue=1;;
1208
1209      P) preserve=1;;
1210
1211      d) delete=1;;
1212
1213      m) fsdb_update_hash=1;;
1214
1215      v) verbose='v';;
1216
1217      h) _show_manual_for 'hide';;
1218
1219      *) _invalid_option_for 'hide';;
1220    esac
1221  done
1222
1223  shift $((OPTIND-1))
1224  [ "$1" = '--' ] && shift
1225
1226  # We need user to continue:
1227  _user_required
1228
1229  # If -c option was provided, it would clean the hidden files
1230  # before creating new ones.
1231  _optional_clean "$clean" "$verbose"
1232
1233  # Encrypting files:
1234
1235  local path_mappings
1236  path_mappings=$(_get_secrets_dir_paths_mapping)
1237  local num_mappings
1238  num_mappings=$(gawk 'END{print NR}' "$path_mappings")
1239
1240  # make sure all the unencrypted files needed are present
1241  local to_hide=()
1242  while read -r record; do
1243    to_hide+=("$record")  # add record to array
1244  done < "$path_mappings"
1245
1246  local counter=0
1247  for record in "${to_hide[@]}"; do
1248    local filename
1249    local fsdb_file_hash
1250    local encrypted_filename
1251    filename=$(_get_record_filename "$record")
1252    fsdb_file_hash=$(_get_record_hash "$record")
1253    encrypted_filename=$(_get_encrypted_filename "$filename")
1254
1255    local recipients
1256    recipients=$(_get_recipients)
1257
1258    local secrets_dir_keys
1259    secrets_dir_keys=$(_get_secrets_dir_keys)
1260
1261    local input_path
1262    local output_path
1263    input_path=$(_append_root_path "$filename")
1264    output_path=$(_append_root_path "$encrypted_filename")
1265
1266    # Checking that file is valid:
1267    if [[ ! -f "$input_path" ]]; then
1268      # this catches the case where some decrypted files don't exist
1269      _warn_or_abort "file not found: $input_path" "1" "$force_continue"
1270    else
1271      file_hash=$(_get_file_hash "$input_path")
1272
1273      # encrypt file only if required
1274      if [[ "$fsdb_file_hash" != "$file_hash" ]]; then
1275
1276        set +e   # disable 'set -e' so we can capture exit_code
1277
1278        # we depend on $recipients being split on whitespace
1279        # shellcheck disable=SC2086
1280        $SECRETS_GPG_COMMAND --homedir "$secrets_dir_keys" "--no-permission-warning" --use-agent --yes --trust-model=always --encrypt \
1281          $recipients -o "$output_path" "$input_path" > /dev/null 2>&1
1282        local exit_code=$?
1283
1284        set -e  # re-enable set -e
1285
1286        if [[ "$exit_code" -ne 0 ]] || [[ ! -f "$output_path" ]]; then
1287          # if gpg can't encrypt a file we asked it to, that's an error unless in force_continue mode.
1288          _warn_or_abort "problem encrypting file with gpg: exit code $exit_code: $filename" "$exit_code" "$force_continue"
1289        fi
1290        if [[ -f "$output_path" ]]; then
1291          counter=$((counter+1))
1292          if [[ "$preserve" == 1 ]]; then
1293            local perms
1294            perms=$($SECRETS_OCTAL_PERMS_COMMAND "$input_path")
1295            chmod "$perms" "$output_path"
1296          fi
1297        fi
1298
1299        # If -m option was provided, it will update unencrypted file hash
1300        local key="$filename"
1301        local hash="$file_hash"
1302        # Update file hash if required in fsdb
1303        [[ "$fsdb_update_hash" -gt 0 ]] && \
1304          _optional_fsdb_update_hash "$key" "$hash"
1305      fi
1306    fi
1307  done
1308
1309  # If -d option was provided, it would delete the source files
1310  # after we have already hidden them.
1311  _optional_delete "$delete" "$verbose"
1312
1313  echo "done. $counter of $num_mappings files are hidden."
1314}
1315#!/usr/bin/env bash
1316
1317# shellcheck disable=2016
1318AWK_ADD_TO_GITIGNORE='
1319BEGIN {
1320  cnt=0
1321}
1322
1323function check_print_line(line){
1324  if (line == pattern) {
1325    cnt++
1326  }
1327  print line
1328}
1329
1330# main function
1331{
1332  check_print_line($0)      # check and print first line
1333  while (getline == 1) {    # check and print all other
1334    check_print_line($0)
1335  }
1336}
1337
1338END {
1339  if ( cnt == 0) {         # if file did not contain pattern add
1340    print pattern
1341  }
1342}
1343'
1344
1345function gitignore_add_pattern {
1346  local pattern
1347  local gitignore_file_path
1348
1349  pattern="$1"
1350  gitignore_file_path=$(_append_root_path '.gitignore')
1351
1352  _maybe_create_gitignore
1353  _gawk_inplace -v pattern="$pattern" "'$AWK_ADD_TO_GITIGNORE'" "$gitignore_file_path"
1354}
1355
1356function init {
1357  OPTIND=1
1358
1359  while getopts 'h' opt; do
1360    case "$opt" in
1361      h) _show_manual_for 'init';;
1362
1363      *) _invalid_option_for 'init';;
1364    esac
1365  done
1366
1367  shift $((OPTIND-1))
1368  [ "$1" = '--' ] && shift
1369
1370  # Check if '.gitsecret/' already exists:
1371  local git_secret_dir
1372  git_secret_dir=$(_get_secrets_dir)
1373
1374  if [[ -d "$git_secret_dir" ]]; then
1375    _abort 'already inited.'
1376  fi
1377
1378  # Check if it is ignored:
1379  _secrets_dir_is_not_ignored
1380
1381  # Create internal files:
1382
1383  mkdir "$git_secret_dir" "$(_get_secrets_dir_keys)" "$(_get_secrets_dir_path)"
1384  touch "$(_get_secrets_dir_keys_mapping)" "$(_get_secrets_dir_paths_mapping)"
1385
1386  echo "'$git_secret_dir/' created."
1387
1388  local random_seed_file
1389  random_seed_file=".gitsecret/keys/random_seed"
1390  gitignore_add_pattern "$random_seed_file"
1391
1392  # TODO: git attributes to view diffs
1393}
1394#!/usr/bin/env bash
1395
1396
1397function killperson {
1398  OPTIND=1
1399
1400  while getopts 'h' opt; do
1401    case "$opt" in
1402      h) _show_manual_for 'killperson';;
1403
1404      *) _invalid_option_for 'killperson';;
1405    esac
1406  done
1407
1408  shift $((OPTIND-1))
1409  [ "$1" = "--" ] && shift
1410
1411  _user_required
1412
1413  # Command logic:
1414
1415  local emails=( "$@" )
1416
1417  if [[ ${#emails[@]} -eq 0 ]]; then
1418    _abort "at least one email is required for killperson."
1419  fi
1420  # Getting the local git-secret `gpg` key directory:
1421  local secrets_dir_keys
1422  secrets_dir_keys=$(_get_secrets_dir_keys)
1423
1424  _assert_keychain_contains_emails "$secrets_dir_keys" "${emails[@]}"
1425
1426  for email in "${emails[@]}"; do
1427    $SECRETS_GPG_COMMAND --homedir "$secrets_dir_keys" --no-permission-warning --batch --yes --delete-key "$email"
1428    local exit_code=$?
1429    if [[ "$exit_code" -ne 0 ]]; then
1430      _abort "problem deleting key for '$email' with gpg: exit code $exit_code"
1431    fi
1432  done
1433
1434  echo 'removed keys.'
1435  echo "now [$*] do not have an access to the repository."
1436  echo 'make sure to hide the existing secrets again.'
1437}
1438#!/usr/bin/env bash
1439
1440
1441function list {
1442  OPTIND=1
1443
1444  while getopts 'h' opt; do
1445    case "$opt" in
1446      h) _show_manual_for 'list';;
1447
1448      *) _invalid_option_for 'list';;
1449    esac
1450  done
1451
1452  shift $((OPTIND-1))
1453  [ "$1" = '--' ] && shift
1454
1455  _user_required
1456
1457  # Command logic:
1458  filenames=()
1459  _list_all_added_files  # exports 'filenames' array
1460  local filename
1461  for filename in "${filenames[@]}"; do
1462    echo "$filename"
1463  done
1464}
1465#!/usr/bin/env bash
1466
1467
1468function remove {
1469  local clean=0
1470
1471  OPTIND=1
1472
1473  while getopts 'ch' opt; do
1474    case "$opt" in
1475      c) clean=1;;
1476
1477      h) _show_manual_for 'remove';;
1478
1479      *) _invalid_option_for 'remove';;
1480    esac
1481  done
1482
1483  shift $((OPTIND-1))
1484  [ "$1" = '--' ] && shift
1485
1486  # Validate if user exists:
1487  _user_required
1488
1489  # Command logic:
1490
1491  local path_mappings
1492  path_mappings=$(_get_secrets_dir_paths_mapping)
1493
1494  for item in "$@"; do
1495    local path # absolute path
1496    local normalized_path # relative to .git folder
1497    normalized_path=$(_git_normalize_filename "$item")
1498    path=$(_append_root_path "$normalized_path")
1499
1500    # Checking if file exists:
1501    if [[ ! -f "$path" ]]; then
1502      _abort "file not found: $item"
1503    fi
1504
1505    # Deleting it from path mappings:
1506    # Remove record from fsdb with matching key
1507    local key
1508    key="$normalized_path"
1509    fsdb="$path_mappings"
1510    _fsdb_rm_record "$key" "$fsdb"
1511
1512    rm -f "${path_mappings}.bak"  # not all systems create '.bak'
1513
1514    # Optional clean:
1515    if [[ "$clean" -eq 1 ]]; then
1516      local encrypted_filename
1517      encrypted_filename=$(_get_encrypted_filename "$path")
1518
1519      rm "$encrypted_filename" # fail on error
1520    fi
1521  done
1522
1523  echo 'removed from index.'
1524  echo "ensure that files: [$*] are now not ignored."
1525}
1526#!/usr/bin/env bash
1527
1528
1529function reveal {
1530  local homedir=''
1531  local passphrase=''
1532  local force=0             # this means 'clobber without warning'
1533  local force_continue=0    # this means 'continue if we have decryption errors'
1534  local preserve=0
1535
1536  OPTIND=1
1537
1538  while getopts 'hfFPd:p:' opt; do
1539    case "$opt" in
1540      h) _show_manual_for 'reveal';;
1541
1542      f) force=1;;
1543
1544      F) force_continue=1;;
1545
1546      P) preserve=1;;
1547
1548      p) passphrase=$OPTARG;;
1549
1550      d) homedir=$OPTARG;;
1551
1552      *) _invalid_option_for 'reveal';;
1553    esac
1554  done
1555
1556  shift $((OPTIND-1))
1557  [ "$1" = '--' ] && shift
1558
1559  _user_required
1560
1561  # Command logic:
1562
1563  local path_mappings
1564  path_mappings=$(_get_secrets_dir_paths_mapping)
1565
1566  local counter=0
1567  local to_show=( "$@" )
1568
1569  if [ ${#to_show[@]} -eq 0 ]; then
1570    while read -r record; do
1571      to_show+=("$record")  # add record to array
1572    done < "$path_mappings"
1573  fi
1574
1575  for line in "${to_show[@]}"; do
1576    local filename
1577    local path
1578    filename=$(_get_record_filename "$line")
1579    path=$(_append_root_path "$filename")
1580
1581    # The parameters are: filename, write-to-file, force, homedir, passphrase, error_ok
1582    _decrypt "$path" "1" "$force" "$homedir" "$passphrase" "$force_continue"
1583
1584    if [[ ! -f "$path" ]]; then
1585      _warn_or_abort "cannot find decrypted version of file: $filename" "2" "$force_continue"
1586    else
1587      counter=$((counter+1))
1588      local secret_file
1589      secret_file=$(_get_encrypted_filename "$path")
1590      if [[ "$preserve" == 1 ]] && [[ -f "$secret_file" ]]; then
1591        local perms
1592        perms=$($SECRETS_OCTAL_PERMS_COMMAND "$secret_file")
1593        chmod "$perms" "$path"
1594      fi
1595    fi
1596
1597  done < "$path_mappings"
1598
1599  echo "done. $counter of ${#to_show[@]} files are revealed."
1600}
1601#!/usr/bin/env bash
1602
1603# shellcheck disable=2016
1604AWK_GPG_KEY_CNT='
1605BEGIN { cnt=0; OFS=":"; FS=":"; }
1606flag=0; $1 == "pub" { cnt++ }
1607END { print cnt }
1608'
1609
1610function get_gpg_key_count {
1611  local secrets_dir_keys
1612  secrets_dir_keys=$(_get_secrets_dir_keys)
1613  $SECRETS_GPG_COMMAND --homedir "$secrets_dir_keys" --no-permission-warning --list-public-keys --with-colon | gawk "$AWK_GPG_KEY_CNT"
1614  local exit_code=$?
1615  if [[ "$exit_code" -ne 0 ]]; then
1616    _abort "problem counting keys with gpg: exit code $exit_code"
1617  fi
1618}
1619
1620function tell {
1621  local emails
1622  local self_email=0
1623  local homedir
1624
1625  # A POSIX variable
1626  # Reset in case getopts has been used previously in the shell.
1627  OPTIND=1
1628
1629  while getopts "hmd:" opt; do
1630    case "$opt" in
1631      h) _show_manual_for "tell";;
1632
1633      m) self_email=1;;
1634
1635      d) homedir=$OPTARG;;
1636
1637      *) _invalid_option_for 'tell';;
1638    esac
1639  done
1640
1641  shift $((OPTIND-1))
1642  [ "$1" = "--" ] && shift
1643
1644  # Validates that application is inited:
1645  _secrets_dir_exists
1646
1647  # Command logic:
1648  emails=( "$@" )
1649  local git_email
1650
1651  if [[ "$self_email" -eq 1 ]]; then
1652    git_email=$(git config user.email)
1653
1654    if [[ -z "$git_email" ]]; then
1655      _abort "'git config user.email' is not set."
1656    fi
1657
1658    emails+=("$git_email")
1659  fi
1660
1661  if [[ "${#emails[@]}" -eq 0 ]]; then
1662    # If after possible addition of git_email, emails are still empty,
1663    # we should raise an exception.
1664    _abort "you must provide at least one email address."
1665  fi
1666
1667  _assert_keychain_contains_emails "$homedir" "${emails[@]}"
1668
1669  local start_key_cnt
1670  start_key_cnt=$(get_gpg_key_count)
1671  for email in "${emails[@]}"; do
1672    # This file will be removed automatically:
1673    _temporary_file  # note, that `_temporary_file` will export `filename` var.
1674    # shellcheck disable=2154
1675    local keyfile="$filename"
1676
1677    local exit_code
1678    if [[ -z "$homedir" ]]; then
1679      $SECRETS_GPG_COMMAND --export -a "$email" > "$keyfile"
1680      exit_code=$?
1681    else
1682      # It means that homedir is set as an extra argument via `-d`:
1683      $SECRETS_GPG_COMMAND --no-permission-warning --homedir="$homedir" \
1684        --export -a "$email" > "$keyfile"
1685      exit_code=$?
1686    fi
1687    if [[ "$exit_code" -ne 0 ]]; then
1688      _abort "problem exporting public key for '$email' with gpg: exit code $exit_code"
1689    fi
1690
1691    if [[ ! -s "$keyfile" ]]; then
1692      _abort "no keyfile found for '$email'. Check your key name: 'gpg --list-keys'."
1693    fi
1694
1695    # Importing public key to the local keychain:
1696    local secrets_dir_keys
1697    secrets_dir_keys=$(_get_secrets_dir_keys)
1698    $SECRETS_GPG_COMMAND --homedir "$secrets_dir_keys" --no-permission-warning --import "$keyfile" > /dev/null 2>&1
1699    exit_code=$?
1700    if [[ "$exit_code" -ne 0 ]]; then
1701      _abort "problem importing public key for '$email' with gpg: exit code $exit_code"
1702    fi
1703  done
1704
1705  echo "done. ${emails[*]} added as someone who know(s) the secret."
1706
1707  # force re-encrypting of files if required
1708  local fsdb
1709  local end_key_cnt
1710  fsdb=$(_get_secrets_dir_paths_mapping)
1711  end_key_cnt=$(get_gpg_key_count)
1712  [[ $start_key_cnt -ne $end_key_cnt ]] && _fsdb_clear_hashes "$fsdb"
1713}
1714#!/usr/bin/env bash
1715
1716
1717function usage {
1718  OPTIND=1
1719
1720  while getopts "h?" opt; do
1721    case "$opt" in
1722      h) _show_manual_for "usage";;
1723
1724      *) _invalid_option_for "usage";;
1725    esac
1726  done
1727
1728  shift $((OPTIND-1))
1729  [ "$1" = "--" ] && shift
1730
1731  # There was a bug with some shells, which were adding extra commands
1732  # to the old dynamic-loading version of this code.
1733  # thanks to @antmak it is now fixed, see:
1734  # https://github.com/sobolevn/git-secret/issues/47
1735  local commands="add|cat|changes|clean|hide|init|killperson|list|remove|reveal|tell|usage|whoknows"
1736
1737  echo "usage: git secret [--version] [$commands]"
1738  echo " 'git secret --version' will show version and exit"
1739  echo "See 'git secret [command] -h' for more info about commands and their options"
1740  echo " add [filename.txt] - adds file to be hidden, optionally adds file to .gitignore"
1741  echo " cat [filename.txt] - cats the decrypted contents of the named file to stdout"
1742  echo " changes [filename.secret] - indicates if the file has changed since checkin"
1743  echo " clean - deletes encrypted files"
1744  echo " hide - encrypts (or re-encrypts) the files to be hidden"
1745  echo " init - creates the .gitsecret directory and contents needed for git-secret"
1746  echo " killperson [emails] - the reverse of 'tell', removes access for the named user"
1747  echo " list - shows files to be hidden/encrypted, as in .gitsecret/paths/mapping.cfg"
1748  echo " remove [files] - removes files from list of hidden files"
1749  echo " reveal - decrypts all hidden files, as mentioned in 'git secret list'"
1750  echo " tell [email] - add access for the user with imported public key with email"
1751  echo " whoknows - shows list of email addresses associated with public keys that can reveal files"
1752}
1753#!/usr/bin/env bash
1754
1755
1756function whoknows {
1757  OPTIND=1
1758
1759  local long_display=0
1760  while getopts "hl?" opt; do
1761    case "$opt" in
1762      h) _show_manual_for "whoknows";;
1763
1764      l) long_display=1;;   # like ls -l
1765
1766      *) _invalid_option_for "whoknows";;
1767    esac
1768  done
1769
1770  shift $((OPTIND-1))
1771  [ "$1" = "--" ] && shift
1772
1773  # Validating, that we have a user:
1774  _user_required
1775
1776  local users
1777
1778  # Getting the users from gpg:
1779  users=$(_get_users_in_gitsecret_keyring)
1780  for user in $users; do
1781      echo -n "$user"
1782
1783      if [[ "$long_display" -eq 1 ]]; then
1784        local expiration
1785        expiration=$(_get_user_key_expiry "$user")
1786        if [[ -n "$expiration"  ]]; then
1787          local expiration_date
1788          expiration_date=$($SECRETS_EPOCH_TO_DATE "$expiration")
1789          echo -n " (expires: $expiration_date)"
1790        else
1791          echo -n " (expires: never)"
1792        fi
1793      fi
1794
1795      echo
1796  done
1797}
1798#!/usr/bin/env bash
1799
1800set -e
1801
1802function _check_setup {
1803  # Checking git and secret-plugin setup:
1804  local is_tree
1805  is_tree=$(_is_inside_git_tree)
1806  if [[ "$is_tree" -ne 0 ]]; then
1807    _abort "not in dir with git repo. Use 'git init' or 'git clone', then in repo use 'git secret init'"
1808  fi
1809
1810  # Checking if the '.gitsecret' dir (or as set by SECRETS_DIR) is not ignored:
1811  _secrets_dir_is_not_ignored
1812
1813  # Checking gpg setup:
1814  local keys_dir
1815  keys_dir=$(_get_secrets_dir_keys)
1816
1817  local secring="$keys_dir/secring.gpg"
1818  if [[ -f $secring ]] && [[ -s $secring ]]; then
1819    # secring.gpg exists and is not empty,
1820    # someone has imported a private key.
1821    _abort 'it seems that someone has imported a secret key.'
1822  fi
1823}
1824
1825
1826function _incorrect_usage {
1827  echo "git-secret: abort: $1"
1828  usage
1829  exit "$2"
1830}
1831
1832
1833function _show_version {
1834  echo "$GITSECRET_VERSION"
1835  exit 0
1836}
1837
1838
1839function _init_script {
1840  if [[ $# == 0 ]]; then
1841    _incorrect_usage 'no input parameters provided.' 126
1842  fi
1843
1844  # Parse plugin-level options:
1845  local dry_run=0
1846
1847  while [[ $# -gt 0 ]]; do
1848    local opt="$1"
1849
1850    case "$opt" in
1851      # Options for quick-exit strategy:
1852      --dry-run)
1853        dry_run=1
1854        shift;;
1855
1856      --version) _show_version;;
1857
1858      *) break;;  # do nothing
1859    esac
1860  done
1861
1862  if [[ "$dry_run" == 0 ]]; then
1863    # Checking for proper set-up:
1864    _check_setup
1865
1866    # Routing the input command:
1867    local function_exists
1868    function_exists=$(_function_exists "$1")
1869
1870    if [[ "$function_exists" == 0 ]] && [[ ! $1 == _* ]]; then
1871      $1 "${@:2}"
1872    else  # TODO: elif [[ $(_plugin_exists $1) == 0 ]]; then
1873      _incorrect_usage "command $1 not found." 126
1874    fi
1875  fi
1876}
1877
1878
1879_init_script "$@"
1880