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