sta.sh (6550B)
1 #!/bin/sh 2 3 # Sta.sh --- Simple account management 4 # Copyright © 2025 Luke Willis <lukejw@loquat.dev> 5 # 6 # This file is part of Sta.sh. 7 # 8 # Sta.sh is free software: you can redistribute it and/or modify it under the 9 # terms of the GNU General Public License as published by the Free Software 10 # Foundation, either version 3 of the License, or (at your option) any later 11 # version. 12 # 13 # Sta.sh is distributed in the hope that it will be useful, but WITHOUT ANY 14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 15 # A PARTICULAR PURPOSE. See the GNU General Public License for more details. 16 # 17 # You should have received a copy of the GNU General Public License along with 18 # Sta.sh. If not, see <https://www.gnu.org/licenses/>. 19 20 DATADIR="$HOME/.stash" 21 CONFIGDIR="$XDG_CONFIG_HOME/stash" 22 23 TOFICONFIG="$CONFIGDIR/tofi/config" 24 25 RAWKEY="$DATADIR/key" 26 PUBKEY="$DATADIR/key.pub" 27 AGEKEY="$DATADIR/key.age" 28 29 # This could allow package maintainers (read: me) to set custom paths for each 30 # program used by Sta.sh. For now, I'm just assuming inputs are propogated. 31 : "${TOFI:=tofi}" 32 : "${AGE:=age}" 33 : "${AGEKEYGEN:=age-keygen}" 34 : "${OATHTOOL:=$OATHTOOL}" 35 36 # Accept a password from the user and output it 37 # No arguments 38 take_password() { 39 echo | $TOFI -c "$TOFICONFIG" \ 40 --prompt-text="Master password:" \ 41 --hide-input=true \ 42 --hidden-character="*" \ 43 --require-match=false 44 } 45 46 # Accept text from the user and output it 47 # $1 is prompt text 48 take_text() { 49 echo | $TOFI -c "$TOFICONFIG" \ 50 --require-match=false \ 51 --prompt-text="$1" 52 } 53 54 # Shortcut function to handle taking and encrypting a field 55 # $1 is login name 56 # $2 is field name 57 encrypt() { 58 # Create login if it doesn't exist 59 # 60 # You would think that this should be done in the create_login function but this 61 # actually prevents an empty login folder from being created 62 DIR="$DATADIR/logins/$1" 63 mkdir -p "$DIR" 64 OUT="$DIR/$2" 65 66 # Take input 67 INPUT=$(take_text "Enter $2: ") 68 69 # Ignore if blank 70 if [ -z "$INPUT" ]; then 71 echo "Blank field, ignoring..." 72 return 0 73 fi 74 75 # Encrypt it with the public key and store it in the login folder 76 # TODO: Require master password to override existing value 77 echo "$INPUT" | $AGE -e -R "$PUBKEY" -o "$OUT" 78 } 79 80 # Decrypt a field according to its special method. Trusts that the field exists 81 # $1 is key 82 # $2 is directory 83 # $3 is field 84 decrypt() { 85 # Decrypt field 86 VALUE=$(echo "$KEY" | $AGE -d -i - "$2/$3") 87 88 # Process value 89 case $3 in 90 "otp") 91 echo "$VALUE" | $OATHTOOL -b --totp - 92 ;; 93 # No special case, return raw value 94 *) 95 echo "$VALUE" 96 ;; 97 esac 98 } 99 100 # Copy a information from a specific login in the specified manner 101 # $1 is login name 102 # $2 is copy style (blank for default) 103 copy() { 104 DIR="$DATADIR/logins/$1" 105 106 # Decrypt master key 107 while true; do 108 PASS=$(take_password) 109 110 if [ -z "$PASS" ]; then 111 echo "Key decryption cancelled" 112 return 0 113 fi 114 115 # Attempt decryption 116 KEY=$(echo "$PASS" | $AGE -d "$AGEKEY") 117 status=$? 118 if [ "$status" -eq 0 ]; then 119 echo "Key decryption successful" 120 break 121 fi 122 123 # Incorrect password, loop again 124 echo "Key decryption failed" 125 done 126 127 # Handle copy style 128 case "$2" in 129 "") 130 # Default. Select one field to decrypt and copy. 131 FIELD=$(ls -1 $DIR | $TOFI -c "$TOFICONFIG" --prompt-text "Copy" --placeholder-text="field") 132 133 if [ -z "$FIELD" ]; then 134 echo "None selected, returning" 135 return 0 136 fi 137 138 # Decrypt and copy field 139 decrypt "$KEY" "$DIR" "$FIELD" | wl-copy -n 140 echo "Copied $FIELD" 141 ;; 142 "quick") 143 # Copy username, password and otp in that order (if they exist). 144 for FIELD in username password otp; do 145 if [ -e "$DIR/$FIELD" ]; then 146 # Decrypt and copy field, hanging until pasted or overwritten. 147 decrypt "$KEY" "$DIR" "$FIELD" | wl-copy -nof 148 echo "Pasted $FIELD" 149 fi 150 done 151 ;; 152 *) 153 echo "Unknown login style $2?" 154 ;; 155 esac 156 } 157 158 # Listing available logins, take a login name and modify its values. 159 # This will create a login and/or values if they do not already exist. 160 modify_logins() { 161 NAME=$(ls -1 "$DATADIR/logins" | $TOFI -c "$TOFICONFIG" --require-match=false --prompt-text="Modify" --placeholder-text="login") 162 163 # Ignore if no name is provided 164 if [ -z "$NAME" ]; then 165 echo "No login selected" 166 return 0 167 fi 168 169 while true; do 170 CHOICE=$(printf "username\npassword\notp" | $TOFI -c "$TOFICONFIG" --require-match=false --prompt-text="Set" --placeholder-text="field") 171 case "$CHOICE" in 172 "") 173 break 174 ;; 175 *) 176 encrypt "$NAME" "$CHOICE" 177 ;; 178 esac 179 done 180 181 echo 'Done!!!' 182 } 183 184 # Select from the available logins and perform the requested action 185 # $1 is passed to `copy` as $2 186 fetch_login() { 187 echo "Fetching login" 188 189 CHOICE=$(ls -1 "$DATADIR/logins" | $TOFI -c "$TOFICONFIG" --prompt-text "Select" --placeholder-text "login") 190 191 case $CHOICE in 192 "") echo "No login selected" ;; 193 *) 194 echo "$CHOICE selected" 195 copy $CHOICE $1 196 ;; 197 esac 198 } 199 200 # Initialize stash if data directory does not exist 201 if [ ! -d "$DATADIR" ]; then 202 # Don't attempt to initialize outside of a terminal 203 # TODO: Add ability to initialize through tofi 204 if [ ! -t 1 ]; then 205 echo | $TOFI -c "$TOFICONFIG" \ 206 --prompt-text "Please run \"sta.sh\" in the terminal to initialize it!" \ 207 --hide-input=true \ 208 --require-match=false 209 return 0 210 fi 211 212 echo "Initializing stash..." 213 214 mkdir "$DATADIR" 215 mkdir "$DATADIR/logins" 216 217 # Generate private and public keys 218 $AGEKEYGEN -o "$RAWKEY" 219 $AGEKEYGEN -y "$RAWKEY" > "$PUBKEY" 220 221 # Encrypt the private key with a master password chosen by the user 222 $AGE -e -p -o "$AGEKEY" "$RAWKEY" 223 rm "$RAWKEY" 224 225 echo "Initialized" 226 fi 227 228 # Parse argument and run respective function 229 case "$1" in 230 ""|fetch) fetch_login ;; 231 quick) fetch_login quick ;; 232 modify) modify_logins ;; 233 *) echo "Unknown command" ;; 234 esac