pashage
Yet Another Opinionated Re-engineering of the Unix Password Store
Core objectives:
- same interface and similar feature set as pass
- simplicity, understandability, and hackability, from using POSIX shell, like pash
- age as encryption backend, like passage
- validation using shellcheck and shellspec tests
Portability is not a core objective, but a nice side-effect of using basic POSIX shell, and it is embraced when possible.
Security is not branded as a core objective, because the author does not have the clout to declare anything secure, and you should probably not trust random READMEs anyway. However the simplicity should help you assess whether this password store is a worthwhile trade-off for your threat model.
For the reference, the author has views similar to those of Filippo Valsorda and considers the password store shell script to be about as critical as the rest of her computer, and relies mostly on age to provide secure encryption at rest and on a YubiKey to gatekeep decryption.
Licencing
This project was written from scratch, and every character of the script was typed with my fingers. However I looked deeply into pass, passage, and pash code bases. I don't know whether that's enough to make it a derivative work covered by the GPL, so to be on the safe side I'm using GPL v2+ too.
Differences with pass
Behavior Differences
Not using a terminal does not imply
--force, insteadpashageasks for a confirmingyon a standard input line.When copying a secret to the clipboard, the script keeps running while waiting for the automatic clearing. This provides a user-facing cue that the secret may still be the clipboard and allows to clear the clipboard earlier.
The commands
copy,edit,insert,list,move, andshowaccept multiple arguments to operate on many secrets at once.The commands
copyandmovealso operate on unencrypted files in the password store.The
editcommand does not warn a about using/tmprather than/dev/shm, because the warning does not seem actionable and quickly becomes ignored noise.The
editcommand uses$VISUALrather than$EDITORwhen it set and the terminal is not dumb.The
findcommand search-pattern is a regular expression rather than a glob.The
initcommand is redesigned to accommodateagebackend. I didn't really understand the originalinitcommand, so I'm not sure how different it is; but now it installs.age-recipientsand re-encrypts.The
insertcommand makes the user try again when entering mismatching passwords.
New Features and Extensions
The commands
copyandmovehave new flags to control re-encryption (always, never, ask for each file).The
generatecommand has a new command-line argument to specify explicitly the character set.The
generatecommand optionally asks for confirmation before storing the generated secret (e.g. for iterative attempts against stupid password rules).The
generatecommand optionally asks for extra lines to append after the generated secret (e.g. for username, login page, or others comments).The
initcommand has new flags to control re-encryption (never or ask for each file).The new
gitconfigcommand configures an existing store repository to decrypt beforediff.The new
randomcommand leverages password generation without touching the password store.The new
reencryptcommand re-encrypts secrets in-place.
Roadmap
The following features are currently under consideration:
- v1.0.0:
- completion for various shells
- better logic for recursivity in re-encryption
- v1.1.0:
- partial display of secrets on standard output
- successive clipboard copy of several lines from a single decryption (e.g. username then password)
- maybe/later:
- rewriting of git history to purge old cyphertexts
- OTP support
- extension support
Manual
pashage is a password manager, which means it manages a database of encrypted secrets, including encrypting externally-provided new secrets, generating and encrypting random strings, and decrypting and displaying stored secrets.
It aims to be simple and composable, but its reliance on Unix philosophy and customs might make steep learning curve for users outside of this culture.
It is used through a shell command, denoted as pashage in this document,
immediately followed by a subcommand and its arguments. When no subcommand
is specified, list or show is implicitly assumed.
The database is optionally versioned using git to help with history audit and synchronization. It should be noted that this prevents re-encryption from erasing old cyphertext, leaving the secret vulnerable to compromised encryption keys.
The cryptography is done by age external command. It decrypts using the identity file given in the environment, and crypts using a list of recipients per subfolder, defaulting to the parent recipient list or the identity.
Command Reference
Here is an alphabetical list of all subcommands and aliases:
--help: alias for help--version: alias for version-h: alias for helpcopycp: alias for copydeleteeditfindgen: alias for generategenerategitgitconfiggrephelpinitinsertlistls: alias for listmovemv: alias for moverandomre-encrypt: alias for reencryptreencryptremove: alias for deleterm: alias for deleteshowversion
copy
Syntax:
pashage copy [--reencrypt,-e | --interactive,-i | --keep,-k ]
[--force,-f] old-path ... new-path
This subcommand copies secrets and recursively copies subfolders,
using the same positional argument scheme as cp(1).
By default it asks before overwriting an existing secret and it re-encrypts
the secret when the destination has a different recipient list.
Flags:
-eor--reencrypt: always re-encrypt secrets-for--force: overwrite existing secrets without asking-ior--interactive: asks whether to re-encrypt or not for each secret-kor--keep: never re-encrypt secrets
Environment:
PASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
delete
Syntax:
pashage delete [--recursive,-r] [--force,-f] pass-name ...
This subcommand deletes secrets from the database. By default it skips subfolders and asks for confirmation for each secret.
Flags:
-for--force: delete without asking for confirmation-ror--recursive: recursively delete all secrets in given subfolders
Environment:
PASHAGE_DIR: database directory to use instead of~/.passage/storePASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
edit
Syntax:
pashage edit pass-name ...
This subcommand starts an interactive editor to update the secrets.
Environment:
EDITOR: editor command to use instead ofviwhenVISUALis not setPASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unsetTMPDIR: temporary directory for the decrypted file to use instead of/tmpwhen/dev/shmis not availableVISUAL: editor command to use instead ofvi
find
Syntax:
pashage find [GREP_OPTIONS] regex
This subcommand lists as a tree the secrets whose name match the given
regular expression, using the corresponding grep(1) options.
Environment:
CLICOLOR: when set to a non-empty value, use ANSI escape sequences to color the outputLC_CTYPE: when it containsUTF, the tree is displayed using Unicode graphic characters instead of ASCIIPASHAGE_DIR: database directory to use instead of~/.passage/storePASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
generate
Syntax:
pashage generate [--no-symbols,-n] [--clip,-c | --qrcode,-q]
[--in-place,-i | --force,-f] [--multiline,-m]
[--try,-t] pass-name [pass-length [character-set]]
This subcommand generates a new secret from /dev/urandom, stores it in
the database, and by default displays it on the standard output and asks
for confirmation before overwriting an existing secret.
Flags:
-cor--clip: paste the secret into the clipboard instead of using the standard output-for--force: replace existing secrets without asking-ior--in-place: when the secret already exists, replace only its first line and re-use the following lines-mor--multiline: read lines from standard input append after the generated data into the secret file-nor--no-symbols: generate a secret using only alphanumeric characters-qor--qrcode: display the secret as a QR-code instead of using the standard output-tor--try: display the secret and ask for confirmation before storing it into the database
Environment:
CLICOLOR: when set to a non-empty value, use ANSI escape sequences to color the outputPASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS: default character set to use withtr(1)when-nis specified, instead of[:alnum:]PASSWORD_STORE_CHARACTER_SET: default character set to use withtr(1)when-nis not specified, instead of[:punct:][:alnum:]PASSWORD_STORE_CLIP_TIME: number of second before clearing the clipboard when-cis used, instead of 45PASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unsetPASSWORD_STORE_GENERATED_LENGTH: number of characters in the generated secret when not explicitly given, instead of 25PASSWORD_STORE_X_SELECTION: selection to use when-candxclipare used, instead ofclipboard
git
Syntax:
pashage git git-command-args ...
This subcommand invokes git in the database repository.
Only git init and git clone are accepted when there is no underlying
repository.
Environment:
PASHAGE_DIR: database directory to use instead of~/.passage/storePASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
gitconfig
Syntax:
pashage gitconfig
This subcommand configures the underlying repository to automatically decrypt secrets to display differences.
Environment:
PASHAGE_DIR: database directory to use instead of~/.passage/storePASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
grep
Syntax:
pashage grep [GREP_OPTIONS] search-regex
This subcommand successively decrypts all the secrets in the store and
filter them through grep(1) using the given options, and outputs all the
matching lines and the corresponding secret.
Environment:
CLICOLOR: when set to a non-empty value, use ANSI escape sequences to color the outputPASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
help
Syntax:
pashage help
This subcommand displays on the standard output the version and help text, including all subcommands and flags and a brief description.
This subcommand is not affected by the environment.
init
Syntax:
pashage init [--interactive,-i | --keep,-k ]
[--path=subfolder,-p subfolder] age-recipient ...
This subcommand initializes an age recipient list, by default of the root of the password store, and re-encrypts all the affected secrets. When the recipient list is a single empty string, the recipient list is instead removed, falling back to a parent recipient list or ultimately to the age identity.
Flags:
-ior--interactive: ask for each secret whether to re-encrypt it or not-kor--keep: do not re-encrypt any secret-por--path: operate on the recipient list in the given subfolder instead of the root of the password store
Environment:
PASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
insert
Syntax:
pashage insert [--echo,-e | --multiline,-m] [--force,-f] pass-name ...
This subcommand adds new secrets in the database, using the provided data from the standard input. By default asks before overwriting an existing secret, and it reads a single secret line after turning off the console echo, and reads it a second time for confirmation.
Flags:
-eor--echo: read a single line once without manipulating the standard input-mor--multiline: an arbitrary amount of lines from the standard input, without trying to manipulate the console, until the end of input or a blank line is entered-for--force: overwrite an existing secret without asking
Environment:
PASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
list
Syntax:
pashage [list] [subfolder ...]
This subcommand displays the given subfolders as a tree, or the whole store when no subfolder is specified.
Note that when a secret is given instead of a subfolder, the show command will be used instead, without any warning or error.
Environment:
CLICOLOR: when set to a non-empty value, use ANSI escape sequences to color the outputLC_CTYPE: when it containsUTF, the tree is displayed using Unicode graphic characters instead of ASCIIPASHAGE_DIR: database directory to use instead of~/.passage/storePASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
move
Syntax:
pashage move [--reencrypt,-e | --interactive,-i | --keep,-k ]
[--force,-f] old-path ... new-path
This subcommand moves or renames secrets and subfolders recursively,
using the same positional argument scheme as mv(1).
By default it asks before overwriting an existing secret and it re-encrypts
the secret when the destination has a different recipient list.
Flags:
-eor--reencrypt: always re-encrypt secrets-for--force: overwrite existing secrets without asking-ior--interactive: asks whether to re-encrypt or not for each secret-kor--keep: never re-encrypt secrets
Environment:
PASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
random
Syntax:
pashage random [pass-length [character-set]]
This subcommand generates a new secret, like the generate subcommand, then directly displays on the standard output without storing it.
Environment:
PASSWORD_STORE_CHARACTER_SET: character set to use withtr(1)whencharacter-setis not specified, instead of[:punct:][:alnum:]PASSWORD_STORE_GENERATED_LENGTH: number of characters in the generated secret when not explicitly given, instead of 25
reencrypt
Syntax:
pashage reencrypt [--interactive,-i] pass-name|subfolder ...
This subcommand re-encrypts in place the given secrets, and all the secrets recursively in the given subfolders.
Flags:
-ior--interactive: asks whether to re-encrypt or not for each secret
Environment:
PASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
show
Syntax:
pashage [show] [--clip[=line-number],-c[line-number] |
--qrcode[=line-number],-q[line-number]] pass-name ...
This subcommand decrypts the given secrets and by default displays the whole text on the standard output.
Note that when a subfolder is given instead of a secret, the list command will be used instead, without any warning or error.
Flags:
-cor--clip: paste the given line (by default the first line) of the secret into the clipboard instead of using the standard output-qor--qrcode: display the given line (by default the first line) of the secret as a QR-code instead of using the standard output
Environment:
PASHAGE_AGE: external command to use instead ofagePASHAGE_DIR: database directory to use instead of~/.passage/storePASHAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitiesPASSAGE_AGE: external command to use instead ofagewhenPASHAGE_AGEis unsetPASSAGE_DIR: database directory to use instead of~/.passage/storewhenPASHAGE_DIRis unsetPASSAGE_IDENTITIES_FILE: identity file to use instead of~/.passage/identitieswhenPASHAGE_IDENTITIES_FILEis unsetPASSWORD_STORE_DIR: database directory to use instead of~/.passage/storewhen bothPASHAGE_DIRandPASSAGE_DIRare unset
version
Syntax:
pashage version
This subcommand displays on the standard output the version and author list.
This subcommand is not affected by the environment.