File Coverage

blib/lib/File/KeePass.pm
Criterion Covered Total %
statement 553 1103 50.1
branch 245 684 35.8
condition 137 452 30.3
subroutine 64 107 59.8
pod 37 42 88.1
total 1036 2388 43.3


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)."";
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'}) . "";
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 .= "";
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 .= "";
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/
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__