File Coverage

blib/lib/App/Greple/pw.pm
Criterion Covered Total %
statement 41 189 21.6
branch 2 96 2.0
condition 0 12 0.0
subroutine 15 31 48.3
pod 3 17 17.6
total 61 345 17.6


line stmt bran cond sub pod time code
1             package App::Greple::pw;
2              
3             our $VERSION = "1.02";
4              
5             =head1 NAME
6              
7             pw - Interactive password and ID information extractor for greple
8              
9              
10             =head1 SYNOPSIS
11              
12             # Basic usage
13             greple -Mpw pattern file
14              
15             # Search in encrypted files
16             greple -Mpw password ~/secure/*.gpg
17              
18             # Configure options
19             greple -Mpw --no-clear-screen --chrome password data.txt
20             greple -Mpw --config timeout=600 --config debug=1 password file.txt
21              
22              
23             =head1 VERSION
24              
25             Version 1.02
26              
27              
28             =head1 DESCRIPTION
29              
30             The B module is a B extension that provides secure, interactive
31             handling of sensitive information such as passwords, user IDs, and account
32             details found in text files. It is designed with security in mind, ensuring
33             that sensitive data doesn't remain visible on screen or in terminal history.
34              
35             =head2 Key Features
36              
37             =over 4
38              
39             =item * B
40              
41             Passwords are masked by default and can be safely copied to clipboard
42             without displaying the actual content on screen.
43              
44             =item * B
45              
46             Terminal scroll buffer and screen are automatically cleared when the
47             command exits, and clipboard content is replaced with a harmless string
48             to prevent sensitive information from persisting.
49              
50             =item * B
51              
52             Seamlessly works with PGP encrypted files using B's standard
53             features. Files with "I<.gpg>" extension are automatically decrypted,
54             and the B<--pgp> option allows entering the passphrase once for
55             multiple files.
56              
57             =item * B
58              
59             Automatically detects ID and password information using configurable
60             keywords like "user", "account", "password", "pin", etc. Custom
61             keywords can be configured to match your specific data format.
62              
63             =item * B
64              
65             Includes browser automation features for automatically filling web
66             forms with extracted credentials.
67              
68             =back
69              
70             Some banks use random number matrices as a countermeasure for tapping.
71             If the module successfully guesses the matrix area, it blacks out the
72             table and remembers them.
73              
74             | A B C D E F G H I J
75             --+--------------------
76             0 | Y W 0 B 8 P 4 C Z H
77             1 | M 0 6 I K U C 8 6 Z
78             2 | 7 N R E Y 1 9 3 G 5
79             3 | 7 F A X 9 B D Y O A
80             4 | S D 2 2 Q V J 5 4 T
81              
82             Enter the field positions to get the cell items like:
83              
84             > E3 I0 C4
85              
86             and you will get the answer:
87              
88             9 Z 2
89              
90             Case is ignored and white space is not necessary, so you can type like
91             this as well:
92              
93             > e3i0c4
94              
95              
96             =head1 INTERFACE
97              
98             =begin comment
99              
100             =head2 Internal Functions (for developers)
101              
102             =over 7
103              
104             =item B
105              
106             Data print function. This function is set for the B<--print> option of
107             B by default, and users don't have to care about it.
108              
109             =item B
110              
111             Epilogue function. This function is set for the B<--end> option of
112             B by default, and users don't have to care about it.
113              
114             =back
115              
116             =end comment
117              
118             =over 7
119              
120             =item B
121              
122             Module parameters can be configured using the B interface from
123             L. There are three ways to configure parameters:
124              
125             =over 4
126              
127             =item Module configuration syntax
128              
129             Use the B<::config=> syntax directly with the module:
130              
131             greple -Mpw::config=clear_screen=0
132              
133             =item Command-line config option
134              
135             Use the B<--config> option to set parameters:
136              
137             greple -Mpw --config clear_screen=0 --
138              
139             Multiple parameters can be set:
140              
141             greple -Mpw --config clear_screen=0 --config debug=1 --
142              
143             =item Direct command-line options
144              
145             Many parameters have direct command-line equivalents:
146              
147             greple -Mpw --no-clear-screen --debug --browser=safari --
148              
149             =back
150              
151             Currently following configuration options are available:
152              
153             clear_clipboard
154             clear_string
155             clear_screen
156             clear_buffer
157             goto_home
158             browser
159             timeout
160             debug
161             parse_matrix
162             parse_id
163             parse_pw
164             id_keys
165             id_chars
166             id_color
167             id_label_color
168             pw_keys
169             pw_chars
170             pw_color
171             pw_label_color
172             pw_blackout
173              
174             =back
175              
176             =head3 Parameter Details
177              
178             =over 4
179              
180             =item B
181              
182             Configuration parameters use underscores (C, C), while
183             command-line options use hyphens (C<--clear-screen>, C<--id-keys>).
184              
185             =item B
186              
187             Parameters like B, B can be set to 0/1. Command-line
188             options support negation with C<--no-> prefix (e.g., C<--no-clear-screen>).
189              
190             =item B
191              
192             B and B are lists of keywords separated by spaces:
193              
194             --config id_keys="USER ACCOUNT LOGIN EMAIL"
195             --config pw_keys="PASS PASSWORD PIN SECRET"
196              
197             =item B
198              
199             B controls password display:
200             0=show passwords, 1=mask with 'x', >1=fixed length mask.
201              
202             =item B
203              
204             Parameters B, B, B, B, and B
205             are passed to the PwBlock module for pattern recognition and display control.
206              
207             =back
208              
209             =over 4
210              
211             =item B
212              
213             Print current configuration status. Next command displays current settings:
214              
215             greple -Mpw::pw_status= dummy /dev/null
216              
217             This shows which parameters are set to non-default values and which are using defaults.
218              
219             =back
220              
221             =head1 BROWSER INTEGRATION
222              
223             The pw module includes browser integration features for automated input.
224             Browser options are available:
225              
226             =over 4
227              
228             =item B<--browser>=I
229              
230             Set the browser for automation (chrome, safari, etc.):
231              
232             greple -Mpw --browser=chrome
233              
234             =item B<--chrome>, B<--safari>
235              
236             Shortcut options for specific browsers:
237              
238             greple -Mpw --chrome # equivalent to --browser=chrome
239             greple -Mpw --safari # equivalent to --browser=safari
240              
241             =back
242              
243             During interactive mode, you can use the C command to send
244             data to browser forms automatically.
245              
246             =head1 EXAMPLES
247              
248             =over 4
249              
250             =item Search for passwords in encrypted files
251              
252             greple -Mpw password ~/secure/*.gpg
253              
254             =item Use with specific browser and no screen clearing
255              
256             greple -Mpw --chrome --no-clear-screen password data.txt
257              
258             =item Configure custom keywords and timeout
259              
260             greple -Mpw --config id_keys="LOGIN EMAIL USER" --config timeout=600 password file.txt
261              
262             =item Check current configuration
263              
264             greple -Mpw::pw_status= dummy /dev/null
265              
266             =back
267              
268             =head1 SEE ALSO
269              
270             L, L
271              
272             L
273              
274             =head1 AUTHOR
275              
276             Kazumasa Utashiro
277              
278             =head1 LICENSE
279              
280             Copyright (C) 2017-2025 Kazumasa Utashiro.
281              
282             This library is free software; you can redistribute it and/or modify
283             it under the same terms as Perl itself.
284              
285             =cut
286              
287              
288 15     15   346660 use v5.14;
  15         57  
289 15     15   99 use strict;
  15         28  
  15         433  
290 15     15   319 use warnings;
  15         197  
  15         981  
291 15     15   785 use utf8;
  15         375  
  15         154  
292              
293 15     15   564 use Exporter 'import';
  15         27  
  15         1617  
294             our @EXPORT = qw(&pw_print &pw_epilogue &pw_status &config);
295             our %EXPORT_TAGS = ( );
296             our @EXPORT_OK = qw();
297              
298 15     15   86 use Carp;
  15         38  
  15         1281  
299 15     15   784 use Data::Dumper;
  15         11307  
  15         880  
300 15     15   2184 use App::Greple::Common;
  15         625  
  15         844  
301 15     15   15416 use App::Greple::PwBlock;
  15         43  
  15         652  
302 15     15   90 use Getopt::EX::Config qw(config);
  15         25  
  15         51  
303              
304             my $execution = 0;
305              
306             # Getopt::EX::Config support
307             my $config = Getopt::EX::Config->new(
308             clear_clipboard => 1,
309             clear_string => 'Hasta la vista.',
310             clear_screen => 1,
311             clear_buffer => 1,
312             goto_home => 0,
313             browser => 'chrome',
314             timeout => 300,
315             debug => 0,
316             # PwBlock parameters - direct references to PwBlock config members
317             parse_matrix => \$App::Greple::PwBlock::config->{parse_matrix},
318             parse_id => \$App::Greple::PwBlock::config->{parse_id},
319             parse_pw => \$App::Greple::PwBlock::config->{parse_pw},
320             id_keys => \$App::Greple::PwBlock::config->{id_keys},
321             id_chars => \$App::Greple::PwBlock::config->{id_chars},
322             id_color => \$App::Greple::PwBlock::config->{id_color},
323             id_label_color => \$App::Greple::PwBlock::config->{id_label_color},
324             pw_keys => \$App::Greple::PwBlock::config->{pw_keys},
325             pw_chars => \$App::Greple::PwBlock::config->{pw_chars},
326             pw_color => \$App::Greple::PwBlock::config->{pw_color},
327             pw_label_color => \$App::Greple::PwBlock::config->{pw_label_color},
328             pw_blackout => \$App::Greple::PwBlock::config->{pw_blackout},
329             );
330              
331             sub finalize {
332 14     14 0 44842 our($mod, $argv) = @_;
333 14         174 $config->deal_with(
334             $argv,
335             "clear_clipboard|clear-clipboard!",
336             "clear_string|clear-string=s",
337             "clear_screen|clear-screen!",
338             "clear_buffer|clear-buffer!",
339             "goto_home|goto-home!",
340             "browser=s",
341             "timeout=i",
342             "debug!",
343             # PwBlock parameters - underscore and hyphen versions
344             "parse_matrix|parse-matrix!",
345             "parse_id|parse-id!",
346             "parse_pw|parse-pw!",
347             "id_chars|id-chars=s",
348             "id_color|id-color=s",
349             "id_label_color|id-label-color=s",
350             "pw_chars|pw-chars=s",
351             "pw_color|pw-color=s",
352             "pw_label_color|pw-label-color=s",
353             "pw_blackout|pw-blackout!",
354             "id_keys|id-keys=s",
355             "pw_keys|pw-keys=s",
356             );
357            
358             # All parameters are automatically managed by Getopt::EX::Config references
359             }
360              
361             sub pw_status {
362 0     0 1 0 binmode STDOUT, ":encoding(utf8)";
363 0         0 for my $key (sort keys %{$config}) {
  0         0  
364 0         0 my $val = config($key);
365 0 0       0 if (defined $val) {
366 0         0 print "$key: $val\n";
367             } else {
368 0         0 print "$key: (default)\n";
369             }
370             }
371             }
372              
373             sub pw_print {
374 0     0 1 0 my %attr = @_;
375 0         0 my @pass;
376              
377 0         0 $execution++;
378              
379 0         0 my $pw = new App::Greple::PwBlock $_;
380              
381 0         0 print $pw->masked;
382              
383 0 0       0 command_loop($pw) or do { pw_epilogue(); exit };
  0         0  
  0         0  
384              
385 0         0 return '';
386             }
387              
388              
389 15     15   7491 use constant { CSI => "\e[" };
  15         30  
  15         30007  
390              
391             sub pw_epilogue {
392 13 50   13 1 369 $execution == 0 and return;
393 0 0         copy(config('clear_string')) if config('clear_clipboard');
394 0 0         print STDERR CSI, "H" if config('goto_home');
395 0 0         print STDERR CSI, "2J" if config('clear_screen');
396 0 0         print STDERR CSI, "3J" if config('clear_buffer');
397             }
398              
399             sub pw_timeout {
400 0 0   0 0   if (config('debug')) {
401 0           warn "pw_timeout() called.\n";
402 0           sleep 1;
403             }
404 0           pw_epilogue();
405 0           exit;
406             }
407              
408             sub command_loop {
409 0     0 0   my $pw = shift;
410              
411 0 0         open TTY, "/dev/tty" or die;
412              
413 0           require Term::ReadLine;
414 0           my $term = Term::ReadLine->new(__PACKAGE__, *TTY, *STDOUT);
415              
416 0           binmode TTY, ":encoding(utf8)";
417 0           binmode STDOUT, ":encoding(utf8)";
418              
419 0           while ($_ = $term->readline("> ")) {
420 0 0         if (config('timeout')) {
421 0           $SIG{ALRM} = \&pw_timeout;
422 0           alarm config('timeout');
423 0 0         warn "Set timeout to ", config('timeout'), " seconds\n" if config('debug');
424             }
425 0 0         /\S/ or next;
426 0           $term->addhistory($_);
427 0           s/\s+\z//;
428 0           $_ = kana2alpha($_);
429              
430 0 0         if (my $id = $pw->id($_)) {
    0          
431 0 0         if (copy($id)) {
432 0           printf "ID [%s] was copied to clipboard.\n", $id;
433             }
434 0           next;
435             }
436             elsif (my $pass = $pw->pw($_)) {
437 0 0         if (copy($pass)) {
438 0           printf "Password [%s] was copied to clipboard.\n", $_;
439             }
440 0           next;
441             }
442              
443 0 0         if (0) {}
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
444 0           elsif (/^dump\b/) { print Dumper $pw }
  0            
445 0           elsif (/^N/i) { last }
446 0           elsif (/^P/i) { print $pw->masked }
447 0           elsif (/^Q/i) { return 0 }
448             elsif (/^V/i) {
449 0           s/^.\s*//;
450 0           my @option = split /\s+/;
451 0 0         if (@option == 0) {
452 0           print $pw->orig;
453             } else {
454 0   0       my @values = map { $pw->any($_) // '[N/A]' } @option;
  0            
455 0           print "@values\n";
456             }
457             }
458             elsif (/^show\b/i) {
459 0           print $pw->masked;
460             }
461             elsif (/^orig\b/i) {
462 0           print $pw->orig;
463             }
464             ##
465             ## INPUT to browser
466             ##
467             elsif (s/^input\s*//i) {
468 0           my %field = do {
469             map {
470 0           m{
  0            
471             ( (?: name: | id: )? \w+ )
472             (?|
473             \s+ (.*) # '=' がなければ残り全部
474             |
475             = ( \/.+\/ | \w+ (?:,\w+)* )
476             )
477             }xg
478             }
479             $pw->orig =~ /^INPUT\s+(.+)/mg;
480             };
481 0 0         warn Dumper \%field if config('debug');
482 0           my @arg = do {
483 0 0         map { /^([a-z]\d\s*){2,}$/i ? /([a-z]\d)/gi : $_ }
484 0 0         map { m{^/(.+)/$} ? get_pattern($1) : $_ }
485 0 0         map { $field{$_} or $_ }
486 0           map { split /[\s=]+/ }
487 0 0         map { $field{$_} or $_ }
  0            
488             split /\s+/;
489             };
490 0 0         warn "@arg\n" if config('debug');
491 0           while (@arg >= 2) {
492 0           my $label = shift @arg;
493 0           my @fields = split /[,]/, $label;
494 0           for my $field (@fields) {
495 0           my $item = shift @arg;
496 0   0       my $value = $pw->any($item) // $item;
497 0           set_browser_field($field, $value);
498             }
499             }
500             }
501             elsif (/^set$/) {
502 0           for my $var (sort keys %{$config}) {
  0            
503 0           print "$var: ";
504 0           print config($var);
505 0           print "\n";
506             }
507             }
508             elsif (s/^set\s+//) {
509 0           my($var, $val) = split /\s+/, $_, 2;
510 0 0         if (exists $config->{$var}) {
511 0           $config->set($var, $val);
512             } else {
513 0           warn "Unknown variable: $var";
514             }
515             }
516             elsif (/^([A-J]\d\s*)+$/i) {
517 0           my @chars;
518 0           while (/([A-J])(\d)/gi) {
519 0   0       push @chars, $pw->cell(uc($1), $2) // 'ERROR';
520             }
521 0           print "@chars\n";
522             }
523             else {
524 0           print "Command error.\n";
525             }
526             }
527 0           close TTY;
528              
529 0           return 1;
530             }
531              
532             my %kana2alpha = (
533             ア => 'A', イ => 'B', ウ => 'C', エ => 'D', オ => 'E',
534             カ => 'F', キ => 'G', ク => 'H', ケ => 'I', コ => 'J',
535             );
536              
537             sub kana2alpha {
538 0     0 0   local $_ = shift;
539 0           s/([アイウエオカキクケコ])/$kana2alpha{$1}/g;
540 0           $_;
541             }
542              
543             my $clipboard;
544             BEGIN {
545 15     15   1542 eval "use Clipboard";
  15     15   8084  
  15         12692  
  15         115  
546 15 50       126660 if (not $@) {
    0          
547 15         21075 $clipboard = "Clipboard";
548             }
549             elsif (-x "/usr/bin/pbcopy") {
550 0         0 $clipboard = "pbcopy";
551             }
552             else {
553 0         0 warn("==========================================\n",
554             "Clipboard is not available on this system.\n",
555             "Install Clipboard module from CPAN.\n",
556             "==========================================\n");
557             }
558             }
559              
560             sub copy {
561 0     0 0   my $text = shift;
562 0 0         if (not $clipboard) {
    0          
    0          
563 0           warn "Clipboard is not available.\n";
564 0           return undef;
565             }
566             elsif ($clipboard eq "Clipboard") {
567 0           Clipboard->copy($text);
568             }
569             elsif ($clipboard eq "pbcopy") {
570 0           dumpto($clipboard, $text);
571             }
572 0           1;
573             }
574              
575             sub dumpto {
576 0     0 0   my $command = shift;
577 0           my $text = shift;
578 0 0         open COM, "| $command" or die "$command: $!\n";
579 0           print COM $text;
580 0           close COM;
581             }
582              
583             sub apple_script {
584 0     0 0   my $app = shift;
585 0 0         shift if $_[0] eq 'to';
586 0           my $do = join "\n", @_;
587 0           my $script = <<" end_script";
588             tell Application "$app"
589             $do
590             end tell
591             end_script
592 0 0         warn $script if config('debug');
593 0 0 0       if ((open(CMD, "-|") // die) == 0) {
594 0 0         exec 'osascript', '-e', $script or die;
595             } else {
596 0           my $result = do { local $/; };
  0            
  0            
597 0           close CMD;
598 0 0         warn $result if config('debug');
599 0 0         return $result =~ /missing value/ ? undef : $result;
600             }
601             }
602              
603             my %js_subs = (
604             chrome => \&js_chrome,
605             safari => \&js_safari,
606             );
607              
608             sub js {
609 0   0 0 0   (my $sub = $js_subs{config('browser')}) // do {
610 0           warn "Unsupported browser: ", config('browser');
611 0           return;
612             };
613 0           goto $sub;
614             }
615              
616             sub _js {
617 0     0     goto &js_chrome;
618             }
619              
620             sub js_google {
621 0     0 0   my $browser = shift;
622 0           my $js = shift;
623 0           $js =~ s/"/\\"/g;
624 0           $js =~ s/\n//g;
625 0           my $script = <<" end_script";
626             tell active tab of window 1
627             execute javascript ("$js")
628             end tell
629             end_script
630 0           apple_script config('browser'), $script;
631             }
632              
633             sub js_chrome {
634 0     0 0   js_google('Google Chrome', @_);
635             }
636              
637             sub js_brave {
638 0     0 0   js_google('Google Brave', @_);
639             }
640              
641             sub js_safari {
642 0     0 0   my $js = shift;
643 0           $js =~ s/"/\\"/g;
644 0           apple_script 'Safari', <<" end_script";
645             tell current tab of window 1
646             do JavaScript ("$js")
647             end tell
648             end_script
649             }
650              
651             sub set_browser_field {
652 0     0 0   my $name = shift;
653 0           my $value = shift;
654 0 0         js "document.getElementsByName('$name')[0].value='$value'"
655             if defined $value;
656             }
657              
658             sub get_pattern {
659 0     0 0   my $pattern = shift;
660 0           js "document.body.textContent.match(/$pattern/)";
661             }
662              
663             1;
664              
665              
666             __DATA__