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 (3331B)


      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 [ "$XDG_SESSION_TYPE" = 'x11' ] &&
     16 	CLIP='xclip -selection clipboard' ||
     17 	CLIP='wl-copy'
     18 
     19 # Loop through all the arguments and set flags/options.
     20 while [ "$#" -gt 0 ] ; do
     21 	case "$1" in
     22 		-c|--clip)
     23 			COPY=1
     24 			shift
     25 			;;
     26 		-i|--in-place)
     27 			INPLACE=1
     28 			shift
     29 			;;
     30 		-n|--no-symbols)
     31 			CHARACTERS='a-zA-Z0-9'
     32 			shift
     33 			;;
     34 		edit)
     35 			ACTION='edit'
     36 			shift
     37 			;;
     38 		generate)
     39 			ACTION='generate'
     40 			shift
     41 			;;
     42 		otp)
     43 			OTP=1
     44 			shift
     45 			;;
     46 		show)
     47 			ACTION='show'
     48 			shift
     49 			;;
     50 		*)
     51 			# Treat anything unidentified as the name of the password.
     52 			KEY="$1"
     53 			shift
     54 
     55 			# When generating a key, it may immediately be followed by
     56 			# an integer to set the length. The third line here
     57 			# verifies it's an integer.
     58 			[ "$ACTION" = 'generate' ] &&
     59 				[ -n "$1" ] &&
     60 				printf %d "$1" > /dev/null 2>&1 &&
     61 				LENGTH="$1" &&
     62 				shift
     63 			;;
     64 	esac
     65 done
     66 
     67 KEY_FILE="$STORE/$KEY.gpg"
     68 
     69 # Dynamically changing the value of ACTION allow us to simulate fall-throughs.
     70 while [ -n "$ACTION" ] ; do
     71 	case "$ACTION" in
     72 		edit)
     73 			# Create the path.
     74 			mkdir -p "${KEY_FILE%/*}"
     75 
     76 			# Decrypt to a temporary file.
     77 			# Set restrictive permissions on the tmp file just in case.
     78 			[ -f "$KEY_FILE" ] &&
     79 				gpg2 --quiet --output "$TMP_FILE" --decrypt "$KEY_FILE" &&
     80 				chmod 600 "$TMP_FILE"
     81 
     82 			# Allow the user to edit the temporary file,
     83 			# then encrypt it and delete the temp file.
     84 			"${EDITOR:-vi}" "$TMP_FILE"
     85 
     86 			[ -f "$TMP_FILE" ] &&
     87 				(
     88 					gpg2 --quiet --yes --encrypt \
     89 						--default-recipient-self \
     90 						--output "$KEY_FILE" "$TMP_FILE" \
     91 						2> /dev/null ||
     92 					echo "No changes..." ;
     93 					rm "$TMP_FILE"
     94 				)
     95 
     96 			ACTION=''
     97 			;;
     98 		generate)
     99 			# Create the path.
    100 			mkdir -p "${KEY_FILE%/*}"
    101 
    102 			# If generating in-place, get all but the first line of the existing file.
    103 			(
    104 				[ "$INPLACE" -eq 1 ] &&
    105 					gpg2 --quiet --decrypt "$KEY_FILE" |
    106 					sed '1d'
    107 			) |
    108 				# Get the value of the new password from /dev/urandom.
    109 				(
    110 					tr -d -c "$CHARACTERS" < /dev/urandom |
    111 						dd bs="$LENGTH" count=1 2> /dev/null &&
    112 						# Terminate the password with a line ending.
    113 						echo &&
    114 						# Append the rest of the existing file, if any.
    115 						cat
    116 				) |
    117 				gpg2 --quiet --encrypt --default-recipient-self --output "$KEY_FILE" 2> /dev/null
    118 
    119 			# Fall through to the "show" logic.
    120 			ACTION='show'
    121 			;;
    122 		show)
    123 			# Decrypt, extract the secret from the otpauth line, and pass it to oathtool.
    124 			if [ "$OTP" -eq 1 ] ; then
    125 				gpg2 --decrypt --quiet "$KEY_FILE" |
    126 					grep 'otpauth' |
    127 					sed 's/.*secret=\([a-zA-Z0-9]*\).*/\1/' |
    128 					oathtool --base32 --totp - |
    129 					([ "$COPY" -eq 1 ] &&
    130 						tr -d '\n' |
    131 						$CLIP ||
    132 						cat)
    133 			# Decrypt and get the first line.
    134 			else
    135 				gpg2 --decrypt --quiet "$KEY_FILE" |
    136 					([ "$COPY" -eq 1 ] &&
    137 						head -n 1 |
    138 						tr -d '\n' |
    139 						$CLIP ||
    140 						cat)
    141 			fi
    142 
    143 			# Launch a background job to clear the clipboard in 30 seconds.
    144 			[ "$COPY" -eq 1 ] &&
    145 				sleep 30 &&
    146 				$CLIP < /dev/null &
    147 
    148 			ACTION=''
    149 			;;
    150 	esac
    151 done