File Coverage

blib/lib/App/locket.pm
Criterion Covered Total %
statement 44 46 95.6
branch n/a
condition n/a
subroutine 18 18 100.0
pod n/a
total 62 64 96.8


line stmt bran cond sub pod time code
1             package App::locket;
2             BEGIN {
3 1     1   119330 $App::locket::VERSION = '0.0022';
4             }
5             # ABSTRACT: Copy secrets from a YAML/JSON cipherstore into the clipboard (pbcopy, xsel, xclip)
6              
7 1     1   9 use strict;
  1         2  
  1         23  
8 1     1   5 use warnings;
  1         2  
  1         29  
9              
10             BEGIN {
11             # Safe path
12 1     1   20 $ENV{ PATH } = '/bin:/usr/bin';
13             }
14              
15 1     1   2026 use Term::ReadKey;
  1         14413  
  1         170  
16             END {
17 1     1   65 ReadMode 0;
18             }
19 1     1   16320 use File::HomeDir;
  1         8381  
  1         87  
20 1     1   903 use Path::Class;
  1         50533  
  1         97  
21 1     1   1270 use JSON; my $JSON = JSON->new->pretty;
  1         14331  
  1         7  
22 1     1   1281 use YAML::XS();
  1         3169  
  1         26  
23 1     1   8 use File::Temp;
  1         2  
  1         139  
24 1     1   909 use Term::EditorEdit;
  1         59411  
  1         28  
25 1     1   10 use Try::Tiny;
  1         2  
  1         51  
26 1     1   824 use String::Util qw/ trim /;
  1         2742  
  1         112  
27             my $usage;
28             BEGIN {
29 1     1   17 $usage = <<_END_;
30              
31             Usage: locket [options] setup|edit|
32              
33             --copy Copy value to clipboard using pbcopy, xsel, or xclip
34              
35             --delay Keep value in clipboard for seconds
36             If value is still in the clipboard at the end of
37             then it will be automatically wiped from
38             the clipboard
39              
40             --unsafe Turn the safety off. This will disable prompting
41             before emitting any sensitive information in
42             plaintext. There will be no opportunity to
43             abort (via CTRL-C)
44              
45             --cfg Use for configuration
46              
47             setup Setup a new or edit an existing user configuration
48             file (~/.locket/cfg)
49              
50             edit Edit the cipherstore
51             The configuration must have an "edit" value, e.g.:
52              
53             /usr/bin/vim -n ~/.locket.gpg
54              
55             / Search the cipherstore for and emit the
56             resulting secret
57            
58             The configuration must have a "read" value to
59             tell it how to read the cipherstore. Only piped
60             commands are supported today, and they should
61             be something like:
62              
63            
64              
65             If the found key in the cipherstore is of the format
66             "@" then the username will be emitted
67             first before the secret (which is assumed to be a password/passphrase)
68              
69             Example YAML cipherstore:
70              
71             %YAML 1.1
72             ---
73             # A GMail identity
74             alice\@gmail: p455w0rd
75             # Some frequently used credit card information
76             cc4123: |
77             4123412341234123
78             01/23
79             123
80              
81             _END_
82             }
83 1     1   693 use Getopt::Usaginator $usage;
  1         32200  
  1         7  
84 1     1   1904 use Digest::SHA qw/ sha1_hex sha512_hex /;
  1         3083  
  1         88  
85 1     1   1673 use List::MoreUtils qw/ :all /;
  0         0  
  0         0  
