├── LICENSE ├── README.md ├── gpg.vim ├── pass └── pentadactyl └── autofill.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jeff King 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 | Pass - a very simple password storage mechanism 2 | =============================================== 3 | 4 | NOTE 5 | ---- 6 | 7 | This software is no longer actively developed or maintained. I have 8 | switched to using the (confusingly similar-named) tool from 9 | https://www.passwordstore.org/. The general ideas are substantially 10 | similar, the main differences being: 11 | 12 | - this tool uses a single file versus one per item, which obscures 13 | some metadata at the cost of less flexibility (e.g., you cannot use 14 | this `pass` to share subsets of your repository with another gpg 15 | key) 16 | 17 | - the zx2c4 `pass` actually has an active community with up-to-date 18 | ecosystem tools, like https://github.com/passff/passff 19 | 20 | So while I don't know of any particular problems with this software, I 21 | overall recommend _not_ using it in favor of the zx2c4 tool. 22 | 23 | Introduction 24 | ------------ 25 | 26 | `Pass` makes it easy to store passwords (or any other secret data). It 27 | aims to be secure and easy to integrate with existing Unix tools and 28 | version control. The encryption is all done by gpg, using one (or more) 29 | keypairs, and the contents themselves are organized as YAML. A vim 30 | plugin is provided to make editing the encrypted file easier. 31 | 32 | Here's an example. The file is encrypted on disk with gpg: 33 | 34 | ``` 35 | $ cat ~/.pass/pass.gpg 36 | -----BEGIN PGP MESSAGE----- 37 | Version: GnuPG v1.4.15 (GNU/Linux) 38 | 39 | hQQOAwC+u3g6GPWoEBAAtdHigCS83FWokK9fFAwjVNx1nhdVCWlReKgIftktoUKj 40 | ZjKzBk9gLmUHi0UMVjAtbxkhscEzLtVyBhlhpIMbG2/iEjoxS90ZnBWhavJWGQtd 41 | [...] 42 | UC6q6YCaR9qcfhVHAvcBIuCq1u3GwsZd0NyY+buJj38APc1JZshugCWu 43 | =/oK0 44 | -----END PGP MESSAGE----- 45 | ``` 46 | 47 | The decrypted contents are YAML: 48 | 49 | ``` 50 | $ gpg -qd <~/.pass/pass.gpg 51 | # recipient: me@example.com 52 | mybank: 53 | desc: MyBank Savings and Loan 54 | username: me 55 | password: foo 56 | ``` 57 | 58 | Note that we didn't enter a passphrase above; this system works much 59 | better if you use `gpg-agent`, which will prompt for and cache your 60 | password outside of the terminal. 61 | 62 | Using the vim plugin, you can edit it just like a regular file. The 63 | decrypted contents are never stored on disk, and you can commit the 64 | result. The version control system will see only the encrypted version, 65 | so you are free to push it. A git diff-helper makes diffs more readable. 66 | 67 | Updating an entry might look like this (the editing happens inside vim, 68 | with the decryption/encryption steps transparent to the user): 69 | 70 | ``` 71 | $ vi ~/.pass/pass.gpg 72 | $ git diff 73 | diff --git a/pass.gpg b/pass.gpg 74 | index f42955f..b42cffe 100644 75 | --- a/pass.gpg 76 | +++ b/pass.gpg 77 | @@ -2,4 +2,4 @@ 78 | mybank: 79 | desc: MyBank Savings and Loan 80 | username: me 81 | - password: foo 82 | + password: bar 83 | ``` 84 | 85 | You can query a whole subtree of data: 86 | 87 | ``` 88 | $ pass mybank 89 | mybank.desc MyBank Savings and Loan 90 | mybank.username me 91 | mybank.password bar 92 | ``` 93 | 94 | or a specific item: 95 | 96 | ``` 97 | $ pass mybank.pass 98 | mybank.password bar 99 | ``` 100 | 101 | Note that we don't need to use the full item name. The keys are 102 | substring regexes, and can match either item names, or their `desc` 103 | fields: 104 | 105 | ``` 106 | $ pass savings.user 107 | mybank.username me 108 | ``` 109 | 110 | You can also send the result straight to the X clipboard with the `-p` 111 | option, or list available keys with the `-l` option. 112 | 113 | 114 | Setup 115 | ----- 116 | 117 | To setup a password store, you need to do the following: 118 | 119 | 1. Copy `pass` somewhere in your `$PATH`. 120 | 121 | cp pass ~/local/bin 122 | 123 | 2. Create a gpg key if you don't already have one. Make sure you're 124 | using the gpg-agent for convenience. 125 | 126 | gpg --gen-key 127 | echo use-agent >>~/.gnupg/gpg.conf 128 | 129 | 3. Create a repository for storing your data. If you choose another 130 | location, set `$PASS_HOME` to point `pass` to your repo. 131 | 132 | git init ~/.pass 133 | cd ~/.pass 134 | echo '# recipient: me@example.com' >pass.gpg 135 | echo '*.gpg diff=gpg' >.gitattributes 136 | git add . && git commit -m 'start pass repo' 137 | 138 | 4. Tell git how to show diffs between gpg files. 139 | 140 | git config --global diff.gpg.textconv 'gpg -qd --no-tty' 141 | 142 | 5. Install the vim plugin to make editing gpg files easier. 143 | 144 | mkdir -p ~/.vim/plugin 145 | cp gpg.vim ~/.vim/plugin 146 | 147 | 6. Add some content. Note that your commit messages are _not_ 148 | encrypted. 149 | 150 | cd ~/.pass 151 | vi pass.gpg 152 | git commit -am 'added password for mysite' 153 | 154 | 155 | File Formats 156 | ------------ 157 | 158 | The gpg plugin does not care about the content of the file (so you can 159 | use it for other things besides pass data), with one exception: any 160 | lines at the beginning of the file starting with "# recipient: ..." 161 | specify gpg key-ids which should be able to access the encrypted file. 162 | Typically this will just be your email or key-id; but if you are sharing 163 | the repository, you may also list the other group members (make sure gpg 164 | knows about their keys, too). 165 | 166 | The pass data itself is loaded via perl's YAML plugin, so any valid YAML 167 | should work. The names of the elements are up to you, and you can make 168 | hierarchies of arbitrary depth (e.g., `myproject.host1.mysql.password`). 169 | 170 | 171 | Query Language 172 | -------------- 173 | 174 | The query language is akin to XPath or CSS selectors, but much less 175 | powerful (and hopefully simpler to use as a result). A key of the form 176 | `a.b.c` will look for a YAML element matching `a`, which has a 177 | sub-element matching `b`, and so forth. Each part of the key is a 178 | case-insensitive regex, and must match either the hash key of the YAML 179 | element, or the value of the `desc` field of the YAML element. 180 | 181 | You may specify a key which is smaller than the full path. "Top" parts 182 | of the hierarchy always match (so `b.c` will match `a.b.c`). "Bottom" 183 | parts also match, so `a.b` will match `a.b.c` and `a.b.d`). Elements 184 | between key parts must be matched (so `a.c` does not match `a.b.c`). 185 | 186 | 187 | Pentadactyl Plugin 188 | ------------------ 189 | 190 | If you use the `pentadactyl` plugin for Firefox, there is a drop-in 191 | plugin that can help with filling form fields from `pass` data: 192 | 193 | 1. Copy the `autofill` plugin to your `.pentadactyl` directory: 194 | 195 | mkdir -p ~/.pentadactyl/plugins 196 | cp pentadactyl/autofill.js ~/.pentadactyl/plugins/ 197 | 198 | 2. Add a `url` field to your pass stanzas. E.g.: 199 | 200 | example: 201 | desc: My Example Site 202 | url: https://example.com 203 | user: foo 204 | password: bar 205 | 206 | 3. Auto-generate a mapping of URLs to stanzas: 207 | 208 | pass --generate-autofill >~/.pentadactyl/pass.js 209 | 210 | You can also do the mapping by hand if you do not want to keep the 211 | URLs in your password file (but take care to make your URL regexes 212 | sufficiently restrictive): 213 | 214 | cat >~/.pentadactyl/pass.js <<\EOF 215 | plugins.autofill.add('^https://example\.com/', 'example'); 216 | EOF 217 | 218 | 4. Bind form-filling to a key (I use `Ctrl-F`), and load the mappings: 219 | 220 | cat >>~/.pentadactylrc <<\EOF 221 | loadplugins autofill 222 | runtime pass.js 223 | map :js plugins.autofill.fill(); 224 | imap :js plugins.autofill.fill(); 225 | EOF 226 | 227 | With the steps above, hitting `Ctrl-F` at `example.com` will fill any 228 | input elements that look like usernames with the contents of 229 | `example.user`, and any that look like passwords with 230 | `example.password`. 231 | 232 | 233 | Todo 234 | ---- 235 | 236 | This system is undoubtedly full of bugs. It works for me, but hasn't 237 | received wide use. Due to offloading the cryptography to gpg, it is 238 | hoped that all bugs are query bugs, and not security bugs. Success (or 239 | failure) reports are welcome. 240 | 241 | Setup could be simpler; possibly the system should provide a script to 242 | help the user setup their keys and repository. 243 | 244 | The query system is ad-hoc. It has worked well in practice, but it may 245 | be that something like XPath would be more flexible, more standardized, 246 | and not too much harder to use. 247 | 248 | Some people might prefer another format, like JSON, over YAML. I think 249 | YAML is easier for humans to write. The system could potentially allow 250 | both (since it never writes, but only reads). 251 | 252 | It would be helpful to integrate with tools that want to access the 253 | passwords. I currently tie this to git with the following config: 254 | 255 | [credential "https://github.com"] 256 | username = peff 257 | helper = "!f() { test $1 = get && echo password=$(pass -n github.password); }; f" 258 | 259 | though you could also build a fancier helper around it (e.g., storing 260 | the URL along with the username and password, and then comparing it to 261 | the URL git is trying to access). 262 | 263 | The pentadactyl extension is rather simplistic. It looks only for `user` 264 | and `password` in input elements; this matching heuristics probably need 265 | to be expanded. Firefox's password manager already solves this problem, 266 | and it would be nice if we could piggyback on that. 267 | -------------------------------------------------------------------------------- /gpg.vim: -------------------------------------------------------------------------------- 1 | autocmd BufReadPre *.gpg call s:GPGSetup() 2 | autocmd BufReadPost *.gpg call s:GPGDecrypt() 3 | autocmd BufWritePre *.gpg call s:GPGEncrypt() 4 | autocmd BufWritePost *.gpg call s:GPGEncryptPost() 5 | 6 | function s:GPGSetup() 7 | set viminfo= 8 | set noswapfile 9 | set binary 10 | endfunction 11 | 12 | function s:GPGDecrypt() 13 | silent! %!gpg -qd --no-tty 14 | if v:shell_error 15 | silent! undo 16 | else 17 | set nobinary 18 | endif 19 | endfunction 20 | 21 | function s:GPGEncrypt() 22 | let line = 1 23 | let recipients = [] 24 | 25 | while getline(line) =~ '^#' 26 | let match = matchlist(getline(line), '# recipient: \(.*\)') 27 | if !empty(match) 28 | let recipients += [match[1]] 29 | endif 30 | let line = line + 1 31 | endwhile 32 | 33 | if !empty(recipients) 34 | let s:GPGSavedView = winsaveview() 35 | exe "%!gpg -aqe " . join(map(recipients, '"-r " . shellescape(v:val)')) 36 | if v:shell_error 37 | silent! undo 38 | call winrestview(s:GPGSavedView) 39 | throw "unable to write encrypted file" 40 | endif 41 | endif 42 | endfunction 43 | 44 | function s:GPGEncryptPost() 45 | silent! undo 46 | call winrestview(s:GPGSavedView) 47 | endfunction 48 | -------------------------------------------------------------------------------- /pass: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings qw(all FATAL); 5 | use YAML qw(); 6 | use Getopt::Long; 7 | 8 | my $home = $ENV{PASS_HOME} || "$ENV{HOME}/.pass"; 9 | my @files = glob($ENV{PASS_FILE} || "$home/*.gpg"); 10 | 11 | # hack because YAML refuses to remember the order of parsing. 12 | # Keys mentioned below take precedence over unmentioned ones, 13 | # and order of mention determines precedence. 14 | my %SORT_MAGIC = do { 15 | my $n = 0; 16 | map { $_ => $n++ } ( 17 | qw(desc), 18 | qw(id user number password pin), 19 | qw(routing account), 20 | ); 21 | }; 22 | 23 | my $no_key; 24 | my $generate_autofill; 25 | my $cmd = 'grep'; 26 | Getopt::Long::Configure("bundling"); 27 | GetOptions( 28 | 'no-key|n' => \$no_key, 29 | 'list|l' => sub { $cmd = 'list' }, 30 | 'paste|p' => sub { $cmd = 'paste' }, 31 | 'autofill' => sub { $cmd = 'autofill' }, 32 | ) or exit 100; 33 | 34 | if ($cmd eq 'autofill') { 35 | foreach my $file (@files) { 36 | my ($user, $pass) = autofill(decrypt($file), @ARGV); 37 | 38 | if (defined $user && defined $pass) { 39 | print "$user\n$pass\n"; 40 | exit 0; 41 | } 42 | } 43 | exit 1; 44 | } 45 | 46 | my @entries = map { match_entries(decrypt($_), @ARGV) } @files; 47 | 48 | if ($cmd eq 'grep') { 49 | foreach my $e (@entries) { 50 | print $e->[0], ' ' unless $no_key; 51 | print $e->[1], "\n"; 52 | } 53 | } 54 | elsif ($cmd eq 'list') { 55 | foreach my $e (@entries) { 56 | print $e->[0], "\n"; 57 | } 58 | } 59 | elsif ($cmd eq 'paste') { 60 | open(my $out, '|-', qw(xsel -i)); 61 | foreach my $e (@entries) { 62 | print $out $e->[1], "\n"; 63 | } 64 | } 65 | exit 0; 66 | 67 | sub decrypt { 68 | my $fn = shift; 69 | return "" unless -e $fn; 70 | open(my $fh, '-|', "gpg -qd --no-tty <" . quotemeta($fn)); 71 | local $/; 72 | return YAML::Load(<$fh>); 73 | } 74 | 75 | # Poor man's xpath. 76 | sub match_entries_recurse { 77 | my ($entry, $k, $root, $can_skip) = @_; 78 | 79 | # If we are at a leaf node, match only if we have 80 | # no key left to match. 81 | if (ref($entry) ne 'HASH') { 82 | if (!@$k) { 83 | my $fullname = join('.', @$root); 84 | return ([$fullname, $entry]); 85 | } 86 | return (); 87 | } 88 | 89 | my @r; 90 | foreach my $name (sort keycmp keys(%$entry)) { 91 | my $child = $entry->{$name}; 92 | push @$root, $name; 93 | 94 | # If we have no key left, then everything under 95 | # us is a match. 96 | if (!@$k) { 97 | push @r, match_entries_recurse($child, $k, $root, $can_skip); 98 | } 99 | # Otherwise, we must match the key, or we 100 | # can skip front nodes. We need to do both 101 | # if possible, because always consuming key 102 | # will miss something like searching for 103 | # "foo.bar" in "foo.foo.bar". 104 | else { 105 | if (entry_matches($name, $child, $k->[0])) { 106 | my $matched = shift @$k; 107 | push @r, match_entries_recurse($child, $k, $root, 0); 108 | unshift @$k, $matched; 109 | } 110 | if ($can_skip) { 111 | push @r, match_entries_recurse($child, $k, $root, $can_skip); 112 | } 113 | } 114 | 115 | pop @$root; 116 | } 117 | return @r; 118 | } 119 | 120 | sub entry_matches { 121 | my ($name, $entry, $k) = @_; 122 | return 1 if $name =~ $k; 123 | return 1 if ref($entry) eq 'HASH' && 124 | exists($entry->{desc}) && 125 | $entry->{desc} =~ $k; 126 | return 0; 127 | } 128 | 129 | sub keycmp { 130 | if (exists $SORT_MAGIC{$a}) { 131 | if (exists $SORT_MAGIC{$b}) { 132 | return $SORT_MAGIC{$a} - $SORT_MAGIC{$b}; 133 | } 134 | return -1; 135 | } 136 | if (exists $SORT_MAGIC{$b}) { 137 | return 1; 138 | } 139 | return $a cmp $b; 140 | } 141 | 142 | sub match_entries { 143 | my $entries = shift; 144 | my @r; 145 | 146 | foreach my $k (@_) { 147 | my @regexes = map { qr/$_/i } split /\./, $k; 148 | push @r, match_entries_recurse($entries, \@regexes, [], 1); 149 | } 150 | 151 | my %seen; 152 | return grep { !$seen{$_->[0]}++; } @r; 153 | } 154 | 155 | sub autofill { 156 | eval 'require URI' or die "unable to load URI module"; 157 | my $data = shift; 158 | my $uri = URI->new(shift); 159 | return autofill_recurse($data, $uri); 160 | } 161 | 162 | sub autofill_recurse { 163 | my ($data, $want) = @_; 164 | 165 | return unless ref($data) eq 'HASH'; 166 | 167 | my $url = $data->{url}; 168 | my $user = one_of($data, qw(user username)); 169 | my $pass = one_of($data, qw(pass password)); 170 | 171 | if (defined $url && defined $user && defined $pass) { 172 | my $have = URI->new($url); 173 | if (autofill_match_uri($have, $want)) { 174 | return ($user, $pass); 175 | } 176 | } 177 | 178 | foreach my $k (sort keys(%$data)) { 179 | my @r = autofill_recurse($data->{$k}, $want); 180 | @r and return @r; 181 | } 182 | return (); 183 | } 184 | 185 | sub autofill_match_uri { 186 | my ($one, $two) = @_; 187 | 188 | # Our strategy is to be picky about hostnames matching exactly, 189 | # which might have false negatives, but not false positives. But 190 | # we are lenient with the path; we assume that all paths at the 191 | # same host are under the same control. 192 | return $one->host eq $two->host; 193 | } 194 | 195 | sub one_of { 196 | my $data = shift; 197 | foreach my $option (@_) { 198 | return $data->{$option} if exists $data->{$option}; 199 | } 200 | return undef; 201 | } 202 | -------------------------------------------------------------------------------- /pentadactyl/autofill.js: -------------------------------------------------------------------------------- 1 | function fill() 2 | { 3 | let doc = window.content.document; 4 | fields = io.system(["pass", "--autofill", doc.URL]).output.split("\n", 2); 5 | if (fields.length != 2) 6 | return; 7 | for (let elem of DOM.XPath('//input', doc)) { 8 | if (elem.id.match(/user/i)) { 9 | elem.focus(); 10 | elem.value = fields[0]; 11 | } 12 | else if (elem.type == "password" && elem.id.match(/pass/i)) { 13 | elem.focus(); 14 | elem.value = fields[1]; 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------