File Coverage

blib/lib/File/KeePass/Agent.pm
Criterion Covered Total %
statement 16 17 94.1
branch 1 2 50.0
condition n/a
subroutine 5 5 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             File::KeePass::Agent - Application agent for working with File::KeePass objects
6              
7             =cut
8              
9             File::KeePass::Agent::run(),exit if $0 eq __FILE__;
10              
11             package File::KeePass::Agent;
12              
13 1     1   21187 use strict;
  1         3  
  1         41  
14 1     1   5 use warnings;
  1         2  
  1         28  
15 1     1   6 use Carp qw(croak);
  1         5  
  1         74  
16 1     1   1217 use File::KeePass '2.02';
  1         47704  
  1         131  
17              
18             our $VERSION = '2.01';
19             our @ISA;
20             BEGIN {
21 1     1   5 my $os = lc($^O);
22 1 50       2 if (! eval { require "File/KeePass/Agent/$os.pm" }) {
  1         692  
23 1         287 croak "It appears that \"$os\" is not yet supported by ".__PACKAGE__.": $@";
24             }
25 0           @ISA = (__PACKAGE__."::$os");
26             }
27              
28             sub new {
29             my $class = shift;
30             return bless {}, $class;
31             }
32              
33             sub run {
34             my $self = ref($_[0]) ? shift() : __PACKAGE__->new;
35              
36             $self->init;
37              
38             # handle args coming in a multitude of ways
39             my @pairs;
40             if (@_) {
41             my ($files, $passes) = @_;
42             if (ref($_[0]) eq 'ARRAY') {
43             push @pairs, [$files->[$_], $passes->[$_]] for 0 .. $#$files;
44             } elsif (ref($_[0] eq 'HASH')) {
45             push @pairs, map {[$_ => $files->{$_}]} sort keys %$files;
46             } else {
47             push @pairs, [$files, $passes]; # single file/pass set
48             }
49             } elsif (@ARGV) {
50             for (my $i = 0; $i < @ARGV; $i++) {
51             my $file = $ARGV[$i];
52             next if $file =~ /^--?\w+$/;
53             my %erg;
54             while ($ARGV[$i+1] && $ARGV[$i+1] =~ /^--?(password|pass|keyfile)(?:(=)(.*))?$/) {
55             $i++;
56             $erg{$1} = $2 ? $3 : $ARGV[++$i];
57             }
58             my $pass = exists($erg{'password'}) ? $erg{'password'} : $erg{'pass'};
59             $pass = [$pass, $erg{'keyfile'}] if exists($erg{'keyfile'});
60             push @pairs, [$file, $pass];
61             }
62             } else {
63             my $file = $self->prompt_for_file or die "Cannot continue without kdb file\n";
64             push @pairs, map {[$_, undef]} glob $file;
65             }
66             die "No files given as input\n" if ! @pairs;
67              
68             # check file existence
69             my @callbacks;
70             for my $pair (@pairs) {
71             my ($file, $pass) = @$pair;
72             die "File \"$file\" does not exist\n" if ! -e $file;
73             die "File \"$file\" does not appear to be readible\n" if ! -r $file;
74             die "File \"$file\" does not appear to be a valid keepass db file\n" if ! -B $file;
75             }
76             OUTER: for my $pair (@pairs) {
77             my ($file, $pass) = @$pair;
78             my $k;
79             if (! defined $pass) {
80             $k = $self->_prompt_for_pass_and_key($file);
81             print "Skipping file $file\n" if ! $k;
82             } else {
83             $k = $self->load_keepass($file, $pass);
84             }
85             }
86              
87             $self->main_loop;
88             }
89              
90             sub _prompt_for_pass_and_key {
91             my ($self, $file) = @_;
92             while (1) {
93             my $pass = $self->prompt_for_pass($file);
94             if (! defined($pass) || !length($pass)) {
95             my $keyfile = $self->prompt_for_keyfile($file);
96             $pass = [$pass, $keyfile] if defined($keyfile) && length($keyfile);
97             }
98             my $k = eval { $self->load_keepass($file, $pass) };
99             my $err = $@;
100             if (! $k && defined($pass) && ref($pass) ne 'ARRAY' && length($pass)) {
101             my $keyfile = $self->prompt_for_keyfile($file);
102             if (defined($keyfile) && length($keyfile)) {
103             $pass = [$pass, $keyfile];
104             $k = eval { $self->load_keepass($file, $pass) };
105             $err = $@;
106             }
107             }
108             return if !defined($pass) || !length($pass);
109             warn "Could not load database: $@" if ! $k;
110             return $k if $k;
111             }
112             }
113              
114              
115             sub load_keepass {
116             my ($self, $file, $pass) = @_;
117             my $kdb = $self->keepass_class->new;
118             $kdb->load_db($file, $pass);
119             push @{ $self->keepass }, [$file, $kdb];
120             return $kdb;
121             }
122              
123             sub keepass { shift->{'keepass'} ||= [] }
124              
125             sub keepass_class { 'File::KeePass' }
126              
127             sub unload_keepass {
128             my ($self, $file) = @_;
129             my $kdbs = $self->keepass;
130             for my $i (0 .. $#$kdbs) {
131             next if $kdbs->[$i]->[0] ne $file;
132             splice @$kdbs, $i, 1, ();
133             last;
134             }
135             }
136              
137             ###----------------------------------------------------------------###
138              
139             sub active_callbacks {
140             my $self = shift;
141             my @callbacks;
142             foreach my $row ($self->active_entries) {
143             my ($file, $entries) = @$row;
144             foreach my $e (@$entries) {
145             next if ! $e->{'comment'} || $e->{'comment'} !~ /^Custom-Global-Shortcut:\s*(.+?)\s*$/m;
146             my %info = map {lc($_) => 1} split /[\s+-]+/, $1;
147             my $at = (($e->{'auto_type'} || [])->[0] || {})->{'keys'} || '{PASSWORD}{ENTER}';
148             my $s = {
149             ctrl => delete($info{'control'}) || delete($info{'cntrl'}) || delete($info{'ctrl'}),
150             shift => delete($info{'shift'}) || delete($info{'shft'}),
151             alt => delete($info{'alt'}),
152             win => delete($info{'win'}),
153             };
154             my @keys = keys %info;
155             if (@keys != 1) {
156             croak "Cannot set global shortcut with more than one key (@keys) for entry \"$e->{'title'}\"\n";
157             }
158             $s->{'key'} = lc $keys[0];
159             push @callbacks, [$s, "entry $e->{'title'}", sub {
160             my ($self, $title, $event) = @_;
161             return $self->do_auto_type({auto_type => $at, entry => $e, file => $file}, $title, $event);
162             }];
163             }
164             }
165             if (my $s = $self->read_config('global_shortcut')) {
166             push @callbacks, [$s, 'global shortcut', 'search_auto_type'];
167             }
168             return @callbacks;
169             }
170              
171             sub shortcut_name {
172             my ($self, $s) = @_;
173             my $mod = join("-", map {ucfirst $_} grep {$s->{$_}} qw(ctrl shift alt win));
174             return $mod ? "$mod $s->{'key'}" : $s->{'key'};
175             }
176              
177             sub active_entries {
178             my $self = shift;
179             my @rows;
180             foreach my $pair (@{ $self->keepass }) {
181             my ($file, $kdb) = @$pair;
182             my @entries = $kdb->find_entries({active => 1, 'group_title !' => 'Backup', 'title !' => 'Meta-Info'});
183             push @rows, [$file, \@entries] if @entries;
184             }
185             return @rows;
186             }
187              
188             sub active_searches {
189             my $self = shift;
190             my $s = $self->{'active_searches'} ||= do {
191             my @s;
192             foreach my $row ($self->active_entries) {
193             my ($file, $entries) = @$row;
194             foreach my $e (@$entries) {
195             foreach my $at (@{ $e->{'auto_type'} || [] }) {
196             my ($win, $keys) = @$at{qw(window keys)};
197             next if ! defined($win) || ! length($win);
198             if (! defined($keys) || ! length($keys)) {
199             my $kdb = (map {$_->[1]} grep {$_->[0] eq $file} @{ $self->keepass })[0];
200             my ($e2, $group) = $kdb->find_entry($e);
201             $keys = $group->{'auto_type_default'};
202             next if ! defined($keys) || ! length($keys);
203             }
204             $win = quotemeta($win);
205             $win =~ s{^\\\*}{.*};
206             $win =~ s{\\\*$}{.*};
207             $win = qr{^$win$};
208             push @s, {'qr' => $win, auto_type => $keys, file => $file, entry => $e};
209             }
210             }
211             }
212             \@s;
213             };
214             return @$s;
215             }
216              
217             sub search_auto_type {
218             my ($self, $title, $event) = @_;
219             my @matches;
220             foreach my $row ($self->active_searches) {
221             next if $title !~ $row->{'qr'};
222             push @matches, $row;
223             }
224             if (!@matches) {
225             $self->do_no_match($title);
226             }
227             elsif (@matches > 1) {
228             $self->do_auto_type_mult(\@matches, $title, $event);
229             }
230             else {
231             $self->do_auto_type($matches[0], $title, $event);
232             }
233             }
234              
235             sub do_no_match {
236             my ($self, $title) = @_;
237             warn "No match for \"$title\"\n";
238             }
239              
240             sub do_auto_type {
241             my ($self, $match, $title, $event) = @_;
242             my ($auto_type, $file, $entry) = @$match{qw(auto_type file entry)};
243             $auto_type =~ s{ \{ TAB \} }{\t}xg;
244             $auto_type =~ s{ \{ ENTER \} }{\n}xg;
245             $auto_type =~ s{ \{ PASSWORD \} }{
246             my %kdbs = map {$_->[0], $_->[1]} @{ $self->keepass };
247             $kdbs{$file}->locked_entry_password($entry);
248             }xeg;
249             $auto_type =~ s{ \{ (\w+) \} }{
250             my $key = lc $1;
251             defined($entry->{$key}) ? $entry->{$key} : return $self->do_auto_type_unsupported($key);
252             }xeg;
253             return if ! length $auto_type;
254             return if $self->{'_last_send'} && time - $self->{'_last_send'} < 2;
255             $self->{'_last_send'} = time;
256             $self->send_key_press($auto_type, $entry, $title, $event);
257             }
258              
259             sub do_auto_type_mult {
260             my ($self, $matches, $title, $event) = @_;
261             warn "Found multiple matches - using the first\n";
262             $self->do_auto_type($matches->[0], $title, $event);
263             }
264              
265             sub do_auto_type_unsupported {
266             my ($self, $key) = @_;
267             warn "Auto-type key \"$key\" is currently not supported.";
268             }
269              
270             1;
271              
272             __END__