balrog

A partial drop-in replacement for pass and pass-otp written in POSIX shell.
git clone https://git.stjo.hn/balrog
Log | Files | Refs | README | LICENSE

balrog (3917B)


      1 #!/usr/bin/env sh
      2 
      3 STORE="$HOME/.password-store"
      4 
      5 ACTION='show'
      6 COPY=0
      7 OTP=0
      8 LENGTH=32
      9 INPLACE=0
     10 # Specify valid characters the long way because character classes are locale-dependent.
     11 CHARACTERS='a-zA-Z0-9!"#$%&'\''()*+,./:;<=>?@[]^_{|}~\\'
     12 TMP_FILE="$HOME/.balrogtmp"
     13 
     14 # Identify the system's clipboard utility.
     15 [ -n "$WAYLAND_DISPLAY" ] &&
     16 	CLIP='waycopy' ||
     17 	CLIP='xclip -selection clipboard'
     18 
     19 # The first argument should be the command.
     20 
     21 case "$1" in
     22 	edit)
     23 		ACTION='edit'
     24 		shift
     25 		;;
     26 	find|search)
     27 		ACTION='find'
     28 		shift
     29 		;;
     30 	generate)
     31 		ACTION='generate'
     32 		shift
     33 		;;
     34 	ls|list)
     35 		ACTION='ls'
     36 		shift
     37 		;;
     38 	otp)
     39 		OTP=1
     40 		shift
     41 		;;
     42 	show)
     43 		ACTION='show'
     44 		shift
     45 		;;
     46 	*)
     47 		ACTION='ls'
     48 		;;
     49 esac
     50 
     51 # Loop through all the arguments and set flags/options.
     52 
     53 while [ "$#" -gt 0 ] ; do
     54 	case "$1" in
     55 		-c|--clip)
     56 			COPY=1
     57 			shift
     58 			;;
     59 		-i|--in-place)
     60 			INPLACE=1
     61 			shift
     62 			;;
     63 		-n|--no-symbols)
     64 			CHARACTERS='a-zA-Z0-9'
     65 			shift
     66 			;;
     67 		*)
     68 			# Treat anything unidentified as the name of the password.
     69 			KEY="$1"
     70 			shift
     71 
     72 			# When generating a key, it may immediately be followed by
     73 			# an integer to set the length. The third line here
     74 			# verifies it's an integer.
     75 			[ "$ACTION" = 'generate' ] &&
     76 				[ -n "$1" ] &&
     77 				printf %d "$1" > /dev/null 2>&1 &&
     78 				LENGTH="$1" &&
     79 				shift
     80 			;;
     81 	esac
     82 done
     83 
     84 KEY_FILE="$STORE/$KEY.gpg"
     85 
     86 # Dynamically changing the value of ACTION allow us to simulate fall-throughs.
     87 while [ -n "$ACTION" ] ; do
     88 	case "$ACTION" in
     89 		edit)
     90 			# Create the path.
     91 			mkdir -p "${KEY_FILE%/*}"
     92 
     93 			# Decrypt to a temporary file.
     94 			# Set restrictive permissions on the tmp file just in case.
     95 			[ -f "$KEY_FILE" ] &&
     96 				gpg2 --quiet --output "$TMP_FILE" --decrypt "$KEY_FILE" &&
     97 				chmod 600 "$TMP_FILE"
     98 
     99 			# Allow the user to edit the temporary file,
    100 			# then encrypt it and delete the temp file.
    101 			"${EDITOR:-vi}" "$TMP_FILE"
    102 
    103 			[ -f "$TMP_FILE" ] &&
    104 				(
    105 					gpg2 --quiet --yes --encrypt \
    106 						--default-recipient-self \
    107 						--output "$KEY_FILE" "$TMP_FILE" \
    108 						2> /dev/null ||
    109 					echo "No changes..." ;
    110 					rm "$TMP_FILE"
    111 				)
    112 
    113 			ACTION=''
    114 			;;
    115 		find)
    116 			find $STORE -type f -path "*$KEY*" |
    117 				sed -e "s|$STORE/||" -e 's/\.gpg$//'
    118 
    119 			ACTION=''
    120 			;;
    121 		generate)
    122 			# Create the path.
    123 			mkdir -p "${KEY_FILE%/*}"
    124 
    125 			# If generating in-place, get all but the first line of the existing file.
    126 			(
    127 				[ "$INPLACE" -eq 1 ] &&
    128 					gpg2 --quiet --decrypt "$KEY_FILE" |
    129 					sed '1d'
    130 			) |
    131 				# Get the value of the new password from /dev/urandom.
    132 				(
    133 					tr -d -c "$CHARACTERS" < /dev/urandom |
    134 						dd bs="$LENGTH" count=1 2> /dev/null &&
    135 						# Terminate the password with a line ending.
    136 						echo &&
    137 						# Append the rest of the existing file, if any.
    138 						cat
    139 				) |
    140 				gpg2 --quiet --encrypt --default-recipient-self --output "$KEY_FILE" 2> /dev/null
    141 
    142 			# Fall through to the "show" logic.
    143 			ACTION='show'
    144 			;;
    145 		ls)
    146 			if [ -e "$KEY_FILE" ] ; then
    147 				# Show the password if the key file exists.
    148 				ACTION='show'
    149 			else
    150 				# Otherwise find all files in the path specified.
    151 				# `tree` is not a POSIX utility so I'm just using `find` here.
    152 				find "$STORE/$KEY" -type f |
    153 					sed -e "s|$STORE/||" -e 's/\.gpg$//'
    154 				ACTION=''
    155 			fi
    156 
    157 			;;
    158 		show)
    159 			# Decrypt, extract the secret from the otpauth line, and pass it to oathtool.
    160 			if [ "$OTP" -eq 1 ] ; then
    161 				gpg2 --decrypt --quiet "$KEY_FILE" |
    162 					grep 'otpauth' |
    163 					sed 's/.*secret=\([a-zA-Z0-9]*\).*/\1/' |
    164 					oathtool --base32 --totp - |
    165 					([ "$COPY" -eq 1 ] &&
    166 						tr -d '\n' |
    167 						$CLIP ||
    168 						cat)
    169 			# Decrypt and get the first line.
    170 			else
    171 				gpg2 --decrypt --quiet "$KEY_FILE" |
    172 					([ "$COPY" -eq 1 ] &&
    173 						head -n 1 |
    174 						tr -d '\n' |
    175 						$CLIP ||
    176 						cat)
    177 			fi
    178 
    179 			# Launch a background job to clear the clipboard in 30 seconds.
    180 			[ "$COPY" -eq 1 ] &&
    181 				sleep 30 &&
    182 				$CLIP < /dev/null &
    183 
    184 			ACTION=''
    185 			;;
    186 	esac
    187 done