line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package File::KeePass; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
=head1 NAME |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
File::KeePass - Interface to KeePass V1 and V2 database files |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
=cut |
8
|
|
|
|
|
|
|
|
9
|
1
|
|
|
1
|
|
1429
|
use strict; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
46
|
|
10
|
1
|
|
|
1
|
|
7
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
34
|
|
11
|
1
|
|
|
1
|
|
677
|
use Crypt::Rijndael; |
|
1
|
|
|
|
|
781
|
|
|
1
|
|
|
|
|
28
|
|
12
|
1
|
|
|
1
|
|
837
|
use Digest::SHA qw(sha256); |
|
1
|
|
|
|
|
4286
|
|
|
1
|
|
|
|
|
101
|
|
13
|
|
|
|
|
|
|
|
14
|
1
|
|
|
1
|
|
9
|
use constant DB_HEADSIZE_V1 => 124; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
64
|
|
15
|
1
|
|
|
1
|
|
6
|
use constant DB_SIG_1 => 0x9AA2D903; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
40
|
|
16
|
1
|
|
|
1
|
|
5
|
use constant DB_SIG_2_v1 => 0xB54BFB65; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
40
|
|
17
|
1
|
|
|
1
|
|
6
|
use constant DB_SIG_2_v2 => 0xB54BFB67; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
50
|
|
18
|
1
|
|
|
1
|
|
5
|
use constant DB_VER_DW_V1 => 0x00030002; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
34
|
|
19
|
1
|
|
|
1
|
|
5
|
use constant DB_VER_DW_V2 => 0x00030000; # recent KeePass is 0x0030001 |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
43
|
|
20
|
1
|
|
|
1
|
|
5
|
use constant DB_FLAG_RIJNDAEL => 2; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
37
|
|
21
|
1
|
|
|
1
|
|
5
|
use constant DB_FLAG_TWOFISH => 8; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
19829
|
|
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
our $VERSION = '2.03'; |
24
|
|
|
|
|
|
|
my %locker; |
25
|
|
|
|
|
|
|
my $salsa20_iv = "\xe8\x30\x09\x4b\x97\x20\x5d\x2a"; |
26
|
|
|
|
|
|
|
my $qr_date = qr/^(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)(\.\d+|)?Z?$/; |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
sub new { |
29
|
5
|
|
|
5
|
1
|
4142
|
my $class = shift; |
30
|
5
|
50
|
|
|
|
26
|
my $args = ref($_[0]) ? {%{shift()}} : {@_}; |
|
0
|
|
|
|
|
0
|
|
31
|
5
|
|
|
|
|
25
|
return bless $args, $class; |
32
|
|
|
|
|
|
|
} |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
sub auto_lock { |
35
|
9
|
|
|
9
|
1
|
715
|
my $self = shift; |
36
|
9
|
100
|
|
|
|
38
|
$self->{'auto_lock'} = shift if @_; |
37
|
9
|
|
100
|
|
|
71
|
return !exists($self->{'auto_lock'}) || $self->{'auto_lock'}; |
38
|
|
|
|
|
|
|
} |
39
|
|
|
|
|
|
|
|
40
|
87
|
100
|
|
87
|
1
|
1338
|
sub groups { shift->{'groups'} || die "No groups loaded yet\n" } |
41
|
|
|
|
|
|
|
|
42
|
12
|
|
|
12
|
1
|
712
|
sub header { shift->{'header'} } |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
sub load_db { |
47
|
3
|
|
|
3
|
1
|
5
|
my $self = shift; |
48
|
3
|
|
100
|
|
|
14
|
my $file = shift || die "Missing file\n"; |
49
|
2
|
|
100
|
|
|
13
|
my $pass = shift || die "Missing pass\n"; |
50
|
1
|
|
50
|
|
|
9
|
my $args = shift || {}; |
51
|
|
|
|
|
|
|
|
52
|
1
|
|
|
|
|
5
|
my $buffer = $self->slurp($file); |
53
|
1
|
|
|
|
|
7
|
return $self->parse_db($buffer, $pass, $args); |
54
|
|
|
|
|
|
|
} |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
sub save_db { |
57
|
5
|
|
|
5
|
1
|
706
|
my ($self, $file, $pass, $head, $groups) = @_; |
58
|
5
|
100
|
|
|
|
23
|
die "Missing file\n" if ! $file; |
59
|
4
|
|
50
|
|
|
21
|
$head ||= {}; |
60
|
4
|
50
|
0
|
|
|
38
|
my $v = $file =~ /\.kdbx$/i ? 2 |
|
|
50
|
|
|
|
|
|
61
|
|
|
|
|
|
|
: $file =~ /\.kdb$/i ? 1 |
62
|
|
|
|
|
|
|
: $head->{'version'} || $self->{'version'}; |
63
|
4
|
|
|
|
|
10
|
$head->{'version'} = $v; |
64
|
|
|
|
|
|
|
|
65
|
4
|
|
|
|
|
15
|
my $buf = $self->gen_db($pass, $head, $groups); |
66
|
3
|
|
|
|
|
11
|
my $bak = "$file.bak"; |
67
|
3
|
|
|
|
|
11
|
my $tmp = "$file.new.".int(time()); |
68
|
3
|
50
|
|
|
|
397
|
open my $fh, '>', $tmp or die "Could not open $tmp: $!\n"; |
69
|
3
|
|
|
|
|
11
|
binmode $fh; |
70
|
3
|
|
|
|
|
52
|
print $fh $buf; |
71
|
3
|
|
|
|
|
241
|
close $fh; |
72
|
3
|
50
|
|
|
|
52
|
if (-s $tmp ne length($buf)) { |
73
|
0
|
|
|
|
|
0
|
die "Written file size of $tmp didn't match (".(-s $tmp)." != ".length($buf).") - not moving into place\n"; |
74
|
0
|
|
|
|
|
0
|
unlink($tmp); |
75
|
|
|
|
|
|
|
} |
76
|
|
|
|
|
|
|
|
77
|
3
|
100
|
|
|
|
36
|
if (-e $bak) { |
78
|
1
|
50
|
33
|
|
|
95
|
unlink($bak) or unlink($tmp) or die "Could not removing already existing backup $bak: $!\n"; |
79
|
|
|
|
|
|
|
} |
80
|
3
|
100
|
|
|
|
42
|
if (-e $file) { |
81
|
2
|
50
|
33
|
|
|
107
|
rename($file, $bak) or unlink($tmp) or die "Could not backup $file to $bak: $!\n"; |
82
|
|
|
|
|
|
|
} |
83
|
3
|
50
|
|
|
|
137
|
rename($tmp, $file) or die "Could not move $tmp to $file: $!\n"; |
84
|
3
|
100
|
100
|
|
|
35
|
if (!$self->{'keep_backup'} && -e $bak) { |
85
|
1
|
50
|
|
|
|
52
|
unlink($bak) or die "Could not removing temporary backup $bak: $!\n"; |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
|
88
|
3
|
|
|
|
|
37
|
return 1; |
89
|
|
|
|
|
|
|
} |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
sub clear { |
92
|
6
|
|
|
6
|
1
|
13
|
my $self = shift; |
93
|
6
|
50
|
|
|
|
36
|
$self->unlock if $self->{'groups'}; |
94
|
6
|
|
|
|
|
249
|
delete @$self{qw(header groups)}; |
95
|
|
|
|
|
|
|
} |
96
|
|
|
|
|
|
|
|
97
|
5
|
|
|
5
|
|
2766
|
sub DESTROY { shift->clear } |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
sub parse_db { |
102
|
6
|
|
|
6
|
1
|
23
|
my ($self, $buffer, $pass, $args) = @_; |
103
|
6
|
50
|
0
|
|
|
21
|
$self = $self->new($args || {}) if ! ref $self; |
104
|
6
|
50
|
|
|
|
19
|
$buffer = $$buffer if ref $buffer; |
105
|
|
|
|
|
|
|
|
106
|
6
|
|
|
|
|
24
|
my $head = $self->parse_header($buffer); |
107
|
6
|
50
|
|
|
|
17
|
local $head->{'raw'} = substr $buffer, 0, $head->{'header_size'} if $head->{'version'} == 2; |
108
|
6
|
|
|
|
|
34
|
$buffer = substr $buffer, $head->{'header_size'}; |
109
|
|
|
|
|
|
|
|
110
|
6
|
100
|
|
|
|
33
|
$self->unlock if $self->{'groups'}; # make sure we don't leave dangling keys should we reopen a new db |
111
|
|
|
|
|
|
|
|
112
|
6
|
0
|
|
|
|
19
|
my $meth = ($head->{'version'} == 1) ? '_parse_v1_body' |
|
|
50
|
|
|
|
|
|
113
|
|
|
|
|
|
|
: ($head->{'version'} == 2) ? '_parse_v2_body' |
114
|
|
|
|
|
|
|
: die "Unsupported keepass database version ($head->{'version'})\n"; |
115
|
6
|
|
|
|
|
28
|
(my $meta, $self->{'groups'}) = $self->$meth($buffer, $pass, $head); |
116
|
6
|
|
|
|
|
167
|
$self->{'header'} = {%$head, %$meta}; |
117
|
6
|
100
|
|
|
|
43
|
$self->auto_lock($args->{'auto_lock'}) if exists $args->{'auto_lock'}; |
118
|
|
|
|
|
|
|
|
119
|
6
|
100
|
|
|
|
24
|
$self->lock if $self->auto_lock; |
120
|
6
|
|
|
|
|
57
|
return $self; |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
sub parse_header { |
124
|
6
|
|
|
6
|
1
|
11
|
my ($self, $buffer) = @_; |
125
|
6
|
|
|
|
|
42
|
my ($sig1, $sig2) = unpack 'LL', $buffer; |
126
|
6
|
50
|
|
|
|
21
|
die "File signature (sig1) did not match ($sig1 != ".DB_SIG_1().")\n" if $sig1 != DB_SIG_1; |
127
|
6
|
50
|
|
|
|
35
|
return $self->_parse_v1_header($buffer) if $sig2 eq DB_SIG_2_v1; |
128
|
0
|
0
|
|
|
|
0
|
return $self->_parse_v2_header($buffer) if $sig2 eq DB_SIG_2_v2; |
129
|
0
|
|
|
|
|
0
|
die "Second file signature did not match ($sig2 != ".DB_SIG_2_v1()." or ".DB_SIG_2_v2().")\n"; |
130
|
|
|
|
|
|
|
} |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
sub _parse_v1_header { |
133
|
6
|
|
|
6
|
|
19
|
my ($self, $buffer) = @_; |
134
|
6
|
|
|
|
|
14
|
my $size = length($buffer); |
135
|
6
|
50
|
|
|
|
19
|
die "File was smaller than db header ($size < ".DB_HEADSIZE_V1().")\n" if $size < DB_HEADSIZE_V1; |
136
|
6
|
|
|
|
|
27
|
my %h = (version => 1, header_size => DB_HEADSIZE_V1); |
137
|
6
|
|
|
|
|
32
|
my @f = qw(sig1 sig2 flags ver seed_rand enc_iv n_groups n_entries checksum seed_key rounds); |
138
|
6
|
|
|
|
|
10
|
my $t = 'L L L L a16 a16 L L a32 a32 L'; |
139
|
6
|
|
|
|
|
71
|
@h{@f} = unpack $t, $buffer; |
140
|
6
|
50
|
|
|
|
27
|
die "Unsupported file version ($h{'ver'}).\n" if $h{'ver'} & 0xFFFFFF00 != DB_VER_DW_V1 & 0xFFFFFF00; |
141
|
6
|
0
|
|
|
|
24
|
$h{'enc_type'} = ($h{'flags'} & DB_FLAG_RIJNDAEL) ? 'rijndael' |
|
|
50
|
|
|
|
|
|
142
|
|
|
|
|
|
|
: ($h{'flags'} & DB_FLAG_TWOFISH) ? 'twofish' |
143
|
|
|
|
|
|
|
: die "Unknown encryption type\n"; |
144
|
6
|
|
|
|
|
30
|
return \%h; |
145
|
|
|
|
|
|
|
} |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
sub _parse_v2_header { |
148
|
0
|
|
|
0
|
|
0
|
my ($self, $buffer) = @_; |
149
|
0
|
|
|
|
|
0
|
my %h = (version => 2, enc_type => 'rijndael'); |
150
|
0
|
|
|
|
|
0
|
@h{qw(sig1 sig2 ver)} = unpack 'L3', $buffer; |
151
|
0
|
0
|
|
|
|
0
|
die "Unsupported file version2 ($h{'ver'}).\n" if $h{'ver'} & 0xFFFF0000 > 0x00020000 & 0xFFFF0000; |
152
|
0
|
|
|
|
|
0
|
my $pos = 12; |
153
|
|
|
|
|
|
|
|
154
|
0
|
|
|
|
|
0
|
while (1) { |
155
|
0
|
|
|
|
|
0
|
my ($type, $size) = unpack "\@$pos CS", $buffer; |
156
|
0
|
|
|
|
|
0
|
$pos += 3; |
157
|
0
|
|
|
|
|
0
|
my $val = substr $buffer, $pos, $size; # #my ($val) = unpack "\@$pos a$size", $buffer; |
158
|
0
|
0
|
|
|
|
0
|
if (!$type) { |
159
|
0
|
|
|
|
|
0
|
$h{'0'} = $val; |
160
|
0
|
|
|
|
|
0
|
$pos += $size; |
161
|
0
|
|
|
|
|
0
|
last; |
162
|
|
|
|
|
|
|
} |
163
|
0
|
|
|
|
|
0
|
$pos += $size; |
164
|
0
|
0
|
|
|
|
0
|
if ($type == 1) { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
165
|
0
|
|
|
|
|
0
|
$h{'comment'} = $val; |
166
|
|
|
|
|
|
|
} elsif ($type == 2) { |
167
|
0
|
0
|
|
|
|
0
|
warn "Cipher id did not match AES\n" if $val ne "\x31\xc1\xf2\xe6\xbf\x71\x43\x50\xbe\x58\x05\x21\x6a\xfc\x5a\xff"; |
168
|
0
|
|
|
|
|
0
|
$h{'cipher'} = 'aes'; |
169
|
|
|
|
|
|
|
} elsif ($type == 3) { |
170
|
0
|
|
|
|
|
0
|
$val = unpack 'V', $val; |
171
|
0
|
0
|
|
|
|
0
|
warn "Compression was too large.\n" if $val > 1; |
172
|
0
|
|
|
|
|
0
|
$h{'compression'} = $val; |
173
|
|
|
|
|
|
|
} elsif ($type == 4) { |
174
|
0
|
0
|
|
|
|
0
|
warn "Length of seed random was not 32\n" if length($val) != 32; |
175
|
0
|
|
|
|
|
0
|
$h{'seed_rand'} = $val; |
176
|
|
|
|
|
|
|
} elsif ($type == 5) { |
177
|
0
|
0
|
|
|
|
0
|
warn "Length of seed key was not 32\n" if length($val) != 32; |
178
|
0
|
|
|
|
|
0
|
$h{'seed_key'} = $val; |
179
|
|
|
|
|
|
|
} elsif ($type == 6) { |
180
|
0
|
|
|
|
|
0
|
$h{'rounds'} = unpack 'L', $val; |
181
|
|
|
|
|
|
|
} elsif ($type == 7) { |
182
|
0
|
0
|
|
|
|
0
|
warn "Length of encryption IV was not 16\n" if length($val) != 16; |
183
|
0
|
|
|
|
|
0
|
$h{'enc_iv'} = $val; |
184
|
|
|
|
|
|
|
} elsif ($type == 8) { |
185
|
0
|
0
|
|
|
|
0
|
warn "Length of stream key was not 32\n" if length($val) != 32; |
186
|
0
|
|
|
|
|
0
|
$h{'protected_stream_key'} = $val; |
187
|
|
|
|
|
|
|
} elsif ($type == 9) { |
188
|
0
|
0
|
|
|
|
0
|
warn "Length of start bytes was not 32\n" if length($val) != 32; |
189
|
0
|
|
|
|
|
0
|
$h{'start_bytes'} = $val; |
190
|
|
|
|
|
|
|
} elsif ($type == 10) { |
191
|
0
|
0
|
|
|
|
0
|
warn "Inner stream id did not match Salsa20\n" if unpack('V', $val) != 2; |
192
|
0
|
|
|
|
|
0
|
$h{'protected_stream'} = 'salsa20'; |
193
|
|
|
|
|
|
|
} else { |
194
|
0
|
|
|
|
|
0
|
warn "Found an unknown header type ($type, $val)\n"; |
195
|
|
|
|
|
|
|
} |
196
|
|
|
|
|
|
|
} |
197
|
|
|
|
|
|
|
|
198
|
0
|
|
|
|
|
0
|
$h{'header_size'} = $pos; |
199
|
0
|
|
|
|
|
0
|
return \%h; |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
sub _parse_v1_body { |
203
|
6
|
|
|
6
|
|
14
|
my ($self, $buffer, $pass, $head) = @_; |
204
|
6
|
50
|
|
|
|
20
|
die "Unimplemented enc_type $head->{'enc_type'}\n" if $head->{'enc_type'} ne 'rijndael'; |
205
|
6
|
|
|
|
|
22
|
my $key = $self->_master_key($pass, $head); |
206
|
6
|
|
|
|
|
55
|
$buffer = $self->decrypt_rijndael_cbc($buffer, $key, $head->{'enc_iv'}); |
207
|
|
|
|
|
|
|
|
208
|
6
|
50
|
33
|
|
|
81
|
die "The file could not be decrypted either because the key is wrong or the file is damaged.\n" |
|
|
|
33
|
|
|
|
|
209
|
|
|
|
|
|
|
if length($buffer) > 2**32-1 || (!length($buffer) && $head->{'n_groups'}); |
210
|
6
|
50
|
|
|
|
84
|
die "The file checksum did not match.\nThe key is wrong or the file is damaged\n" |
211
|
|
|
|
|
|
|
if $head->{'checksum'} ne sha256($buffer); |
212
|
|
|
|
|
|
|
|
213
|
6
|
|
|
|
|
45
|
my ($groups, $gmap, $pos) = $self->_parse_v1_groups($buffer, $head->{'n_groups'}); |
214
|
6
|
|
|
|
|
29
|
$self->_parse_v1_entries($buffer, $head->{'n_entries'}, $pos, $gmap, $groups); |
215
|
6
|
|
|
|
|
43
|
return ({}, $groups); |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
sub _parse_v2_body { |
219
|
0
|
|
|
0
|
|
0
|
my ($self, $buffer, $pass, $head) = @_; |
220
|
0
|
|
|
|
|
0
|
my $key = $self->_master_key($pass, $head); |
221
|
0
|
|
|
|
|
0
|
$buffer = $self->decrypt_rijndael_cbc($buffer, $key, $head->{'enc_iv'}); |
222
|
0
|
0
|
|
|
|
0
|
die "The database key appears invalid or else the database is corrupt.\n" |
223
|
|
|
|
|
|
|
if substr($buffer, 0, 32, '') ne $head->{'start_bytes'}; |
224
|
0
|
|
|
|
|
0
|
$buffer = $self->unchunksum($buffer); |
225
|
0
|
0
|
0
|
|
|
0
|
$buffer = eval { $self->decompress($buffer) } or die "Failed to decompress document: $@" if ($head->{'compression'} || '') eq '1'; |
|
0
|
|
0
|
|
|
0
|
|
226
|
0
|
0
|
0
|
|
|
0
|
$self->{'xml_in'} = $buffer if $self->{'keep_xml'} || $head->{'keep_xml'}; |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
my $uuid = sub { |
229
|
0
|
|
|
0
|
|
0
|
my $id = shift; |
230
|
0
|
0
|
|
|
|
0
|
if ($id) { |
231
|
0
|
|
|
|
|
0
|
$id = $self->decode_base64($id); |
232
|
0
|
0
|
|
|
|
0
|
$id = 0 if $id eq "\0"x16; |
233
|
0
|
0
|
|
|
|
0
|
$id =~ s/^0+(?=\d)// if $id =~ /^\d{16}$/; |
234
|
|
|
|
|
|
|
} |
235
|
0
|
|
|
|
|
0
|
return $id; |
236
|
0
|
|
|
|
|
0
|
}; |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
# parse the XML - use our own parser since XML::Simple does not do event based actions |
239
|
0
|
0
|
|
0
|
|
0
|
my $tri = sub { return !defined($_[0]) ? undef : ('true' eq lc $_[0]) ? 1 : ('false' eq lc $_[0]) ? 0 : undef }; |
|
0
|
0
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
|
240
|
0
|
|
|
|
|
0
|
my $s20_stream = $self->salsa20_stream({key => sha256($head->{'protected_stream_key'}), iv => $salsa20_iv, rounds => 20}); |
241
|
0
|
|
|
|
|
0
|
my %BIN; |
242
|
|
|
|
|
|
|
my $META; |
243
|
0
|
|
|
|
|
0
|
my @GROUPS; |
244
|
0
|
|
|
|
|
0
|
my $level = 0; |
245
|
0
|
|
|
|
|
0
|
my $data = $self->parse_xml($buffer, { |
246
|
|
|
|
|
|
|
top => 'KeePassFile', |
247
|
|
|
|
|
|
|
force_array => {map {$_ => 1} qw(Binaries Binary Group Entry String Association Item DeletedObject)}, |
248
|
0
|
|
|
0
|
|
0
|
start_handlers => {Group => sub { $level++ }}, |
249
|
|
|
|
|
|
|
end_handlers => { |
250
|
|
|
|
|
|
|
Meta => sub { |
251
|
0
|
|
|
0
|
|
0
|
my ($node, $parent) = @_; |
252
|
0
|
0
|
|
|
|
0
|
die "Found multiple intances of Meta.\n" if $META; |
253
|
0
|
|
|
|
|
0
|
$META = {}; |
254
|
0
|
|
0
|
|
|
0
|
my $pro = delete($node->{'MemoryProtection'}) || {}; # flatten out protection |
255
|
0
|
|
|
|
|
0
|
@$node{map {s/Protect/protect_/; lc $_} keys %$pro} = map {$tri->($_)} values %$pro; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
256
|
0
|
|
|
|
|
0
|
for my $key (keys %$node) { |
257
|
0
|
0
|
|
|
|
0
|
next if $key eq 'Binaries'; |
258
|
0
|
|
|
|
|
0
|
(my $copy = $key) =~ s/([a-z])([A-Z])/${1}_${2}/g; |
259
|
0
|
0
|
|
|
|
0
|
$META->{lc $copy} = $copy =~ /_changed$/i ? $self->_parse_v2_date($node->{$key}) : $node->{$key}; |
260
|
|
|
|
|
|
|
} |
261
|
0
|
|
|
|
|
0
|
$META->{'recycle_bin_enabled'} = $tri->($META->{'recycle_bin_enabled'}); |
262
|
0
|
|
|
|
|
0
|
$META->{$_} = $uuid->($META->{$_}) for qw(entry_templates_group last_selected_group last_top_visible_group recycle_bin_uuid); |
263
|
0
|
0
|
0
|
|
|
0
|
die "HeaderHash recorded in file did not match actual hash of header.\n" |
|
|
|
0
|
|
|
|
|
264
|
|
|
|
|
|
|
if $META->{'header_hash'} && $head->{'raw'} && $META->{'header_hash'} ne $self->encode_base64(sha256($head->{'raw'})); |
265
|
|
|
|
|
|
|
}, |
266
|
|
|
|
|
|
|
Binary => sub { |
267
|
0
|
|
|
0
|
|
0
|
my ($node, $parent, $parent_tag, $tag) = @_; |
268
|
0
|
0
|
|
|
|
0
|
if ($parent_tag eq 'Binaries') { |
|
|
0
|
|
|
|
|
|
269
|
0
|
|
|
|
|
0
|
my ($content, $id, $comp) = @$node{qw(content ID Compressed)}; |
270
|
0
|
0
|
|
|
|
0
|
$content = '' if ! defined $content; |
271
|
0
|
0
|
|
|
|
0
|
$content = $self->decode_base64($content) if length $content; |
272
|
0
|
0
|
0
|
|
|
0
|
if ($comp && $comp eq 'True' && length $content) { |
|
|
|
0
|
|
|
|
|
273
|
0
|
0
|
|
|
|
0
|
eval { $content = $self->decompress($content) } or warn "Could not decompress associated binary ($id): $@"; |
|
0
|
|
|
|
|
0
|
|
274
|
|
|
|
|
|
|
} |
275
|
0
|
0
|
|
|
|
0
|
warn "Duplicate binary id $id - using most recent.\n" if exists $BIN{$id}; |
276
|
0
|
|
|
|
|
0
|
$BIN{$id} = $content; |
277
|
|
|
|
|
|
|
} elsif ($parent_tag eq 'Entry') { |
278
|
0
|
|
|
|
|
0
|
my $key = $node->{'Key'}; |
279
|
0
|
0
|
|
|
|
0
|
$key = do { warn "Missing key for binary."; 'unknown' } if ! defined $key; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
280
|
0
|
0
|
|
|
|
0
|
warn "Duplicate binary key for entry." if $parent->{'__binary__'}->{$key}; |
281
|
0
|
|
|
|
|
0
|
$parent->{'__binary__'}->{$key} = $BIN{$node->{'Value'}->{'Ref'}}; |
282
|
|
|
|
|
|
|
} |
283
|
|
|
|
|
|
|
}, |
284
|
|
|
|
|
|
|
CustomData => sub { |
285
|
0
|
|
|
0
|
|
0
|
my ($node, $parent, $parent_tag, $tag) = @_; |
286
|
0
|
0
|
|
|
|
0
|
$parent->{$tag} = {map {$_->{'Key'} => $_->{'Value'}} @{ $node->{'Item'} || [] }}; # is order important? |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
287
|
|
|
|
|
|
|
}, |
288
|
|
|
|
|
|
|
Group => sub { |
289
|
0
|
|
|
0
|
|
0
|
my ($node, $parent, $parent_tag) = @_; |
290
|
0
|
|
0
|
|
|
0
|
my $group = { |
|
|
|
0
|
|
|
|
|
291
|
|
|
|
|
|
|
id => $uuid->($node->{'UUID'}), |
292
|
|
|
|
|
|
|
icon => $node->{'IconID'}, |
293
|
|
|
|
|
|
|
title => $node->{'Name'}, |
294
|
|
|
|
|
|
|
expanded => $tri->($node->{'IsExpanded'}), |
295
|
|
|
|
|
|
|
level => $level, |
296
|
|
|
|
|
|
|
accessed => $self->_parse_v2_date($node->{'Times'}->{'LastAccessTime'}), |
297
|
|
|
|
|
|
|
expires => $self->_parse_v2_date($node->{'Times'}->{'ExpiryTime'}), |
298
|
|
|
|
|
|
|
created => $self->_parse_v2_date($node->{'Times'}->{'CreationTime'}), |
299
|
|
|
|
|
|
|
modified => $self->_parse_v2_date($node->{'Times'}->{'LastModificationTime'}), |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
auto_type_default => $node->{'DefaultAutoTypeSequence'}, |
302
|
|
|
|
|
|
|
auto_type_enabled => $tri->($node->{'EnableAutoType'}), |
303
|
|
|
|
|
|
|
enable_searching => $tri->($node->{'EnableSearching'}), |
304
|
|
|
|
|
|
|
last_top_entry => $uuid->($node->{'LastTopVisibleEntry'}), |
305
|
|
|
|
|
|
|
expires_enabled => $tri->($node->{'Times'}->{'Expires'}), |
306
|
|
|
|
|
|
|
location_changed => $self->_parse_v2_date($node->{'Times'}->{'LocationChanged'}), |
307
|
|
|
|
|
|
|
usage_count => $node->{'Times'}->{'UsageCount'}, |
308
|
|
|
|
|
|
|
notes => $node->{'Notes'}, |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
entries => delete($node->{'__entries__'}) || [], |
311
|
|
|
|
|
|
|
groups => delete($node->{'__groups__'}) || [], |
312
|
|
|
|
|
|
|
}; |
313
|
0
|
0
|
|
|
|
0
|
if ($parent_tag eq 'Group') { |
314
|
0
|
|
|
|
|
0
|
push @{ $parent->{'__groups__'} }, $group; |
|
0
|
|
|
|
|
0
|
|
315
|
|
|
|
|
|
|
} else { |
316
|
0
|
|
|
|
|
0
|
push @GROUPS, $group; |
317
|
|
|
|
|
|
|
} |
318
|
|
|
|
|
|
|
}, |
319
|
|
|
|
|
|
|
Entry => sub { |
320
|
0
|
|
|
0
|
|
0
|
my ($node, $parent, $parent_tag) = @_; |
321
|
0
|
|
|
|
|
0
|
my %str; |
322
|
0
|
0
|
|
|
|
0
|
for my $s (@{ $node->{'String'} || [] }) { |
|
0
|
|
|
|
|
0
|
|
323
|
0
|
|
|
|
|
0
|
$str{$s->{'Key'}} = $s->{'Value'}; |
324
|
0
|
0
|
|
|
|
0
|
$str{'__protected__'}->{$s->{'Key'} =~ /^(Password|UserName|URL|Notes|Title)$/i ? lc($s->{'Key'}) : $s->{'Key'}} = 1 if $s->{'__protected__'}; |
|
|
0
|
|
|
|
|
|
325
|
|
|
|
|
|
|
} |
326
|
0
|
0
|
0
|
|
|
0
|
my $entry = { |
327
|
|
|
|
|
|
|
accessed => $self->_parse_v2_date($node->{'Times'}->{'LastAccessTime'}), |
328
|
|
|
|
|
|
|
created => $self->_parse_v2_date($node->{'Times'}->{'CreationTime'}), |
329
|
|
|
|
|
|
|
expires => $self->_parse_v2_date($node->{'Times'}->{'ExpiryTime'}), |
330
|
|
|
|
|
|
|
modified => $self->_parse_v2_date($node->{'Times'}->{'LastModificationTime'}), |
331
|
|
|
|
|
|
|
comment => delete($str{'Notes'}), |
332
|
|
|
|
|
|
|
icon => $node->{'IconID'}, |
333
|
|
|
|
|
|
|
id => $uuid->($node->{'UUID'}), |
334
|
|
|
|
|
|
|
title => delete($str{'Title'}), |
335
|
|
|
|
|
|
|
url => delete($str{'URL'}), |
336
|
|
|
|
|
|
|
username => delete($str{'UserName'}), |
337
|
|
|
|
|
|
|
password => delete($str{'Password'}), |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
expires_enabled => $tri->($node->{'Times'}->{'Expires'}), |
340
|
|
|
|
|
|
|
location_changed => $self->_parse_v2_date($node->{'Times'}->{'LocationChanged'}), |
341
|
|
|
|
|
|
|
usage_count => $node->{'Times'}->{'UsageCount'}, |
342
|
|
|
|
|
|
|
tags => $node->{'Tags'}, |
343
|
|
|
|
|
|
|
background_color => $node->{'BackgroundColor'}, |
344
|
|
|
|
|
|
|
foreground_color => $node->{'ForegroundColor'}, |
345
|
|
|
|
|
|
|
override_url => $node->{'OverrideURL'}, |
346
|
|
|
|
|
|
|
auto_type => delete($node->{'AutoType'}->{'__auto_type__'}) || [], |
347
|
|
|
|
|
|
|
auto_type_enabled => $tri->($node->{'AutoType'}->{'Enabled'}), |
348
|
|
|
|
|
|
|
auto_type_munge => $node->{'AutoType'}->{'DataTransferObfuscation'} ? 1 : 0, |
349
|
|
|
|
|
|
|
protected => delete($str{'__protected__'}), |
350
|
|
|
|
|
|
|
}; |
351
|
0
|
0
|
|
|
|
0
|
$entry->{'history'} = $node->{'History'} if defined $node->{'History'}; |
352
|
0
|
0
|
|
|
|
0
|
$entry->{'custom_icon_uuid'} = $node->{'CustomIconUUID'} if defined $node->{'CustomIconUUID'}; |
353
|
0
|
0
|
|
|
|
0
|
$entry->{'strings'} = \%str if scalar keys %str; |
354
|
0
|
0
|
|
|
|
0
|
$entry->{'binary'} = delete($node->{'__binary__'}) if $node->{'__binary__'}; |
355
|
0
|
|
|
|
|
0
|
push @{ $parent->{'__entries__'} }, $entry; |
|
0
|
|
|
|
|
0
|
|
356
|
|
|
|
|
|
|
}, |
357
|
|
|
|
|
|
|
String => sub { |
358
|
0
|
|
|
0
|
|
0
|
my $node = shift; |
359
|
0
|
|
|
|
|
0
|
my $val = $node->{'Value'}; |
360
|
0
|
0
|
0
|
|
|
0
|
if (ref($val) eq 'HASH' && $val->{'Protected'} && $val->{'Protected'} eq 'True') { |
|
|
|
0
|
|
|
|
|
361
|
0
|
|
|
|
|
0
|
$val = $val->{'content'}; |
362
|
0
|
0
|
0
|
|
|
0
|
$node->{'Value'} = (defined($val) && length($val)) ? $s20_stream->($self->decode_base64($val)) : ''; |
363
|
0
|
|
|
|
|
0
|
$node->{'__protected__'} = 1; |
364
|
|
|
|
|
|
|
} |
365
|
|
|
|
|
|
|
}, |
366
|
|
|
|
|
|
|
Association => sub { |
367
|
0
|
|
|
0
|
|
0
|
my ($node, $parent) = @_; |
368
|
0
|
|
|
|
|
0
|
push @{ $parent->{'__auto_type__'} }, {window => $node->{'Window'}, keys => $node->{'KeystrokeSequence'}}; |
|
0
|
|
|
|
|
0
|
|
369
|
|
|
|
|
|
|
}, |
370
|
|
|
|
|
|
|
History => sub { |
371
|
0
|
|
|
0
|
|
0
|
my ($node, $parent, $parent_tag, $tag) = @_; |
372
|
0
|
|
0
|
|
|
0
|
$parent->{$tag} = delete($node->{'__entries__'}) || []; |
373
|
|
|
|
|
|
|
}, |
374
|
|
|
|
|
|
|
Association => sub { |
375
|
0
|
|
|
0
|
|
0
|
my ($node, $parent) = @_; |
376
|
0
|
|
|
|
|
0
|
push @{ $parent->{'__auto_type__'} }, {window => $node->{'Window'}, keys => $node->{'KeystrokeSequence'}}; |
|
0
|
|
|
|
|
0
|
|
377
|
|
|
|
|
|
|
}, |
378
|
|
|
|
|
|
|
DeletedObject => sub { |
379
|
0
|
|
|
0
|
|
0
|
my ($node) = @_; |
380
|
0
|
0
|
0
|
|
|
0
|
push @{ $GROUPS[0]->{'deleted_objects'} }, { |
|
0
|
|
0
|
|
|
0
|
|
381
|
|
|
|
|
|
|
uuid => $self->decode_base64($node->{'UUID'}), |
382
|
|
|
|
|
|
|
date => $self->_parse_v2_date($node->{'DeletionTime'}), |
383
|
|
|
|
|
|
|
} if $GROUPS[0] && $node->{'UUID'} && $node->{'DeletionTime'}; |
384
|
|
|
|
|
|
|
}, |
385
|
|
|
|
|
|
|
}, |
386
|
0
|
|
|
|
|
0
|
}); |
387
|
|
|
|
|
|
|
|
388
|
0
|
|
|
|
|
0
|
my $g = $GROUPS[0]; |
389
|
0
|
0
|
|
|
|
0
|
@GROUPS = @{ $g->{'groups'} } if @GROUPS == 1 |
|
0
|
|
|
|
|
0
|
|
390
|
|
|
|
|
|
|
&& $g && $g->{'notes'} && $g->{'notes'} eq "Added as a top group by File::KeePass" |
391
|
0
|
0
|
0
|
|
|
0
|
&& @{ $g->{'groups'} || [] } && !@{ $g->{'entries'} || [] } && !$g->{'auto_type_default'}; |
|
0
|
0
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
392
|
0
|
|
|
|
|
0
|
return ($META, \@GROUPS); |
393
|
|
|
|
|
|
|
} |
394
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
sub _parse_v1_groups { |
396
|
6
|
|
|
6
|
|
20
|
my ($self, $buffer, $n_groups) = @_; |
397
|
6
|
|
|
|
|
6
|
my $pos = 0; |
398
|
|
|
|
|
|
|
|
399
|
6
|
|
|
|
|
11
|
my @groups; |
400
|
|
|
|
|
|
|
my %gmap; # allow entries to find their groups (group map) |
401
|
6
|
|
|
|
|
21
|
my @gref = (\@groups); # group ref pointer stack - let levels nest safely |
402
|
6
|
|
|
|
|
16
|
my $group = {}; |
403
|
6
|
|
|
|
|
24
|
while ($n_groups) { |
404
|
189
|
|
|
|
|
321
|
my $type = unpack 'S', substr($buffer, $pos, 2); |
405
|
189
|
|
|
|
|
202
|
$pos += 2; |
406
|
189
|
50
|
|
|
|
327
|
die "Group header offset is out of range. ($pos)" if $pos >= length($buffer); |
407
|
|
|
|
|
|
|
|
408
|
189
|
|
|
|
|
305
|
my $size = unpack 'L', substr($buffer, $pos, 4); |
409
|
189
|
|
|
|
|
190
|
$pos += 4; |
410
|
189
|
50
|
|
|
|
339
|
die "Group header offset is out of range. ($pos, $size)" if $pos + $size > length($buffer); |
411
|
|
|
|
|
|
|
|
412
|
189
|
100
|
|
|
|
653
|
if ($type == 1) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
413
|
21
|
|
|
|
|
54
|
$group->{'id'} = unpack 'L', substr($buffer, $pos, 4); |
414
|
|
|
|
|
|
|
} elsif ($type == 2) { |
415
|
21
|
|
|
|
|
138
|
($group->{'title'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
416
|
|
|
|
|
|
|
} elsif ($type == 3) { |
417
|
21
|
|
|
|
|
73
|
$group->{'created'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
418
|
|
|
|
|
|
|
} elsif ($type == 4) { |
419
|
21
|
|
|
|
|
49
|
$group->{'modified'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
420
|
|
|
|
|
|
|
} elsif ($type == 5) { |
421
|
21
|
|
|
|
|
53
|
$group->{'accessed'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
422
|
|
|
|
|
|
|
} elsif ($type == 6) { |
423
|
21
|
|
|
|
|
50
|
$group->{'expires'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
424
|
|
|
|
|
|
|
} elsif ($type == 7) { |
425
|
21
|
|
|
|
|
57
|
$group->{'icon'} = unpack 'L', substr($buffer, $pos, 4); |
426
|
|
|
|
|
|
|
} elsif ($type == 8) { |
427
|
21
|
|
|
|
|
72
|
$group->{'level'} = unpack 'S', substr($buffer, $pos, 2); |
428
|
|
|
|
|
|
|
} elsif ($type == 0xFFFF) { |
429
|
21
|
|
50
|
|
|
50
|
$group->{'created'} ||= ''; |
430
|
21
|
|
|
|
|
23
|
$n_groups--; |
431
|
21
|
|
|
|
|
70
|
$gmap{$group->{'id'}} = $group; |
432
|
21
|
|
100
|
|
|
69
|
my $level = $group->{'level'} || 0; |
433
|
21
|
100
|
|
|
|
70
|
if (@gref > $level + 1) { # gref is index base 1 because the root is a pointer to \@groups |
|
|
100
|
|
|
|
|
|
434
|
3
|
|
|
|
|
9
|
splice @gref, $level + 1; |
435
|
|
|
|
|
|
|
} elsif (@gref < $level + 1) { |
436
|
8
|
|
|
|
|
27
|
push @gref, ($gref[-1]->[-1]->{'groups'} = []); |
437
|
|
|
|
|
|
|
} |
438
|
21
|
|
|
|
|
23
|
push @{ $gref[-1] }, $group; |
|
21
|
|
|
|
|
45
|
|
439
|
21
|
|
|
|
|
35
|
$group = {}; |
440
|
|
|
|
|
|
|
} else { |
441
|
0
|
|
|
|
|
0
|
$group->{'unknown'}->{$type} = substr($buffer, $pos, $size); |
442
|
|
|
|
|
|
|
} |
443
|
189
|
|
|
|
|
438
|
$pos += $size; |
444
|
|
|
|
|
|
|
} |
445
|
|
|
|
|
|
|
|
446
|
6
|
|
|
|
|
28
|
return (\@groups, \%gmap, $pos); |
447
|
|
|
|
|
|
|
} |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
sub _parse_v1_entries { |
450
|
6
|
|
|
6
|
|
14
|
my ($self, $buffer, $n_entries, $pos, $gmap, $groups) = @_; |
451
|
|
|
|
|
|
|
|
452
|
6
|
|
|
|
|
11
|
my $entry = {}; |
453
|
6
|
|
|
|
|
21
|
while ($n_entries) { |
454
|
150
|
|
|
|
|
245
|
my $type = unpack 'S', substr($buffer, $pos, 2); |
455
|
150
|
|
|
|
|
158
|
$pos += 2; |
456
|
150
|
50
|
|
|
|
271
|
die "Entry header offset is out of range. ($pos)" if $pos >= length($buffer); |
457
|
|
|
|
|
|
|
|
458
|
150
|
|
|
|
|
219
|
my $size = unpack 'L', substr($buffer, $pos, 4); |
459
|
150
|
|
|
|
|
145
|
$pos += 4; |
460
|
150
|
50
|
|
|
|
265
|
die "Entry header offset is out of range for type $type. ($pos, ".length($buffer).", $size)" if $pos + $size > length($buffer); |
461
|
|
|
|
|
|
|
|
462
|
150
|
100
|
|
|
|
726
|
if ($type == 1) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
463
|
10
|
|
|
|
|
29
|
$entry->{'id'} = substr($buffer, $pos, $size); |
464
|
|
|
|
|
|
|
} elsif ($type == 2) { |
465
|
10
|
|
|
|
|
27
|
$entry->{'group_id'} = unpack 'L', substr($buffer, $pos, 4); |
466
|
|
|
|
|
|
|
} elsif ($type == 3) { |
467
|
10
|
|
|
|
|
23
|
$entry->{'icon'} = unpack 'L', substr($buffer, $pos, 4); |
468
|
|
|
|
|
|
|
} elsif ($type == 4) { |
469
|
10
|
|
|
|
|
49
|
($entry->{'title'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
470
|
|
|
|
|
|
|
} elsif ($type == 5) { |
471
|
10
|
|
|
|
|
61
|
($entry->{'url'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
472
|
|
|
|
|
|
|
} elsif ($type == 6) { |
473
|
10
|
|
|
|
|
50
|
($entry->{'username'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
474
|
|
|
|
|
|
|
} elsif ($type == 7) { |
475
|
10
|
|
|
|
|
47
|
($entry->{'password'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
476
|
|
|
|
|
|
|
} elsif ($type == 8) { |
477
|
10
|
|
|
|
|
58
|
($entry->{'comment'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
478
|
|
|
|
|
|
|
} elsif ($type == 9) { |
479
|
10
|
|
|
|
|
42
|
$entry->{'created'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
480
|
|
|
|
|
|
|
} elsif ($type == 0xA) { |
481
|
10
|
|
|
|
|
46
|
$entry->{'modified'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
482
|
|
|
|
|
|
|
} elsif ($type == 0xB) { |
483
|
10
|
|
|
|
|
29
|
$entry->{'accessed'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
484
|
|
|
|
|
|
|
} elsif ($type == 0xC) { |
485
|
10
|
|
|
|
|
29
|
$entry->{'expires'} = $self->_parse_v1_date(substr($buffer, $pos, $size)); |
486
|
|
|
|
|
|
|
} elsif ($type == 0xD) { |
487
|
10
|
|
|
|
|
51
|
($entry->{'binary_name'} = substr($buffer, $pos, $size)) =~ s/\0$//; |
488
|
|
|
|
|
|
|
} elsif ($type == 0xE) { |
489
|
10
|
|
|
|
|
26
|
$entry->{'binary'} = substr($buffer, $pos, $size); |
490
|
|
|
|
|
|
|
} elsif ($type == 0xFFFF) { |
491
|
10
|
|
50
|
|
|
35
|
$entry->{'created'} ||= ''; |
492
|
10
|
|
|
|
|
37
|
$n_entries--; |
493
|
10
|
|
|
|
|
24
|
my $gid = delete $entry->{'group_id'}; |
494
|
10
|
|
|
|
|
20
|
my $ref = $gmap->{$gid}; |
495
|
10
|
50
|
|
|
|
26
|
if (!$ref) { # orphaned nodes go in special group |
496
|
0
|
|
|
|
|
0
|
$gid = -1; |
497
|
0
|
0
|
|
|
|
0
|
if (!$gmap->{$gid}) { |
498
|
0
|
|
|
|
|
0
|
push @$groups, ($gmap->{$gid} = {id => $gid, title => '*Orphaned*', icon => 0, created => $self->now}); |
499
|
|
|
|
|
|
|
} |
500
|
0
|
|
|
|
|
0
|
$ref = $gmap->{$gid}; |
501
|
|
|
|
|
|
|
} |
502
|
|
|
|
|
|
|
|
503
|
10
|
100
|
100
|
|
|
40
|
if ($entry->{'comment'} && $entry->{'comment'} eq 'KPX_GROUP_TREE_STATE') { |
504
|
3
|
50
|
33
|
|
|
20
|
if (!defined($entry->{'binary'}) || length($entry->{'binary'}) < 4) { |
505
|
0
|
|
|
|
|
0
|
warn "Discarded metastream KPX_GROUP_TREE_STATE because of a parsing error." |
506
|
|
|
|
|
|
|
} else { |
507
|
3
|
|
|
|
|
13
|
my $n = unpack 'L', substr($entry->{'binary'}, 0, 4); |
508
|
3
|
50
|
|
|
|
12
|
if ($n * 5 != length($entry->{'binary'}) - 4) { |
509
|
0
|
|
|
|
|
0
|
warn "Discarded metastream KPX_GROUP_TREE_STATE because of a parsing error."; |
510
|
|
|
|
|
|
|
} else { |
511
|
3
|
|
|
|
|
21
|
for (my $i = 0; $i < $n; $i++) { |
512
|
6
|
|
|
|
|
14
|
my $group_id = unpack 'L', substr($entry->{'binary'}, 4 + $i * 5, 4); |
513
|
6
|
|
|
|
|
13
|
my $is_expanded = unpack 'C', substr($entry->{'binary'}, 8 + $i * 5, 1); |
514
|
6
|
|
|
|
|
26
|
$gmap->{$group_id}->{'expanded'} = $is_expanded; |
515
|
|
|
|
|
|
|
} |
516
|
|
|
|
|
|
|
} |
517
|
|
|
|
|
|
|
} |
518
|
3
|
|
|
|
|
6
|
$entry = {}; |
519
|
3
|
|
|
|
|
17
|
next; |
520
|
|
|
|
|
|
|
} |
521
|
|
|
|
|
|
|
|
522
|
7
|
|
|
|
|
23
|
$self->_check_v1_binary($entry); |
523
|
7
|
|
|
|
|
24
|
$self->_check_v1_auto_type($entry); |
524
|
7
|
|
|
|
|
9
|
push @{ $ref->{'entries'} }, $entry; |
|
7
|
|
|
|
|
21
|
|
525
|
7
|
|
|
|
|
15
|
$entry = {}; |
526
|
|
|
|
|
|
|
} else { |
527
|
0
|
|
|
|
|
0
|
$entry->{'unknown'}->{$type} = substr($buffer, $pos, $size); |
528
|
|
|
|
|
|
|
} |
529
|
147
|
|
|
|
|
362
|
$pos += $size; |
530
|
|
|
|
|
|
|
} |
531
|
|
|
|
|
|
|
} |
532
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
sub _check_v1_binary { |
534
|
13
|
|
|
13
|
|
27
|
my ($self, $e) = @_; |
535
|
13
|
100
|
|
|
|
38
|
if (ref($e->{'binary'}) eq 'HASH') { |
536
|
3
|
|
|
|
|
7
|
delete $e->{'binary_name'}; |
537
|
3
|
|
|
|
|
8
|
return; |
538
|
|
|
|
|
|
|
} |
539
|
10
|
|
|
|
|
23
|
my $bin = delete $e->{'binary'}; |
540
|
10
|
|
|
|
|
20
|
my $bname = delete $e->{'binary_name'}; |
541
|
10
|
100
|
100
|
|
|
100
|
if ((defined($bin) && length($bin)) || (defined($bname) && length($bname))) { |
|
|
|
66
|
|
|
|
|
|
|
|
66
|
|
|
|
|
542
|
2
|
|
50
|
|
|
13
|
defined($_) or $_ = '' for $bin, $bname; |
543
|
2
|
|
|
|
|
10
|
$e->{'binary'} = {$bname => $bin}; |
544
|
|
|
|
|
|
|
} |
545
|
|
|
|
|
|
|
} |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
sub _check_v1_auto_type { |
548
|
13
|
|
|
13
|
|
23
|
my ($self, $e, $del) = @_; |
549
|
13
|
50
|
|
|
|
39
|
$e->{'auto_type'} = [$e->{'auto_type'}] if ref($e->{'auto_type'}) eq 'HASH'; |
550
|
13
|
100
|
|
|
|
35
|
if (ref($e->{'auto_type'}) eq 'ARRAY') { |
551
|
1
|
|
|
|
|
2
|
delete $e->{'auto_type_window'}; |
552
|
1
|
|
|
|
|
2
|
return; |
553
|
|
|
|
|
|
|
} |
554
|
12
|
|
|
|
|
16
|
my @AT; |
555
|
12
|
|
|
|
|
32
|
my $key = delete $e->{'auto_type'}; |
556
|
12
|
|
|
|
|
29
|
my $win = delete $e->{'auto_type_window'}; |
557
|
12
|
100
|
66
|
|
|
79
|
if ((defined($key) && length($key)) || (defined($win) && length($win))) { |
|
|
|
33
|
|
|
|
|
|
|
|
66
|
|
|
|
|
558
|
1
|
|
|
|
|
5
|
push @AT, {keys => $key, window => $win}; |
559
|
|
|
|
|
|
|
} |
560
|
12
|
100
|
|
|
|
39
|
return if ! $e->{'comment'}; |
561
|
4
|
|
|
|
|
49
|
my %atw = my @atw = $e->{'comment'} =~ m{ ^Auto-Type-Window((?:-?\d+)?): [\t ]* (.*?) [\t ]*$ }mxg; |
562
|
4
|
|
|
|
|
67
|
my %atk = my @atk = $e->{'comment'} =~ m{ ^Auto-Type((?:-?\d+)?): [\t ]* (.*?) [\t ]*$ }mxg; |
563
|
4
|
|
|
|
|
28
|
$e->{'comment'} =~ s{ ^Auto-Type(?:-Window)?(?:-?\d+)?: .* \n? }{}mxg; |
564
|
4
|
|
|
|
|
14
|
while (@atw) { |
565
|
4
|
|
|
|
|
11
|
my ($n, $w) = (shift(@atw), shift(@atw)); |
566
|
4
|
50
|
|
|
|
29
|
push @AT, {window => $w, keys => exists($atk{$n}) ? $atk{$n} : $atk{''}}; |
567
|
|
|
|
|
|
|
} |
568
|
4
|
|
|
|
|
14
|
while (@atk) { |
569
|
3
|
|
|
|
|
7
|
my ($n, $k) = (shift(@atk), shift(@atk)); |
570
|
3
|
50
|
|
|
|
19
|
push @AT, {keys => $k, window => exists($atw{$n}) ? $atw{$n} : $atw{''}}; |
571
|
|
|
|
|
|
|
} |
572
|
4
|
50
|
|
|
|
11
|
for (@AT) { $_->{'window'} = '' if ! defined $_->{'window'}; $_->{'keys'} = '' if ! defined $_->{'keys'} } |
|
8
|
50
|
|
|
|
18
|
|
|
8
|
|
|
|
|
26
|
|
573
|
4
|
|
|
|
|
7
|
my %uniq; |
574
|
4
|
|
|
|
|
10
|
@AT = grep {!$uniq{"$_->{'window'}\e$_->{'keys'}"}++} @AT; |
|
8
|
|
|
|
|
35
|
|
575
|
4
|
100
|
|
|
|
23
|
$e->{'auto_type'} = \@AT if @AT; |
576
|
|
|
|
|
|
|
} |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
sub _parse_v1_date { |
579
|
124
|
|
|
124
|
|
237
|
my ($self, $packed) = @_; |
580
|
124
|
|
|
|
|
301
|
my @b = unpack('C*', $packed); |
581
|
124
|
|
|
|
|
189
|
my $year = ($b[0] << 6) | ($b[1] >> 2); |
582
|
124
|
|
|
|
|
161
|
my $mon = (($b[1] & 0b11) << 2) | ($b[2] >> 6); |
583
|
124
|
|
|
|
|
126
|
my $day = (($b[2] & 0b111111) >> 1); |
584
|
124
|
|
|
|
|
135
|
my $hour = (($b[2] & 0b1) << 4) | ($b[3] >> 4); |
585
|
124
|
|
|
|
|
143
|
my $min = (($b[3] & 0b1111) << 2) | ($b[4] >> 6); |
586
|
124
|
|
|
|
|
135
|
my $sec = (($b[4] & 0b111111)); |
587
|
124
|
|
|
|
|
588
|
return sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $mon, $day, $hour, $min, $sec; |
588
|
|
|
|
|
|
|
} |
589
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
sub _parse_v2_date { |
591
|
0
|
|
|
0
|
|
0
|
my ($self, $date) = @_; |
592
|
0
|
0
|
0
|
|
|
0
|
return ($date && $date =~ $qr_date) ? "$1-$2-$3 $4:$5:$6$7" : ''; |
593
|
|
|
|
|
|
|
} |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
sub _master_key { |
596
|
13
|
|
|
13
|
|
26
|
my ($self, $pass, $head) = @_; |
597
|
13
|
|
|
|
|
16
|
my $file; |
598
|
13
|
50
|
|
|
|
40
|
($pass, $file) = @$pass if ref($pass) eq 'ARRAY'; |
599
|
13
|
50
|
33
|
|
|
213
|
$pass = sha256($pass) if defined($pass) && length($pass); |
600
|
13
|
50
|
|
|
|
36
|
if ($file) { |
601
|
0
|
0
|
|
|
|
0
|
$file = ref($file) ? $$file : $self->slurp($file); |
602
|
0
|
0
|
|
|
|
0
|
if (length($file) == 64) { |
|
|
0
|
|
|
|
|
|
603
|
0
|
|
|
|
|
0
|
$file = join '', map {chr hex} ($file =~ /\G([a-f0-9A-F]{2})/g); |
|
0
|
|
|
|
|
0
|
|
604
|
|
|
|
|
|
|
} elsif (length($file) != 32) { |
605
|
0
|
|
|
|
|
0
|
$file = sha256($file); |
606
|
|
|
|
|
|
|
} |
607
|
|
|
|
|
|
|
} |
608
|
0
|
|
|
|
|
0
|
my $key = (!$pass && !$file) ? die "One or both of password or key file must be passed\n" |
609
|
13
|
50
|
33
|
|
|
168
|
: ($head->{'version'} && $head->{'version'} eq '2') ? sha256(grep {$_} $pass, $file) |
|
|
50
|
33
|
|
|
|
|
|
|
50
|
33
|
|
|
|
|
|
|
50
|
|
|
|
|
|
610
|
|
|
|
|
|
|
: ($pass && $file) ? sha256($pass, $file) : $pass ? $pass : $file; |
611
|
13
|
|
66
|
|
|
46
|
$head->{'enc_iv'} ||= join '', map {chr rand 256} 1..16; |
|
64
|
|
|
|
|
128
|
|
612
|
13
|
50
|
33
|
|
|
88
|
$head->{'seed_rand'} ||= join '', map {chr rand 256} 1..($head->{'version'} && $head->{'version'} eq '2' ? 32 : 16); |
|
64
|
|
66
|
|
|
113
|
|
613
|
13
|
|
66
|
|
|
88
|
$head->{'seed_key'} ||= sha256(time.rand(2**32-1).$$); |
614
|
13
|
|
33
|
|
|
72
|
$head->{'rounds'} ||= $self->{'rounds'} || ($head->{'version'} && $head->{'version'} eq '2' ? 6_000 : 50_000); |
|
|
|
66
|
|
|
|
|
615
|
|
|
|
|
|
|
|
616
|
13
|
|
|
|
|
182
|
my $cipher = Crypt::Rijndael->new($head->{'seed_key'}, Crypt::Rijndael::MODE_ECB()); |
617
|
13
|
|
|
|
|
1533975
|
$key = $cipher->encrypt($key) for 1 .. $head->{'rounds'}; |
618
|
13
|
|
|
|
|
255
|
$key = sha256($key); |
619
|
13
|
|
|
|
|
124
|
$key = sha256($head->{'seed_rand'}, $key); |
620
|
13
|
|
|
|
|
137
|
return $key; |
621
|
|
|
|
|
|
|
} |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
sub gen_db { |
626
|
9
|
|
|
9
|
1
|
2425
|
my ($self, $pass, $head, $groups) = @_; |
627
|
9
|
|
100
|
|
|
41
|
$head ||= {}; |
628
|
9
|
|
33
|
|
|
34
|
$groups ||= $self->groups; |
629
|
9
|
50
|
|
|
|
23
|
local $self->{'keep_xml'} = $head->{'keep_xml'} if exists $head->{'keep_xml'}; |
630
|
9
|
|
66
|
|
|
41
|
my $v = $head->{'version'} || $self->{'version'}; |
631
|
9
|
|
33
|
|
|
78
|
my $reuse = $head->{'reuse_header'} # explicit yes |
632
|
|
|
|
|
|
|
|| (!exists($head->{'reuse_header'}) # not explicit no |
633
|
|
|
|
|
|
|
&& ($self->{'reuse_header'} # explicit yes |
634
|
|
|
|
|
|
|
|| !exists($self->{'reuse_header'}))); # not explicit no |
635
|
9
|
50
|
|
|
|
23
|
if ($reuse) { |
636
|
9
|
|
100
|
|
|
23
|
($head, my $args) = ($self->header || {}, $head); |
637
|
9
|
|
|
|
|
35
|
@$head{keys %$args} = values %$args; |
638
|
|
|
|
|
|
|
} |
639
|
9
|
|
50
|
|
|
52
|
$head->{'version'} = $v ||= $head->{'version'} || '1'; |
|
|
|
66
|
|
|
|
|
640
|
9
|
50
|
33
|
|
|
95
|
delete @$head{qw(enc_iv seed_key seed_rand protected_stream_key start_bytes)} if $reuse && $reuse < 0; |
641
|
|
|
|
|
|
|
|
642
|
9
|
100
|
|
|
|
37
|
die "Missing pass\n" if ! defined($pass); |
643
|
7
|
50
|
|
|
|
24
|
die "Please unlock before calling gen_db\n" if $self->is_locked($groups); |
644
|
|
|
|
|
|
|
|
645
|
7
|
50
|
|
|
|
127
|
srand(rand(time() ^ $$)) if ! $self->{'no_srand'}; |
646
|
7
|
50
|
|
|
|
19
|
if ($v eq '2') { |
647
|
0
|
|
|
|
|
0
|
return $self->_gen_v2_db($pass, $head, $groups); |
648
|
|
|
|
|
|
|
} else { |
649
|
7
|
|
|
|
|
25
|
return $self->_gen_v1_db($pass, $head, $groups); |
650
|
|
|
|
|
|
|
} |
651
|
|
|
|
|
|
|
} |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
sub _gen_v1_db { |
654
|
7
|
|
|
7
|
|
14
|
my ($self, $pass, $head, $groups) = @_; |
655
|
7
|
50
|
66
|
|
|
46
|
if ($head->{'sig2'} && $head->{'sig2'} eq DB_SIG_2_v2) { |
656
|
0
|
0
|
0
|
|
|
0
|
substr($head->{'seed_rand'}, 16, 16, '') if $head->{'seed_rand'} && length($head->{'seed_rand'}) == 32; # if coming from a v2 db use a smaller key (roundtripable) |
657
|
|
|
|
|
|
|
} |
658
|
7
|
|
|
|
|
29
|
my $key = $self->_master_key($pass, $head); |
659
|
7
|
|
|
|
|
24
|
my $buffer = ''; |
660
|
7
|
|
|
|
|
17
|
my $entries = ''; |
661
|
7
|
|
|
|
|
13
|
my %gid; |
662
|
|
|
|
|
|
|
my $gid = sub { # v1 groups id size can only be a 32 bit int - v2 is supposed to be a 16 digit string |
663
|
44
|
|
|
44
|
|
85
|
local $_ = my $gid = shift; |
664
|
44
|
|
66
|
|
|
479
|
return $gid{$gid} ||= do { |
665
|
23
|
0
|
33
|
|
|
172
|
$_ = (/^\d+$/ && $_ < 2**32) ? $_ : /^([a-f0-9]{16})/i ? hex($1) : int(rand 2**32); |
|
|
50
|
|
|
|
|
|
666
|
23
|
|
|
|
|
108
|
$_ = int(rand 2**32) while $gid{"\e$_\e"}++; |
667
|
23
|
|
|
|
|
221
|
$_; |
668
|
|
|
|
|
|
|
}; |
669
|
7
|
|
|
|
|
74
|
}; |
670
|
7
|
|
|
|
|
15
|
my %uniq; |
671
|
7
|
|
|
13
|
|
40
|
my $uuid = sub { return $self->uuid(shift, \%uniq) }; |
|
13
|
|
|
|
|
44
|
|
672
|
|
|
|
|
|
|
|
673
|
7
|
|
|
|
|
59
|
my @g = $self->find_groups({}, $groups); |
674
|
7
|
100
|
|
|
|
23
|
if (grep {$_->{'expanded'}} @g) { |
|
23
|
|
|
|
|
69
|
|
675
|
4
|
|
|
|
|
22
|
my $bin = pack 'L', scalar(@g); |
676
|
4
|
100
|
|
|
|
20
|
$bin .= pack('LC', $gid->($_->{'id'}), $_->{'expanded'} ? 1 : 0) for @g; |
677
|
4
|
|
66
|
|
|
38
|
my $e = ($self->find_entries({title => 'Meta-Info', username => 'SYSTEM', comment => 'KPX_GROUP_TREE_STATE', url => '$'}))[0] || $self->add_entry({ |
678
|
|
|
|
|
|
|
comment => 'KPX_GROUP_TREE_STATE', |
679
|
|
|
|
|
|
|
title => 'Meta-Info', |
680
|
|
|
|
|
|
|
username => 'SYSTEM', |
681
|
|
|
|
|
|
|
url => '$', |
682
|
|
|
|
|
|
|
id => '0000000000000000', |
683
|
|
|
|
|
|
|
group => $g[0], |
684
|
|
|
|
|
|
|
binary => {'bin-stream' => $bin}, |
685
|
|
|
|
|
|
|
}); |
686
|
|
|
|
|
|
|
} |
687
|
7
|
|
|
|
|
37
|
$head->{'n_groups'} = $head->{'n_entries'} = 0; |
688
|
7
|
|
|
|
|
16
|
foreach my $g (@g) { |
689
|
23
|
|
|
|
|
35
|
$head->{'n_groups'}++; |
690
|
23
|
|
33
|
|
|
60
|
my @d = ([1, pack('LL', 4, $gid->($g->{'id'}))], |
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
691
|
|
|
|
|
|
|
[2, pack('L', length($g->{'title'})+1)."$g->{'title'}\0"], |
692
|
|
|
|
|
|
|
[3, pack('L', 5). $self->_gen_v1_date($g->{'created'} || $self->now)], |
693
|
|
|
|
|
|
|
[4, pack('L', 5). $self->_gen_v1_date($g->{'modified'} || $self->now)], |
694
|
|
|
|
|
|
|
[5, pack('L', 5). $self->_gen_v1_date($g->{'accessed'} || $self->now)], |
695
|
|
|
|
|
|
|
[6, pack('L', 5). $self->_gen_v1_date($g->{'expires'} || $self->default_exp)], |
696
|
|
|
|
|
|
|
[7, pack('LL', 4, $g->{'icon'} || 0)], |
697
|
|
|
|
|
|
|
[8, pack('LS', 2, $g->{'level'} || 0)], |
698
|
|
|
|
|
|
|
[0xFFFF, pack('L', 0)]); |
699
|
0
|
0
|
|
|
|
0
|
push @d, [$_, map {pack('L',length $_).$_} $g->{'unknown'}->{$_}] |
|
0
|
|
|
|
|
0
|
|
700
|
23
|
50
|
|
|
|
54
|
for grep {/^\d+$/ && $_ > 8} keys %{ $g->{'unknown'} || {} }; |
|
23
|
|
|
|
|
139
|
|
701
|
23
|
|
|
|
|
90
|
$buffer .= pack('S',$_->[0]).$_->[1] for sort {$a->[0] <=> $b->[0]} @d; |
|
414
|
|
|
|
|
668
|
|
702
|
23
|
100
|
|
|
|
37
|
foreach my $e (@{ $g->{'entries'} || [] }) { |
|
23
|
|
|
|
|
139
|
|
703
|
13
|
|
|
|
|
24
|
$head->{'n_entries'}++; |
704
|
|
|
|
|
|
|
|
705
|
13
|
50
|
100
|
|
|
65
|
my $bins = $e->{'binary'} || {}; if (ref($bins) ne 'HASH') { warn "Entry binary field was not a hashref of name/content pairs.\n"; $bins = {} } |
|
13
|
|
|
|
|
52
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
706
|
13
|
|
|
|
|
39
|
my @bkeys = sort keys %$bins; |
707
|
13
|
50
|
|
|
|
32
|
warn "Found more than one entry in the binary hashref. Encoding only the first one of (@bkeys) on a version 1 database.\n" if @bkeys > 1; |
708
|
13
|
100
|
|
|
|
30
|
my $bname = @bkeys ? $bkeys[0] : ''; |
709
|
13
|
100
|
|
|
|
23
|
my $bin = $bins->{$bname}; $bin = '' if ! defined $bin; |
|
13
|
|
|
|
|
27
|
|
710
|
|
|
|
|
|
|
|
711
|
13
|
50
|
100
|
|
|
54
|
my $at = $e->{'auto_type'} || []; if (ref($at) ne 'ARRAY') { warn "Entry auto_type field was not an arrayref of auto_type info.\n"; $at = [] } |
|
13
|
|
|
|
|
33
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
712
|
13
|
|
|
|
|
17
|
my %AT; my @AT; |
713
|
13
|
|
|
|
|
26
|
for (@$at) { |
714
|
3
|
50
|
|
|
|
10
|
my ($k, $w) = map {defined($_) ? $_ : ''} @$_{qw(keys window)}; |
|
6
|
|
|
|
|
21
|
|
715
|
3
|
100
|
|
|
|
10
|
push @AT, $k if ! grep {$_ eq $k} @AT; |
|
2
|
|
|
|
|
8
|
|
716
|
3
|
|
|
|
|
5
|
push @{ $AT{$k} }, $w; |
|
3
|
|
|
|
|
13
|
|
717
|
|
|
|
|
|
|
} |
718
|
13
|
|
|
|
|
21
|
my $txt = ''; |
719
|
13
|
|
|
|
|
32
|
for my $i (1 .. @AT) { |
720
|
2
|
100
|
|
|
|
13
|
$txt .= "Auto-Type".($i>1 ? "-$i" : '').": $AT[$i-1]\n"; |
721
|
2
|
100
|
|
|
|
3
|
$txt .= "Auto-Type-Window".($i>1 ? "-$i" : '').": $_\n" for @{ $AT{$AT[$i-1]} }; |
|
2
|
|
|
|
|
20
|
|
722
|
|
|
|
|
|
|
} |
723
|
13
|
50
|
|
|
|
44
|
my $com = defined($e->{'comment'}) ? "$txt$e->{'comment'}" : $txt; |
724
|
13
|
|
50
|
|
|
39
|
my @d = ([1, pack('L', 16). $uuid->($e->{'id'})], |
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
725
|
|
|
|
|
|
|
[2, pack('LL', 4, $gid->($g->{'id'}))], |
726
|
|
|
|
|
|
|
[3, pack('LL', 4, $e->{'icon'} || 0)], |
727
|
|
|
|
|
|
|
[4, pack('L', length($e->{'title'})+1)."$e->{'title'}\0"], |
728
|
|
|
|
|
|
|
[5, pack('L', length($e->{'url'})+1). "$e->{'url'}\0"], |
729
|
|
|
|
|
|
|
[6, pack('L', length($e->{'username'})+1). "$e->{'username'}\0"], |
730
|
|
|
|
|
|
|
[7, pack('L', length($e->{'password'})+1). "$e->{'password'}\0"], |
731
|
|
|
|
|
|
|
[8, pack('L', length($com)+1). "$com\0"], |
732
|
|
|
|
|
|
|
[9, pack('L', 5). $self->_gen_v1_date($e->{'created'} || $self->now)], |
733
|
|
|
|
|
|
|
[0xA, pack('L', 5). $self->_gen_v1_date($e->{'modified'} || $self->now)], |
734
|
|
|
|
|
|
|
[0xB, pack('L', 5). $self->_gen_v1_date($e->{'accessed'} || $self->now)], |
735
|
|
|
|
|
|
|
[0xC, pack('L', 5). $self->_gen_v1_date($e->{'expires'} || $self->default_exp)], |
736
|
|
|
|
|
|
|
[0xD, pack('L', length($bname)+1)."$bname\0"], |
737
|
|
|
|
|
|
|
[0xE, pack('L', length($bin)).$bin], |
738
|
|
|
|
|
|
|
[0xFFFF, pack('L', 0)]); |
739
|
0
|
0
|
|
|
|
0
|
push @d, [$_, pack('L', length($e->{'unknown'}->{$_})).$e->{'unknown'}->{$_}] |
740
|
13
|
50
|
|
|
|
40
|
for grep {/^\d+$/ && $_ > 0xE} keys %{ $e->{'unknown'} || {} }; |
|
13
|
|
|
|
|
85
|
|
741
|
13
|
|
|
|
|
42
|
$entries .= pack('S',$_->[0]).$_->[1] for sort {$a->[0] <=> $b->[0]} @d; |
|
403
|
|
|
|
|
730
|
|
742
|
|
|
|
|
|
|
} |
743
|
|
|
|
|
|
|
} |
744
|
7
|
|
|
|
|
18
|
$buffer .= $entries; $entries = ''; |
|
7
|
|
|
|
|
10
|
|
745
|
|
|
|
|
|
|
|
746
|
7
|
|
|
|
|
1244
|
require utf8; |
747
|
7
|
|
|
|
|
32
|
utf8::downgrade($buffer); |
748
|
7
|
|
|
|
|
110
|
$head->{'checksum'} = sha256($buffer); |
749
|
|
|
|
|
|
|
|
750
|
7
|
|
|
|
|
28
|
return $self->_gen_v1_header($head) . $self->encrypt_rijndael_cbc($buffer, $key, $head->{'enc_iv'}); |
751
|
|
|
|
|
|
|
} |
752
|
|
|
|
|
|
|
|
753
|
|
|
|
|
|
|
sub _gen_v1_header { |
754
|
7
|
|
|
7
|
|
12
|
my ($self, $head) = @_; |
755
|
7
|
|
|
|
|
20
|
$head->{'sig1'} = DB_SIG_1; |
756
|
7
|
|
|
|
|
20
|
$head->{'sig2'} = DB_SIG_2_v1; |
757
|
7
|
|
|
|
|
23
|
$head->{'flags'} = DB_FLAG_RIJNDAEL; |
758
|
7
|
|
|
|
|
16
|
$head->{'ver'} = DB_VER_DW_V1; |
759
|
7
|
|
50
|
|
|
21
|
$head->{'n_groups'} ||= 0; |
760
|
7
|
|
100
|
|
|
25
|
$head->{'n_entries'} ||= 0; |
761
|
7
|
|
|
|
|
13
|
die "Length of $_ was not 32 (".length($head->{$_}).")\n" for grep {length($head->{$_}) != 32} qw(seed_key checksum); |
|
14
|
|
|
|
|
51
|
|
762
|
7
|
|
|
|
|
13
|
die "Length of $_ was not 16 (".length($head->{$_}).")\n" for grep {length($head->{$_}) != 16} qw(enc_iv seed_rand); |
|
14
|
|
|
|
|
51
|
|
763
|
7
|
|
|
|
|
42
|
my @f = qw(sig1 sig2 flags ver seed_rand enc_iv n_groups n_entries checksum seed_key rounds); |
764
|
7
|
|
|
|
|
10
|
my $t = 'L L L L a16 a16 L L a32 a32 L'; |
765
|
7
|
|
|
|
|
67
|
my $header = pack $t, @$head{@f}; |
766
|
7
|
50
|
|
|
|
26
|
die "Invalid generated header\n" if length($header) != DB_HEADSIZE_V1; |
767
|
7
|
|
|
|
|
45
|
return $header; |
768
|
|
|
|
|
|
|
} |
769
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
sub _gen_v1_date { |
771
|
144
|
|
|
144
|
|
224
|
my ($self, $date) = @_; |
772
|
144
|
50
|
|
|
|
241
|
return "\0\0\0\0\0" if ! $date; |
773
|
144
|
50
|
|
|
|
750
|
my ($year, $mon, $day, $hour, $min, $sec) = $date =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ ? ($1,$2,$3,$4,$5,$6) : die "Invalid date ($date)"; |
774
|
144
|
|
|
|
|
1534
|
return pack('C*', |
775
|
|
|
|
|
|
|
($year >> 6) & 0b111111, |
776
|
|
|
|
|
|
|
(($year & 0b111111) << 2) | (($mon >> 2) & 0b11), |
777
|
|
|
|
|
|
|
(($mon & 0b11) << 6) | (($day & 0b11111) << 1) | (($hour >> 4) & 0b1), |
778
|
|
|
|
|
|
|
(($hour & 0b1111) << 4) | (($min >> 2) & 0b1111), |
779
|
|
|
|
|
|
|
(($min & 0b11) << 6) | ($sec & 0b111111), |
780
|
|
|
|
|
|
|
); |
781
|
|
|
|
|
|
|
} |
782
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
sub _gen_v2_db { |
784
|
0
|
|
|
0
|
|
0
|
my ($self, $pass, $head, $groups) = @_; |
785
|
0
|
0
|
0
|
|
|
0
|
if ($head->{'sig2'} && $head->{'sig2'} eq DB_SIG_2_v1) { |
786
|
0
|
0
|
0
|
|
|
0
|
$head->{'seed_rand'} = $head->{'seed_rand'}x2 if $head->{'seed_rand'} && length($head->{'seed_rand'}) == 16; # if coming from a v1 db augment the key (roundtripable) |
787
|
|
|
|
|
|
|
} |
788
|
0
|
0
|
|
|
|
0
|
$head->{'compression'} = 1 if ! defined $head->{'compression'}; |
789
|
0
|
|
0
|
|
|
0
|
$head->{'start_bytes'} ||= join '', map {chr rand 256} 1 .. 32; |
|
0
|
|
|
|
|
0
|
|
790
|
0
|
|
0
|
|
|
0
|
$head->{'protected_stream_key'} ||= join '', map {chr rand 256} 1..32; |
|
0
|
|
|
|
|
0
|
|
791
|
0
|
|
|
|
|
0
|
my $key = $self->_master_key($pass, $head); |
792
|
0
|
|
|
|
|
0
|
my $header = $self->_gen_v2_header($head); |
793
|
|
|
|
|
|
|
|
794
|
0
|
|
|
|
|
0
|
my $buffer = ''; |
795
|
0
|
0
|
0
|
0
|
|
0
|
my $untri = sub { return (!defined($_[0]) && !$_[1]) ? 'null' : !$_[0] ? 'False' : 'True' }; |
|
0
|
0
|
|
|
|
0
|
|
796
|
0
|
|
|
|
|
0
|
my %uniq; |
797
|
0
|
0
|
0
|
0
|
|
0
|
my $uuid = sub { my $id = (defined($_[0]) && $_[0] eq '0') ? "\0"x16 : $self->uuid($_[0], \%uniq); return $self->encode_base64($id) }; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
798
|
|
|
|
|
|
|
|
799
|
0
|
|
|
|
|
0
|
my @mfld = qw(Generator HeaderHash DatabaseName DatabaseNameChanged DatabaseDescription DatabaseDescriptionChanged DefaultUserName DefaultUserNameChanged |
800
|
|
|
|
|
|
|
MaintenanceHistoryDays Color MasterKeyChanged MasterKeyChangeRec MasterKeyChangeForce MemoryProtection |
801
|
|
|
|
|
|
|
RecycleBinEnabled RecycleBinUUID RecycleBinChanged EntryTemplatesGroup EntryTemplatesGroupChanged HistoryMaxItems HistoryMaxSize |
802
|
|
|
|
|
|
|
LastSelectedGroup LastTopVisibleGroup Binaries CustomData); |
803
|
0
|
|
|
|
|
0
|
my $META = {__sort__ => \@mfld}; |
804
|
0
|
|
|
|
|
0
|
for my $key (@mfld) { |
805
|
0
|
|
|
|
|
0
|
(my $copy = $key) =~ s/([a-z])([A-Z])/${1}_${2}/g; |
806
|
0
|
|
|
|
|
0
|
$META->{$key} = $head->{lc $copy}; |
807
|
|
|
|
|
|
|
} |
808
|
|
|
|
|
|
|
my $def = sub { |
809
|
0
|
|
|
0
|
|
0
|
my ($k, $d, $r) = @_; |
810
|
0
|
0
|
0
|
|
|
0
|
$META->{$k} = $d if !defined($META->{$k}) || ($r and $META->{$k} !~ $r); |
|
|
|
0
|
|
|
|
|
811
|
0
|
0
|
|
|
|
0
|
$META->{$k} = $self->_gen_v2_date($META->{$k}) if $k =~ /Changed$/; |
812
|
0
|
|
|
|
|
0
|
}; |
813
|
0
|
|
|
|
|
0
|
my $now = $self->_gen_v2_date; |
814
|
0
|
|
|
|
|
0
|
$META->{'HeaderHash'} = $self->encode_base64(sha256($header)); |
815
|
0
|
|
|
|
|
0
|
$def->(Color => ''); |
816
|
0
|
|
|
|
|
0
|
$def->(DatabaseDescription => ''); |
817
|
0
|
|
|
|
|
0
|
$def->(DatabaseDescriptionChanged => $now, $qr_date); |
818
|
0
|
|
|
|
|
0
|
$def->(DatabaseName => ''); |
819
|
0
|
|
|
|
|
0
|
$def->(DatabaseNameChanged => $now, $qr_date); |
820
|
0
|
|
|
|
|
0
|
$def->(DefaultUserName => ''); |
821
|
0
|
|
|
|
|
0
|
$def->(DefaultUserNameChanged => $now, $qr_date); |
822
|
0
|
|
|
|
|
0
|
$def->(EntryTemplatesGroupChanged => $now, $qr_date); |
823
|
0
|
|
|
|
|
0
|
$def->(Generator => ref($self)); |
824
|
0
|
|
|
|
|
0
|
$def->(HistoryMaxItems => 10, qr{^\d+$}); |
825
|
0
|
|
|
|
|
0
|
$def->(HistoryMaxSize => 6291456, qr{^\d+$}); |
826
|
0
|
|
|
|
|
0
|
$def->(MaintenanceHistoryDays => 365, qr{^\d+$}); |
827
|
0
|
|
|
|
|
0
|
$def->(MasterKeyChangeForce => -1); |
828
|
0
|
|
|
|
|
0
|
$def->(MasterKeyChangeRec => -1); |
829
|
0
|
|
|
|
|
0
|
$def->(MasterKeyChanged => $now, $qr_date); |
830
|
0
|
|
|
|
|
0
|
$def->(RecycleBinChanged => $now, $qr_date); |
831
|
0
|
|
0
|
|
|
0
|
$META->{$_} = $uuid->($META->{$_} || 0) for qw(EntryTemplatesGroup LastSelectedGroup LastTopVisibleGroup RecycleBinUUID); |
832
|
0
|
0
|
|
|
|
0
|
$META->{'RecycleBinEnabled'} = $untri->(exists($META->{'RecycleBinEnabled'}) ? $META->{'RecycleBinEnabled'} : 1, 1); |
833
|
0
|
|
0
|
|
|
0
|
my $p = $META->{'MemoryProtection'} ||= {}; |
834
|
0
|
|
|
|
|
0
|
for my $new (qw(ProtectTitle ProtectUserName ProtectPassword ProtectURL ProtectNotes)) { # unflatten protection |
835
|
0
|
|
|
|
|
0
|
(my $key = lc $new) =~ s/protect/protect_/; |
836
|
0
|
|
|
|
|
0
|
push @{$p->{'__sort__'}}, $new; |
|
0
|
|
|
|
|
0
|
|
837
|
0
|
0
|
|
|
|
0
|
$p->{$new} = (exists($META->{$key}) ? delete($META->{$key}) : ($key eq 'protect_password')) ? 'True' : 'False'; |
|
|
0
|
|
|
|
|
|
838
|
|
|
|
|
|
|
} |
839
|
0
|
|
0
|
|
|
0
|
my $cd = $META->{'CustomData'} ||= {}; |
840
|
0
|
0
|
0
|
|
|
0
|
$META->{'CustomData'} = {Item => [map {{Key => $_, Value => $cd->{$_}}} sort keys %$cd]} if ref($cd) eq 'HASH' && scalar keys %$cd; |
|
0
|
|
|
|
|
0
|
|
841
|
|
|
|
|
|
|
|
842
|
0
|
|
|
|
|
0
|
my @GROUPS; |
843
|
0
|
|
|
|
|
0
|
my $BIN = $META->{'Binaries'}->{'Binary'} = []; |
844
|
0
|
|
|
|
|
0
|
my @PROTECT_BIN; |
845
|
|
|
|
|
|
|
my @PROTECT_STR; |
846
|
0
|
|
|
|
|
0
|
my $data = { |
847
|
|
|
|
|
|
|
Meta => $META, |
848
|
|
|
|
|
|
|
Root => { |
849
|
|
|
|
|
|
|
__sort__ => [qw(Group DeletedObjects)], |
850
|
|
|
|
|
|
|
Group => \@GROUPS, |
851
|
|
|
|
|
|
|
DeletedObjects => undef, |
852
|
|
|
|
|
|
|
}, |
853
|
|
|
|
|
|
|
}; |
854
|
|
|
|
|
|
|
|
855
|
0
|
|
|
|
|
0
|
my $gen_entry; $gen_entry = sub { |
856
|
0
|
|
|
0
|
|
0
|
my ($e, $parent) = @_; |
857
|
0
|
0
|
0
|
|
|
0
|
push @$parent, my $E = { |
|
|
0
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
858
|
|
|
|
|
|
|
__sort__ => [qw(UUID IconID ForegroundColor BackgroundColor OverrideURL Tags Times String AutoType History)], |
859
|
|
|
|
|
|
|
UUID => $uuid->($e->{'id'}), |
860
|
|
|
|
|
|
|
IconID => $e->{'icon'} || 0, |
861
|
|
|
|
|
|
|
Times => { |
862
|
|
|
|
|
|
|
__sort__ => [qw(LastModificationTime CreationTime LastAccessTime ExpiryTime Expires UsageCount LocationChanged)], |
863
|
|
|
|
|
|
|
Expires => $untri->($e->{'expires_enabled'}, 1), |
864
|
|
|
|
|
|
|
UsageCount => $e->{'usage_count'} || 0, |
865
|
|
|
|
|
|
|
LastAccessTime => $self->_gen_v2_date($e->{'accessed'}), |
866
|
|
|
|
|
|
|
ExpiryTime => $self->_gen_v2_date($e->{'expires'} || $self->default_exp), |
867
|
|
|
|
|
|
|
CreationTime => $self->_gen_v2_date($e->{'created'}), |
868
|
|
|
|
|
|
|
LastModificationTime => $self->_gen_v2_date($e->{'modified'}), |
869
|
|
|
|
|
|
|
LocationChanged => $self->_gen_v2_date($e->{'location_changed'}), |
870
|
|
|
|
|
|
|
}, |
871
|
|
|
|
|
|
|
Tags => $e->{'tags'}, |
872
|
|
|
|
|
|
|
BackgroundColor => $e->{'background_color'}, |
873
|
|
|
|
|
|
|
ForegroundColor => $e->{'foreground_color'}, |
874
|
|
|
|
|
|
|
CustomIconUUID => $uuid->($e->{'custom_icon_uuid'} || 0), |
875
|
|
|
|
|
|
|
OverrideURL => $e->{'override_url'}, |
876
|
|
|
|
|
|
|
AutoType => { |
877
|
|
|
|
|
|
|
Enabled => $untri->(exists($e->{'auto_type_enabled'}) ? $e->{'auto_type_enabled'} : 1, 1), |
878
|
|
|
|
|
|
|
DataTransferObfuscation => $e->{'auto_type_munge'} ? 1 : 0, |
879
|
|
|
|
|
|
|
}, |
880
|
|
|
|
|
|
|
}; |
881
|
0
|
0
|
|
|
|
0
|
foreach my $key (sort(keys %{ $e->{'strings'} || {} }), qw(Notes Password Title URL UserName)) { |
|
0
|
|
|
|
|
0
|
|
882
|
0
|
0
|
|
|
|
0
|
my $val = ($key eq 'Notes') ? $e->{'comment'} : ($key=~/^(Password|Title|URL|UserName)$/) ? $e->{lc $key} : $e->{'strings'}->{$key}; |
|
|
0
|
|
|
|
|
|
883
|
0
|
0
|
|
|
|
0
|
next if ! defined $val; |
884
|
0
|
|
|
|
|
0
|
push @{ $E->{'String'} }, my $s = { |
|
0
|
|
|
|
|
0
|
|
885
|
|
|
|
|
|
|
Key => $key, |
886
|
|
|
|
|
|
|
Value => $val, |
887
|
|
|
|
|
|
|
}; |
888
|
0
|
0
|
0
|
|
|
0
|
if (($META->{'MemoryProtection'}->{"Protect${key}"} || '') eq 'True' |
|
|
0
|
0
|
|
|
|
|
889
|
|
|
|
|
|
|
|| $e->{'protected'}->{$key =~ /^(Password|UserName|URL|Notes|Title)$/ ? lc($key) : $key}) { |
890
|
0
|
|
|
|
|
0
|
$s->{'Value'} = {Protected => 'True', content => $val}; |
891
|
0
|
0
|
|
|
|
0
|
push @PROTECT_STR, \$s->{'Value'}->{'content'} if length $s->{'Value'}->{'content'}; |
892
|
|
|
|
|
|
|
} |
893
|
|
|
|
|
|
|
} |
894
|
0
|
0
|
|
|
|
0
|
foreach my $at (@{ $e->{'auto_type'} || [] }) { |
|
0
|
|
|
|
|
0
|
|
895
|
0
|
|
|
|
|
0
|
push @{ $E->{'AutoType'}->{'Association'} }, { |
|
0
|
|
|
|
|
0
|
|
896
|
|
|
|
|
|
|
Window => $at->{'window'}, |
897
|
|
|
|
|
|
|
KeystrokeSequence => $at->{'keys'}, |
898
|
|
|
|
|
|
|
}; |
899
|
|
|
|
|
|
|
} |
900
|
0
|
0
|
0
|
|
|
0
|
my $bin = $e->{'binary'} || {}; $bin = {__anon__ => $bin} if ref($bin) ne 'HASH'; |
|
0
|
|
|
|
|
0
|
|
901
|
0
|
0
|
|
|
|
0
|
splice @{ $E->{'__sort__'} }, -2, 0, 'Binary' if scalar keys %$bin; |
|
0
|
|
|
|
|
0
|
|
902
|
0
|
|
|
|
|
0
|
foreach my $key (sort keys %$bin) { |
903
|
0
|
0
|
|
|
|
0
|
push @$BIN, my $b = { |
904
|
|
|
|
|
|
|
__attr__ => [qw(ID Compressed)], |
905
|
|
|
|
|
|
|
ID => $#$BIN+1, |
906
|
|
|
|
|
|
|
content => defined($bin->{$key}) ? $bin->{$key} : '', |
907
|
|
|
|
|
|
|
}; |
908
|
0
|
0
|
0
|
|
|
0
|
$b->{'Compressed'} = (length($b->{'content'}) < 100 || $self->{'no_binary_compress'}) ? 'False' : 'True'; |
909
|
0
|
0
|
|
|
|
0
|
if ($b->{'Compressed'} eq 'True') { |
910
|
0
|
0
|
|
|
|
0
|
eval { $b->{'content'} = $self->compress($b->{'content'}) } or warn "Could not compress associated binary ($b->{'ID'}): $@"; |
|
0
|
|
|
|
|
0
|
|
911
|
|
|
|
|
|
|
} |
912
|
0
|
|
|
|
|
0
|
$b->{'content'} = $self->encode_base64($b->{'content'}); |
913
|
0
|
|
|
|
|
0
|
push @{ $E->{'Binary'} }, {Key => $key, Value => {__attr__ => [qw(Ref)], Ref => $b->{'ID'}, content => ''}}; |
|
0
|
|
|
|
|
0
|
|
914
|
|
|
|
|
|
|
} |
915
|
0
|
0
|
|
|
|
0
|
foreach my $h (@{ $e->{'history'}||[] }) { |
|
0
|
|
|
|
|
0
|
|
916
|
0
|
|
0
|
|
|
0
|
$gen_entry->($h, $E->{'History'}->{'Entry'} ||= []); |
917
|
|
|
|
|
|
|
} |
918
|
0
|
|
|
|
|
0
|
}; |
919
|
|
|
|
|
|
|
|
920
|
0
|
|
|
|
|
0
|
my $rec; $rec = sub { |
921
|
0
|
|
|
0
|
|
0
|
my ($group, $parent) = @_; |
922
|
0
|
0
|
|
|
|
0
|
return if ref($group) ne 'HASH'; |
923
|
0
|
0
|
0
|
|
|
0
|
push @$parent, my $G = { |
|
|
0
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
924
|
|
|
|
|
|
|
__sort__ => [qw(UUID Name Notes IconID Times IsExpanded DefaultAutoTypeSequence EnableAutoType EnableSearching LastTopVisibleEntry)], |
925
|
|
|
|
|
|
|
UUID => $uuid->($group->{'id'}), |
926
|
|
|
|
|
|
|
Name => $group->{'title'} || '', |
927
|
|
|
|
|
|
|
Notes => $group->{'notes'}, |
928
|
|
|
|
|
|
|
IconID => $group->{'icon'} || 0, |
929
|
|
|
|
|
|
|
Times => { |
930
|
|
|
|
|
|
|
__sort__ => [qw(LastModificationTime CreationTime LastAccessTime ExpiryTime Expires UsageCount LocationChanged)], |
931
|
|
|
|
|
|
|
Expires => $untri->($group->{'expires_enabled'}, 1), |
932
|
|
|
|
|
|
|
UsageCount => $group->{'usage_count'} || 0, |
933
|
|
|
|
|
|
|
LastAccessTime => $self->_gen_v2_date($group->{'accessed'}), |
934
|
|
|
|
|
|
|
ExpiryTime => $self->_gen_v2_date($group->{'expires'} || $self->default_exp), |
935
|
|
|
|
|
|
|
CreationTime => $self->_gen_v2_date($group->{'created'}), |
936
|
|
|
|
|
|
|
LastModificationTime => $self->_gen_v2_date($group->{'modified'}), |
937
|
|
|
|
|
|
|
LocationChanged => $self->_gen_v2_date($group->{'location_changed'}), |
938
|
|
|
|
|
|
|
}, |
939
|
|
|
|
|
|
|
IsExpanded => $untri->($group->{'expanded'}, 1), |
940
|
|
|
|
|
|
|
DefaultAutoTypeSequence => $group->{'auto_type_default'}, |
941
|
|
|
|
|
|
|
EnableAutoType => lc($untri->(exists($group->{'auto_type_enabled'}) ? $group->{'auto_type_enabled'} : 1)), |
942
|
|
|
|
|
|
|
EnableSearching => lc($untri->(exists($group->{'enable_searching'}) ? $group->{'enable_searching'} : 1)), |
943
|
|
|
|
|
|
|
LastTopVisibleEntry => $uuid->($group->{'last_top_entry'} || 0), |
944
|
|
|
|
|
|
|
}; |
945
|
0
|
0
|
|
|
|
0
|
$G->{'CustomIconUUID'} = $uuid->($group->{'custom_icon_uuid'}) if $group->{'custom_icon_uuid'}; # TODO |
946
|
0
|
0
|
|
|
|
0
|
push @{$G->{'__sort__'}}, 'Entry' if @{ $group->{'entries'} || [] }; |
|
0
|
0
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
947
|
0
|
0
|
|
|
|
0
|
foreach my $e (@{ $group->{'entries'} || [] }) { |
|
0
|
|
|
|
|
0
|
|
948
|
0
|
|
0
|
|
|
0
|
$gen_entry->($e, $G->{'Entry'} ||= []); |
949
|
|
|
|
|
|
|
} |
950
|
0
|
0
|
|
|
|
0
|
push @{$G->{'__sort__'}}, 'Group' if @{ $group->{'groups'} || [] }; |
|
0
|
0
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
951
|
0
|
0
|
0
|
|
|
0
|
$rec->($_, $G->{'Group'} ||= []) for @{ $group->{'groups'} || []}; |
|
0
|
|
|
|
|
0
|
|
952
|
0
|
|
|
|
|
0
|
}; |
953
|
0
|
0
|
|
|
|
0
|
$groups = [{title => "Database", groups => [@$groups], notes => "Added as a top group by File::KeePass", expanded => 1}] if @$groups > 1; |
954
|
0
|
|
|
|
|
0
|
$rec->($_, \@GROUPS) for @$groups; |
955
|
|
|
|
|
|
|
|
956
|
0
|
0
|
0
|
|
|
0
|
if (@$groups && $groups->[0]->{'deleted_objects'}) { |
957
|
0
|
|
|
|
|
0
|
foreach my $dob (@{ $groups->[0]->{'deleted_objects'} }) { |
|
0
|
|
|
|
|
0
|
|
958
|
0
|
|
|
|
|
0
|
push @{ $data->{'Root'}->{'DeletedObjects'}->{'DeletedObject'} }, { |
|
0
|
|
|
|
|
0
|
|
959
|
|
|
|
|
|
|
UUID => $self->encode_base64($dob->{'uuid'}), |
960
|
|
|
|
|
|
|
DeletionTime => $self->_gen_v2_date($dob->{'date'}), |
961
|
|
|
|
|
|
|
} |
962
|
|
|
|
|
|
|
} |
963
|
|
|
|
|
|
|
} |
964
|
|
|
|
|
|
|
|
965
|
0
|
|
|
|
|
0
|
my $s20_stream = $self->salsa20_stream({key => sha256($head->{'protected_stream_key'}), iv => $salsa20_iv, rounds => 20}); |
966
|
0
|
|
|
|
|
0
|
for my $ref (@PROTECT_BIN, @PROTECT_STR) { |
967
|
0
|
|
|
|
|
0
|
$$ref = $self->encode_base64($s20_stream->($$ref)); |
968
|
|
|
|
|
|
|
} |
969
|
|
|
|
|
|
|
|
970
|
|
|
|
|
|
|
# gen the XML - use our own generator since XML::Simple does not do event based actions |
971
|
0
|
|
|
|
|
0
|
$buffer = $self->gen_xml($data, { |
972
|
|
|
|
|
|
|
top => 'KeePassFile', |
973
|
|
|
|
|
|
|
indent => "\t", |
974
|
|
|
|
|
|
|
declaration => '', |
975
|
|
|
|
|
|
|
sort => { |
976
|
|
|
|
|
|
|
AutoType => [qw(Enabled DataTransferObfuscation Association)], |
977
|
|
|
|
|
|
|
Association => [qw(Window KeystrokeSequence)], |
978
|
|
|
|
|
|
|
DeletedObject => [qw(UUID DeletionTime)], |
979
|
|
|
|
|
|
|
}, |
980
|
|
|
|
|
|
|
no_trailing_newline => 1, |
981
|
|
|
|
|
|
|
}); |
982
|
0
|
0
|
0
|
|
|
0
|
$self->{'xml_out'} = $buffer if $self->{'keep_xml'} || $head->{'keep_xml'}; |
983
|
|
|
|
|
|
|
|
984
|
0
|
0
|
|
|
|
0
|
$buffer = $self->compress($buffer) if $head->{'compression'} eq '1'; |
985
|
0
|
|
|
|
|
0
|
$buffer = $self->chunksum($buffer); |
986
|
|
|
|
|
|
|
|
987
|
0
|
|
|
|
|
0
|
substr $buffer, 0, 0, $head->{'start_bytes'}; |
988
|
|
|
|
|
|
|
|
989
|
0
|
|
|
|
|
0
|
return $header . $self->encrypt_rijndael_cbc($buffer, $key, $head->{'enc_iv'}); |
990
|
|
|
|
|
|
|
} |
991
|
|
|
|
|
|
|
|
992
|
|
|
|
|
|
|
sub _gen_v2_date { |
993
|
0
|
|
|
0
|
|
0
|
my ($self, $date) = @_; |
994
|
0
|
0
|
0
|
|
|
0
|
$date = $self->now($date) if !$date || $date =~ /^\d+$/; |
995
|
0
|
0
|
|
|
|
0
|
my ($year, $mon, $day, $hour, $min, $sec) = $date =~ $qr_date ? ($1,$2,$3,$4,$5,$6) : die "Invalid date ($date)"; |
996
|
0
|
|
|
|
|
0
|
return "${year}-${mon}-${day}T${hour}:${min}:${sec}Z"; |
997
|
|
|
|
|
|
|
} |
998
|
|
|
|
|
|
|
|
999
|
|
|
|
|
|
|
sub _gen_v2_header { |
1000
|
0
|
|
|
0
|
|
0
|
my ($self, $head) = @_; |
1001
|
0
|
|
|
|
|
0
|
$head->{'sig1'} = DB_SIG_1; |
1002
|
0
|
|
|
|
|
0
|
$head->{'sig2'} = DB_SIG_2_v2; |
1003
|
0
|
|
|
|
|
0
|
$head->{'ver'} = DB_VER_DW_V2; |
1004
|
0
|
0
|
|
|
|
0
|
$head->{'comment'} = '' if ! defined $head->{'comment'}; |
1005
|
0
|
0
|
0
|
|
|
0
|
$head->{'compression'} = (!defined($head->{'compression'}) || $head->{'compression'} eq '1') ? 1 : 0; |
1006
|
0
|
|
0
|
|
|
0
|
$head->{'0'} ||= "\r\n\r\n"; |
1007
|
0
|
|
0
|
|
|
0
|
$head->{'protected_stream_key'} ||= join '', map {chr rand 256} 1..32; |
|
0
|
|
|
|
|
0
|
|
1008
|
0
|
0
|
|
|
|
0
|
die "Missing start_bytes\n" if ! $head->{'start_bytes'}; |
1009
|
0
|
|
|
|
|
0
|
die "Length of $_ was not 32 (".length($head->{$_}).")\n" for grep {length($head->{$_}) != 32} qw(seed_rand seed_key protected_stream_key start_bytes); |
|
0
|
|
|
|
|
0
|
|
1010
|
0
|
0
|
|
|
|
0
|
die "Length of enc_iv was not 16\n" if length($head->{'enc_iv'}) != 16; |
1011
|
|
|
|
|
|
|
|
1012
|
0
|
|
|
|
|
0
|
my $buffer = pack 'L3', @$head{qw(sig1 sig2 ver)}; |
1013
|
|
|
|
|
|
|
|
1014
|
0
|
|
|
0
|
|
0
|
my $pack = sub { my ($type, $str) = @_; $buffer .= pack('C S', $type, length($str)) . $str }; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
1015
|
0
|
0
|
0
|
|
|
0
|
$pack->(1, $head->{'comment'}) if defined($head->{'comment'}) && length($head->{'comment'}); |
1016
|
0
|
|
|
|
|
0
|
$pack->(2, "\x31\xc1\xf2\xe6\xbf\x71\x43\x50\xbe\x58\x05\x21\x6a\xfc\x5a\xff"); # aes cipher |
1017
|
0
|
0
|
|
|
|
0
|
$pack->(3, pack 'V', $head->{'compression'} ? 1 : 0); |
1018
|
0
|
|
|
|
|
0
|
$pack->(4, $head->{'seed_rand'}); |
1019
|
0
|
|
|
|
|
0
|
$pack->(5, $head->{'seed_key'}); |
1020
|
0
|
|
|
|
|
0
|
$pack->(6, pack 'LL', $head->{'rounds'}, 0); # a little odd to be double the length but not used |
1021
|
0
|
|
|
|
|
0
|
$pack->(7, $head->{'enc_iv'}); |
1022
|
0
|
|
|
|
|
0
|
$pack->(8, $head->{'protected_stream_key'}); |
1023
|
0
|
|
|
|
|
0
|
$pack->(9, $head->{'start_bytes'}); |
1024
|
0
|
|
|
|
|
0
|
$pack->(10, pack('V', 2)); # salsa20 protection |
1025
|
0
|
|
|
|
|
0
|
$pack->(0, $head->{'0'}); |
1026
|
0
|
|
|
|
|
0
|
return $buffer; |
1027
|
|
|
|
|
|
|
} |
1028
|
|
|
|
|
|
|
|
1029
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
1030
|
|
|
|
|
|
|
|
1031
|
|
|
|
|
|
|
sub slurp { |
1032
|
1
|
|
|
1
|
0
|
2
|
my ($self, $file) = @_; |
1033
|
1
|
50
|
|
|
|
48
|
open my $fh, '<', $file or die "Could not open $file: $!\n"; |
1034
|
1
|
|
50
|
|
|
15
|
my $size = -s $file || die "File $file appears to be empty.\n"; |
1035
|
1
|
|
|
|
|
3
|
binmode $fh; |
1036
|
1
|
|
|
|
|
32
|
read($fh, my $buffer, $size); |
1037
|
1
|
|
|
|
|
12
|
close $fh; |
1038
|
1
|
50
|
|
|
|
5
|
die "Could not read entire file contents of $file.\n" if length($buffer) != $size; |
1039
|
1
|
|
|
|
|
5
|
return $buffer; |
1040
|
|
|
|
|
|
|
} |
1041
|
|
|
|
|
|
|
|
1042
|
|
|
|
|
|
|
sub decrypt_rijndael_cbc { |
1043
|
11
|
|
|
11
|
1
|
33
|
my ($self, $buffer, $key, $enc_iv) = @_; |
1044
|
|
|
|
|
|
|
#use Crypt::CBC; return Crypt::CBC->new(-cipher => 'Rijndael', -key => $key, -iv => $enc_iv, -regenerate_key => 0, -prepend_iv => 0)->decrypt($buffer); |
1045
|
11
|
|
|
|
|
150
|
my $cipher = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_CBC()); |
1046
|
11
|
|
|
|
|
40
|
$cipher->set_iv($enc_iv); |
1047
|
11
|
|
|
|
|
162
|
$buffer = $cipher->decrypt($buffer); |
1048
|
11
|
|
|
|
|
36
|
my $extra = ord(substr $buffer, -1, 1); |
1049
|
11
|
|
|
|
|
32
|
substr($buffer, length($buffer) - $extra, $extra, ''); |
1050
|
11
|
|
|
|
|
60
|
return $buffer; |
1051
|
|
|
|
|
|
|
} |
1052
|
|
|
|
|
|
|
|
1053
|
|
|
|
|
|
|
sub encrypt_rijndael_cbc { |
1054
|
13
|
|
|
13
|
1
|
34
|
my ($self, $buffer, $key, $enc_iv) = @_; |
1055
|
|
|
|
|
|
|
#use Crypt::CBC; return Crypt::CBC->new(-cipher => 'Rijndael', -key => $key, -iv => $enc_iv, -regenerate_key => 0, -prepend_iv => 0)->encrypt($buffer); |
1056
|
13
|
|
|
|
|
187
|
my $cipher = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_CBC()); |
1057
|
13
|
|
|
|
|
48
|
$cipher->set_iv($enc_iv); |
1058
|
13
|
|
50
|
|
|
48
|
my $extra = (16 - length($buffer) % 16) || 16; # always pad so we can always trim |
1059
|
13
|
|
|
|
|
92
|
$buffer .= chr($extra) for 1 .. $extra; |
1060
|
13
|
|
|
|
|
402
|
return $cipher->encrypt($buffer); |
1061
|
|
|
|
|
|
|
} |
1062
|
|
|
|
|
|
|
|
1063
|
|
|
|
|
|
|
sub unchunksum { |
1064
|
0
|
|
|
0
|
1
|
0
|
my ($self, $buffer) = @_; |
1065
|
0
|
|
|
|
|
0
|
my ($new, $pos) = ('', 0); |
1066
|
0
|
|
|
|
|
0
|
while ($pos < length($buffer)) { |
1067
|
0
|
|
|
|
|
0
|
my ($index, $hash, $size) = unpack "\@$pos L a32 i", $buffer; |
1068
|
0
|
|
|
|
|
0
|
$pos += 40; |
1069
|
0
|
0
|
|
|
|
0
|
if ($size == 0) { |
1070
|
0
|
0
|
|
|
|
0
|
warn "Found mismatch for 0 chunksize\n" if $hash ne "\0"x32; |
1071
|
0
|
|
|
|
|
0
|
last; |
1072
|
|
|
|
|
|
|
} |
1073
|
|
|
|
|
|
|
#print "$index $hash $size\n"; |
1074
|
0
|
|
|
|
|
0
|
my $chunk = substr $buffer, $pos, $size; |
1075
|
0
|
0
|
|
|
|
0
|
die "Chunk hash of index $index did not match\n" if $hash ne sha256($chunk); |
1076
|
0
|
|
|
|
|
0
|
$pos += $size; |
1077
|
0
|
|
|
|
|
0
|
$new .= $chunk; |
1078
|
|
|
|
|
|
|
} |
1079
|
0
|
|
|
|
|
0
|
return $new; |
1080
|
|
|
|
|
|
|
} |
1081
|
|
|
|
|
|
|
|
1082
|
|
|
|
|
|
|
sub chunksum { |
1083
|
0
|
|
|
0
|
0
|
0
|
my ($self, $buffer) = @_; |
1084
|
0
|
|
|
|
|
0
|
my $new; |
1085
|
0
|
|
|
|
|
0
|
my $index = 0; |
1086
|
0
|
|
|
|
|
0
|
my $chunk_size = 8192; |
1087
|
0
|
|
|
|
|
0
|
my $pos = 0; |
1088
|
0
|
|
|
|
|
0
|
while ($pos < length($buffer)) { |
1089
|
0
|
|
|
|
|
0
|
my $chunk = substr($buffer, $pos, $chunk_size); |
1090
|
0
|
|
|
|
|
0
|
$new .= pack "L a32 i", $index++, sha256($chunk), length($chunk); |
1091
|
0
|
|
|
|
|
0
|
$new .= $chunk; |
1092
|
0
|
|
|
|
|
0
|
$pos += length($chunk); |
1093
|
|
|
|
|
|
|
} |
1094
|
0
|
|
|
|
|
0
|
$new .= pack "L a32 i", $index++, "\0"x32, 0; |
1095
|
0
|
|
|
|
|
0
|
return $new; |
1096
|
|
|
|
|
|
|
} |
1097
|
|
|
|
|
|
|
|
1098
|
|
|
|
|
|
|
sub decompress { |
1099
|
0
|
|
|
0
|
1
|
0
|
my ($self, $buffer) = @_; |
1100
|
0
|
0
|
|
|
|
0
|
eval { require Compress::Raw::Zlib } or die "Cannot load compression library to decompress database: $@"; |
|
0
|
|
|
|
|
0
|
|
1101
|
0
|
|
|
|
|
0
|
my ($i, $status) = Compress::Raw::Zlib::Inflate->new(-WindowBits => 31); |
1102
|
0
|
0
|
|
|
|
0
|
die "Failed to initialize inflator ($status)\n" if $status != Compress::Raw::Zlib::Z_OK(); |
1103
|
0
|
|
|
|
|
0
|
$status = $i->inflate($buffer, my $out); |
1104
|
0
|
0
|
|
|
|
0
|
die "Failed to uncompress buffer ($status)\n" if $status != Compress::Raw::Zlib::Z_STREAM_END(); |
1105
|
0
|
|
|
|
|
0
|
return $out; |
1106
|
|
|
|
|
|
|
} |
1107
|
|
|
|
|
|
|
|
1108
|
|
|
|
|
|
|
sub compress { |
1109
|
0
|
|
|
0
|
1
|
0
|
my ($self, $buffer) = @_; |
1110
|
0
|
0
|
|
|
|
0
|
eval { require Compress::Raw::Zlib } or die "Cannot load compression library to compress database: $@"; |
|
0
|
|
|
|
|
0
|
|
1111
|
0
|
|
|
|
|
0
|
my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1); |
1112
|
0
|
0
|
|
|
|
0
|
die "Failed to initialize inflator ($status)\n" if $status != Compress::Raw::Zlib::Z_OK(); |
1113
|
0
|
|
|
|
|
0
|
$status = $d->deflate($buffer, my $out); |
1114
|
0
|
0
|
|
|
|
0
|
die "Failed to compress buffer ($status)\n" if $status != Compress::Raw::Zlib::Z_OK(); |
1115
|
0
|
|
|
|
|
0
|
$status = $d->flush($out); |
1116
|
0
|
0
|
|
|
|
0
|
die "Failed to compress buffer ($status).\n" if $status != Compress::Raw::Zlib::Z_OK(); |
1117
|
0
|
|
|
|
|
0
|
return $out; |
1118
|
|
|
|
|
|
|
} |
1119
|
|
|
|
|
|
|
|
1120
|
|
|
|
|
|
|
sub decode_base64 { |
1121
|
0
|
|
|
0
|
1
|
0
|
my ($self, $content) = @_; |
1122
|
0
|
0
|
|
|
|
0
|
eval { require MIME::Base64 } or die "Cannot load Base64 library to decode item: $@"; |
|
0
|
|
|
|
|
0
|
|
1123
|
0
|
|
|
|
|
0
|
return MIME::Base64::decode_base64($content); |
1124
|
|
|
|
|
|
|
} |
1125
|
|
|
|
|
|
|
|
1126
|
|
|
|
|
|
|
sub encode_base64 { |
1127
|
4
|
|
|
4
|
1
|
5
|
my ($self, $content) = @_; |
1128
|
4
|
50
|
|
|
|
6
|
eval { require MIME::Base64 } or die "Cannot load Base64 library to encode item: $@"; |
|
4
|
|
|
|
|
1086
|
|
1129
|
4
|
|
|
|
|
1066
|
($content = MIME::Base64::encode_base64($content)) =~ s/\n//g; |
1130
|
4
|
|
|
|
|
13
|
return $content; |
1131
|
|
|
|
|
|
|
} |
1132
|
|
|
|
|
|
|
|
1133
|
|
|
|
|
|
|
sub parse_xml { |
1134
|
0
|
|
|
0
|
1
|
0
|
my ($self, $buffer, $args) = @_; |
1135
|
0
|
0
|
|
|
|
0
|
eval { require XML::Parser } or die "Cannot load XML library to parse database: $@"; |
|
0
|
|
|
|
|
0
|
|
1136
|
0
|
|
|
|
|
0
|
my $top = $args->{'top'}; |
1137
|
0
|
|
0
|
|
|
0
|
my $force_array = $args->{'force_array'} || {}; |
1138
|
0
|
|
0
|
|
|
0
|
my $s_handlers = $args->{'start_handlers'} || {}; |
1139
|
0
|
|
0
|
|
|
0
|
my $e_handlers = $args->{'end_handlers'} || $args->{'handlers'} || {}; |
1140
|
0
|
|
|
|
|
0
|
my $data; |
1141
|
|
|
|
|
|
|
my $ptr; |
1142
|
|
|
|
|
|
|
my $x = XML::Parser->new(Handlers => { |
1143
|
|
|
|
|
|
|
Start => sub { |
1144
|
0
|
|
|
0
|
|
0
|
my ($x, $tag, %attr) = @_; # loses multiple values of duplicately named attrs |
1145
|
0
|
|
|
|
|
0
|
my $prev_ptr = $ptr; |
1146
|
0
|
0
|
|
|
|
0
|
$top = $tag if !defined $top; |
1147
|
0
|
0
|
0
|
|
|
0
|
if ($tag eq $top) { |
|
|
0
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
1148
|
0
|
0
|
0
|
|
|
0
|
die "The $top tag should only be used at the top level.\n" if $ptr || $data; |
1149
|
0
|
|
|
|
|
0
|
$ptr = $data = {}; |
1150
|
|
|
|
|
|
|
} elsif (exists($prev_ptr->{$tag}) || ($force_array->{$tag} and $prev_ptr->{$tag} ||= [])) { |
1151
|
0
|
0
|
|
|
|
0
|
$prev_ptr->{$tag} = [$prev_ptr->{$tag}] if 'ARRAY' ne ref $prev_ptr->{$tag}; |
1152
|
0
|
|
|
|
|
0
|
push @{ $prev_ptr->{$tag} }, ($ptr = {}); |
|
0
|
|
|
|
|
0
|
|
1153
|
|
|
|
|
|
|
} else { |
1154
|
0
|
|
0
|
|
|
0
|
$ptr = $prev_ptr->{$tag} ||= {}; |
1155
|
|
|
|
|
|
|
} |
1156
|
0
|
|
|
|
|
0
|
@$ptr{keys %attr} = values %attr; |
1157
|
0
|
0
|
0
|
|
|
0
|
$_->($ptr, $prev_ptr, $prev_ptr->{'__tag__'}, $tag) if $_ = $s_handlers->{$tag} || $s_handlers->{'__any__'}; |
1158
|
0
|
|
|
|
|
0
|
@$ptr{qw(__parent__ __tag__)} = ($prev_ptr, $tag); |
1159
|
|
|
|
|
|
|
}, |
1160
|
|
|
|
|
|
|
End => sub { |
1161
|
0
|
|
|
0
|
|
0
|
my ($x, $tag) = @_; |
1162
|
0
|
|
|
|
|
0
|
my $cur_ptr = $ptr; |
1163
|
0
|
|
|
|
|
0
|
$ptr = delete $cur_ptr->{'__parent__'}; |
1164
|
0
|
0
|
|
|
|
0
|
die "End tag mismatch on $tag.\n" if $tag ne delete($cur_ptr->{'__tag__'}); |
1165
|
0
|
|
|
|
|
0
|
my $n_keys = scalar keys %$cur_ptr; |
1166
|
0
|
0
|
|
|
|
0
|
if (!$n_keys) { |
|
|
0
|
|
|
|
|
|
1167
|
0
|
|
|
|
|
0
|
$ptr->{$tag} = ''; # SuppressEmpty |
1168
|
|
|
|
|
|
|
} elsif (exists $cur_ptr->{'content'}) { |
1169
|
0
|
0
|
|
|
|
0
|
if ($n_keys == 1) { |
|
|
0
|
|
|
|
|
|
1170
|
0
|
0
|
|
|
|
0
|
if ($ptr->{$tag} eq 'ARRAY') { |
1171
|
0
|
|
|
|
|
0
|
$ptr->{$tag}->[-1] = $cur_ptr->{'content'}; |
1172
|
|
|
|
|
|
|
} else { |
1173
|
0
|
|
|
|
|
0
|
$ptr->{$tag} = $cur_ptr->{'content'}; |
1174
|
|
|
|
|
|
|
} |
1175
|
|
|
|
|
|
|
} elsif ($cur_ptr->{'content'} !~ /\S/) { |
1176
|
0
|
|
|
|
|
0
|
delete $cur_ptr->{'content'}; |
1177
|
|
|
|
|
|
|
} |
1178
|
|
|
|
|
|
|
} |
1179
|
0
|
0
|
0
|
|
|
0
|
$_->($cur_ptr, $ptr, $ptr->{'__tag__'}, $tag) if $_ = $e_handlers->{$tag} || $e_handlers->{'__any__'}; |
1180
|
|
|
|
|
|
|
}, |
1181
|
0
|
0
|
|
0
|
|
0
|
Char => sub { if (defined $ptr->{'content'}) { $ptr->{'content'} .= $_[1] } else { $ptr->{'content'} = $_[1] } }, |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
1182
|
0
|
|
|
|
|
0
|
}); |
1183
|
0
|
|
|
|
|
0
|
$x->parse($buffer); |
1184
|
0
|
|
|
|
|
0
|
return $data; |
1185
|
|
|
|
|
|
|
} |
1186
|
|
|
|
|
|
|
|
1187
|
|
|
|
|
|
|
sub gen_xml { |
1188
|
0
|
|
|
0
|
1
|
0
|
my ($self, $ref, $args) = @_; |
1189
|
0
|
0
|
|
|
|
0
|
my $indent = !$args->{'indent'} ? '' : $args->{'indent'} eq "1" ? " " : $args->{'indent'}; |
|
|
0
|
|
|
|
|
|
1190
|
0
|
|
|
|
|
0
|
my $level = 0; |
1191
|
0
|
|
0
|
|
|
0
|
my $top = $args->{'top'} || 'root'; |
1192
|
0
|
|
0
|
|
|
0
|
my $xml = $args->{'declaration'} || ''; |
1193
|
0
|
0
|
0
|
|
|
0
|
$xml .= "\n" . ($indent x $level) if $xml && $indent; |
1194
|
0
|
|
|
|
|
0
|
$xml .= "<$top>"; |
1195
|
0
|
|
|
|
|
0
|
my $rec; $rec = sub { |
1196
|
0
|
|
|
0
|
|
0
|
$level++; |
1197
|
0
|
|
|
|
|
0
|
my ($ref, $tag) = @_; |
1198
|
0
|
|
|
|
|
0
|
my $n = 0; |
1199
|
0
|
|
0
|
|
|
0
|
my $order = delete($ref->{'__sort__'}) || $args->{'sort'}->{$tag} || [sort grep {$_ ne '__attr__'} keys %$ref]; |
1200
|
0
|
|
|
|
|
0
|
for my $key (@$order) { |
1201
|
0
|
0
|
|
|
|
0
|
next if ! exists $ref->{$key}; |
1202
|
0
|
0
|
|
|
|
0
|
for my $node (ref($ref->{$key}) eq 'ARRAY' ? @{ $ref->{$key} } : $ref->{$key}) { |
|
0
|
|
|
|
|
0
|
|
1203
|
0
|
|
|
|
|
0
|
$n++; |
1204
|
0
|
0
|
|
|
|
0
|
$xml .= "\n" . ($indent x $level) if $indent; |
1205
|
0
|
0
|
|
|
|
0
|
if (!ref $node) { |
1206
|
0
|
0
|
0
|
|
|
0
|
$xml .= (!defined($node) || !length($node)) ? "<$key />" : "<$key>".$self->escape_xml($node)."$key>"; |
1207
|
0
|
|
|
|
|
0
|
next; |
1208
|
|
|
|
|
|
|
} |
1209
|
0
|
0
|
0
|
|
|
0
|
if ($node->{'__attr__'} || exists($node->{'content'})) { |
1210
|
0
|
0
|
|
|
|
0
|
$xml .= "<$key".join('', map {" $_=\"".$self->escape_xml($node->{$_})."\""} @{$node->{'__attr__'}||[sort grep {$_ ne 'content'} keys %$node]}).">"; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
1211
|
|
|
|
|
|
|
} else { |
1212
|
0
|
|
|
|
|
0
|
$xml .= "<$key>"; |
1213
|
|
|
|
|
|
|
} |
1214
|
0
|
0
|
|
|
|
0
|
if (exists $node->{'content'}) { |
1215
|
0
|
0
|
0
|
|
|
0
|
if (defined($node->{'content'}) && length $node->{'content'}) { |
1216
|
0
|
|
|
|
|
0
|
$xml .= $self->escape_xml($node->{'content'}) . "$key>"; |
1217
|
|
|
|
|
|
|
} else { |
1218
|
0
|
|
|
|
|
0
|
$xml =~ s|(>\s*)$| /$1|; |
1219
|
|
|
|
|
|
|
} |
1220
|
0
|
|
|
|
|
0
|
next; |
1221
|
|
|
|
|
|
|
} |
1222
|
0
|
0
|
|
|
|
0
|
if ($rec->($node, $key)) { |
1223
|
0
|
0
|
|
|
|
0
|
$xml .= "\n" . ($indent x $level) if $indent; |
1224
|
0
|
|
|
|
|
0
|
$xml .= "$key>"; |
1225
|
|
|
|
|
|
|
} else { |
1226
|
0
|
|
|
|
|
0
|
$xml =~ s|(>\s*)$| /$1|; |
1227
|
|
|
|
|
|
|
} |
1228
|
|
|
|
|
|
|
} |
1229
|
|
|
|
|
|
|
} |
1230
|
0
|
|
|
|
|
0
|
$level--; |
1231
|
0
|
|
|
|
|
0
|
return $n; |
1232
|
0
|
|
|
|
|
0
|
}; |
1233
|
0
|
|
|
|
|
0
|
$rec->($ref, $top); |
1234
|
0
|
0
|
|
|
|
0
|
$xml .= "\n" . ($indent x $level) if $indent; |
1235
|
0
|
|
|
|
|
0
|
$xml .= "$top>"; |
1236
|
0
|
0
|
0
|
|
|
0
|
$xml .= "\n" if $indent && ! $args->{'no_trailing_newline'}; |
1237
|
0
|
|
|
|
|
0
|
return $xml; |
1238
|
|
|
|
|
|
|
} |
1239
|
|
|
|
|
|
|
|
1240
|
|
|
|
|
|
|
sub escape_xml { |
1241
|
0
|
|
|
0
|
0
|
0
|
my $self = shift; |
1242
|
0
|
|
|
|
|
0
|
local $_ = shift; |
1243
|
0
|
0
|
|
|
|
0
|
return '' if ! defined; |
1244
|
0
|
|
|
|
|
0
|
s/&/&/g; |
1245
|
0
|
|
|
|
|
0
|
s/</g; |
1246
|
0
|
|
|
|
|
0
|
s/>/>/g; |
1247
|
0
|
|
|
|
|
0
|
s/"/"/g; |
1248
|
0
|
|
|
|
|
0
|
s/([^\x00-\x7F])/''.(ord $1).';'/ge; |
|
0
|
|
|
|
|
0
|
|
1249
|
0
|
|
|
|
|
0
|
return $_; |
1250
|
|
|
|
|
|
|
} |
1251
|
|
|
|
|
|
|
|
1252
|
|
|
|
|
|
|
sub uuid { |
1253
|
19
|
|
|
19
|
0
|
40
|
my ($self, $id, $uniq) = @_; |
1254
|
19
|
100
|
66
|
|
|
83
|
$id = $self->gen_uuid if !defined($id) || !length($id); |
1255
|
19
|
|
33
|
|
|
82
|
return $uniq->{$id} ||= do { |
1256
|
19
|
50
|
|
|
|
44
|
if (length($id) != 16) { |
1257
|
0
|
0
|
0
|
|
|
0
|
$id = substr($self->encode_base64($id), 0, 16) if $id !~ /^\d+$/ || $id > 2**32-1; |
1258
|
0
|
0
|
|
|
|
0
|
$id = sprintf '%016s', $id if $id ne '0'; |
1259
|
|
|
|
|
|
|
} |
1260
|
19
|
|
|
|
|
57
|
$id = $self->gen_uuid while $uniq->{$id}++; |
1261
|
19
|
|
|
|
|
110
|
$id; |
1262
|
|
|
|
|
|
|
}; |
1263
|
|
|
|
|
|
|
} |
1264
|
|
|
|
|
|
|
|
1265
|
4
|
|
|
4
|
0
|
895
|
sub gen_uuid { shift->encode_base64(join '', map {chr rand 256} 1..12) } # (3072 bit vs 4096) only 8e28 entries vs 3e38 - but readable |
|
48
|
|
|
|
|
102
|
|
1266
|
|
|
|
|
|
|
|
1267
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
1268
|
|
|
|
|
|
|
|
1269
|
|
|
|
|
|
|
sub dump_groups { |
1270
|
7
|
|
|
7
|
1
|
35
|
my ($self, $args, $groups) = @_; |
1271
|
7
|
|
|
|
|
14
|
my $t = ''; |
1272
|
7
|
0
|
|
|
|
14
|
my %gargs; for (keys %$args) { $gargs{$2} = $args->{$1} if /^(group_(.+))$/ }; |
|
7
|
|
|
|
|
29
|
|
|
0
|
|
|
|
|
0
|
|
1273
|
7
|
|
|
|
|
22
|
foreach my $g ($self->find_groups(\%gargs, $groups)) { |
1274
|
32
|
|
|
|
|
68
|
my $indent = ' ' x $g->{'level'}; |
1275
|
32
|
100
|
|
|
|
145
|
$t .= $indent.($g->{'expanded'} ? '-' : '+')." $g->{'title'} ($g->{'id'}) $g->{'created'}\n"; |
1276
|
32
|
|
|
|
|
70
|
local $g->{'groups'}; # don't recurse while looking for entries since we are already flat |
1277
|
32
|
|
|
|
|
96
|
$t .= "$indent > $_->{'title'}\t($_->{'id'}) $_->{'created'}\n" for $self->find_entries($args, [$g]); |
1278
|
|
|
|
|
|
|
} |
1279
|
7
|
|
|
|
|
50
|
return $t; |
1280
|
|
|
|
|
|
|
} |
1281
|
|
|
|
|
|
|
|
1282
|
|
|
|
|
|
|
sub add_group { |
1283
|
21
|
|
|
21
|
1
|
118
|
my ($self, $args, $top_groups) = @_; |
1284
|
21
|
|
|
|
|
77
|
$args = {%$args}; |
1285
|
21
|
|
|
|
|
30
|
my $groups; |
1286
|
21
|
|
|
|
|
39
|
my $parent_group = delete $args->{'group'}; |
1287
|
21
|
100
|
|
|
|
48
|
if (defined $parent_group) { |
1288
|
11
|
100
|
|
|
|
29
|
$parent_group = $self->find_group({id => $parent_group}, $top_groups) if ! ref($parent_group); |
1289
|
11
|
50
|
100
|
|
|
56
|
$groups = $parent_group->{'groups'} ||= [] if $parent_group; |
1290
|
|
|
|
|
|
|
} |
1291
|
21
|
|
33
|
|
|
115
|
$groups ||= $top_groups || ($self->{'groups'} ||= []); |
|
|
|
66
|
|
|
|
|
1292
|
|
|
|
|
|
|
|
1293
|
21
|
|
|
|
|
40
|
$args->{$_} = $self->now for grep {!defined $args->{$_}} qw(created accessed modified);; |
|
63
|
|
|
|
|
168
|
|
1294
|
21
|
|
33
|
|
|
93
|
$args->{'expires'} ||= $self->default_exp; |
1295
|
|
|
|
|
|
|
|
1296
|
21
|
|
|
|
|
32
|
push @$groups, $args; |
1297
|
21
|
|
|
|
|
54
|
$self->find_groups({}, $groups); # sets title, level, icon and id |
1298
|
21
|
|
|
|
|
71
|
return $args; |
1299
|
|
|
|
|
|
|
} |
1300
|
|
|
|
|
|
|
|
1301
|
|
|
|
|
|
|
sub finder_tests { |
1302
|
265
|
|
|
265
|
1
|
321
|
my ($self, $args) = @_; |
1303
|
265
|
|
|
|
|
278
|
my @tests; |
1304
|
265
|
50
|
|
|
|
264
|
foreach my $key (keys %{ $args || {} }) { |
|
265
|
|
|
|
|
1173
|
|
1305
|
68
|
100
|
|
|
|
160
|
next if ! defined $args->{$key}; |
1306
|
67
|
50
|
|
|
|
422
|
my ($field, $op) = ($key =~ m{ ^ (\w+) \s* (|!|=|!~|=~|gt|lt) $ }x) ? ($1, $2) : die "Invalid find match criteria \"$key\"\n"; |
1307
|
89
|
50
|
|
89
|
|
690
|
push @tests, (!$op || $op eq '=') ? sub { defined($_[0]->{$field}) && $_[0]->{$field} eq $args->{$key} } |
1308
|
2
|
50
|
|
2
|
|
16
|
: ($op eq '!') ? sub { !defined($_[0]->{$field}) || $_[0]->{$field} ne $args->{$key} } |
1309
|
2
|
50
|
|
2
|
|
24
|
: ($op eq '=~') ? sub { defined($_[0]->{$field}) && $_[0]->{$field} =~ $args->{$key} } |
1310
|
2
|
|
66
|
2
|
|
22
|
: ($op eq '!~') ? sub { !defined($_[0]->{$field}) || $_[0]->{$field} !~ $args->{$key} } |
1311
|
4
|
50
|
|
4
|
|
39
|
: ($op eq 'gt') ? sub { defined($_[0]->{$field}) && $_[0]->{$field} gt $args->{$key} } |
1312
|
2
|
50
|
|
2
|
|
22
|
: ($op eq 'lt') ? sub { defined($_[0]->{$field}) && $_[0]->{$field} lt $args->{$key} } |
1313
|
67
|
50
|
100
|
|
|
498
|
: die "Unknown op \"$op\"\n"; |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
1314
|
|
|
|
|
|
|
} |
1315
|
265
|
|
|
|
|
688
|
return @tests; |
1316
|
|
|
|
|
|
|
} |
1317
|
|
|
|
|
|
|
|
1318
|
|
|
|
|
|
|
sub find_groups { |
1319
|
207
|
|
|
207
|
1
|
321
|
my ($self, $args, $groups, $level) = @_; |
1320
|
207
|
|
|
|
|
427
|
my @tests = $self->finder_tests($args); |
1321
|
207
|
|
|
|
|
245
|
my @groups; |
1322
|
|
|
|
|
|
|
my %uniq; |
1323
|
207
|
|
66
|
|
|
524
|
my $container = $groups || $self->groups; |
1324
|
207
|
|
|
|
|
382
|
for my $g (@$container) { |
1325
|
259
|
|
100
|
|
|
811
|
$g->{'level'} = $level || 0; |
1326
|
259
|
100
|
|
|
|
572
|
$g->{'title'} = '' if ! defined $g->{'title'}; |
1327
|
259
|
|
100
|
|
|
827
|
$g->{'icon'} ||= 0; |
1328
|
259
|
50
|
|
|
|
475
|
if ($self->{'force_v2_gid'}) { |
1329
|
0
|
|
|
|
|
0
|
$g->{'id'} = $self->uuid($g->{'id'}, \%uniq); |
1330
|
|
|
|
|
|
|
} else { |
1331
|
259
|
|
66
|
|
|
1703
|
$g->{'id'} = int(rand 2**32-1) while !defined($g->{'id'}) || $uniq{$g->{'id'}}++; # the non-v2 gid is compatible with both v1 and our v2 implementation |
1332
|
|
|
|
|
|
|
} |
1333
|
|
|
|
|
|
|
|
1334
|
259
|
100
|
66
|
|
|
631
|
if (!@tests || !grep{!$_->($g)} @tests) { |
|
42
|
|
|
|
|
75
|
|
1335
|
234
|
|
|
|
|
340
|
push @groups, $g; |
1336
|
234
|
100
|
|
|
|
511
|
push @{ $self->{'__group_groups'} }, $container if $self->{'__group_groups'}; |
|
7
|
|
|
|
|
16
|
|
1337
|
|
|
|
|
|
|
} |
1338
|
259
|
100
|
|
|
|
930
|
push @groups, $self->find_groups($args, $g->{'groups'}, $g->{'level'} + 1) if $g->{'groups'}; |
1339
|
|
|
|
|
|
|
} |
1340
|
207
|
|
|
|
|
803
|
return @groups; |
1341
|
|
|
|
|
|
|
} |
1342
|
|
|
|
|
|
|
|
1343
|
|
|
|
|
|
|
sub find_group { |
1344
|
22
|
|
|
22
|
1
|
3737
|
my $self = shift; |
1345
|
22
|
100
|
|
|
|
65
|
local $self->{'__group_groups'} = [] if wantarray; |
1346
|
22
|
|
|
|
|
51
|
my @g = $self->find_groups(@_); |
1347
|
22
|
100
|
|
|
|
79
|
die "Found too many groups (@g)\n" if @g > 1; |
1348
|
21
|
100
|
|
|
|
119
|
return wantarray ? ($g[0], $self->{'__group_groups'}->[0]) : $g[0]; |
1349
|
|
|
|
|
|
|
} |
1350
|
|
|
|
|
|
|
|
1351
|
|
|
|
|
|
|
sub delete_group { |
1352
|
5
|
|
|
5
|
1
|
14
|
my $self = shift; |
1353
|
5
|
|
|
|
|
13
|
my ($g, $c) = $self->find_group(@_); |
1354
|
4
|
50
|
33
|
|
|
18
|
return if !$g || !$c; |
1355
|
4
|
|
|
|
|
10
|
for my $i (0 .. $#$c) { |
1356
|
8
|
100
|
|
|
|
25
|
next if $c->[$i] ne $g; |
1357
|
4
|
|
|
|
|
7
|
splice(@$c, $i, 1, ()); |
1358
|
4
|
|
|
|
|
7
|
last; |
1359
|
|
|
|
|
|
|
} |
1360
|
4
|
|
|
|
|
20
|
return $g; |
1361
|
|
|
|
|
|
|
} |
1362
|
|
|
|
|
|
|
|
1363
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
1364
|
|
|
|
|
|
|
|
1365
|
|
|
|
|
|
|
sub add_entry { |
1366
|
6
|
|
|
6
|
1
|
1684
|
my ($self, $args, $groups) = @_; |
1367
|
6
|
|
100
|
|
|
27
|
$groups ||= eval { $self->groups } || []; |
|
|
|
33
|
|
|
|
|
1368
|
6
|
50
|
|
|
|
27
|
die "You must unlock the passwords before adding new entries.\n" if $self->is_locked($groups); |
1369
|
6
|
|
|
|
|
39
|
$args = {%$args}; |
1370
|
6
|
|
66
|
|
|
40
|
my $group = delete($args->{'group'}) || $groups->[0] || $self->add_group({}); |
1371
|
6
|
50
|
|
|
|
22
|
if (! ref($group)) { |
1372
|
0
|
|
0
|
|
|
0
|
$group = $self->find_group({id => $group}, $groups) || die "Could not find a matching group to add entry to.\n"; |
1373
|
|
|
|
|
|
|
} |
1374
|
|
|
|
|
|
|
|
1375
|
6
|
|
|
|
|
9
|
my %uniq; |
1376
|
6
|
|
|
|
|
21
|
foreach my $g ($self->find_groups({}, $groups)) { |
1377
|
8
|
100
|
|
|
|
12
|
$uniq{$_->{'id'}}++ for @{ $g->{'entries'} || [] }; |
|
8
|
|
|
|
|
70
|
|
1378
|
|
|
|
|
|
|
} |
1379
|
6
|
|
|
|
|
37
|
$args->{'id'} = $self->uuid($args->{'id'}, \%uniq); |
1380
|
6
|
|
|
|
|
17
|
$args->{$_} = '' for grep {!defined $args->{$_}} qw(title url username password comment); |
|
30
|
|
|
|
|
82
|
|
1381
|
6
|
|
|
|
|
15
|
$args->{$_} = 0 for grep {!defined $args->{$_}} qw(icon); |
|
6
|
|
|
|
|
27
|
|
1382
|
6
|
|
|
|
|
12
|
$args->{$_} = $self->now for grep {!defined $args->{$_}} qw(created accessed modified); |
|
18
|
|
|
|
|
50
|
|
1383
|
6
|
|
66
|
|
|
50
|
$args->{'expires'} ||= $self->default_exp; |
1384
|
6
|
|
|
|
|
19
|
$self->_check_v1_binary($args); |
1385
|
6
|
|
|
|
|
20
|
$self->_check_v1_auto_type($args); |
1386
|
|
|
|
|
|
|
|
1387
|
|
|
|
|
|
|
|
1388
|
6
|
|
100
|
|
|
9
|
push @{ $group->{'entries'} ||= [] }, $args; |
|
6
|
|
|
|
|
41
|
|
1389
|
6
|
|
|
|
|
28
|
return $args; |
1390
|
|
|
|
|
|
|
} |
1391
|
|
|
|
|
|
|
|
1392
|
|
|
|
|
|
|
sub find_entries { |
1393
|
58
|
|
|
58
|
1
|
108
|
my ($self, $args, $groups) = @_; |
1394
|
58
|
100
|
|
|
|
148
|
local @{ $args }{'expires gt', 'active'} = ($self->now, undef) if $args->{'active'}; |
|
1
|
|
|
|
|
6
|
|
1395
|
58
|
|
|
|
|
146
|
my @tests = $self->finder_tests($args); |
1396
|
58
|
|
|
|
|
74
|
my @entries; |
1397
|
58
|
|
|
|
|
157
|
foreach my $g ($self->find_groups({}, $groups)) { |
1398
|
101
|
100
|
|
|
|
125
|
foreach my $e (@{ $g->{'entries'} || [] }) { |
|
101
|
|
|
|
|
448
|
|
1399
|
43
|
|
|
|
|
116
|
local $e->{'group_id'} = $g->{'id'}; |
1400
|
43
|
|
|
|
|
99
|
local $e->{'group_title'} = $g->{'title'}; |
1401
|
43
|
100
|
66
|
|
|
126
|
if (!@tests || !grep{!$_->($e)} @tests) { |
|
59
|
|
|
|
|
99
|
|
1402
|
27
|
|
|
|
|
36
|
push @entries, $e; |
1403
|
27
|
100
|
|
|
|
120
|
push @{ $self->{'__entry_groups'} }, $g if $self->{'__entry_groups'}; |
|
5
|
|
|
|
|
27
|
|
1404
|
|
|
|
|
|
|
} |
1405
|
|
|
|
|
|
|
} |
1406
|
|
|
|
|
|
|
} |
1407
|
58
|
|
|
|
|
342
|
return @entries; |
1408
|
|
|
|
|
|
|
} |
1409
|
|
|
|
|
|
|
|
1410
|
|
|
|
|
|
|
sub find_entry { |
1411
|
10
|
|
|
10
|
1
|
2739
|
my $self = shift; |
1412
|
10
|
100
|
|
|
|
39
|
local $self->{'__entry_groups'} = [] if wantarray; |
1413
|
10
|
|
|
|
|
27
|
my @e = $self->find_entries(@_); |
1414
|
10
|
100
|
|
|
|
37
|
die "Found too many entries (@e)\n" if @e > 1; |
1415
|
9
|
100
|
|
|
|
50
|
return wantarray ? ($e[0], $self->{'__entry_groups'}->[0]) : $e[0]; |
1416
|
|
|
|
|
|
|
} |
1417
|
|
|
|
|
|
|
|
1418
|
|
|
|
|
|
|
sub delete_entry { |
1419
|
2
|
|
|
2
|
1
|
459
|
my $self = shift; |
1420
|
2
|
|
|
|
|
9
|
my ($e, $g) = $self->find_entry(@_); |
1421
|
1
|
50
|
33
|
|
|
9
|
return if !$e || !$g; |
1422
|
1
|
50
|
|
|
|
3
|
for my $i (0 .. $#{ $g->{'entries'} || [] }) { |
|
1
|
|
|
|
|
7
|
|
1423
|
1
|
50
|
|
|
|
9
|
next if $g->{'entries'}->[$i] ne $e; |
1424
|
1
|
|
|
|
|
3
|
splice(@{ $g->{'entries'} }, $i, 1, ()); |
|
1
|
|
|
|
|
4
|
|
1425
|
1
|
|
|
|
|
3
|
last; |
1426
|
|
|
|
|
|
|
} |
1427
|
1
|
|
|
|
|
7
|
return $e; |
1428
|
|
|
|
|
|
|
} |
1429
|
|
|
|
|
|
|
|
1430
|
|
|
|
|
|
|
sub now { |
1431
|
80
|
|
|
80
|
1
|
112
|
my ($self, $time) = @_; |
1432
|
80
|
|
33
|
|
|
2080
|
my ($sec, $min, $hour, $day, $mon, $year) = localtime($time || time); |
1433
|
80
|
|
|
|
|
573
|
return sprintf '%04d-%02d-%02d %02d:%02d:%02d', $year+1900, $mon+1, $day, $hour, $min, $sec; |
1434
|
|
|
|
|
|
|
} |
1435
|
|
|
|
|
|
|
|
1436
|
26
|
50
|
|
26
|
1
|
161
|
sub default_exp { shift->{'default_exp'} || '2999-12-31 23:23:59' } |
1437
|
|
|
|
|
|
|
|
1438
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
1439
|
|
|
|
|
|
|
|
1440
|
|
|
|
|
|
|
sub is_locked { |
1441
|
17
|
|
|
17
|
1
|
1710
|
my $self = shift; |
1442
|
17
|
|
66
|
|
|
47
|
my $groups = shift || $self->groups; |
1443
|
17
|
100
|
|
|
|
101
|
return $locker{"$groups"} ? 1 : 0; |
1444
|
|
|
|
|
|
|
} |
1445
|
|
|
|
|
|
|
|
1446
|
|
|
|
|
|
|
sub lock { |
1447
|
5
|
|
|
5
|
1
|
12
|
my $self = shift; |
1448
|
5
|
|
33
|
|
|
30
|
my $groups = shift || $self->groups; |
1449
|
5
|
50
|
|
|
|
32
|
return 2 if $locker{"$groups"}; # not quite as fast as Scalar::Util::refaddr |
1450
|
|
|
|
|
|
|
|
1451
|
5
|
|
|
|
|
19
|
my $ref = $locker{"$groups"} = {}; |
1452
|
5
|
|
|
|
|
22
|
$ref->{'_key'} = join '', map {chr rand 256} 1..32; |
|
160
|
|
|
|
|
299
|
|
1453
|
5
|
|
|
|
|
27
|
$ref->{'_enc_iv'} = join '', map {chr rand 256} 1..16; |
|
80
|
|
|
|
|
139
|
|
1454
|
|
|
|
|
|
|
|
1455
|
5
|
|
|
|
|
31
|
foreach my $e ($self->find_entries({}, $groups)) { |
1456
|
6
|
50
|
|
|
|
15
|
my $pass = delete $e->{'password'}; $pass = '' if ! defined $pass; |
|
6
|
|
|
|
|
19
|
|
1457
|
6
|
|
|
|
|
23
|
$ref->{"$e"} = $self->encrypt_rijndael_cbc($pass, $ref->{'_key'}, $ref->{'_enc_iv'}); # we don't leave plaintext in memory |
1458
|
|
|
|
|
|
|
} |
1459
|
|
|
|
|
|
|
|
1460
|
5
|
|
|
|
|
18
|
return 1; |
1461
|
|
|
|
|
|
|
} |
1462
|
|
|
|
|
|
|
|
1463
|
|
|
|
|
|
|
sub unlock { |
1464
|
13
|
|
|
13
|
1
|
21
|
my $self = shift; |
1465
|
13
|
|
33
|
|
|
56
|
my $groups = shift || $self->groups; |
1466
|
13
|
100
|
|
|
|
59
|
return 2 if !$locker{"$groups"}; |
1467
|
5
|
|
|
|
|
12
|
my $ref = $locker{"$groups"}; |
1468
|
5
|
|
|
|
|
20
|
foreach my $e ($self->find_entries({}, $groups)) { |
1469
|
4
|
|
|
|
|
9
|
my $pass = $ref->{"$e"}; |
1470
|
4
|
50
|
|
|
|
10
|
$pass = eval { $self->decrypt_rijndael_cbc($pass, $ref->{'_key'}, $ref->{'_enc_iv'}) } if $pass; |
|
4
|
|
|
|
|
15
|
|
1471
|
4
|
50
|
|
|
|
11
|
$pass = '' if ! defined $pass; |
1472
|
4
|
|
|
|
|
12
|
$e->{'password'} = $pass; |
1473
|
|
|
|
|
|
|
} |
1474
|
5
|
|
|
|
|
19
|
delete $locker{"$groups"}; |
1475
|
5
|
|
|
|
|
18
|
return 1; |
1476
|
|
|
|
|
|
|
} |
1477
|
|
|
|
|
|
|
|
1478
|
|
|
|
|
|
|
sub locked_entry_password { |
1479
|
2
|
|
|
2
|
1
|
1075
|
my $self = shift; |
1480
|
2
|
|
|
|
|
6
|
my $entry = shift; |
1481
|
2
|
|
33
|
|
|
11
|
my $groups = shift || $self->groups; |
1482
|
2
|
|
100
|
|
|
17
|
my $ref = $locker{"$groups"} || die "Passwords are not locked\n"; |
1483
|
1
|
50
|
|
|
|
6
|
$entry = $self->find_entry({id => $entry}, $groups) if ! ref $entry; |
1484
|
1
|
50
|
|
|
|
4
|
return if ! $entry; |
1485
|
1
|
|
|
|
|
4
|
my $pass = $ref->{"$entry"}; |
1486
|
1
|
50
|
|
|
|
4
|
$pass = eval { $self->decrypt_rijndael_cbc($pass, $ref->{'_key'}, $ref->{'_enc_iv'}) } if $pass; |
|
1
|
|
|
|
|
6
|
|
1487
|
1
|
50
|
|
|
|
3
|
$pass = '' if ! defined $pass; |
1488
|
1
|
|
|
|
|
5
|
$entry->{'accessed'} = $self->now; |
1489
|
1
|
|
|
|
|
5
|
return $pass; |
1490
|
|
|
|
|
|
|
} |
1491
|
|
|
|
|
|
|
|
1492
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
1493
|
|
|
|
|
|
|
|
1494
|
|
|
|
|
|
|
sub salsa20_stream { |
1495
|
0
|
|
|
0
|
1
|
|
my ($self, $args) = @_; |
1496
|
0
|
|
|
|
|
|
delete $args->{'data'}; |
1497
|
0
|
|
|
|
|
|
my $salsa20 = $self->salsa20($args); |
1498
|
0
|
|
|
|
|
|
my $buffer = ''; |
1499
|
|
|
|
|
|
|
return sub { |
1500
|
0
|
|
|
0
|
|
|
my $enc = shift; |
1501
|
0
|
|
|
|
|
|
$buffer .= $salsa20->("\0" x 64) while length($buffer) < length($enc); |
1502
|
0
|
|
|
|
|
|
my $data = join '', map {chr(ord(substr $enc, $_, 1) ^ ord(substr $buffer, $_, 1))} 0 .. length($enc)-1; |
|
0
|
|
|
|
|
|
|
1503
|
0
|
|
|
|
|
|
substr $buffer, 0, length($enc), ''; |
1504
|
0
|
|
|
|
|
|
return $data; |
1505
|
0
|
|
|
|
|
|
}; |
1506
|
|
|
|
|
|
|
} |
1507
|
|
|
|
|
|
|
|
1508
|
|
|
|
|
|
|
|
1509
|
|
|
|
|
|
|
sub salsa20 { # http://cr.yp.to/snuffle/salsa20/regs/salsa20.c |
1510
|
0
|
|
|
0
|
1
|
|
my ($self, $args) = @_; |
1511
|
0
|
|
|
|
|
|
my ($key, $iv, $rounds) = @$args{qw(key iv rounds)}; |
1512
|
0
|
|
0
|
|
|
|
$rounds ||= 20; |
1513
|
|
|
|
|
|
|
|
1514
|
0
|
|
|
|
|
|
my (@k, @c); |
1515
|
0
|
0
|
|
|
|
|
if (32 == length $key) { |
|
|
0
|
|
|
|
|
|
1516
|
0
|
|
|
|
|
|
@k = unpack 'L8', $key; |
1517
|
0
|
|
|
|
|
|
@c = (0x61707865, 0x3320646e, 0x79622d32, 0x6b206574); # SIGMA |
1518
|
|
|
|
|
|
|
} elsif (16 == length $key) { |
1519
|
0
|
|
|
|
|
|
@k = unpack 'L8', $key x 2; |
1520
|
0
|
|
|
|
|
|
@c = (0x61707865, 0x3120646e, 0x79622d36, 0x6b206574); # TAU |
1521
|
|
|
|
|
|
|
} else { |
1522
|
0
|
|
|
|
|
|
die "Salsa20 key length must be 16 or 32\n"; |
1523
|
|
|
|
|
|
|
} |
1524
|
0
|
0
|
|
|
|
|
die "Salsa20 IV length must be 8\n" if length($iv) != 8; |
1525
|
0
|
0
|
|
|
|
|
die "Salsa20 rounds must be 8, 12, or 20.\n" if !grep {$rounds != $_} 8, 12, 20; |
|
0
|
|
|
|
|
|
|
1526
|
0
|
|
|
|
|
|
my @v = unpack('L2', $iv); |
1527
|
|
|
|
|
|
|
|
1528
|
|
|
|
|
|
|
# 0 5 6 7 10 # 15 |
1529
|
0
|
|
|
|
|
|
my @state = ($c[0], $k[0], $k[1], $k[2], $k[3], $c[1], $v[0], $v[1], 0, 0, $c[2], $k[4], $k[5], $k[6], $k[7], $c[3]); |
1530
|
|
|
|
|
|
|
|
1531
|
0
|
|
|
0
|
|
|
my $rotl32 = sub { return (($_[0] << $_[1]) | ($_[0] >> (32 - $_[1]))) & 0xffffffff }; |
|
0
|
|
|
|
|
|
|
1532
|
|
|
|
|
|
|
my $word_to_byte = sub { |
1533
|
0
|
|
|
0
|
|
|
my @x = @state; |
1534
|
0
|
|
|
|
|
|
for (1 .. $rounds/2) { |
1535
|
0
|
|
|
|
|
|
$x[ 4] ^= $rotl32->(($x[ 0] + $x[12]) & 0xffffffff, 7); |
1536
|
0
|
|
|
|
|
|
$x[ 8] ^= $rotl32->(($x[ 4] + $x[ 0]) & 0xffffffff, 9); |
1537
|
0
|
|
|
|
|
|
$x[12] ^= $rotl32->(($x[ 8] + $x[ 4]) & 0xffffffff, 13); |
1538
|
0
|
|
|
|
|
|
$x[ 0] ^= $rotl32->(($x[12] + $x[ 8]) & 0xffffffff, 18); |
1539
|
0
|
|
|
|
|
|
$x[ 9] ^= $rotl32->(($x[ 5] + $x[ 1]) & 0xffffffff, 7); |
1540
|
0
|
|
|
|
|
|
$x[13] ^= $rotl32->(($x[ 9] + $x[ 5]) & 0xffffffff, 9); |
1541
|
0
|
|
|
|
|
|
$x[ 1] ^= $rotl32->(($x[13] + $x[ 9]) & 0xffffffff, 13); |
1542
|
0
|
|
|
|
|
|
$x[ 5] ^= $rotl32->(($x[ 1] + $x[13]) & 0xffffffff, 18); |
1543
|
0
|
|
|
|
|
|
$x[14] ^= $rotl32->(($x[10] + $x[ 6]) & 0xffffffff, 7); |
1544
|
0
|
|
|
|
|
|
$x[ 2] ^= $rotl32->(($x[14] + $x[10]) & 0xffffffff, 9); |
1545
|
0
|
|
|
|
|
|
$x[ 6] ^= $rotl32->(($x[ 2] + $x[14]) & 0xffffffff, 13); |
1546
|
0
|
|
|
|
|
|
$x[10] ^= $rotl32->(($x[ 6] + $x[ 2]) & 0xffffffff, 18); |
1547
|
0
|
|
|
|
|
|
$x[ 3] ^= $rotl32->(($x[15] + $x[11]) & 0xffffffff, 7); |
1548
|
0
|
|
|
|
|
|
$x[ 7] ^= $rotl32->(($x[ 3] + $x[15]) & 0xffffffff, 9); |
1549
|
0
|
|
|
|
|
|
$x[11] ^= $rotl32->(($x[ 7] + $x[ 3]) & 0xffffffff, 13); |
1550
|
0
|
|
|
|
|
|
$x[15] ^= $rotl32->(($x[11] + $x[ 7]) & 0xffffffff, 18); |
1551
|
|
|
|
|
|
|
|
1552
|
0
|
|
|
|
|
|
$x[ 1] ^= $rotl32->(($x[ 0] + $x[ 3]) & 0xffffffff, 7); |
1553
|
0
|
|
|
|
|
|
$x[ 2] ^= $rotl32->(($x[ 1] + $x[ 0]) & 0xffffffff, 9); |
1554
|
0
|
|
|
|
|
|
$x[ 3] ^= $rotl32->(($x[ 2] + $x[ 1]) & 0xffffffff, 13); |
1555
|
0
|
|
|
|
|
|
$x[ 0] ^= $rotl32->(($x[ 3] + $x[ 2]) & 0xffffffff, 18); |
1556
|
0
|
|
|
|
|
|
$x[ 6] ^= $rotl32->(($x[ 5] + $x[ 4]) & 0xffffffff, 7); |
1557
|
0
|
|
|
|
|
|
$x[ 7] ^= $rotl32->(($x[ 6] + $x[ 5]) & 0xffffffff, 9); |
1558
|
0
|
|
|
|
|
|
$x[ 4] ^= $rotl32->(($x[ 7] + $x[ 6]) & 0xffffffff, 13); |
1559
|
0
|
|
|
|
|
|
$x[ 5] ^= $rotl32->(($x[ 4] + $x[ 7]) & 0xffffffff, 18); |
1560
|
0
|
|
|
|
|
|
$x[11] ^= $rotl32->(($x[10] + $x[ 9]) & 0xffffffff, 7); |
1561
|
0
|
|
|
|
|
|
$x[ 8] ^= $rotl32->(($x[11] + $x[10]) & 0xffffffff, 9); |
1562
|
0
|
|
|
|
|
|
$x[ 9] ^= $rotl32->(($x[ 8] + $x[11]) & 0xffffffff, 13); |
1563
|
0
|
|
|
|
|
|
$x[10] ^= $rotl32->(($x[ 9] + $x[ 8]) & 0xffffffff, 18); |
1564
|
0
|
|
|
|
|
|
$x[12] ^= $rotl32->(($x[15] + $x[14]) & 0xffffffff, 7); |
1565
|
0
|
|
|
|
|
|
$x[13] ^= $rotl32->(($x[12] + $x[15]) & 0xffffffff, 9); |
1566
|
0
|
|
|
|
|
|
$x[14] ^= $rotl32->(($x[13] + $x[12]) & 0xffffffff, 13); |
1567
|
0
|
|
|
|
|
|
$x[15] ^= $rotl32->(($x[14] + $x[13]) & 0xffffffff, 18); |
1568
|
|
|
|
|
|
|
} |
1569
|
0
|
|
|
|
|
|
return pack 'L16', map {($x[$_] + $state[$_]) & 0xffffffff} 0 .. 15; |
|
0
|
|
|
|
|
|
|
1570
|
0
|
|
|
|
|
|
}; |
1571
|
|
|
|
|
|
|
|
1572
|
|
|
|
|
|
|
my $encoder = sub { |
1573
|
0
|
|
|
0
|
|
|
my $enc = shift; |
1574
|
0
|
|
|
|
|
|
my $out = ''; |
1575
|
0
|
|
|
|
|
|
while (length $enc) { |
1576
|
0
|
|
|
|
|
|
my $stream = $word_to_byte->(); |
1577
|
0
|
|
|
|
|
|
$state[8] = ($state[8] + 1) & 0xffffffff; |
1578
|
0
|
0
|
|
|
|
|
$state[9] = ($state[9] + 1) & 0xffffffff if $state[8] == 0; |
1579
|
0
|
|
|
|
|
|
my $chunk = substr $enc, 0, 64, ''; |
1580
|
0
|
|
|
|
|
|
$out .= join '', map {chr(ord(substr $stream, $_, 1) ^ ord(substr $chunk, $_, 1))} 0 .. length($chunk)-1; |
|
0
|
|
|
|
|
|
|
1581
|
|
|
|
|
|
|
} |
1582
|
0
|
|
|
|
|
|
return $out; |
1583
|
0
|
|
|
|
|
|
}; |
1584
|
0
|
0
|
|
|
|
|
return $encoder if !exists $args->{'data'}; |
1585
|
0
|
0
|
|
|
|
|
return $encoder->(defined($args->{'data'}) ? $args->{'data'} : ''); |
1586
|
|
|
|
|
|
|
} |
1587
|
|
|
|
|
|
|
|
1588
|
|
|
|
|
|
|
###----------------------------------------------------------------### |
1589
|
|
|
|
|
|
|
|
1590
|
|
|
|
|
|
|
1; |
1591
|
|
|
|
|
|
|
|
1592
|
|
|
|
|
|
|
__END__ |