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 or pod2usage(1);
49 cat => [\&cmd_cat, "entries..."],
50 find => [\&cmd_find, "[pattern]"],
51 gen => [\&cmd_gen, "[-c chars] [-l length] entry"],
52 got => [\&cmd_got, "arguments ..."],
53 mv => [\&cmd_mv, "from to"],
54 oneshot => [\&cmd_oneshot, "[-c chars] [-l length]"],
55 regen => [\&cmd_regen, "[-c chars] [-l length] entry"],
56 rm => [\&cmd_rm, "entry"],
57 tog => [\&cmd_tog, "arguments ..."],
58 write => [\&cmd_write, "entry"],
60 pod2usage(1) unless defined $subcmd{$cmd};
61 my ($fn, $usage) = @{$subcmd{$cmd}};
70 my $prog = basename $0;
71 say STDERR "Usage: $prog $cmd $usage";
77 $f .= ".gpg" unless $f =~ m,\.gpg$,;
81 # tr -cd -- $chars < /dev/random | dd bs=$len count=1 status=none
83 my ($chars, $length) = @_;
86 open(my $fh, '<:raw', '/dev/random')
87 or die "can't open /dev/random: $!";
90 read($fh, my $t, $length * 4)
91 or die "failed to read /dev/random: $!";
98 return substr($pass, 0, $length);
102 # todo some stty black magic to avoid echo
105 die "failed to read stdin: $!" unless defined($pass);
112 my $parent = dirname $dir;
113 mkdirs($parent) unless -d $parent || $parent eq '/';
114 mkdir $dir or die "mkdir $dir: $!"
119 my ($file, $pass) = @_;
121 mkdirs(dirname $file);
123 my @args = ($gpg, @gpg_flags, '-e', '-r', recipient(),
125 open my $fh, '|-', @args;
131 open my $fh, '<', "$store/.gpg-id"
132 or die "can't open recipient file";
145 if (m,/.git$, || m,/.got$,) {
146 $File::Find::prune = 1;
150 return if defined($pattern) && ! m/$pattern/;
151 return unless -f && m,.gpg$,;
160 return sort(@entries);
164 open my $fh, '-|', ($got, @_);
166 die "\"@_\" failed" if $? != 0;
170 got 'add', '-I', shift;
174 got 'remove', '-f', shift;
178 system ($got, 'commit', '-m', shift);
179 die "failed to commit changes: $!" if $? == -1;
186 GetOptions('h|?' => \&usage) or usage;
190 my $file = name2file(shift @ARGV);
191 system ($gpg, @gpg_flags, '-d', $file);
192 die "failed to exec $gpg: $!" if $? == -1;
197 GetOptions('h|?' => \&usage) or usage;
200 map { say $_ } passfind(shift @ARGV);
204 my $chars = $default_chars;
205 my $length = $default_length;
208 'c=s' => sub { $chars = $_[1] },
210 'l=i' => sub { $length = $_[1] },
214 my $name = shift @ARGV;
215 my $file = name2file $name;
216 die "password already exists: $file\n" if -e $file;
218 my $pass = gen($chars, $length);
219 writepass($file, $pass);
230 # TODO: handle moving directories?
232 GetOptions('h|?' => \&usage) or usage;
238 my $pa = name2file $a;
239 my $pb = name2file $b;
241 die "source password doesn't exist" unless -f $pa;
242 die "target password exists" if -f $pb;
244 rename $pa, $pb or die "can't rename $a to $b: $!";
252 my $chars = $default_chars;
253 my $length = $default_length;
256 'c=s' => sub { $chars = $_[1] },
258 'l=i' => sub { $length = $_[1] },
262 say gen($chars, $length);
266 my $chars = $default_chars;
267 my $length = $default_length;
270 'c=s' => sub { $chars = $_[1] },
272 'l=i' => sub { $length = $_[1] },
276 my $name = shift @ARGV;
277 my $file = name2file $name;
278 die "password doesn't exist" unless -f $file;
280 my $pass = gen($chars, $length);
281 writepass($file, $pass);
284 got_ci "regen $name";
288 GetOptions('h|?' => \&usage) or usage;
291 my $name = shift @ARGV;
292 my $file = name2file $name;
304 GetOptions('h|?' => \&usage) or usage;
307 my $name = shift @ARGV;
308 my $file = name2file $name;
310 my $pass = readpass "Enter the password: ";
311 writepass($file, $pass);
321 B<plass> - manage passwords
325 B<plass> I<command> [-h] [arg ...]
327 Valid subcommands are: cat, find, gen, got, mv, oneshot, regen, rm,
332 B<plass> is a simple password manager. It manages passwords stored in
333 a directory tree rooted at I<~/.password-store> (or I<$PLASS_STORE>),
334 where every password is a single file encrypted with gpg2(1).
336 Passwords entries can be referenced using the path relative to the
337 store directory. The extension ".gpg" is optional.
339 The whole store is supposed to be managed by the got(1) version
342 The commands for B<plass> are as follows:
346 =item B<cat> I<entries ...>
348 Decrypt and print the passwords of the given I<entries>.
350 =item B<find> [I<pattern>]
352 Print one per line all the entries of the store, optionally filtered
353 by the given I<pattern>.
355 =item B<gen> [B<-c> I<chars>] [B<-l> I<length>] I<entry>
357 Generate and persist a password for the given I<entry> in the store.
358 B<-c> can be used to control the characters allowed in the password
359 (by default I<!-~> i.e. all the printable ASCII character) and B<-l>
360 the length (32 by default.)
362 =item B<got> I<arguments ...>
364 Execute got(1) in the password store directory with the given
367 =item B<mv> I<from> I<to>
369 Rename a password entry, doesn't work with directories. I<from> must
370 exist and I<to> mustn't.
372 =item B<oneshot> [B<-c> I<chars>] [B<-l> I<length>]
374 Like B<gen> but prints the the generated password instead of persist
377 =item B<regen> [B<-c> I<chars>] [B<-l> I<length>] I<entry>
379 Like B<gen> but re-generates a password in-place.
383 Remove the password I<entry> from the store.
385 =item B<tog> I<arguments ...>
387 Execute tog(1) in the password store directory with the given
390 =item B<write> I<entry>
392 Prompt for a password and persist it in the store under the given
397 =head1 CREATING A PASSWORD STORE
399 A password store is just a normal got(1) repository with a worktree
400 checked out in I<~/.password-store> (or I<$PLASS_STORE>). The only
401 restriction is that a file called I<.gpg-id> must exist in the root of
402 the work tree for most B<plass> commands to work.
404 For example, a got repository and password store can be created as
407 $ mkdir .password-store
409 $ echo foo@example.com > .gpg-id
413 $ got import -m 'initial import' ~/.password-store
414 $ cd ~/.password-store
415 $ got checkout -E ~/git/pass.git
417 See got(1) for more information.
419 Otherwise, if a repository already exists, a password-store can be
420 checked out more simply as:
422 $ got checkout ~/git/pass.git ~/.password-store
424 To migrate from pass(1), just delete I<~/.password-store> and checkout
425 it again usign got(1).
433 Default range of characters to use to generate passwords.
437 Path to the got(1) executable.
441 Path to the gpg2(1) executable.
445 Default length for the passwords generated.
449 Path to the password-store directory tree. I<~/.password-store> by
454 Path to the tog(1) executable.
462 =item I<~/.password-store>
464 Password store used by default.
466 =item I<~/.password-store/.gpg-id>
468 File containing the gpg recipient used to encrypt the passwords.
472 =head1 ACKNOWLEDGEMENTS
474 B<plass> was heavily influenced by pass(1) in the design, but it's a
475 complete different implementation with different tools involved.
479 The B<plass> utility was written by Omar Polo <I<op@omarpolo.com>>.
483 B<plass> B<find> output format isn't designed to handle files with
484 newlines in them. Use find(1) B<-print0> or similar if it's a
487 There isn't a B<init> sub-command, the store initialisation must be