├── .shellspec ├── .gitignore ├── spec ├── env.sh ├── testdata │ └── store │ │ ├── directory1 │ │ ├── file1.age │ │ ├── file2.age │ │ └── file3.age │ │ └── directory2 │ │ ├── file1.age │ │ └── file2.age ├── passage_generate_spec.sh └── passage_show_spec.sh ├── LICENSE ├── Makefile ├── passage.1.adoc ├── README ├── passage.1 └── passage.in /.shellspec: -------------------------------------------------------------------------------- 1 | --env-from spec/env.sh 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | passage 2 | 3 | spec/testdata/store/tmp 4 | 5 | config.mk 6 | -------------------------------------------------------------------------------- /spec/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PATH="${PWD}:${PATH}" 4 | export XDG_CONFIG_HOME="${PWD}/spec/testdata/config" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | passage is in the public domain. 2 | 3 | To the extent possible under law, Kylie McClain 4 | has waived all copyright and related or neighboring rights to this work. 5 | 6 | http://creativecommons.org/publicdomain/zero/1.0/ 7 | 8 | -------------------------------------------------------------------------------- /spec/testdata/store/directory1/file1.age: -------------------------------------------------------------------------------- 1 | -----BEGIN AGE ENCRYPTED FILE----- 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvMTErMjdjUk9ZdGF5a2pC 3 | TXpGN1BGenZxVmE2SU9mbVhGRDE3TDluSlhvCmxJbGxtbWIzUE9xQU1wc2xzcE9t 4 | RnROekVRYnc3U3V2bENHL2cvSE9IZVkKLS0tIEYvWVVaZWNxTUgyQmgzdzZoREZT 5 | dlNFMEpEUkdTalZWL29Ld2UzdmFJT0UKvwAQQ4C/FsiGt4VvyDPnqIp72ZzzXPVC 6 | 4FtxAqwNxLAvPD7UJbGiiUrb/m1pT2VJUVxOM5kHs/U= 7 | -----END AGE ENCRYPTED FILE----- 8 | -------------------------------------------------------------------------------- /spec/testdata/store/directory1/file2.age: -------------------------------------------------------------------------------- 1 | -----BEGIN AGE ENCRYPTED FILE----- 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSOU1lQmtXS3N6SVFLTUtU 3 | MHB4ZmlQNzhnZ2dNV000OENBZWxsOHloRkFJCnlCWGJHSkRndDFON2N0WUsxVmpE 4 | U3VOSjBCU3Y5ZjhLSmVrNVg2UEU2ajgKLS0tIGZxOE5lbXlFUitDOWd2NzIyRW5E 5 | ekdqT0NNQks5cE5CbG9idVZKTnZkOVUKJJXRC5DeTvIluP3w0b9GWRmpHP8HC0pQ 6 | 88OII/D9ZqxRCNOk38h8rZN/U3kD0J7ovBZcuYoXEpo= 7 | -----END AGE ENCRYPTED FILE----- 8 | -------------------------------------------------------------------------------- /spec/testdata/store/directory1/file3.age: -------------------------------------------------------------------------------- 1 | -----BEGIN AGE ENCRYPTED FILE----- 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3YWlCa3YrUUF0ZE1nRHNx 3 | UHhzK0R1MzRXRGp0N3lPa3ErWGoyeW5JeFFVCkVHSkdKSU0yby9aK1dERHZaRkZ1 4 | d3VXcFJrM3BadWZuelJqTTB1OWhzL1UKLS0tIHBSZUpTMDZyQ1ZOYWR6anlSUzYx 5 | eS9aTXd0bXg5NmZsdEtnUEtHM3FnVXMK2Rgwecho0AtPOtEB7YmtvclevSPFwzcQ 6 | P97zV9d4tUhTckkDwEOzDprF/ivQOclpbuO3hcU7Dc9s 7 | -----END AGE ENCRYPTED FILE----- 8 | -------------------------------------------------------------------------------- /spec/testdata/store/directory2/file1.age: -------------------------------------------------------------------------------- 1 | -----BEGIN AGE ENCRYPTED FILE----- 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArMlUzak81MC8zajFoQ0Y4 3 | NEN2bEJub1lBRzh0VzIvajdwSVBEbGs1clJnCkpHdGhnRnkzUVVHNk9wTVBFVXFS 4 | QnNuTWVKZkJkTTB1SW83d2s0L3NRRlEKLS0tIEQxZ25veW1QTjg2bWNhZkZuM3Bl 5 | NU9lSEpuakdXV1ZiRi9XMnhCT3lzck0Kwezum853X+qCjg4a0UpP+QG+mnYN6FV1 6 | Ks8wOc0xnoLr9JKrLA/kac2dLnPpcZQnxlHMr3jbXtTH 7 | -----END AGE ENCRYPTED FILE----- 8 | -------------------------------------------------------------------------------- /spec/testdata/store/directory2/file2.age: -------------------------------------------------------------------------------- 1 | -----BEGIN AGE ENCRYPTED FILE----- 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TjFHZ1NqWnpDd28zMjln 3 | OG9nR1FZWjdLdWpmVUp1K0Rld2lvRUZmNkQwCkFnc1VjUlY2VHFIeG8rd1NvaWhZ 4 | WWl5cm9JRGdPVTlZazFCeEdUYnVTeUEKLS0tIGdobU1Ma2VzN2RqSmhFNHh2UFoy 5 | YmpuaU1wSkFObnhnN2FVdjlldDRXdHMKZlyrzBregMrllaXPzi+QE6DISEjnEy7M 6 | WUMt87ROuem/d0QgfAT8l3ASQoPLEFJl2HfKrTxGUohc 7 | -----END AGE ENCRYPTED FILE----- 8 | -------------------------------------------------------------------------------- /spec/passage_generate_spec.sh: -------------------------------------------------------------------------------- 1 | passage_generate_show() { 2 | passage mkdir "${1%/*}" && passage generate "$1" && passage show "$1" && passage rm -r "${1%/*}" 3 | } 4 | 5 | Describe "passage-generate" 6 | It "generates a random, encrypted password, writing it to the password file given" 7 | When call passage_generate_show tmp/generated 8 | The status should equal 0 9 | The length of stdout should equal 24 10 | End 11 | End 12 | 13 | -------------------------------------------------------------------------------- /spec/passage_show_spec.sh: -------------------------------------------------------------------------------- 1 | Describe "passage-show" 2 | It "recursively lists the contents of a password store when not given arguments" 3 | list() { 4 | %text 5 | #|directory1/ 6 | #|directory1/file1 7 | #|directory1/file2 8 | #|directory1/file3 9 | #|directory2/ 10 | #|directory2/file1 11 | #|directory2/file2 12 | #|emptydir/ 13 | } 14 | When call passage show 15 | The status should equal 0 16 | The stdout should equal "$(list)" 17 | End 18 | 19 | It "prints the decrypted contents of a password file when given one as an argument" 20 | When call passage show directory1/file1 21 | The status should equal 0 22 | The stdout should equal ")hUDX(P_tG0=DV/Lg5.gs.&(" 23 | End 24 | 25 | It "copies the X11 clipboard when you pass it a file, and the -c switch" 26 | Pending "TODO: how would this test be done without trashing the Xorg session?" 27 | #When call passage show -c directory1/file1 28 | #The status should equal 0 29 | #The stderr lines should equal 1 30 | End 31 | End 32 | 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | name = passage 2 | version = 0 3 | 4 | prefix ?= /usr/local 5 | bindir ?= ${prefix}/bin 6 | datadir ?= ${prefix}/share 7 | mandir ?= ${datadir}/man 8 | man1dir ?= ${mandir}/man1 9 | 10 | ASCIIDOCTOR ?= asciidoctor 11 | ASCIIDOCTOR_FLAGS := --failure-level=WARNING 12 | ASCIIDOCTOR_FLAGS += -a manmanual="Mutineer's Guide - ${name}" 13 | ASCIIDOCTOR_FLAGS += -a mansource="${name} ${version}" 14 | 15 | SHELLCHECK ?= shellcheck 16 | 17 | SHELLSPEC ?= shellspec 18 | 19 | -include config.mk 20 | 21 | BINS = \ 22 | passage 23 | 24 | MAN1 = ${BINS:=.1} 25 | 26 | MANS = ${MAN1} 27 | HTMLS = ${MANS:=.html} 28 | 29 | all: FRC ${BINS} ${MANS} 30 | dev: FRC README all lint check 31 | 32 | bin: FRC ${BINS} 33 | man: FRC ${MANS} 34 | html: FRC ${HTMLS} 35 | 36 | # NOTE: disable built-in rules which otherwise mess up creating .sh files 37 | .SUFFIXES: 38 | 39 | # ${IDIOMS_LIBDIR} is used to allow for testing prior to installation. 40 | .SUFFIXES: .in 41 | .in: 42 | sed \ 43 | -e "s|@@name@@|${name}|g" \ 44 | -e "s|@@version@@|${version}|g" \ 45 | -e "s|@@prefix@@|${prefix}|g" \ 46 | -e "s|@@bindir@@|${bindir}|g" \ 47 | -e "s|@@mandir@@|${mandir}|g" \ 48 | -e "s|@@man1dir@@|${man1dir}|g" \ 49 | $< > $@ 50 | chmod +x $@ 51 | 52 | .sh: 53 | sed \ 54 | -e "s|@@name@@|${name}|g" \ 55 | -e "s|@@version@@|${version}|g" \ 56 | -e "s|@@prefix@@|${prefix}|g" \ 57 | -e "s|@@bindir@@|${bindir}|g" \ 58 | -e "s|@@mandir@@|${mandir}|g" \ 59 | -e "s|@@man1dir@@|${man1dir}|g" \ 60 | $< > $@ 61 | 62 | .SUFFIXES: .adoc 63 | .html.adoc: 64 | ${ASCIIDOCTOR} ${ASCIIDOCTOR_FLAGS} -b html5 -d manpage -o $@ $< 65 | 66 | .adoc: 67 | ${ASCIIDOCTOR} ${ASCIIDOCTOR_FLAGS} -b manpage -d manpage -o $@ $< 68 | 69 | install: FRC all 70 | install -d \ 71 | ${DESTDIR}${bindir} \ 72 | ${DESTDIR}${mandir} \ 73 | ${DESTDIR}${man1dir} \ 74 | 75 | for bin in ${BINS}; do install -m0755 $${bin} ${DESTDIR}${bindir}; done 76 | for man1 in ${MAN1}; do install -m0644 $${man1} ${DESTDIR}${man1dir}; done 77 | 78 | clean: FRC 79 | rm -f ${BINS} ${MANS} ${HTMLS} 80 | 81 | .DELETE_ON_ERROR: README 82 | README: passage.1 83 | man ./$? | col -bx > $@ 84 | 85 | lint: FRC ${BINS} 86 | ${SHELLCHECK} ${BINS} 87 | 88 | check: FRC ${BINS} 89 | ${SHELLSPEC} ${SHELLSPEC_FLAGS} 90 | 91 | FRC: 92 | -------------------------------------------------------------------------------- /passage.1.adoc: -------------------------------------------------------------------------------- 1 | = passage(1) 2 | 3 | == Name 4 | 5 | passage - password store utilizing age for encryption 6 | 7 | == Synopsis 8 | 9 | *passage* convert [*-v*] [_DIRECTORY_] 10 | 11 | *passage* cp [*-fr*] _SOURCE_ _DESTINATION_ 12 | 13 | *passage* edit _FILE_... 14 | 15 | *passage* generate [*-f*] [*-l* _LENGTH_] _FILE_ 16 | 17 | *passage* init 18 | 19 | *passage* ln [*-f*] _TARGET_ _DESTINATION_ 20 | 21 | *passage* mkdir [*-p*] _DIRECTORY_... 22 | 23 | *passage* mv [*-f*] _SOURCE_ _DESTINATION_ 24 | 25 | *passage* rm [*-fr*] _FILE/DIRECTORY_... 26 | 27 | *passage* show [*-c*] [_FILE/DIRECTORY_...] 28 | 29 | == Description 30 | 31 | // TODO: age(1) doesn't actually exist, I should probably write one and contribute it upstream. 32 | 33 | *passage* is a password store. It allows for storing passwords within a directory, by default 34 | _${XDG_DATA_HOME}/share/passage_, encrypting them with age(1). It is based on the design of pass(1), 35 | with deviation where pass(1) seems inconsistent or prone to feature-creep. 36 | 37 | === Goals 38 | 39 | * Usage of age(1) rather than gpg(1). This helps to reduce the overall amount of cruft and bloat 40 | being used in cryptography programs, and while it's mostly a philosophical reason, it's the main 41 | reason this was written. 42 | 43 | * POSIX sh(1) compatibility. pass(1) depends on bash(1), and uses quite a lot of bashisms, making 44 | using it with other default system shells require a lot of changes to the code. *passage* doesn't 45 | have any non-POSIX compatible requirements, aside from age(1). 46 | 47 | * A preference of printing easily machine-readable content by default. pass(1)'s output tends to be 48 | geared more towards human-readability, which can occasionally be annoying for scripting and 49 | automated usage. 50 | 51 | * Avoidance of piece-meal operations on password files. Most password editing operations, other 52 | than generating a new password, should just be done through a text editor. 53 | 54 | * Don't automate much by default, and don't duplicate functionality; for example, this means that 55 | unlike pass(1), the generate command does *not* copy to the clipboard with a switch, because 56 | *show -c* will do that just fine. Directories are also not automatically created when necessary. 57 | 58 | * Conforming to XDG specifications by default. pass(1) does not, though it can be made to. 59 | 60 | == Commands 61 | 62 | When providing a password file as an argument, do not include the ".age" suffix. 63 | 64 | *cp* [*-fr*] _SOURCE_ _DESTINATION_:: 65 | Copy a password file or directory from _SOURCE_ to _DESTINATION_. *-f* will allow overwriting 66 | existing files. *-r* will copy a directory recursively. 67 | 68 | *convert* [*-v*] [_DIRECTORY_]:: 69 | Convert a pass(1) password store, _DIRECTORY_ to a passage(1) store. 70 | All files are decrypted using gpg(1), and then reencrypted with age(1). 71 | If *-v* is specified, the gpg(1)-encrypted files will be listed to standard error 72 | as they are converted. 73 | If unspecified, _DIRECTORY_ is _PASSWORD_STORE_DIR_ (which is _~/.password-store_ by default). 74 | 75 | *edit* _FILE_:: 76 | Decrypt and open _FILE_ in your editor. The decrypted file is written to a temporary file with 77 | owner-only read and write permissions, and then written back to the original location. 78 | 79 | *generate* [*-f*] [*-l* _LENGTH_] _FILE_:: 80 | Generate a password of _LENGTH_ characters long to _FILE_. If a custom _LENGTH_ is unspecified, 81 | the default length is 24 characters long. The password contains alphabetical, numeric, and 82 | punctuation symbols. If the password file already exists, it will not overwrite it. 83 | If *-f* is specified, it will. 84 | 85 | *init*:: 86 | Create the password store directory. No arguments are taken, as the directory and keys (if not 87 | defaults) should be set in the config or environment (see _ENVIRONMENT_ and _FILES_) anyway. 88 | 89 | *ln* [*-f*] _TARGET_ _DESTINATION_:: 90 | Create a symbolic link at _DESTINATION_ that resolves to _TARGET_. *-f* will allow overwriting 91 | existing files and links. 92 | 93 | *mkdir* [*-p*] _DIRECTORY_...:: 94 | Create a directory named _DIRECTORY_. *-p* will create all parents of the directory if they do 95 | not exist. 96 | 97 | *mv* [*-f*] _SOURCE_ _DESTINATION_:: 98 | Move the _SOURCE_ file or directory to _DESTINATION_. *-f* will allow overwriting files. 99 | 100 | *rm* [*-fr*] _FILE/DIRECTORY_...:: 101 | Delete a file/directory in the password store. *-f* will not error out if the file or directory 102 | doesn't exist. *-r* will remove a directory recursively. 103 | 104 | *show* [*-c*] [_FILE/DIRECTORY_...]:: 105 | Show a list of password files and directories, or show the contents of a password file. 106 | Directories are listed with a trailing slash (_/_) at the end of a line, files are not. 107 | Files have their ".age" suffix removed for readability. 108 | If a path to a password file is given, the contents of that file are printed to standard output. 109 | If *-c* is specified and a password file is given, the contents are copied to your X clipboard 110 | using xsel(1), rather than being printed, and then the clipboard is cleared after 45 seconds. 111 | If you provide a path to a password directory, the directory is listed. 112 | If you provide no path of any sort, the entire password store is listed. 113 | 114 | == Variables 115 | 116 | These variables can be set in the configuration file (see _FILES_). If they are in the environment, 117 | the environment variables will take priority. 118 | 119 | _EDITOR_:: 120 | The default editor used by the *edit* command. If not set, it defaults to vi(1). 121 | 122 | _PASSAGE_DIR_:: 123 | A directory containing the password store. 124 | If not specified, _${XDG_DATA_HOME}/passage_ is used instead. 125 | 126 | _PASSAGE_KEY_:: 127 | A file path. This is the key used for encrypting passwords; your private key. 128 | If not specified, _${XDG_CONFIG_HOME}/passage/privkey_ is used. 129 | 130 | _PASSAGE_RECIPIENTS_:: 131 | A file path. This is the list of public keys that password files are encrypted *for*; as in, 132 | this is what public keys should be able to decrypt password files. 133 | If not specified, _${XDG_CONFIG_HOME}/passage/recipients_ is used. 134 | 135 | _PASSWORD_STORE_DIR_:: 136 | If set, this directory is used by *convert*, rather than pass(1)'s own default, 137 | _~/.password-store_. It's not used if you provide directories as arguments to *convert*, though. 138 | This environment variable is also used by pass(1), thus the reason it is used here. 139 | 140 | == Files 141 | 142 | _${PASSAGE_DIR}/*.age_:: 143 | Files encrypted with age(1). 144 | 145 | _${XDG_DATA_HOME}/passage_:: 146 | The default location of the password store. 147 | The location can be changed with _PASSAGE_DIR_. 148 | By default, _XDG_DATA_HOME_ is set to *~/.local/share*. 149 | 150 | _${XDG_CONFIG_HOME}/passage/passage.conf_:: 151 | The default location of the configuration. Any variable in _VARIABLES_ can be set here. 152 | By default _XDG_CONFIG_HOME_ is set to *~/.config*. 153 | 154 | _${XDG_CONFIG_HOME}/passage/privkey_:: 155 | The default location of the encrypting key, or private key. 156 | The encrypting key can be changed with _PASSAGE_KEY_. 157 | By default _XDG_CONFIG_HOME_ is set to *~/.config*. 158 | 159 | _${XDG_CONFIG_HOME}/passage/recipients_:: 160 | The default location of the recipients list. 161 | The location can be changed with _PASSAGE_RECIPIENTS_. 162 | By default _XDG_CONFIG_HOME_ is set to *~/.config*. 163 | 164 | == Notes 165 | 166 | Since age(1) supports using SSH public/private key pairs for encrypting and decrypting, you can 167 | actually just set _PASSAGE_KEY_ to *~/.ssh/id_rsa* (or similar) and _PASSAGE_RECIPIENTS_ to 168 | *~/.ssh/id_rsa.pub* (again, or similar), and use your SSH keys for things. 169 | 170 | == Contributing 171 | 172 | The canonical URL of this repository is . 173 | Submit patches and bugs to kylie@somas.is. 174 | 175 | There is also an IRC channel for *passage* and other projects at . 176 | Please don't hesitate to message if you need help. 177 | 178 | == License 179 | 180 | *passage* is in the public domain. 181 | 182 | To the extent possible under law, Kylie McClain has waived all copyright and related or neighboring 183 | rights to this work. 184 | 185 | http://creativecommons.org/publicdomain/zero/1.0/ 186 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | PASSAGE(1) Mutineer's Guide - passage PASSAGE(1) 2 | 3 | 4 | 5 | NAME 6 | passage - password store utilizing age for encryption 7 | 8 | SYNOPSIS 9 | passage convert [-v] [DIRECTORY] 10 | 11 | passage cp [-fr] SOURCE DESTINATION 12 | 13 | passage edit FILE... 14 | 15 | passage generate [-f] [-l LENGTH] FILE 16 | 17 | passage init 18 | 19 | passage ln [-f] TARGET DESTINATION 20 | 21 | passage mkdir [-p] DIRECTORY... 22 | 23 | passage mv [-f] SOURCE DESTINATION 24 | 25 | passage rm [-fr] FILE/DIRECTORY... 26 | 27 | passage show [-c] [FILE/DIRECTORY...] 28 | 29 | DESCRIPTION 30 | passage is a password store. It allows for storing passwords within a 31 | directory, by default ${XDG_DATA_HOME}/share/passage, encrypting them 32 | with age(1). It is based on the design of pass(1), with deviation where 33 | pass(1) seems inconsistent or prone to feature-creep. 34 | 35 | Goals 36 | • Usage of age(1) rather than gpg(1). This helps to reduce the 37 | overall amount of cruft and bloat being used in cryptography 38 | programs, and while it’s mostly a philosophical reason, it’s the 39 | main reason this was written. 40 | 41 | • POSIX sh(1) compatibility. pass(1) depends on bash(1), and uses 42 | quite a lot of bashisms, making using it with other default system 43 | shells require a lot of changes to the code. passage doesn’t have 44 | any non-POSIX compatible requirements, aside from age(1). 45 | 46 | • A preference of printing easily machine-readable content by 47 | default. pass(1)'s output tends to be geared more towards 48 | human-readability, which can occasionally be annoying for scripting 49 | and automated usage. 50 | 51 | • Avoidance of piece-meal operations on password files. Most password 52 | editing operations, other than generating a new password, should 53 | just be done through a text editor. 54 | 55 | • Don’t automate much by default, and don’t duplicate functionality; 56 | for example, this means that unlike pass(1), the generate command 57 | does not copy to the clipboard with a switch, because show -c will 58 | do that just fine. Directories are also not automatically created 59 | when necessary. 60 | 61 | • Conforming to XDG specifications by default. pass(1) does not, 62 | though it can be made to. 63 | 64 | COMMANDS 65 | When providing a password file as an argument, do not include the 66 | ".age" suffix. 67 | 68 | cp [-fr] SOURCE DESTINATION 69 | Copy a password file or directory from SOURCE to DESTINATION. -f 70 | will allow overwriting existing files. -r will copy a directory 71 | recursively. 72 | 73 | convert [-v] [DIRECTORY] 74 | Convert a pass(1) password store, DIRECTORY to a passage(1) store. 75 | All files are decrypted using gpg(1), and then reencrypted with 76 | age(1). If -v is specified, the gpg(1)-encrypted files will be 77 | listed to standard error as they are converted. If unspecified, 78 | DIRECTORY is PASSWORD_STORE_DIR (which is ~/.password-store by 79 | default). 80 | 81 | edit FILE 82 | Decrypt and open FILE in your editor. The decrypted file is written 83 | to a temporary file with owner-only read and write permissions, and 84 | then written back to the original location. 85 | 86 | generate [-f] [-l LENGTH] FILE 87 | Generate a password of LENGTH characters long to FILE. If a custom 88 | LENGTH is unspecified, the default length is 24 characters long. 89 | The password contains alphabetical, numeric, and punctuation 90 | symbols. If the password file already exists, it will not overwrite 91 | it. If -f is specified, it will. 92 | 93 | init 94 | Create the password store directory. No arguments are taken, as the 95 | directory and keys (if not defaults) should be set in the config or 96 | environment (see ENVIRONMENT and FILES) anyway. 97 | 98 | ln [-f] TARGET DESTINATION 99 | Create a symbolic link at DESTINATION that resolves to TARGET. -f 100 | will allow overwriting existing files and links. 101 | 102 | mkdir [-p] DIRECTORY... 103 | Create a directory named DIRECTORY. -p will create all parents of 104 | the directory if they do not exist. 105 | 106 | mv [-f] SOURCE DESTINATION 107 | Move the SOURCE file or directory to DESTINATION. -f will allow 108 | overwriting files. 109 | 110 | rm [-fr] FILE/DIRECTORY... 111 | Delete a file/directory in the password store. -f will not error 112 | out if the file or directory doesn’t exist. -r will remove a 113 | directory recursively. 114 | 115 | show [-c] [FILE/DIRECTORY...] 116 | Show a list of password files and directories, or show the contents 117 | of a password file. Directories are listed with a trailing slash 118 | (/) at the end of a line, files are not. Files have their ".age" 119 | suffix removed for readability. If a path to a password file is 120 | given, the contents of that file are printed to standard output. 121 | If -c is specified and a password file is given, the contents are 122 | copied to your X clipboard using xsel(1), rather than being 123 | printed, and then the clipboard is cleared after 45 seconds. If 124 | you provide a path to a password directory, the directory is 125 | listed. If you provide no path of any sort, the entire password 126 | store is listed. 127 | 128 | VARIABLES 129 | These variables can be set in the configuration file (see FILES). If 130 | they are in the environment, the environment variables will take 131 | priority. 132 | 133 | EDITOR 134 | The default editor used by the edit command. If not set, it 135 | defaults to vi(1). 136 | 137 | PASSAGE_DIR 138 | A directory containing the password store. If not specified, 139 | ${XDG_DATA_HOME}/passage is used instead. 140 | 141 | PASSAGE_KEY 142 | A file path. This is the key used for encrypting passwords; your 143 | private key. If not specified, ${XDG_CONFIG_HOME}/passage/privkey 144 | is used. 145 | 146 | PASSAGE_RECIPIENTS 147 | A file path. This is the list of public keys that password files 148 | are encrypted for; as in, this is what public keys should be able 149 | to decrypt password files. If not specified, 150 | ${XDG_CONFIG_HOME}/passage/recipients is used. 151 | 152 | PASSWORD_STORE_DIR 153 | If set, this directory is used by convert, rather than pass(1)'s 154 | own default, ~/.password-store. It’s not used if you provide 155 | directories as arguments to convert, though. This environment 156 | variable is also used by pass(1), thus the reason it is used here. 157 | 158 | FILES 159 | ${PASSAGE_DIR}/*.age 160 | Files encrypted with age(1). 161 | 162 | ${XDG_DATA_HOME}/passage 163 | The default location of the password store. The location can be 164 | changed with PASSAGE_DIR. By default, XDG_DATA_HOME is set to 165 | ~/.local/share. 166 | 167 | ${XDG_CONFIG_HOME}/passage/passage.conf 168 | The default location of the configuration. Any variable in 169 | VARIABLES can be set here. By default XDG_CONFIG_HOME is set to 170 | ~/.config. 171 | 172 | ${XDG_CONFIG_HOME}/passage/privkey 173 | The default location of the encrypting key, or private key. The 174 | encrypting key can be changed with PASSAGE_KEY. By default 175 | XDG_CONFIG_HOME is set to ~/.config. 176 | 177 | ${XDG_CONFIG_HOME}/passage/recipients 178 | The default location of the recipients list. The location can be 179 | changed with PASSAGE_RECIPIENTS. By default XDG_CONFIG_HOME is set 180 | to ~/.config. 181 | 182 | NOTES 183 | Since age(1) supports using SSH public/private key pairs for encrypting 184 | and decrypting, you can actually just set PASSAGE_KEY to ~/.ssh/id_rsa 185 | (or similar) and PASSAGE_RECIPIENTS to ~/.ssh/id_rsa.pub (again, or 186 | similar), and use your SSH keys for things. 187 | 188 | CONTRIBUTING 189 | The canonical URL of this repository is 190 | . Submit patches and bugs to 191 | . 192 | 193 | There is also an IRC channel for passage and other projects at 194 | . Please don’t hesitate to message if 195 | you need help. 196 | 197 | LICENSE 198 | passage is in the public domain. 199 | 200 | To the extent possible under law, Kylie McClain has waived all 201 | copyright and related or neighboring rights to this work. 202 | 203 | 204 | 205 | 206 | 207 | passage 0 2020-06-26 PASSAGE(1) 208 | -------------------------------------------------------------------------------- /passage.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: passage 3 | .\" Author: [see the "AUTHOR(S)" section] 4 | .\" Generator: Asciidoctor 2.0.10 5 | .\" Date: 2020-06-26 6 | .\" Manual: Mutineer's Guide - passage 7 | .\" Source: passage 0 8 | .\" Language: English 9 | .\" 10 | .TH "PASSAGE" "1" "2020-06-26" "passage 0" "Mutineer\(aqs Guide \- passage" 11 | .ie \n(.g .ds Aq \(aq 12 | .el .ds Aq ' 13 | .ss \n[.ss] 0 14 | .nh 15 | .ad l 16 | .de URL 17 | \fI\\$2\fP <\\$1>\\$3 18 | .. 19 | .als MTO URL 20 | .if \n[.g] \{\ 21 | . mso www.tmac 22 | . am URL 23 | . ad l 24 | . . 25 | . am MTO 26 | . ad l 27 | . . 28 | . LINKSTYLE blue R < > 29 | .\} 30 | .SH "NAME" 31 | passage \- password store utilizing age for encryption 32 | .SH "SYNOPSIS" 33 | .sp 34 | \fBpassage\fP convert [\fB\-v\fP] [\fIDIRECTORY\fP] 35 | .sp 36 | \fBpassage\fP cp [\fB\-fr\fP] \fISOURCE\fP \fIDESTINATION\fP 37 | .sp 38 | \fBpassage\fP edit \fIFILE\fP... 39 | .sp 40 | \fBpassage\fP generate [\fB\-f\fP] [\fB\-l\fP \fILENGTH\fP] \fIFILE\fP 41 | .sp 42 | \fBpassage\fP init 43 | .sp 44 | \fBpassage\fP ln [\fB\-f\fP] \fITARGET\fP \fIDESTINATION\fP 45 | .sp 46 | \fBpassage\fP mkdir [\fB\-p\fP] \fIDIRECTORY\fP... 47 | .sp 48 | \fBpassage\fP mv [\fB\-f\fP] \fISOURCE\fP \fIDESTINATION\fP 49 | .sp 50 | \fBpassage\fP rm [\fB\-fr\fP] \fIFILE/DIRECTORY\fP... 51 | .sp 52 | \fBpassage\fP show [\fB\-c\fP] [\fIFILE/DIRECTORY\fP...] 53 | .SH "DESCRIPTION" 54 | .sp 55 | \fBpassage\fP is a password store. It allows for storing passwords within a directory, by default 56 | \fI${XDG_DATA_HOME}/share/passage\fP, encrypting them with age(1). It is based on the design of pass(1), 57 | with deviation where pass(1) seems inconsistent or prone to feature\-creep. 58 | .SS "Goals" 59 | .sp 60 | .RS 4 61 | .ie n \{\ 62 | \h'-04'\(bu\h'+03'\c 63 | .\} 64 | .el \{\ 65 | . sp -1 66 | . IP \(bu 2.3 67 | .\} 68 | Usage of age(1) rather than gpg(1). This helps to reduce the overall amount of cruft and bloat 69 | being used in cryptography programs, and while it\(cqs mostly a philosophical reason, it\(cqs the main 70 | reason this was written. 71 | .RE 72 | .sp 73 | .RS 4 74 | .ie n \{\ 75 | \h'-04'\(bu\h'+03'\c 76 | .\} 77 | .el \{\ 78 | . sp -1 79 | . IP \(bu 2.3 80 | .\} 81 | POSIX sh(1) compatibility. pass(1) depends on bash(1), and uses quite a lot of bashisms, making 82 | using it with other default system shells require a lot of changes to the code. \fBpassage\fP doesn\(cqt 83 | have any non\-POSIX compatible requirements, aside from age(1). 84 | .RE 85 | .sp 86 | .RS 4 87 | .ie n \{\ 88 | \h'-04'\(bu\h'+03'\c 89 | .\} 90 | .el \{\ 91 | . sp -1 92 | . IP \(bu 2.3 93 | .\} 94 | A preference of printing easily machine\-readable content by default. pass(1)\(aqs output tends to be 95 | geared more towards human\-readability, which can occasionally be annoying for scripting and 96 | automated usage. 97 | .RE 98 | .sp 99 | .RS 4 100 | .ie n \{\ 101 | \h'-04'\(bu\h'+03'\c 102 | .\} 103 | .el \{\ 104 | . sp -1 105 | . IP \(bu 2.3 106 | .\} 107 | Avoidance of piece\-meal operations on password files. Most password editing operations, other 108 | than generating a new password, should just be done through a text editor. 109 | .RE 110 | .sp 111 | .RS 4 112 | .ie n \{\ 113 | \h'-04'\(bu\h'+03'\c 114 | .\} 115 | .el \{\ 116 | . sp -1 117 | . IP \(bu 2.3 118 | .\} 119 | Don\(cqt automate much by default, and don\(cqt duplicate functionality; for example, this means that 120 | unlike pass(1), the generate command does \fBnot\fP copy to the clipboard with a switch, because 121 | \fBshow \-c\fP will do that just fine. Directories are also not automatically created when necessary. 122 | .RE 123 | .sp 124 | .RS 4 125 | .ie n \{\ 126 | \h'-04'\(bu\h'+03'\c 127 | .\} 128 | .el \{\ 129 | . sp -1 130 | . IP \(bu 2.3 131 | .\} 132 | Conforming to XDG specifications by default. pass(1) does not, though it can be made to. 133 | .RE 134 | .SH "COMMANDS" 135 | .sp 136 | When providing a password file as an argument, do not include the ".age" suffix. 137 | .sp 138 | \fBcp\fP [\fB\-fr\fP] \fISOURCE\fP \fIDESTINATION\fP 139 | .RS 4 140 | Copy a password file or directory from \fISOURCE\fP to \fIDESTINATION\fP. \fB\-f\fP will allow overwriting 141 | existing files. \fB\-r\fP will copy a directory recursively. 142 | .RE 143 | .sp 144 | \fBconvert\fP [\fB\-v\fP] [\fIDIRECTORY\fP] 145 | .RS 4 146 | Convert a pass(1) password store, \fIDIRECTORY\fP to a passage(1) store. 147 | All files are decrypted using gpg(1), and then reencrypted with age(1). 148 | If \fB\-v\fP is specified, the gpg(1)\-encrypted files will be listed to standard error 149 | as they are converted. 150 | If unspecified, \fIDIRECTORY\fP is \fIPASSWORD_STORE_DIR\fP (which is \fI~/.password\-store\fP by default). 151 | .RE 152 | .sp 153 | \fBedit\fP \fIFILE\fP 154 | .RS 4 155 | Decrypt and open \fIFILE\fP in your editor. The decrypted file is written to a temporary file with 156 | owner\-only read and write permissions, and then written back to the original location. 157 | .RE 158 | .sp 159 | \fBgenerate\fP [\fB\-f\fP] [\fB\-l\fP \fILENGTH\fP] \fIFILE\fP 160 | .RS 4 161 | Generate a password of \fILENGTH\fP characters long to \fIFILE\fP. If a custom \fILENGTH\fP is unspecified, 162 | the default length is 24 characters long. The password contains alphabetical, numeric, and 163 | punctuation symbols. If the password file already exists, it will not overwrite it. 164 | If \fB\-f\fP is specified, it will. 165 | .RE 166 | .sp 167 | \fBinit\fP 168 | .RS 4 169 | Create the password store directory. No arguments are taken, as the directory and keys (if not 170 | defaults) should be set in the config or environment (see \fIENVIRONMENT\fP and \fIFILES\fP) anyway. 171 | .RE 172 | .sp 173 | \fBln\fP [\fB\-f\fP] \fITARGET\fP \fIDESTINATION\fP 174 | .RS 4 175 | Create a symbolic link at \fIDESTINATION\fP that resolves to \fITARGET\fP. \fB\-f\fP will allow overwriting 176 | existing files and links. 177 | .RE 178 | .sp 179 | \fBmkdir\fP [\fB\-p\fP] \fIDIRECTORY\fP... 180 | .RS 4 181 | Create a directory named \fIDIRECTORY\fP. \fB\-p\fP will create all parents of the directory if they do 182 | not exist. 183 | .RE 184 | .sp 185 | \fBmv\fP [\fB\-f\fP] \fISOURCE\fP \fIDESTINATION\fP 186 | .RS 4 187 | Move the \fISOURCE\fP file or directory to \fIDESTINATION\fP. \fB\-f\fP will allow overwriting files. 188 | .RE 189 | .sp 190 | \fBrm\fP [\fB\-fr\fP] \fIFILE/DIRECTORY\fP... 191 | .RS 4 192 | Delete a file/directory in the password store. \fB\-f\fP will not error out if the file or directory 193 | doesn\(cqt exist. \fB\-r\fP will remove a directory recursively. 194 | .RE 195 | .sp 196 | \fBshow\fP [\fB\-c\fP] [\fIFILE/DIRECTORY\fP...] 197 | .RS 4 198 | Show a list of password files and directories, or show the contents of a password file. 199 | Directories are listed with a trailing slash (\fI/\fP) at the end of a line, files are not. 200 | Files have their ".age" suffix removed for readability. 201 | If a path to a password file is given, the contents of that file are printed to standard output. 202 | If \fB\-c\fP is specified and a password file is given, the contents are copied to your X clipboard 203 | using xsel(1), rather than being printed, and then the clipboard is cleared after 45 seconds. 204 | If you provide a path to a password directory, the directory is listed. 205 | If you provide no path of any sort, the entire password store is listed. 206 | .RE 207 | .SH "VARIABLES" 208 | .sp 209 | These variables can be set in the configuration file (see \fIFILES\fP). If they are in the environment, 210 | the environment variables will take priority. 211 | .sp 212 | \fIEDITOR\fP 213 | .RS 4 214 | The default editor used by the \fBedit\fP command. If not set, it defaults to vi(1). 215 | .RE 216 | .sp 217 | \fIPASSAGE_DIR\fP 218 | .RS 4 219 | A directory containing the password store. 220 | If not specified, \fI${XDG_DATA_HOME}/passage\fP is used instead. 221 | .RE 222 | .sp 223 | \fIPASSAGE_KEY\fP 224 | .RS 4 225 | A file path. This is the key used for encrypting passwords; your private key. 226 | If not specified, \fI${XDG_CONFIG_HOME}/passage/privkey\fP is used. 227 | .RE 228 | .sp 229 | \fIPASSAGE_RECIPIENTS\fP 230 | .RS 4 231 | A file path. This is the list of public keys that password files are encrypted \fBfor\fP; as in, 232 | this is what public keys should be able to decrypt password files. 233 | If not specified, \fI${XDG_CONFIG_HOME}/passage/recipients\fP is used. 234 | .RE 235 | .sp 236 | \fIPASSWORD_STORE_DIR\fP 237 | .RS 4 238 | If set, this directory is used by \fBconvert\fP, rather than pass(1)\(aqs own default, 239 | \fI~/.password\-store\fP. It\(cqs not used if you provide directories as arguments to \fBconvert\fP, though. 240 | This environment variable is also used by pass(1), thus the reason it is used here. 241 | .RE 242 | .SH "FILES" 243 | .sp 244 | \fI${PASSAGE_DIR}/*.age\fP 245 | .RS 4 246 | Files encrypted with age(1). 247 | .RE 248 | .sp 249 | \fI${XDG_DATA_HOME}/passage\fP 250 | .RS 4 251 | The default location of the password store. 252 | The location can be changed with \fIPASSAGE_DIR\fP. 253 | By default, \fIXDG_DATA_HOME\fP is set to \fB~/.local/share\fP. 254 | .RE 255 | .sp 256 | \fI${XDG_CONFIG_HOME}/passage/passage.conf\fP 257 | .RS 4 258 | The default location of the configuration. Any variable in \fIVARIABLES\fP can be set here. 259 | By default \fIXDG_CONFIG_HOME\fP is set to \fB~/.config\fP. 260 | .RE 261 | .sp 262 | \fI${XDG_CONFIG_HOME}/passage/privkey\fP 263 | .RS 4 264 | The default location of the encrypting key, or private key. 265 | The encrypting key can be changed with \fIPASSAGE_KEY\fP. 266 | By default \fIXDG_CONFIG_HOME\fP is set to \fB~/.config\fP. 267 | .RE 268 | .sp 269 | \fI${XDG_CONFIG_HOME}/passage/recipients\fP 270 | .RS 4 271 | The default location of the recipients list. 272 | The location can be changed with \fIPASSAGE_RECIPIENTS\fP. 273 | By default \fIXDG_CONFIG_HOME\fP is set to \fB~/.config\fP. 274 | .RE 275 | .SH "NOTES" 276 | .sp 277 | Since age(1) supports using SSH public/private key pairs for encrypting and decrypting, you can 278 | actually just set \fIPASSAGE_KEY\fP to \fB~/.ssh/id_rsa\fP (or similar) and \fIPASSAGE_RECIPIENTS\fP to 279 | \fB~/.ssh/id_rsa.pub\fP (again, or similar), and use your SSH keys for things. 280 | .SH "CONTRIBUTING" 281 | .sp 282 | The canonical URL of this repository is \c 283 | .URL "https://git.mutiny.red/somasis/passage" "" "." 284 | Submit patches and bugs to \c 285 | .MTO "kylie\(atsomas.is" "" "." 286 | .sp 287 | There is also an IRC channel for \fBpassage\fP and other projects at \c 288 | .URL "irc://irc.freenode.net/#mutiny" "" "." 289 | Please don\(cqt hesitate to message if you need help. 290 | .SH "LICENSE" 291 | .sp 292 | \fBpassage\fP is in the public domain. 293 | .sp 294 | To the extent possible under law, Kylie McClain has waived all copyright and related or neighboring 295 | rights to this work. 296 | .sp 297 | .URL "http://creativecommons.org/publicdomain/zero/1.0/" "" "" -------------------------------------------------------------------------------- /passage.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ef 4 | 5 | ## Default variables 6 | 7 | # Cause variables declared in the environment to prevent any further assignments. 8 | 9 | [ -n "${PASSAGE_DIR}" ] \ 10 | && PASSAGE_DIR=$(readlink -f "${PASSAGE_DIR}") \ 11 | && readonly PASSAGE_DIR 12 | [ -n "${PASSAGE_KEY}" ] && readonly PASSAGE_KEY 13 | [ -n "${PASSAGE_RECIPIENTS}" ] && readonly PASSAGE_RECIPIENTS 14 | [ -n "${PASSWORD_STORE_DIR}" ] && readonly PASSWORD_STORE_DIR 15 | 16 | : "${XDG_CONFIG_HOME:=${HOME}/.config}" 17 | : "${XDG_DATA_HOME:=${HOME}/.local/share}" 18 | 19 | # This may or may not be used, so don't try and check the source. 20 | # shellcheck disable=SC1090 21 | [ -f "${XDG_CONFIG_HOME}"/passage/passage.conf ] && \ 22 | . "${XDG_CONFIG_HOME}"/passage/passage.conf 2>/dev/null 23 | 24 | : "${PASSAGE_DIR:=${XDG_DATA_HOME}/passage}" 25 | : "${PASSAGE_KEY:=${XDG_CONFIG_HOME}/passage/privkey}" 26 | : "${PASSAGE_RECIPIENTS:=${XDG_CONFIG_HOME}/passage/recipients}" 27 | : "${PASSWORD_STORE_DIR:=${HOME}/.password-store}" 28 | 29 | PASSAGE_DIR=$(readlink -f "${PASSAGE_DIR}") 30 | 31 | readonly PASSAGE_DIR PASSAGE_KEY PASSAGE_RECIPIENTS PASSWORD_STORE_DIR 32 | 33 | # Only use a directory for temporary files that is guaranteed to be on memory. 34 | export TMPDIR=/dev/shm 35 | 36 | ## Functions 37 | 38 | decrypt() { 39 | age -d -i "${PASSAGE_KEY}" "$@" 40 | } 41 | 42 | encrypt() { 43 | file="$1" 44 | 45 | set -- 46 | while read -r recipient; do 47 | set -- -r "${recipient}" 48 | done < "${PASSAGE_RECIPIENTS}" 49 | 50 | age "$@" -a "${file}" 51 | } 52 | 53 | is_beneath_store() { 54 | while [ "$#" -gt 0 ]; do 55 | case "$(readlink -f "${PASSAGE_DIR}"/"${1}")" in 56 | "${PASSAGE_DIR}"/*|"${PASSAGE_DIR}") : ;; 57 | *) 58 | printf 'error: path "%s" is not within the password store\n' "${1}" >&2 59 | exit 4 60 | ;; 61 | esac 62 | shift 63 | done 64 | } 65 | 66 | usage() { 67 | cat >&2 </dev/null 2>&1; do 98 | case "${arg}" in 99 | v) 100 | verbose=true 101 | ;; 102 | ?) 103 | printf 'unknown argument -- %s\n' "${1}" >&2 104 | usage 105 | ;; 106 | esac 107 | done 108 | 109 | shift $(( OPTIND - 1 )) 110 | 111 | if "${verbose}"; then 112 | gpg_args= 113 | fi 114 | 115 | [ "$#" -eq 0 ] && set -- "${PASSWORD_STORE_DIR}" 116 | 117 | # Temporary enable path globbing. 118 | set +f 119 | while [ $# -gt 0 ]; do 120 | cp -r "$1"/* ./ 121 | shift 122 | done 123 | set -f 124 | 125 | # Convert all gpg-encrypted files to age-encrypted files. 126 | # 127 | # The lack of word splitting on ${gpg_args} is necessary. 128 | # shellcheck disable=SC2086 129 | find . -name '*.gpg' -type f -not -type l | while read -r file; do 130 | "${verbose}" && printf '%s\n' "${file}" >&2 131 | gpg ${gpg_args} --batch -d "${file}" | encrypt > ./"${file%.gpg}.age" 132 | rm -f "${file}" 133 | done 134 | 135 | # Fix symbolic links that point to .gpg files. 136 | find . -name '*.gpg' -type l | while read -r name; do 137 | new_link=$(readlink "${name}") 138 | new_link="${new_link%.gpg}".age 139 | new_name="${name%.gpg}".age 140 | ln -sf "${new_link}" ./"${new_name}" 141 | rm -f "${name}" 142 | done 143 | } 144 | 145 | passage_edit() { 146 | while [ $# -gt 0 ]; do 147 | is_beneath_store "$1" 148 | 149 | temp=$(mktemp "${TMPDIR}"/passage.XXXXXX) 150 | chmod 700 "${temp}" 151 | 152 | [ -f ./"${1}".age ] && decrypt ./"${1}".age > "${temp}" 153 | 154 | "${EDITOR:-vi}" "${temp}" || { 155 | printf \ 156 | 'error: editor died with error code %s, cleaning up decrypted file\n' "$?" >&2 157 | rm -f "${temp}" 158 | } 159 | 160 | encrypt "${temp}" > ./"${1}".age 161 | rm -f "${temp}" 162 | 163 | shift 164 | done 165 | 166 | } 167 | 168 | passage_generate() { 169 | clobber=false 170 | length=24 171 | while getopts :fl arg >/dev/null 2>&1; do 172 | case "${arg}" in 173 | f) 174 | clobber=true 175 | ;; 176 | l) 177 | length="${OPTARG}" 178 | ;; 179 | ?) 180 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 181 | usage 182 | ;; 183 | esac 184 | done 185 | 186 | shift $(( OPTIND - 1 )) 187 | 188 | case "${length}" in 189 | *[!0-9]*) 190 | printf 'error: length "%s" is not an integer\n' "${length}" >&2 191 | exit 127 192 | ;; 193 | esac 194 | 195 | while [ $# -gt 0 ]; do 196 | is_beneath_store "$1" 197 | 198 | if [ -e ./"${1}" ] && ! "${clobber}"; then 199 | printf \ 200 | 'error: password file "%s" already exists, refusing to clobber without -f\n' \ 201 | "${1}" >&2 202 | exit 2 203 | fi 204 | 205 | [ -d ./"${1%/*}" ] || { 206 | printf \ 207 | 'error: password directory "%s" does not exist\n' \ 208 | "${1}" >&2 209 | exit 5 210 | } 211 | 212 | # NOTE: Using bs="${length}" count=1 rather than bs=1 count="${length}" saves syscalls. 213 | LC_ALL=C tr -cd '[:alnum:][:punct:]' /dev/null | encrypt > ./"${1}".age 215 | 216 | shift 217 | done 218 | } 219 | 220 | passage_init() { 221 | mkdir -p "${PASSAGE_DIR}" 222 | mkdir -p "$(dirname "${PASSAGE_KEY}")" "$(dirname "${PASSAGE_RECIPIENTS}")" 223 | [ -f "${PASSAGE_KEY}" ] || age-keygen -o "${PASSAGE_KEY}" 224 | [ -f "${PASSAGE_RECIPIENTS}" ] \ 225 | || sed '/^#.* age1.*/ !d; s/.*age1/age1/' "${PASSAGE_KEY}" > "${PASSAGE_RECIPIENTS}" 226 | } 227 | 228 | passage_show() { 229 | copy=false 230 | while getopts :c arg >/dev/null 2>&1; do 231 | case "${arg}" in 232 | c) 233 | copy=true 234 | ;; 235 | ?) 236 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 237 | usage 238 | ;; 239 | esac 240 | done 241 | 242 | shift $(( OPTIND - 1 )) 243 | 244 | if [ -d ./"${1}" ] || [ -z "${1}" ]; then 245 | pretty_list "$@" 246 | return $? 247 | fi 248 | 249 | is_beneath_store "${1}".age 250 | 251 | if [ -f ./"${1}".age ]; then 252 | if "${copy}"; then 253 | decrypt ./"${1}".age | xsel -ib 254 | printf 'Password copied to clipboard. Clipboard will be cleared in 45 seconds.\n' >&2 255 | ( 256 | sleep 45 257 | xsel -cb 258 | ) & 259 | else 260 | decrypt ./"${1}".age 261 | fi 262 | else 263 | printf 'error: password file "%s" does not exist\n' "${1}" >&2 264 | exit 3 265 | fi 266 | } 267 | 268 | ## File operation commands 269 | 270 | passage_cp() { 271 | force= 272 | recurse= 273 | while getopts :fr arg >/dev/null 2>&1; do 274 | case "${arg}" in 275 | f) 276 | force=true 277 | ;; 278 | r) 279 | recurse=true 280 | ;; 281 | ?) 282 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 283 | usage 284 | ;; 285 | esac 286 | done 287 | 288 | shift $(( OPTIND - 1 )) 289 | 290 | [ "$#" -eq 2 ] || { 291 | printf 'error: missing destination path\n' >&2 292 | usage 293 | } 294 | 295 | is_beneath_store "$1" 296 | 297 | if [ -d "${1}" ]; then 298 | cp ${force:+-f} ${recurse:+-r} ./"$1" ./"$2" 299 | else 300 | cp ${force:+-f} ${recurse:+-r} ./"$1".age ./"$2".age 301 | fi 302 | } 303 | 304 | passage_ln() { 305 | force= 306 | while getopts :f arg >/dev/null 2>&1; do 307 | case "${arg}" in 308 | f) 309 | force=true 310 | ;; 311 | ?) 312 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 313 | usage 314 | ;; 315 | esac 316 | done 317 | 318 | shift $(( OPTIND - 1 )) 319 | 320 | [ "$#" -eq 2 ] || { 321 | printf 'error: missing destination path\n' >&2 322 | usage 323 | } 324 | 325 | is_beneath_store "$@" 326 | 327 | if [ -d "${1}" ]; then 328 | ln -s ${force:+-f} "$1" ./"$2" 329 | else 330 | ln -s ${force:+-f} "$1".age ./"$2".age 331 | fi 332 | shift 333 | } 334 | 335 | passage_mkdir() { 336 | parents= 337 | while getopts :p arg >/dev/null 2>&1; do 338 | case "${arg}" in 339 | p) 340 | parents=true 341 | ;; 342 | ?) 343 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 344 | usage 345 | ;; 346 | esac 347 | done 348 | 349 | shift $(( OPTIND - 1 )) 350 | 351 | is_beneath_store "$@" 352 | 353 | while [ $# -gt 0 ]; do 354 | mkdir ${parents:+-p} ./"${1}" 355 | 356 | shift 357 | done 358 | } 359 | 360 | passage_mv() { 361 | force= 362 | while getopts :f arg >/dev/null 2>&1; do 363 | case "${arg}" in 364 | f) 365 | force=true 366 | ;; 367 | ?) 368 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 369 | usage 370 | ;; 371 | esac 372 | done 373 | 374 | shift $(( OPTIND - 1 )) 375 | 376 | [ "$#" -eq 2 ] || { 377 | printf 'error: missing destination path\n' >&2 378 | usage 379 | } 380 | 381 | is_beneath_store "$@" 382 | 383 | if [ -d ./"${1}" ] && [ -d ./"${2}" ]; then 384 | mv ${force:+-f} ./"$1" ./"$2" 385 | elif [ -e ./"${1}" ] && [ -d ./"${2}" ]; then 386 | mv ${force:+-f} ./"$1".age ./"$2" 387 | else 388 | mv ${force:+-f} ./"$1".age ./"$2".age 389 | fi 390 | } 391 | 392 | passage_rm() { 393 | recurse= 394 | force= 395 | while getopts :fr arg >/dev/null 2>&1; do 396 | case "${arg}" in 397 | f) 398 | force=true 399 | ;; 400 | r) 401 | recurse=true 402 | ;; 403 | ?) 404 | printf 'unknown argument -- %s\n' "${OPTARG}" >&2 405 | usage 406 | ;; 407 | esac 408 | done 409 | 410 | shift $(( OPTIND - 1 )) 411 | 412 | while [ "$#" -gt 0 ]; do 413 | is_beneath_store "$1" 414 | 415 | if [ -d "${1}" ]; then 416 | rm ${force:+-f} ${recurse:+-r} ./"$1" 417 | else 418 | rm ${force:+-f} ${recurse:+-r} ./"$1".age 419 | fi 420 | 421 | shift 422 | done 423 | } 424 | 425 | shift $(( OPTIND - 1 )) 426 | 427 | mode= 428 | case "${1:-help}" in 429 | convert|cp|edit|generate|init|ln|mkdir|mv|rm|show) 430 | mode="${1}" 431 | ;; 432 | help) usage ;; 433 | *) 434 | printf 'unknown argument -- %s\n' "${1}" >&2 435 | usage 436 | ;; 437 | esac 438 | 439 | shift 440 | 441 | case "${mode}" in 442 | help|init) : ;; 443 | *) 444 | [ -s "${PASSAGE_KEY}" ] || { 445 | printf 'error: key file "%s" either does not exist or is empty\n' "${PASSAGE_KEY}" >&2 446 | exit 4 447 | } 448 | [ -s "${PASSAGE_RECIPIENTS}" ] || { 449 | printf 'error: recipient file "%s" either does not exist or is empty\n' "${PASSAGE_RECIPIENTS}" >&2 450 | exit 4 451 | } 452 | 453 | cd "${PASSAGE_DIR}" 454 | ;; 455 | esac 456 | 457 | passage_"${mode}" "$@" 458 | --------------------------------------------------------------------------------