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