stash

Simple password manager shell script
Log | Files | Refs | README

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