86             use Hash::Dispatch;
87              
88             use App::locket::Locket;
89             use App::locket::TextRandomart;
90             use App::locket::Util;
91              
92             use App::locket::Moose;
93              
94             my %n2k = (
95             ( map { $_ => $_ + 1 } 0 .. 8 ),
96             9 => 0,
97             );
98             my %k2n = map { trim $_ } reverse %n2k;
99              
100             my %default_options = (
101             delay => 45,
102             );
103              
104             has locket => qw/ reader locket writer _locket isa App::locket::Locket lazy_build 1 /, handles =>
105             [qw/
106             cfg plaincfg write_cfg reload_cfg can_read read passphrase require_passphrase
107             store
108             /];
109             sub _build_locket {
110             my $self = shift;
111             my $locket = App::locket::Locket->open( $self->cfg_file );
112             return $locket;
113             }
114              
115             has home => qw/ is ro lazy_build 1 /;
116             sub _build_home {
117             my $self = shift;
118             my $home = File::HomeDir->my_data;
119             if ( defined $home ) {
120             $home = dir $home, '.locket';
121             }
122             return $home;
123             }
124              
125             has_file cfg_file => qw/ is ro lazy_build 1 /;
126             sub _build_cfg_file {
127             my $self = shift;
128             if ( defined ( my $file = $self->argument_options->{ cfg } ) ) {
129             return file $file;
130             }
131             my $home = $self->home;
132             return unless $home;
133             return $home->file( 'cfg' );
134             }
135              
136             has argument_options => qw/ is ro lazy_build 1 /;
137             sub _build_argument_options {
138             return {};
139             }
140              
141             has options => qw/ is ro lazy_build 1 /;
142             sub _build_options {
143             my $self = shift;
144              
145             my $cfg = $self->cfg;
146             my @options;
147             defined $cfg->{ $_ } and length $cfg->{ $_ } and push @options, $_ => $cfg->{ $_ } for qw/ delay /;
148             push @options, $_ => $cfg->{ $_ } for qw/ unsafe /;
149              
150             my %argument_options = %{ $self->argument_options };
151              
152             return { %default_options, @options, %argument_options };
153             }
154              
155             has stash => qw/ is ro lazy_build 1 /;
156             sub _build_stash {
157             return {};
158             }
159              
160             has [qw/ query found /] => qw/ is rw isa ArrayRef /, default => sub { [] };
161              
162             sub run {
163             my $self = shift;
164             my @arguments = @_;
165              
166             my $options = $self->argument_options;
167             my ( $help );
168             Getopt::Usaginator->parse( \@arguments, $options,
169             qw/ delay=s help|h cfg|config=s unsafe /,
170             );
171              
172             if ( $self->require_passphrase ) {
173             my $passphrase = $self->read_passphrase( 'Passphrase: ' );
174             $self->say_stderr;
175             if ( ! defined $passphrase || ! length $passphrase ) {
176             $self->say_stderr( "# No passphrase entered" );
177             exit 64;
178             }
179             $self->passphrase( $passphrase );
180             }
181              
182             $options = $self->options;
183              
184             $self->dispatch( '?' );
185             $self->dispatch( join( ' ', @arguments ) );
186             $self->dispatch( '' ) if @arguments;
187             }
188              
189             sub _select {
190             my $self = shift;
191             my $n = shift;
192              
193             my $found = $self->found;
194             my $k;
195             if ( !defined $n ) {
196             $k = $self->stash->{_}[0];
197             $n = $k2n{ $k };
198             }
199              
200             return unless defined $found->[ $n ];
201             my $target = $found->[ $n ];
202             my $entry = $self->store->get( $target );
203             return ( $target, $entry, $k, $n );
204             }
205              
206             sub get_target_entry {
207             my $self = shift;
208             my $stash = $self->stash;
209             my ( $target, $entry ) = @$stash{qw/ target entry /};
210             if ( !defined $target ) {
211             return $self->_select( 0 );
212             }
213             }
214              
215             my $_show = sub {
216             my ( $self, $method ) = @_;
217              
218             return unless my ( $target, $entry, $k, $n ) = $self->_select;
219             $self->emit_entry( $target, $entry );
220             $self->dispatch( '.' );
221             };
222              
223             my $_copy = sub {
224             my ( $self, $method ) = @_;
225              
226             return unless my ( $target, $entry, $k, $n ) = $self->_select;
227             $self->emit_entry( $target, $entry, copy => 1 );
228             $self->dispatch( '.' );
229             };
230              
231             sub emit_entry {
232             my $self = shift;
233             my $target = shift;
234             my $entry = shift;
235             my %options = @_;
236              
237             $self->say_stdout( sprintf "\n === %s ===\n", $target );
238             if ( $target =~ m/^([^@]+)@/ ) {
239             $self->emit_username_password( $1, $entry, copy => $options{ copy } );
240             }
241             else {
242             $self->emit_secret( $entry, copy => $options{ copy } );
243             }
244             }
245              
246             sub emit_username_password {
247             my $self = shift;
248             my ( $username, $password, %options ) = @_;
249              
250             if ( $options{ copy } ) {
251             $self->copy( username => $username );
252             $self->copy( password => $password );
253             }
254             else {
255             $self->safe_stdout( <<_END_ );
256             $username
257             $password
258             _END_
259             }
260             }
261              
262             sub emit_secret {
263             my $self = shift;
264             my ( $secret, %options ) = @_;
265              
266             if ( $options{ copy } ) {
267             $self->copy( secret => $secret );
268             }
269             else {
270             $self->safe_stdout( $secret, "\n" );
271             }
272             }
273              
274              
275             our $DISPATCH = Hash::Dispatch->dispatch(
276              
277             '' => sub {
278             my ( $self, $method ) = @_;
279              
280             while ( 1 ) {
281             $self->stdout( "> " );
282             my $line = $self->stdin_readline;
283             next unless defined $line;
284             next unless length $line;
285             $self->dispatch( $line );
286             }
287              
288             },
289              
290             '?' => sub {
291             my ( $self, $method ) = @_;
292              
293             my $cfg_file = $self->cfg_file;
294             my $cfg_file_size = -f $cfg_file && -s _;
295             defined && length or $_ = '-1' for $cfg_file_size;
296             {
297             my ( $read, $edit, $copy, $paste ) =
298             map { defined $_ ? $_ : '~' } @{ $self->cfg }{qw/ read edit copy paste /};
299              
300             my $randomart = App::locket::TextRandomart->randomart( sha1_hex $self->plaincfg );
301             my @randomart = split "\n", $randomart;
302             @randomart = map { " $_ " } @randomart;
303             $randomart[ 1 ] .= "$cfg_file ($cfg_file_size)";
304             $randomart[ 3 ] .= " $read";
305             $randomart[ 4 ] .= " $edit";
306             $randomart[ 5 ] .= " $copy";
307             $randomart[ 6 ] .= " $paste";
308              
309             $randomart = join "\n", @randomart;
310              
311             $self->stdout( <<_END_ );
312             App::locket @{[ $App::locket::VERSION || '0.0' ]}
313              
314             $randomart
315             _END_
316             $self->say_stdout;
317             }
318              
319             return;
320              
321             my $stash = $self->stash;
322             my $query = $self->query;
323             if ( @$query ) {
324             $self->say_stdout( sprintf " /: %s", join '/', @$query );
325             }
326              
327             my ( $target, $entry ) = @$stash{qw/ target entry /};
328             if ( defined $target ) {
329             $self->say_stdout( sprintf " =: %s", $target );
330             }
331              
332             },
333              
334             'setup' => sub {
335             my ( $self, $method ) = @_;
336              
337             my $cfg_file = $self->cfg_file;
338             my $plaincfg = $self->plaincfg;
339             if ( ! defined $plaincfg || $plaincfg =~ m/^\S*$/ ) {
340             $plaincfg = <<_END_;
341             %YAML 1.1
342             ---
343             #read: ''
344             #read: ''
345             #edit: '/usr/bin/vim -n '
346             #copy: -
347             #paste: -
348             _END_
349             }
350             my $file = File::Temp->new( template => '.locket.cfg.XXXXXXX', dir => '.', unlink => 1 ); # TODO A better dir?
351             my $plaincfg_edit = Term::EditorEdit->edit( file => $file, document => $plaincfg );
352             if ( length $plaincfg_edit ) {
353             $self->write_cfg( $plaincfg_edit );
354             $self->stdout_clear;
355             $self->say_stdout( "# Reload\n---\n" );
356             $self->reload_cfg;
357             $self->dispatch( '?' );
358             }
359             else {
360             }
361             },
362              
363             'cfg' => 'setup',
364             'config' => 'setup',
365              
366             qr/^q(?:u(?:i?)?)?$/ => 'quit',
367             'quit' => sub {
368             my ( $self, $method ) = @_;
369             exit 0;
370             },
371              
372             qr/^h(?:e(?:l?)?)?$/ => 'help',
373             'help' => sub {
374             my ( $self, $method ) = @_;
375              
376             my $edit = $self->cfg->{ edit };
377             my $read = $self->cfg->{ read };
378             my $cfg_file = $self->locket->cfg_file;
379              
380             $self->stdout( <<_END_ );
381            
382             / Search the store for and emit the
383             resulting secret
384              
385             Alternatively, append a term to the last query
386             and re-search
387              
388             . Redisplay the results of the last query
389              
390             .. Pop the last term off the last query (if any)
391             and re-search
392              
393             // Search the store for , ignoring
394             any previous query
395              
396             list List all the entries in the store
397              
398             edit Edit the store (via $edit)
399              
400             read Show the plainstore through \$PAGER/sensible-pager (via $read)
401              
402             cfg Configure locket ($cfg_file)
403              
404             reset/clear Clear the screen and wipe the last query/
405             current search
406              
407             reload Reload the configuration file and
408             the store (the secret database)
409              
410             _END_
411             },
412              
413             'lock' => sub {
414             my ( $self, $method ) = @_;
415              
416             my $passphrase = $self->read_passphrase( 'Passphrase: ' );
417             $self->say_stderr;
418             if ( ! defined $passphrase || ! length $passphrase ) {
419             $self->say_stderr( "# No passphrase entered" );
420             }
421             else {
422             $self->passphrase( $passphrase );
423             $self->write_cfg( $self->plaincfg );
424             }
425             },
426              
427             'unlock' => sub {
428             my ( $self, $method ) = @_;
429              
430             $self->passphrase( undef );
431             $self->write_cfg( $self->plaincfg );
432             },
433              
434             qr/^e(?:d(?:i?)?)?$/ => 'edit',
435             'edit' => sub {
436             my ( $self, $method ) = @_;
437              
438             my $edit = $self->cfg->{ edit };
439             if ( defined $edit && length $edit ) {
440             system( $edit );
441             # TODO If error...
442             $self->stdout_clear;
443             $self->say_stdout( "# Reload\n---\n" );
444             $self->locket->reload;
445             $self->dispatch( '?' );
446              
447             }
448             else {
449             $self->say_stderr( "% Missing (edit) in cfg" );
450             }
451             },
452              
453             qr/^r(?:e(?:a?)?)?$/ => 'read',
454             'read' => sub {
455             my ( $self, $method ) = @_;
456             return unless $self->check_read;
457              
458             my $plainstore = $self->read;
459             $self->safe_pager( sub {
460             my $fh = shift;
461             $fh->print( $plainstore );
462             }, clear => 1 );
463             $self->dispatch( '?' );
464             },
465              
466              
467             qr/^l(?:i(?:s?)?)?$/ => 'list',
468             'list' => sub {
469             my ( $self, $method ) = @_;
470             return unless $self->check_read;
471              
472             my @keys = $self->store->all;
473              
474             $self->do_pager( sub {
475             my $fh = shift;
476             $fh->print( "\n" );
477             $fh->print( sprintf "# Total: %d\n", scalar @keys );
478             $fh->print( "\n" );
479             $fh->print( join "\n", ( map { " $_" } @keys ), '' );
480             $fh->print( "\n" );
481             }, clear => 1 );
482             },
483              
484             qr/^(\/+|\.\.|\.)(.*)/ => sub {
485             my ( $self, $method ) = @_;
486              
487             my $store = $self->store;
488             my $last_query = $self->query;
489             my $last_found = $self->found;
490              
491             my $stash = $self->stash;
492             my $dotted = $stash->{_}[0] eq '.';
493             my $dotdotted = $stash->{_}[0] eq '..';
494              
495             my ( @query, @result_query, @result_found );
496             @query = @$last_query;
497              
498             if ( $dotdotted ) {
499             pop @query;
500             }
501             if ( $dotted ) {
502             @result_query = @$last_query;
503             @result_found = @$last_found;
504             }
505             else {
506             my $slashes = length( $stash->{_}[0] ) || 0;
507             my $target = $stash->{_}[1];
508              
509             if ( !$dotdotted && 2 == $slashes ) {
510             undef @query;
511             }
512              
513             $target = trim $target;
514             if ( length $target ) {
515             # Last search was a dud, so we'll pop the last term
516             pop @query unless @$last_found;
517             push @query, $target;
518             }
519              
520             my $result = $self->store->search( \@query );
521             @result_query = @{ $result->{ query } };
522             @result_found = @{ $result->{ found } };
523              
524             $self->query( \@result_query );
525             $self->found( \@result_found );
526             }
527              
528             my $total = @result_found;
529             my ( @visible, @invisible );
530             @visible = @result_found;
531             if ( @visible > 10 ) {
532             @invisible = splice @visible, 10;
533             }
534              
535             $self->stdout_clear;
536             if ( @result_query and @query != @result_query ) {
537             $self->say_stdout( sprintf "# Search: %s (%s)", join( '/', @result_query ), join( '/', @query ) );
538             }
539             elsif ( @query ) {
540             $self->say_stdout( sprintf "# Search: %s", join '/', @query );
541             }
542             else {
543             $self->say_stdout( sprintf "# Search: %s", '' );
544             }
545             $self->say_stdout( "---\n\n" );
546             if ( @visible ) {
547             my $n = 0;
548             $self->say_stdout( " $n2k{$n++}. $_" ) for @visible;
549             $self->say_stdout;
550             }
551              
552             if ( @invisible ) {
553             $self->say_stdout( sprintf "# Showing %d out of %d", scalar @visible, $total );
554             $self->say_stdout( "# Refine your search: /" );
555             }
556             else {
557             $self->say_stdout( sprintf "# Found %d", $total );
558             }
559             $self->say_stdout( "# Redo your search: //" );
560             #$self->say_stdout( sprintf "# Select an entry: [%s]", join '', map { $n2k{$_} } 0 .. @visible - 1 );
561             #$self->say_stdout( sprintf "# Show an entry: show " );
562             if ( @visible ) {
563             $self->say_stdout( sprintf "# Unrefine your search: .." );
564             $self->say_stdout( sprintf "# Show an entry: show [%s]", join '', map { $n2k{$_} } 0 .. @visible - 1 );
565             $self->say_stdout( sprintf "# Copy the an entry to the clipboard: copy " );
566             }
567             else {
568             $self->say_stdout( sprintf "# Show list: list" );
569             }
570              
571             $self->say_stdout;
572             },
573              
574             qr/^s(?:h(?:o(?:w)?)?)?\s*(\d+)/ => $_show,
575              
576             qr/^c(?:o(?:p(?:y)?)?)?\s*(\d+)/ => $_copy,
577              
578             qr/^cp\s*(\d+)/ => $_copy,
579              
580             reset => sub {
581             my ( $self, $method ) = @_;
582             $self->query( [] );
583             $self->found( [] );
584             delete @{ $self->stash }{qw/ target entry /};
585             $self->stdout_clear;
586             $self->dispatch( '?' );
587             },
588              
589             clear => 'reset',
590              
591             reload => sub {
592             my ( $self, $method ) = @_;
593             $self->reload_cfg;
594             $self->stdout_clear;
595             $self->say_stdout( "# Reload\n---\n" );
596             $self->dispatch( '?' );
597             },
598              
599             qr/^([0-9])$/ => sub {
600             my ( $self, $method ) = @_;
601              
602             return unless my ( $target, $entry, $k, $n ) = $self->_select;
603             my $stash = $self->stash;
604             @$stash{qw/ target entry /} = ( $target, $entry );
605              
606             my $query = $self->query;
607              
608             $self->stdout_clear;
609             $self->say_stdout( "# Select $k ($target)\n---\n" );
610             $self->say_stdout( "# Show entry ($target): show" );
611             $self->say_stdout( "# Copy the entry into the clipboard: copy" );
612             $self->say_stdout( sprintf "# Show last search: / (%s)", join '/', @$query ) if @$query;
613             $self->say_stdout;
614             },
615              
616             qr/^s(?:h(?:o?)?)?$/ => 'show',
617             show => sub {
618             my ( $self, $method ) = @_;
619              
620             my ( $target, $entry ) = $self->get_target_entry;
621             return unless defined $target;
622             $self->emit_entry( $target, $entry );
623             $self->dispatch( '.' );
624             },
625              
626             qr/^c(?:o(?:p)?)?$|cp$/ => 'copy',
627             copy => sub {
628             my ( $self, $method ) = @_;
629              
630             my ( $target, $entry ) = $self->get_target_entry;
631             return unless defined $target;
632             $self->emit_entry( $target, $entry, copy => 1 );
633             },
634              
635             );
636              
637             sub dispatch {
638             my $self = shift;
639             my $method = shift;
640              
641             defined or $_ = '' for $method;
642              
643             my $result = $DISPATCH->dispatch( $method );
644              
645             return unless $result;
646              
647             $self->stash->{_} = [ $result->captured ];
648             return $result->value->( $self, $method );
649             }
650              
651             sub stdout {
652             my $self = shift;
653             my $fh = \*STDOUT if 1;
654             $fh->print( join '', @_ ) if @_;
655             return $fh;
656             }
657              
658             sub say_stdout {
659             my $self = shift;
660             my $emit = join '', @_;
661             chomp $emit;
662             $self->stdout( $emit, "\n" );
663             }
664              
665             sub safe_pager {
666             my $self = shift;
667             if ( $self->options->{ unsafe } ) {
668             }
669             else {
670             $self->stderr( "\n# Press RETURN to show the plaintext" );
671             $self->stdin_readreturn;
672             }
673             $self->do_pager( @_ );
674             $self->stdout_clear;
675             }
676              
677             sub safe_stdout {
678             my $self = shift;
679             if ( $self->options->{ unsafe } ) {
680             }
681             else {
682             $self->stderr( "\n# Press RETURN to show the plaintext" );
683             $self->stdin_readreturn;
684             }
685             $self->stdout_clear;
686             $self->stdout( "\n", @_ );
687             $self->stderr( "\n# Press RETURN to clear the screen and continue" );
688             $self->stdin_readreturn;
689             $self->stdout_clear;
690             }
691              
692             sub stdout_clear {
693             my $self = shift;
694             $self->stdout( "\x1b[2J\x1b[H" );
695             }
696              
697             sub stderr {
698             my $self = shift;
699             my $fh = \*STDERR if 1;
700             $fh->print( join '', @_ ) if @_;
701             return $fh;
702             }
703              
704             sub say_stderr {
705             my $self = shift;
706             my $emit = join '', @_;
707             chomp $emit;
708             $self->stderr( $emit, "\n" );
709             }
710              
711             sub stdin {
712             return \*STDIN;
713             }
714              
715             sub read_passphrase {
716             my $self = shift;
717             my $prompt = shift;
718             if ( defined $prompt ) {
719             $self->stderr( $prompt );
720             }
721              
722             my $passphrase;
723             ReadMode 2;
724             try {
725             $passphrase = $self->stdin->getline;
726             chomp $passphrase;
727             }
728             finally {
729             ReadMode 0;
730             };
731              
732             return $passphrase;
733             }
734              
735             sub stdin_readline {
736             my $self = shift;
737              
738             my $input = "";
739              
740             try {
741             ReadMode 3;
742             my $escape = 0;
743             my $chr;
744             while ( defined ( $chr = ReadKey ) ) {
745             if ( $escape ) {
746             $escape--;
747             next;
748             }
749             my $ord = ord $chr;
750             if ( $ord >= 32 && $ord < 127 ) {
751             print $chr;
752             $input .= $chr;
753             }
754             elsif ( $ord == 27 ) {
755             $escape = 2;
756             }
757             elsif ( $ord == 13 || $ord == 10 ) {
758             print "\n";
759             last;
760             }
761             else {
762             if ( $ord == 8 || $ord == 127 ) {
763             if ( length $input ) {
764             $input = substr $input, 0, -1 + length $input;
765             print "\b \b";
766             }
767             }
768             elsif ( $ord == 21 ) {
769             print "\r", (" " x ( 2 * length $input ) ), "\r";
770             print "> ";
771             $input = "";
772             }
773             }
774             }
775             }
776             catch {
777             print "\n";
778             }
779             finally {
780             ReadMode 0;
781             };
782              
783             return $input;
784             }
785              
786             sub check_read {
787             my $self = shift;
788             return 1 if $self->can_read;
789             $self->say_stderr( "% Missing (read) in cfg" );
790             return 0;
791             }
792              
793             sub stdin_readreturn {
794             my $self = shift;
795             my $delay = shift;
796             ReadMode 2; # Disable keypress echo
797             while ( 1 ) {
798             my $continue = ReadKey $delay;
799             last unless defined $continue;
800             chomp $continue;
801             last unless length $continue;
802             }
803             ReadMode 0;
804             }
805              
806             sub copy {
807             my $self = shift;
808             my $name = shift;
809             my $value = shift;
810              
811             my $SIG_INT = $SIG{ INT } || sub { exit 1 };
812             local $SIG{ INT } = sub {
813             $self->do_copy( '' );
814             ReadMode 0;
815             $SIG_INT->();
816             };
817              
818             my $delay = $self->options->{ delay };
819             if ( $delay ) {
820             $self->say_stdout( sprintf "# Press RETURN to copy {$name} into clipboard with %d:%02d delay", int( $delay / 60 ), $delay % 60 );
821             }
822             else {
823             $self->say_stdout( "# Press RETURN to copy {$name} into clipboard for NO delay" );
824             }
825             $self->stdin_readreturn;
826             $self->do_copy( $value );
827             $self->say_stdout( "# Copied -- Press RETURN again to wipe clipboard and continue" );
828             $self->stdin_readreturn( $delay );
829             $self->say_stdout;
830             my $paste = $self->do_paste;
831             if ( ! defined $paste || $paste eq $value ) {
832             # To be safe, we wipe out the clipboard in the case where
833             # we were unable to get a read on the clipboard (pbpaste, xsel, or
834             # xclip failed)
835             $self->do_copy( '' ); # Wipe out clipboard
836             }
837             }
838              
839             sub editor_prgm {
840             my $self = shift;
841              
842             my $found = $self->cfg->{ editor };
843             defined and return $_ for $found;
844              
845             $found = $self->_find_prgm( 'sensible-editor' );
846             defined and return $_ for $found;
847              
848             $found = $ENV{ VISUAL };
849             defined and return $_ for $found;
850              
851             $found = $ENV{ EDITOR };
852             defined and return $_ for $found;
853              
854             return;
855             }
856              
857             sub do_pager {
858             my $self = shift;
859             my $content = shift;
860             my %options = @_;
861              
862             my $prgm = $self->pager_prgm;
863              
864             if ( ! defined $prgm ) {
865             $self->say_stderr( "% Missing (pager) in cfg/\$PAGER" );
866             return;
867             }
868              
869             if ( $options{ clear } ) {
870             $self->stdout_clear;
871             }
872              
873             open my $fh, '|-', $prgm;
874             if ( ref $content eq 'CODE' ) {
875             $content->( $fh );
876             }
877             else {
878             $fh->print( $content );
879             }
880             close $fh;
881              
882             return 1;
883             }
884              
885             sub pager_prgm {
886             my $self = shift;
887              
888             my $found = $self->cfg->{ pager };
889             defined and return $_ for $found;
890              
891             $found = $self->_find_prgm( 'sensible-pager' );
892             defined and return $_ for $found;
893              
894             $found = $ENV{ PAGER };
895             defined and return $_ for $found;
896              
897             $found = $self->_find_prgm( 'less' );
898             defined and return $_ for $found;
899              
900             return;
901             }
902              
903             sub _find_prgm {
904             my $self = shift;
905             my $name = shift;
906              
907             for (qw{ /bin /usr/bin }) {
908             my $cmd = file split( '/', $_ ), $name;
909             return $cmd if -f $cmd && -x $cmd;
910             }
911              
912             return undef;
913             }
914              
915             sub do_copy {
916             my $self = shift;
917             my $value = shift;
918              
919             my $copy = $self->cfg->{ copy };
920             if ( defined $copy ) {
921             $self->_pipe_into( $copy => $value );
922             return 1;
923             }
924              
925             if ( lc $^O eq 'darwin' ) {
926             return 1 if $self->_try_copy( 'pbcopy', $value );
927             }
928              
929             return 1 if $self->_try_copy( 'xsel', $value );
930             return 1 if $self->_try_copy( 'xclip', $value );
931             return;
932             }
933              
934             sub _try_copy {
935             my $self = shift;
936             my $name = shift;
937             my $value = shift;
938              
939             my $execute = $App::locket::Util::COPY{ $name };
940             if ( ! $execute ) {
941             warn "*** Missing (copy) CODE for $name";
942             return;
943             }
944             return unless my $prgm = $self->_find_prgm( $name );
945             $execute->( $self, $prgm, $value );
946             return 1;
947             }
948              
949             sub _pipe_into {
950             my $self = shift;
951             my $cmd = shift;
952             my $value = shift;
953              
954             open my $pipe, '|-', $cmd or die $!;
955             $pipe->print( $value );
956             close $pipe;
957             }
958              
959             sub do_paste {
960             my $self = shift;
961              
962             my $paste = $self->cfg->{ paste };
963             if ( defined $paste ) {
964             return $self->_pipe_outfrom( $paste );
965             }
966              
967             my $value;
968             if ( lc $^O eq 'darwin' ) {
969             $value = $self->_try_paste( 'pbpaste' );
970             return $value if defined $value;
971             }
972              
973             $value = $self->_try_paste( 'xsel' );
974             return $value if defined $value;
975              
976             $value = $self->_try_paste( 'xclip' );
977             return $value if defined $value;
978              
979             return;
980             }
981              
982             sub _try_paste {
983             my $self = shift;
984             my $name = shift;
985             my $value = shift;
986              
987             my $execute = $App::locket::Util::PASTE{ $name };
988             if ( ! $execute ) {
989             warn "*** Missing (paste) CODE for $name";
990             return;
991             }
992             return unless my $prgm = $self->_find_prgm( $name );
993             return $execute->( $self, $prgm );
994             }
995              
996             sub _pipe_outfrom {
997             my $self = shift;
998             my $cmd = shift;
999             my $value = shift;
1000              
1001             open my $pipe, '-|', $cmd or die $!;
1002             return join '', <$pipe>;
1003             }
1004              
1005             1;
1006              
1007              
1008              
1009             =pod
1010              
1011             =head1 NAME
1012              
1013             App::locket - Copy secrets from a YAML/JSON cipherstore into the clipboard (pbcopy, xsel, xclip)
1014              
1015             =head1 VERSION
1016              
1017             version 0.0022
1018              
1019             =head1 SYNOPSIS
1020              
1021             # Setup the configuration file for the cipherstore:
1022             # (How to read the cipherstore, how to edit the cipherstore, etc.)
1023             $ locket setup
1024              
1025             # Add or change data in the cipherstore:
1026             $ locket edit
1027              
1028             # List all the entries in the cipherstore:
1029             $ locket /
1030              
1031             # Show a secret from the cipherstore:
1032             $ locket /alice@gmail
1033              
1034             =head1 DESCRIPTION
1035              
1036             App::locket is a tool for querying a simple YAML/JSON-based cipherstore
1037              
1038             It has a simple commandline-based querying method and supports copying into the clipboard
1039              
1040             Currently, encryption and decryption is performed via external tools (e.g. GnuPG, OpenSSL, etc.)
1041              
1042             App::locket is best used with:
1043              
1044             * gnupg.vim L
1045              
1046             * openssl.vim L
1047              
1048             * EasyPG L
1049              
1050             =head1 SECURITY
1051              
1052             =head2 Encryption/decryption
1053              
1054             App::locket defers actual encryption/decryption to external tools. The choice of the actual
1055             cipher/encryption method is left up to you
1056              
1057             If you're using GnuPG, then you could use C for passphrase prompting and limited retention
1058              
1059             =head2 In-memory encryption
1060              
1061             App::locket does not perform any in-memory encryption; once the cipherstore is loaded it is exposed in memory
1062              
1063             In addition, if the process is swapped out while running then the plaintextstore could be written to disk
1064              
1065             Encrypting swap is one way of mitigating this problem
1066              
1067             =head2 Clipboard access
1068              
1069             App::locket uses third-party tools for read/write access to the clipboard. It tries to detect if
1070             C, C, or C are available. It does this by looking in C and C
1071              
1072             =head2 Purging the clipboard
1073              
1074             By default, App::locket will purge the clipboard of a secret it put there after a set delay. It will try to verify that it is
1075             wiping what it put there in the first place (so it doesn't accidentally erase something else you copied)
1076              
1077             If for some reason App::locket cannot read from the clipboard, it will purge it just in case
1078              
1079             If you prematurely cancel a secret copying operation via CTRL-C, App::locket will catch the signal and purge the clipboard first
1080              
1081             =head2 Attack via configuration
1082              
1083             Currently, App::locket does not encrypt/protect the configuration file. This means an attacker can potentially (unknown to you) modify
1084             the reading/editing commands to divert the plaintext elsewhere
1085              
1086             There is an option to lock the configuration file, but given the ease of code injection you're probably better off installing and using App::locket in a dedicated VM
1087              
1088             =head2 Resetting $PATH
1089              
1090             C<$PATH> is reset to C
1091              
1092             =head1 INSTALL
1093              
1094             $ cpanm -i App::locket
1095              
1096             =head1 INSTALL cpanm
1097              
1098             L
1099              
1100             =head1 USAGE
1101              
1102             locket [options] setup|edit|
1103              
1104             --delay Keep value in clipboard for seconds
1105             If value is still in the clipboard at the end of
1106             then it will be automatically wiped from
1107             the clipboard
1108              
1109             --unsafe Turn the safety off. This will disable prompting
1110             before emitting any sensitive information in
1111             plaintext. There will be no opportunity to
1112             abort (via CTRL-C)
1113              
1114             setup Setup a new or edit an existing user configuration
1115             file (~/.locket/cfg)
1116              
1117             edit Edit the cipherstore
1118             The configuration must have an "edit" value, e.g.:
1119              
1120             /usr/bin/vim -n ~/.locket.gpg
1121              
1122              
1123             / Search the cipherstore for and emit the
1124             resulting secret
1125            
1126             The configuration must have a "read" value to
1127             tell it how to read the cipherstore. Only piped
1128             commands are supported today, and they should
1129             be something like:
1130              
1131            
1132              
1133             If the found key in the cipherstore is of the format
1134             "@" then the username will be emitted
1135             first before the secret (which is assumed to be a password/passphrase)
1136              
1137             Type in-process for additional usage
1138              
1139             =head1 Example YAML cipherstore
1140              
1141             %YAML 1.1
1142             ---
1143             # A GMail identity
1144             alice@gmail: p455w0rd
1145             # Some frequently used credit card information
1146             cc4123: |
1147             4123412341234123
1148             01/23
1149             123
1150              
1151             =head1 Example configuration file
1152              
1153             %YAML 1.1
1154             ---
1155             read: '
1156             edit: '/usr/bin/vim -n ~/.locket.gpg'
1157              
1158             =head1 AUTHOR
1159              
1160             Robert Krimen
1161              
1162             =head1 COPYRIGHT AND LICENSE
1163              
1164             This software is copyright (c) 2011 by Robert Krimen.
1165              
1166             This is free software; you can redistribute it and/or modify it under
1167             the same terms as the Perl 5 programming language system itself.
1168              
1169             =cut
1170              
1171              
1172             __END__