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);
111 my ($file, $pass) = @_;
113 my @args = ($gpg, @gpg_flags, '-e', '-r', recipient(),
115 open my $fh, '|-', @args;
121 open my $fh, '<', "$store/.gpg-id"
122 or die "can't open recipient file";
135 if (m,/.git$, || m,/.got$,) {
136 $File::Find::prune = 1;
140 return if defined($pattern) && ! m/$pattern/;
141 return unless -f && m,.gpg$,;
150 return sort(@entries);
154 open my $fh, '-|', ($got, @_);
156 die "\"@_\" failed" if $? != 0;
160 got 'add', '-I', shift;
164 got 'remove', '-f', shift;
168 system ($got, 'commit', '-m', shift);
169 die "failed to commit changes: $!" if $? == -1;
176 GetOptions('h|?' => \&usage) or usage;
180 my $file = name2file(shift @ARGV);
181 system ($gpg, @gpg_flags, '-d', $file);
182 die "failed to exec $gpg: $!" if $? == -1;
187 GetOptions('h|?' => \&usage) or usage;
190 map { say $_ } passfind(shift @ARGV);
194 my $chars = $default_chars;
195 my $length = $default_length;
198 'c=s' => sub { $chars = $_[1] },
200 'l=i' => sub { $length = $_[1] },
204 my $name = shift @ARGV;
205 my $file = name2file $name;
206 die "password already exists: $file\n" if -e $file;
208 my $pass = gen($chars, $length);
209 writepass($file, $pass);
220 # TODO: handle moving directories?
222 GetOptions('h|?' => \&usage) or usage;
228 my $pa = name2file $a;
229 my $pb = name2file $b;
231 die "source password doesn't exist" unless -f $pa;
232 die "target password exists" if -f $pb;
234 rename $pa, $pb or die "can't rename $a to $b: $!";
242 my $chars = $default_chars;
243 my $length = $default_length;
246 'c=s' => sub { $chars = $_[1] },
248 'l=i' => sub { $length = $_[1] },
252 say gen($chars, $length);
256 my $chars = $default_chars;
257 my $length = $default_length;
260 'c=s' => sub { $chars = $_[1] },
262 'l=i' => sub { $length = $_[1] },
266 my $name = shift @ARGV;
267 my $file = name2file $name;
268 die "password doesn't exist" unless -f $file;
270 my $pass = gen($chars, $length);
271 writepass($file, $pass);
274 got_ci "regen $name";
278 GetOptions('h|?' => \&usage) or usage;
281 my $name = shift @ARGV;
282 my $file = name2file $name;
294 GetOptions('h|?' => \&usage) or usage;
297 my $name = shift @ARGV;
298 my $file = name2file $name;
300 my $pass = readpass "Enter the password: ";
301 writepass($file, $pass);
311 B<plass> - manage passwords
315 B<plass> I<command> [-h] [arg ...]
317 Valid subcommands are: cat, find, gen, got, mv, oneshot, regen, rm,
322 B<plass> is a simple password manager. It manages passwords stored in
323 a directory tree rooted at I<~/.password-store> (or I<$PLASS_STORE>),
324 where every password is a single file encrypted with gpg2(1).
326 Passwords entries can be referenced using the path relative to the
327 store directory. The extension ".gpg" is optional.
329 The whole store is supposed to be managed by the got(1) version
332 The commands for B<plass> are as follows:
336 =item B<cat> I<entries ...>
338 Decrypt and print the passwords of the given I<entries>.
340 =item B<find> [I<pattern>]
342 Print one per line all the entries of the store, optionally filtered
343 by the given I<pattern>.
345 =item B<gen> [B<-c> I<chars>] [B<-l> I<length>] I<entry>
347 Generate and persist a password for the given I<entry> in the store.
348 B<-c> can be used to control the characters allowed in the password
349 (by default I<!-~> i.e. all the printable ASCII character) and B<-l>
350 the length (32 by default.)
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.
362 =item B<oneshot> [B<-c> I<chars>] [B<-l> I<length>]
364 Like B<gen> but prints the the generated password instead of persist
367 =item B<regen> [B<-c> I<chars>] [B<-l> I<length>] I<entry>
369 Like B<gen> but re-generates a password in-place.
373 Remove the password I<entry> from the store.
375 =item B<tog> I<arguments ...>
377 Execute tog(1) in the password store directory with the given
380 =item B<write> I<entry>
382 Prompt for a password and persist it in the store under the given
387 =head1 CREATING A PASSWORD STORE
389 A password store is just a normal got(1) repository with a worktree
390 checked out in I<~/.password-store> (or I<$PLASS_STORE>). The only
391 restriction is that a file called I<.gpg-id> must exist in the root of
392 the work tree for most B<plass> commands to work.
394 For example, a got repository and password store can be created as
397 $ mkdir .password-store
399 $ echo foo@example.com > .gpg-id
403 $ got import -m 'initial import' ~/.password-store
404 $ cd ~/.password-store
405 $ got checkout -E ~/git/pass.git
407 See got(1) for more information.
409 Otherwise, if a repository already exists, a password-store can be
410 checked out more simply as:
412 $ got checkout ~/git/pass.git ~/.password-store
414 To migrate from pass(1), just delete I<~/.password-store> and checkout
415 it again usign got(1).
423 Default range of characters to use to generate passwords.
427 Path to the got(1) executable.
431 Path to the gpg2(1) executable.
435 Default length for the passwords generated.
439 Path to the password-store directory tree. I<~/.password-store> by
444 Path to the tog(1) executable.
452 =item I<~/.password-store>
454 Password store used by default.
456 =item I<~/.password-store/.gpg-id>
458 File containing the gpg recipient used to encrypt the passwords.
462 =head1 ACKNOWLEDGEMENTS
464 B<plass> was heavily influenced by pass(1) in the design, but it's a
465 complete different implementation with different tools involved.
469 The B<plass> utility was written by Omar Polo <I<op@omarpolo.com>>.
473 B<plass> B<find> output format isn't designed to handle files with
474 newlines in them. Use find(1) B<-print0> or similar if it's a
477 There isn't a B<init> sub-command, the store initialisation must be