├── .editorconfig ├── .github └── workflows │ └── main.yml ├── LICENSE.md ├── README.md └── pash /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Force GitHub to display tabs 4 | # mixed with [4] spaces properly. 5 | [pash] 6 | indent_style = tab 7 | indent_size = 4 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Shellcheck 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Run shellcheck. 9 | run: shellcheck pash 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2019 Dylan Araps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pash 2 | 3 | A simple password manager using GPG written in POSIX `sh`. 4 | 5 | - Written in safe and [shellcheck](https://www.shellcheck.net/) compliant POSIX `sh`. 6 | - Only `120~` LOC (*minus blank lines and comments*). 7 | - Compatible with `pass`'s password store. 8 | - Clears the clipboard after a timeout. 9 | - Configurable password generation using `/dev/urandom`. 10 | - Guards against `set -x`, `ps` and `/proc` leakage. 11 | - Easily extendible through the shell. 12 | 13 | ## Table of Contents 14 | 15 | 16 | 17 | * [Dependencies](#dependencies) 18 | * [Usage](#usage) 19 | * [FAQ](#faq) 20 | * [How does this differ from `pass` or etc?](#how-does-this-differ-from-pass-or-etc) 21 | * [Where are passwords stored?](#where-are-passwords-stored) 22 | * [How can I use a public key?](#how-can-i-use-a-public-key) 23 | * [How do I change the password length?](#how-do-i-change-the-password-length) 24 | * [How do I change the password generation pattern?](#how-do-i-change-the-password-generation-pattern) 25 | * [How do I change the password store location?](#how-do-i-change-the-password-store-location) 26 | * [How do I change the clipboard tool?](#how-do-i-change-the-clipboard-tool) 27 | * [How do I change the clipboard timeout?](#how-do-i-change-the-clipboard-timeout) 28 | * [How do I rename an entry?](#how-do-i-rename-an-entry) 29 | * [How can I migrate from `pass` to `pash`?](#how-can-i-migrate-from-pass-to-pash) 30 | * [How can I extend `pash`?](#how-can-i-extend-pash) 31 | 32 | 33 | 34 | ## Dependencies 35 | 36 | - `gpg` or `gpg2` 37 | 38 | **Clipboard Support**: 39 | 40 | - `xclip` (*can be customized through `PASH_CLIP`*). 41 | 42 | 43 | ## Usage 44 | 45 | Examples: `pash add web/gmail`, `pash list`, `pash del google`, `pash show github`, `pash copy github`. 46 | 47 | ``` 48 | SYNOPSIS 49 | 50 | pash [ add|del|show|list|copy ] [name] 51 | 52 | COMMANDS 53 | 54 | [a]dd [name] - Create a new password entry. 55 | [c]opy [name] - Copy entry to the clipboard. 56 | [d]el [name] - Delete a password entry. 57 | [l]ist - List all entries. 58 | [s]how [name] - Show password for an entry. 59 | [t]ree - List all entries in a tree. 60 | 61 | OPTIONS 62 | 63 | Using a key pair: export PASH_KEYID=XXXXXXXX 64 | Password length: export PASH_LENGTH=50 65 | Password pattern: export PASH_PATTERN=_A-Z-a-z-0-9 66 | Store location: export PASH_DIR=~/.local/share/pash 67 | Clipboard tool: export PASH_CLIP='xclip -sel c' 68 | Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable) 69 | ``` 70 | 71 | ## FAQ 72 | 73 | ### How does this differ from `pass` or etc? 74 | 75 | I was looking for a CLI password manager (*written in shell*) and wasn't happy with the options I had found. They either had multiple instances of `eval` (*on user inputted data*), lots of unsafe shell (*nowhere near being `shellcheck` compliant.*) or they were overly complex. The opposites for what I'd want in a password manager. 76 | 77 | I decided to write my own. `pash` is written in POSIX `sh` and the codebase is minimal (*120~ LOC minus blank lines and comments*). 78 | 79 | ### Where are passwords stored? 80 | 81 | The passwords are stored in GPG encrypted files located at `${XDG_DATA_HOME:=$HOME/.local/share}/pash}`. 82 | 83 | 84 | ### How can I use a public key? 85 | 86 | Set the environment variable `PASH_KEYID` to the ID of the key you'd like to encrypt and decrypt passwords with. 87 | 88 | Example: 89 | 90 | ```sh 91 | # Default: 'unset'. 92 | export PASH_KEYID=XXXXXXXX 93 | 94 | # This can also be an email. 95 | export PASH_KEYID=dylan.araps@gmail.com 96 | 97 | # This can also be used as a one-off. 98 | PASH_KEYID=XXXXXXXX pash add github 99 | ``` 100 | 101 | ### How do I change the password length? 102 | 103 | Set the environment variable `PASH_LENGTH` to a valid integer. 104 | 105 | Example: 106 | 107 | ```sh 108 | # Default: '50'. 109 | export PASH_LENGTH=50 110 | 111 | # This can also be used as a one-off. 112 | PASH_LENGTH=10 pash add github 113 | ``` 114 | 115 | ### How do I change the password generation pattern? 116 | 117 | Set the environment variable `PASH_PATTERN` to a valid `tr` string. 118 | 119 | ```sh 120 | # Default: '_A-Z-a-z-0-9'. 121 | export PASH_PATTERN=_A-Z-a-z-0-9 122 | 123 | # This can also be used as a one-off. 124 | PASH_PATTERN=_A-Z-a-z-0-9 pash add hackernews 125 | ``` 126 | 127 | 128 | ### How do I change the password store location? 129 | 130 | Set the environment variable `PASH_DIR` to a directory. 131 | 132 | ```sh 133 | # Default: '~/.local/share/pash'. 134 | export PASH_DIR=~/.local/share/pash 135 | 136 | # This can also be used as a one-off. 137 | PASH_DIR=/mnt/drive/pash pash list 138 | ``` 139 | 140 | ### How do I change the clipboard tool? 141 | 142 | Set the environment variable `PASH_CLIP` to a command. 143 | 144 | **NOTE**: I advise that you disable clipboard history in managers like KDE's `klipper` before copying passwords through `pash`. Your Desktop Environment's clipboard manager may read entries from the X clipboard when `xclip` is used. 145 | 146 | **NOTE**: `pash` will correctly clear all clipboards which have history disabled. 147 | 148 | ```sh 149 | # Default: 'xclip -sel c'. 150 | export PASH_CLIP='xclip -sel c' 151 | 152 | # This can also be used as a one-off. 153 | PASH_CLIP='xclip -sel c' pash copy github 154 | ``` 155 | 156 | ### How do I change the clipboard timeout? 157 | 158 | Set the environment variable `PASH_TIMEOUT` to a valid `sleep` interval or `off` to disable the feature. 159 | 160 | ```sh 161 | # Default: '15' 162 | export PASH_TIMEOUT=15 163 | 164 | # Disable timeout. 165 | export PASH_TIMEOUT=off 166 | 167 | # This can also be used as a one-off. 168 | PASH_TIMEOUT=5 pash copy github 169 | ``` 170 | 171 | ### How do I rename an entry? 172 | 173 | It's a file! Standard UNIX utilities can be used here. 174 | 175 | 176 | ### How can I migrate from `pass` to `pash`? 177 | 178 | I cannot guarantee 100% compatibility with the stores from `pass` as `pash` wasn't written as a 1:1 replacement, however users have reported that `pash` does in fact work fine with `pass`' store. 179 | 180 | Add the following to your `.shellrc` or `.profile`. 181 | 182 | ``` 183 | read -r PASH_KEYID < "$PASH_DIR/.gpg-id" 184 | 185 | export PASH_DIR=${PASSWORD_STORE_DIR:-$HOME/.password-store} 186 | export PASH_KEYID 187 | ``` 188 | 189 | ### How can I extend `pash`? 190 | 191 | A shell function can be used to add new commands and functionality to `pash`. The following example adds `pash git` to execute `git` commands on the password store. 192 | 193 | ```sh 194 | pash() { 195 | case $1 in 196 | g*) 197 | cd "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" 198 | shift 199 | git "$@" 200 | ;; 201 | 202 | *) 203 | command pash "$@" 204 | ;; 205 | esac 206 | } 207 | ``` 208 | -------------------------------------------------------------------------------- /pash: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # pash - simple password manager. 4 | 5 | pw_add() { 6 | name=$1 7 | 8 | if yn "Generate a password?"; then 9 | # Generate a password by reading '/dev/urandom' with the 10 | # 'tr' command to translate the random bytes into a 11 | # configurable character set. 12 | # 13 | # The 'dd' command is then used to read only the desired 14 | # password length. 15 | # 16 | # Regarding usage of '/dev/urandom' instead of '/dev/random'. 17 | # See: https://www.2uo.de/myths-about-urandom 18 | pass=$(LC_ALL=C tr -dc "${PASH_PATTERN:-_A-Z-a-z-0-9}" < /dev/urandom | 19 | dd ibs=1 obs=1 count="${PASH_LENGTH:-50}" 2>/dev/null) 20 | 21 | else 22 | # 'sread()' is a simple wrapper function around 'read' 23 | # to prevent user input from being printed to the terminal. 24 | sread pass "Enter password" 25 | sread pass2 "Enter password (again)" 26 | 27 | # Disable this check as we dynamically populate the two 28 | # passwords using the 'sread()' function. 29 | # shellcheck disable=2154 30 | [ "$pass" = "$pass2" ] || die "Passwords do not match" 31 | fi 32 | 33 | [ "$pass" ] || die "Failed to generate a password" 34 | 35 | # Mimic the use of an array for storing arguments by... using 36 | # the function's argument list. This is very apt isn't it? 37 | if [ "$PASH_KEYID" ]; then 38 | set -- --trust-model always -aer "$PASH_KEYID" 39 | else 40 | set -- -c 41 | fi 42 | 43 | # Use 'gpg' to store the password in an encrypted file. 44 | # A heredoc is used here instead of a 'printf' to avoid 45 | # leaking the password through the '/proc' filesystem. 46 | # 47 | # Heredocs are sometimes implemented via temporary files, 48 | # however this is typically done using 'mkstemp()' which 49 | # is more secure than a leak in '/proc'. 50 | "$gpg" "$@" -o "$name.gpg" <<-EOF && 51 | $pass 52 | EOF 53 | printf '%s\n' "Saved '$name' to the store." 54 | } 55 | 56 | pw_del() { 57 | yn "Delete pass file '$1'?" && { 58 | rm -f "$1.gpg" 59 | 60 | # Remove empty parent directories of a password 61 | # entry. It's fine if this fails as it means that 62 | # another entry also lives in the same directory. 63 | rmdir -p "${1%/*}" 2>/dev/null || : 64 | } 65 | } 66 | 67 | pw_show() { 68 | "$gpg" -dq "$1.gpg" 69 | } 70 | 71 | pw_copy() { 72 | # Disable warning against word-splitting as it is safe 73 | # and intentional (globbing is disabled). 74 | # shellcheck disable=2086 75 | : "${PASH_CLIP:=xclip -sel c}" 76 | 77 | # Wait in the background for the password timeout and 78 | # clear the clipboard when the timer runs out. 79 | # 80 | # If the 'sleep' fails, kill the script. This is the 81 | # simplest method of aborting from a subshell. 82 | [ "$PASH_TIMEOUT" != off ] && { 83 | printf 'Clearing clipboard in "%s" seconds.\n' "${PASH_TIMEOUT:=15}" 84 | 85 | sleep "$PASH_TIMEOUT" || kill 0 86 | $PASH_CLIP /dev/null 2>&1 || 98 | die "'tree' command not found" 99 | 100 | tree --noreport | sed 's/\.gpg$//' 101 | } 102 | 103 | yn() { 104 | printf '%s [y/n]: ' "$1" 105 | 106 | # Enable raw input to allow for a single byte to be read from 107 | # stdin without needing to wait for the user to press Return. 108 | stty -icanon 109 | 110 | # Read a single byte from stdin using 'dd'. POSIX 'read' has 111 | # no support for single/'N' byte based input from the user. 112 | answer=$(dd ibs=1 count=1 2>/dev/null) 113 | 114 | # Disable raw input, leaving the terminal how we *should* 115 | # have found it. 116 | stty icanon 117 | 118 | printf '\n' 119 | 120 | # Handle the answer here directly, enabling this function's 121 | # return status to be used in place of checking for '[yY]' 122 | # throughout this program. 123 | glob "$answer" '[yY]' 124 | } 125 | 126 | sread() { 127 | printf '%s: ' "$2" 128 | 129 | # Disable terminal printing while the user inputs their 130 | # password. POSIX 'read' has no '-s' flag which would 131 | # effectively do the same thing. 132 | stty -echo 133 | read -r "$1" 134 | stty echo 135 | 136 | printf '\n' 137 | } 138 | 139 | glob() { 140 | # This is a simple wrapper around a case statement to allow 141 | # for simple string comparisons against globs. 142 | # 143 | # Example: if glob "Hello World" '* World'; then 144 | # 145 | # Disable this warning as it is the intended behavior. 146 | # shellcheck disable=2254 147 | case $1 in $2) return 0; esac; return 1 148 | } 149 | 150 | die() { 151 | printf 'error: %s.\n' "$1" >&2 152 | exit 1 153 | } 154 | 155 | usage() { printf %s "\ 156 | pash 2.3.0 - simple password manager. 157 | 158 | => [a]dd [name] - Create a new password entry. 159 | => [c]opy [name] - Copy entry to the clipboard. 160 | => [d]el [name] - Delete a password entry. 161 | => [l]ist - List all entries. 162 | => [s]how [name] - Show password for an entry. 163 | => [t]ree - List all entries in a tree. 164 | 165 | Using a key pair: export PASH_KEYID=XXXXXXXX 166 | Password length: export PASH_LENGTH=50 167 | Password pattern: export PASH_PATTERN=_A-Z-a-z-0-9 168 | Store location: export PASH_DIR=~/.local/share/pash 169 | Clipboard tool: export PASH_CLIP='xclip -sel c' 170 | Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable) 171 | " 172 | exit 0 173 | } 174 | 175 | main() { 176 | : "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}" 177 | 178 | # Look for both 'gpg' and 'gpg2', 179 | # preferring 'gpg2' if it is available. 180 | command -v gpg >/dev/null 2>&1 && gpg=gpg 181 | command -v gpg2 >/dev/null 2>&1 && gpg=gpg2 182 | 183 | [ "$gpg" ] || 184 | die "GPG not found" 185 | 186 | mkdir -p "$PASH_DIR" || 187 | die "Couldn't create password directory" 188 | 189 | cd "$PASH_DIR" || 190 | die "Can't access password directory" 191 | 192 | glob "$1" '[acds]*' && [ -z "$2" ] && 193 | die "Missing [name] argument" 194 | 195 | glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] && 196 | die "Pass file '$2' doesn't exist" 197 | 198 | glob "$1" 'a*' && [ -f "$2.gpg" ] && 199 | die "Pass file '$2' already exists" 200 | 201 | glob "$2" '*/*' && glob "$2" '*../*' && 202 | die "Category went out of bounds" 203 | 204 | glob "$2" '/*' && 205 | die "Category can't start with '/'" 206 | 207 | glob "$2" '*/*' && { mkdir -p "${2%/*}" || 208 | die "Couldn't create category '${2%/*}'"; } 209 | 210 | # Set 'GPG_TTY' to the current 'TTY' if it 211 | # is unset. Fixes a somewhat rare `gpg` issue. 212 | export GPG_TTY=${GPG_TTY:-$(tty)} 213 | 214 | # Restrict permissions of any new files to 215 | # only the current user. 216 | umask 077 217 | 218 | # Ensure that we leave the terminal in a usable 219 | # state on exit or Ctrl+C. 220 | [ -t 1 ] && trap 'stty echo icanon' INT EXIT 221 | 222 | case $1 in 223 | a*) pw_add "$2" ;; 224 | c*) pw_copy "$2" ;; 225 | d*) pw_del "$2" ;; 226 | s*) pw_show "$2" ;; 227 | l*) pw_list ;; 228 | t*) pw_tree ;; 229 | *) usage 230 | esac 231 | } 232 | 233 | # Ensure that debug mode is never enabled to 234 | # prevent the password from leaking. 235 | set +x 236 | 237 | # Ensure that globbing is globally disabled 238 | # to avoid insecurities with word-splitting. 239 | set -f 240 | 241 | [ "$1" ] || usage && main "$@" 242 | --------------------------------------------------------------------------------