3 # Copyright (c) 2022 Omar Polo <op@omarpolo.com>
5 # Permission to use, copy, modify, and distribute this software for any
6 # purpose with or without fee is hereby granted, provided that the above
7 # copyright notice and this permission notice appear in all copies.
9 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 use open ":std", ":encoding(UTF-8)";
23 use Getopt::Long qw(:config bundling require_order);
28 my $store = $ENV{'PLASS_STORE'} // $ENV{'HOME'}.'/.password-store';
30 my $got = $ENV{'PLASS_GOT'} // 'got';
31 my $tog = $ENV{'PLASS_TOG'} // 'tog';
33 my $gpg = $ENV{'PLASS_GPG'} // 'gpg2';
34 my @gpg_flags = qw(--quiet --compress-algo=none --no-encrypt-to);
36 my $default_chars = $ENV{'PLASS_CHARS'} // '!-~';
37 my $default_length = $ENV{'PLASS_LENGTH'};
38 if (!defined($default_length) || $default_length lt 0) {
43 "h|?" => sub { pod2usage(0) },
46 my $cmd = shift // 'find';
49 cat => [\&cmd_cat, "entries..."],
50 find => [\&cmd_find, "[pattern]"],
51 gen => [\&cmd_gen, "[-nq] [-c chars] [-l length] entry"],
52 got => [\&cmd_got, "args ..."],
53 mv => [\&cmd_mv, "from to"],
54 rm => [\&cmd_rm, "entry"],
55 tee => [\&cmd_tee, "[-q] entry"],
56 tog => [\&cmd_tog, "args ..."],
58 pod2usage(1) unless defined $subcmd{$cmd};
59 my ($fn, $usage) = @{$subcmd{$cmd}};
68 my $prog = basename $0;
69 say STDERR "Usage: $prog $cmd $usage";
75 $f .= ".gpg" unless $f =~ m,\.gpg$,;
79 # tr -cd -- $chars < /dev/random | dd bs=$len count=1 status=none
81 my ($chars, $length) = @_;
84 open(my $fh, '<:raw', '/dev/random')
85 or die "can't open /dev/random: $!";
88 read($fh, my $t, $length * 4)
89 or die "failed to read /dev/random: $!";
96 return substr($pass, 0, $length);
100 # todo some stty black magic to avoid echo
103 die "failed to read stdin: $!" unless defined($pass);
110 my $parent = dirname $dir;
111 mkdirs($parent) unless -d $parent || $parent eq '/';
112 mkdir $dir or die "mkdir $dir: $!"
117 my ($file, $pass) = @_;
119 mkdirs(dirname $file);
121 my @args = ($gpg, @gpg_flags, '-e', '-r', recipient(),
123 open my $fh, '|-', @args;
129 open my $fh, '<', "$store/.gpg-id"
130 or die "can't open recipient file";
143 if (m,/.git$, || m,/.got$,) {
144 $File::Find::prune = 1;
148 return if defined($pattern) && ! m/$pattern/;
149 return unless -f && m,.gpg$,;
158 return sort(@entries);
163 open my $fh, '-|', ($got, @_);
169 return got 'add', '-I', shift;
173 got 'remove', '-f', shift
179 die "failed to fork: $!" unless defined $pid;
183 die "failed to commit changes" if $?;
187 open (STDOUT, ">&", \*STDERR)
188 or die "can't redirect stdout to stderr";
189 exec ($got, 'commit', '-m', shift)
190 or die "failed to exec $got: $!";
197 GetOptions('h|?' => \&usage) or usage;
201 my $file = name2file(shift @ARGV);
202 system ($gpg, @gpg_flags, '-d', $file);
203 die "failed to exec $gpg: $!" if $? == -1;
208 GetOptions('h|?' => \&usage) or usage;
211 map { say $_ } passfind(shift @ARGV);
215 my $chars = $default_chars;
216 my $length = $default_length;
221 'c=s' => sub { $chars = $_[1] },
223 'l=i' => sub { $length = $_[1] },
229 my $name = shift @ARGV;
230 my $file = name2file $name;
231 my $renamed = -f $file;
233 my $pass = gen($chars, $length);
236 writepass($file, $pass);
238 got_ci($renamed ? "update $name" : "+$name");
247 # TODO: handle moving directories?
249 GetOptions('h|?' => \&usage) or usage;
255 my $pa = name2file $a;
256 my $pb = name2file $b;
258 die "source password doesn't exist" unless -f $pa;
259 die "target password exists" if -f $pb;
261 rename $pa, $pb or die "can't rename $a to $b: $!";
264 got_add $pb or die "can't add $pb\n";
269 GetOptions('h|?' => \&usage) or usage;
272 my $name = shift @ARGV;
273 my $file = name2file $name;
287 my $name = shift @ARGV;
288 my $file = name2file $name;
290 my $pass = readpass "Enter the password: ";
291 writepass($file, $pass);
294 got_ci (-f $file ? "update $name" : "+$name");
306 B<plass> - manage passwords
310 B<plass> I<command> [-h] [arg ...]
312 Valid subcommands are: cat, find, gen, got, mv, rm, tee, tog.
314 If no I<command> is given, B<find> is assumed.
318 B<plass> is a simple password manager. It manages passwords stored in
319 a directory tree rooted at I<~/.password-store> (or I<$PLASS_STORE>),
320 where every password is a single file encrypted with gpg2(1).
322 Passwords entries can be referenced using the path relative to the
323 store directory. The extension ".gpg" is optional.
325 The whole store is supposed to be managed by the got(1) version
328 The commands for B<plass> are as follows:
332 =item B<cat> I<entries ...>
334 Decrypt and print the passwords of the given I<entries>.
336 =item B<find> [I<pattern>]
338 Print one per line all the entries of the store, optionally filtered
339 by the given I<pattern>.
341 =item B<gen> [B<-nq>] [B<-c> I<chars>] [B<-l> I<length>] I<entry>
343 Generate and persist a password for the given I<entry> in the store.
344 B<-c> can be used to control the characters allowed in the password
345 (by default I<!-~> i.e. all the printable ASCII character) and B<-l>
346 the length (32 by default.)
348 Unless B<-q> is provided, plass prints the generated password.
350 If the B<-n> option is given, plass won't persist the password.
352 =item B<got> I<arguments ...>
354 Execute got(1) in the password store directory with the given
357 =item B<mv> I<from> I<to>
359 Rename a password entry, doesn't work with directories. I<from> must
360 exist and I<to> mustn't.
364 Remove the password I<entry> from the store.
366 =item B<tee> [B<-q>] I<entry>
368 Prompt for a password, persist it in the store under the given
369 I<entry> name and then print it again to standard output.
371 =item B<tog> I<arguments ...>
373 Execute tog(1) in the password store directory with the given
378 =head1 CREATING A PASSWORD STORE
380 A password store is just a normal got(1) repository with a worktree
381 checked out in I<~/.password-store> (or I<$PLASS_STORE>). The only
382 restriction is that a file called I<.gpg-id> must exist in the root of
383 the work tree for most B<plass> commands to work.
385 For example, a got repository and password store can be created as
388 $ mkdir .password-store
390 $ echo foo@example.com > .gpg-id
393 $ got import -r pass.git -m 'initial import' ~/.password-store
394 $ cd ~/.password-store
395 $ got checkout -E ~/git/pass.git .
397 See got(1) for more information.
399 Otherwise, if a repository already exists, a password-store can be
400 checked out more simply as:
402 $ got checkout ~/git/pass.git ~/.password-store
404 To migrate from pass(1), just delete I<~/.password-store> and checkout
405 it again using got(1).
413 Default range of characters to use to generate passwords.
417 Path to the got(1) executable.
421 Path to the gpg2(1) executable.
425 Default length for the passwords generated.
429 Path to the password-store directory tree. I<~/.password-store> by
434 Path to the tog(1) executable.
442 =item I<~/.password-store>
444 Password store used by default.
446 =item I<~/.password-store/.gpg-id>
448 File containing the gpg recipient used to encrypt the passwords.
452 =head1 ACKNOWLEDGEMENTS
454 B<plass> was heavily influenced by pass(1) in the design, but it's a
455 complete different implementation with different tools involved.
459 The B<plass> utility was written by Omar Polo <I<op@omarpolo.com>>.
463 B<plass> B<find> output format isn't designed to handle files with
464 newlines in them. Use find(1) B<-print0> or similar if it's a
467 There isn't a B<init> sub-command, the store initialisation must be