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__ |