File Coverage

blib/lib/Palm/Keyring.pm
Criterion Covered Total %
statement 485 590 82.2
branch 150 236 63.5
condition 73 152 48.0
subroutine 38 41 92.6
pod 13 13 100.0
total 759 1032 73.5


line stmt bran cond sub pod time code
1             package Palm::Keyring;
2             # $RedRiver: Keyring.pm,v 1.54 2007/12/05 05:42:29 andrew Exp $
3             ########################################################################
4             # Keyring.pm *** Perl class for Keyring for Palm OS databases.
5             #
6             # This started as Memo.pm, I just made it work for Keyring.
7             #
8             # 2006.01.26 #*#*# andrew fresh
9             ########################################################################
10             # Copyright (C) 2006, 2007 by Andrew Fresh
11             #
12             # This program is free software; you can redistribute it and/or modify
13             # it under the same terms as Perl itself.
14             ########################################################################
15 3     3   149260 use strict;
  3         9  
  3         112  
16 3     3   20 use warnings;
  3         6  
  3         114  
17              
18             require 5.006_001;
19              
20 3     3   18 use Carp;
  3         10  
  3         436  
21              
22 3     3   17 use base qw/ Palm::StdAppInfo /;
  3         16  
  3         3124  
23              
24             my $ENCRYPT = 1;
25             my $DECRYPT = 0;
26             my $MD5_CBLOCK = 64;
27             my $kSalt_Size = 4;
28             my $EMPTY = q{};
29             my $SPACE = q{ };
30             my $NULL = chr 0;
31              
32             my @CRYPTS = (
33             {
34             alias => 'None',
35             name => 'None',
36             keylen => 8,
37             blocksize => 1,
38             default_iter => 500,
39             },
40             {
41             alias => 'DES-EDE3',
42             name => 'DES_EDE3',
43             keylen => 24,
44             blocksize => 8,
45             DES_odd_parity => 1,
46             default_iter => 1000,
47             },
48             {
49             alias => 'AES128',
50             name => 'Rijndael',
51             keylen => 16,
52             blocksize => 16,
53             default_iter => 100,
54             },
55             {
56             alias => 'AES256',
57             name => 'Rijndael',
58             keylen => 32,
59             blocksize => 16,
60             default_iter => 250,
61             },
62             );
63              
64             my %LABELS = (
65             0 => {
66             id => 0,
67             name => 'name',
68             },
69             1 => {
70             id => 1,
71             name => 'account',
72             },
73             2 => {
74             id => 2,
75             name => 'password',
76             },
77             3 => {
78             id => 3,
79             name => 'lastchange',
80             },
81             255 => {
82             id => 255,
83             name => 'notes',
84             },
85             );
86              
87              
88             our $VERSION = '0.96_07';
89              
90             sub new
91             {
92 2     2 1 10709 my $classname = shift;
93 2         7 my $options = {};
94              
95 2 50       10 if (@_) {
96             # hashref arguments
97 2 50       12 if (ref $_[0] eq 'HASH') {
    0          
98 2         7 $options = shift;
99             }
100              
101             # CGI style arguments
102             elsif ($_[0] =~ /^-[a-zA-Z0-9_]{1,20}$/) {
103 0         0 my %tmp = @_;
104 0         0 while ( my($key,$value) = each %tmp) {
105 0         0 $key =~ s/^-//;
106 0         0 $options->{lc $key} = $value;
107             }
108             }
109              
110             else {
111 0         0 $options->{password} = shift;
112 0         0 $options->{version} = shift;
113             }
114             }
115              
116             # Create a generic PDB. No need to rebless it, though.
117 2         24 my $self = $classname->SUPER::new();
118              
119 2         443 $self->{name} = 'Keys-Gtkr'; # Default
120 2         6 $self->{creator} = 'Gtkr';
121 2         6 $self->{type} = 'Gkyr';
122              
123             # The PDB is not a resource database by
124             # default, but it's worth emphasizing,
125             # since MemoDB is explicitly not a PRC.
126 2         6 $self->{attributes}{resource} = 0;
127              
128             # Set the version
129 2   50     13 $self->{version} = $options->{version} || 4;
130              
131             # Set options
132 2         6 $self->{options} = $options;
133              
134             # Set defaults
135 2 100       14 if ($self->{version} == 5) {
136 1   50     8 $self->{options}->{cipher} ||= 0; # 'None'
137 1 50       3 my $c = crypts($self->{options}->{cipher})
138             or croak('Unknown cipher ' . $self->{options}->{cipher});
139 1   33     8 $self->{options}->{iterations} ||= $c->{default_iter};
140 1   33     7 $self->{appinfo}->{cipher} ||= $self->{options}->{cipher};
141 1   33     8 $self->{appinfo}->{iter} ||= $self->{options}->{iterations};
142             };
143              
144 2 50       8 if ( defined $options->{password} ) {
145 2         12 $self->Password($options->{password});
146             }
147              
148 2         24 return $self;
149             }
150              
151             sub import
152             {
153 3     3   81 Palm::PDB::RegisterPDBHandlers( __PACKAGE__, [ 'Gtkr', 'Gkyr' ], );
154 3         86 return 1;
155             }
156              
157             # Accessors
158              
159             sub crypts
160             {
161 64     64 1 29608 my $crypt = shift;
162 64 50 33     785 if ((! defined $crypt) || (! length $crypt)) {
    50          
163 0         0 return;
164             } elsif ($crypt =~ /\D/) {
165 0         0 foreach my $c (@CRYPTS) {
166 0 0       0 if ($c->{alias} eq $crypt) {
167 0         0 return $c;
168             }
169             }
170             # didn't find it.
171 0         0 return;
172             } else {
173 64         275 return $CRYPTS[$crypt];
174             }
175             }
176              
177             sub labels
178             {
179 80     80 1 137 my $label = shift;
180              
181 80 50 33     725 if ((! defined $label) || (! length $label)) {
    100          
182 0         0 return;
183             } elsif (exists $LABELS{$label}) {
184 60         192 return $LABELS{$label};
185             } else {
186 20         55 foreach my $l (keys %LABELS) {
187 62 100       163 if ($LABELS{$l}{name} eq $label) {
188 20         55 return $LABELS{$l};
189             }
190             }
191              
192             # didn't find it, make one.
193 0 0       0 if ($label =~ /^\d+$/) {
194             return {
195 0         0 id => $label,
196             name => undef,
197             };
198             } else {
199 0         0 return;
200             }
201             }
202             }
203              
204             # Write
205              
206             sub Write
207             {
208 2     2 1 478 my $self = shift;
209              
210 2 100       12 if ($self->{version} == 4) {
211             # Give the PDB the first record that will hold the encrypted password
212 1         5 my $rec = $self->new_Record;
213 1         20 $rec->{data} = $self->{encpassword};
214              
215 1 50       6 if (ref $self->{records} eq 'ARRAY') {
216 1         2 unshift @{ $self->{records} }, $rec;
  1         3  
217             } else {
218 0         0 $self->{records} = [ $rec ];
219             }
220             }
221              
222 2         25 my $rc = $self->SUPER::Write(@_);
223              
224 2 100       411 if ($self->{version} == 4) {
225 1         3 shift @{ $self->{records} };
  1         3  
226             }
227              
228 2         10 return $rc;
229             }
230              
231             # ParseRecord
232              
233             sub ParseRecord
234             {
235 17     17 1 532 my $self = shift;
236              
237 17         88 my $rec = $self->SUPER::ParseRecord(@_);
238 17 50       217 return $rec if ! exists $rec->{data};
239              
240 17 100       82 if ($self->{version} == 4) {
    50          
241             # skip the first record because it contains the password.
242 2 100       10 if (! exists $self->{records}) {
243 1         3 $self->{encpassword} = $rec->{data};
244 1         7 return '__DELETE_ME__';
245             }
246              
247 1 50       4 if ($self->{records}->[0] eq '__DELETE_ME__') {
248 1         2 shift @{ $self->{records} };
  1         3  
249             }
250              
251 1         20 my ( $name, $encrypted ) = split /$NULL/xm, $rec->{data}, 2;
252              
253 1 50       7 return $rec if ! $encrypted;
254 1         8 $rec->{plaintext}->{0} = {
255             label => 'name',
256             label_id => 0,
257             data => $name,
258             font => 0,
259             };
260 1         4 $rec->{encrypted} = $encrypted;
261 1         4 delete $rec->{data};
262              
263             } elsif ($self->{version} == 5) {
264 15 50       52 my $c = crypts( $self->{appinfo}->{cipher} )
265             or croak('Unknown cipher ' . $self->{appinfo}->{cipher});
266 15         35 my $blocksize = $c->{blocksize};
267 15         42 my ($field, $extra) = _parse_field($rec->{data});
268 15         41 delete $rec->{data};
269              
270 15         51 $rec->{plaintext}->{0} = $field;
271 15         52 $rec->{ivec} = substr $extra, 0, $blocksize;
272 15         72 $rec->{encrypted} = substr $extra, $blocksize;
273              
274             } else {
275 0         0 croak "Unsupported Version $self->{version}";
276 0         0 return;
277             }
278              
279 16         50 return $rec;
280             }
281              
282             # PackRecord
283              
284             sub PackRecord
285             {
286 5     5 1 487 my $self = shift;
287 5         7 my $rec = shift;
288              
289 5 100       24 if ($self->{version} == 4) {
    50          
290 2 100       8 if ($rec->{encrypted}) {
291 1   33     5 my $name = $rec->{plaintext}->{0}->{data} || $EMPTY;
292 1         5 $rec->{data} = join $NULL, $name, $rec->{encrypted};
293 1         3 delete $rec->{plaintext};
294 1         4 delete $rec->{encrypted};
295             }
296              
297             } elsif ($self->{version} == 5) {
298 3         4 my $field;
299 3 50       7 if ($rec->{plaintext}->{0}) {
300 3         6 $field = $rec->{plaintext}->{0};
301             } else {
302 0         0 $field = {
303             'label' => 'name',
304             'label_id' => 0,
305             'data' => $EMPTY,
306             'font' => 0,
307             };
308             }
309 3         6 my $packed = _pack_field($field);
310              
311 3         19 $rec->{data} = join $EMPTY, $packed, $rec->{ivec}, $rec->{encrypted};
312              
313             } else {
314 0         0 croak "Unsupported Version $self->{version}";
315             }
316              
317 5         25 return $self->SUPER::PackRecord($rec, @_);
318             }
319              
320             # ParseAppInfoBlock
321              
322             sub ParseAppInfoBlock
323             {
324 6     6 1 22746 my $self = shift;
325 6         20 my $data = shift;
326 6         17 my $appinfo = {};
327              
328 6         34 &Palm::StdAppInfo::parse_StdAppInfo($appinfo, $data);
329              
330             # int8/uint8
331             # - Signed or Unsigned Byte (8 bits). C types: char, unsigned char
332             # int16/uint16
333             # - Signed or Unsigned Word (16 bits). C types: short, unsigned short
334             # int32/uint32
335             # - Signed or Unsigned Doubleword (32 bits). C types: int, unsigned int
336             # sz
337             # - Zero-terminated C-style string
338              
339 6 100       2020 if ($self->{version} == 4) {
    50          
340             # Nothing extra for version 4
341              
342             } elsif ($self->{version} == 5) {
343 5 50       22 _parse_appinfo_v5($appinfo) || return;
344              
345             } else {
346 0         0 croak "Unsupported Version $self->{version}";
347             }
348              
349 6         82 return $appinfo;
350             }
351              
352             sub _parse_appinfo_v5
353             {
354 5     5   13 my $appinfo = shift;
355              
356 5 50       25 if (! exists $appinfo->{other}) {
357             # XXX Corrupt appinfo?
358 0         0 return;
359             }
360              
361 5         12 my $unpackstr
362             = ("C1" x 8) # 8 uint8s in an array for the salt
363             . ("n1" x 2) # the iter (uint16) and the cipher (uint16)
364             . ("C1" x 8); # and finally 8 more uint8s for the hash
365              
366 5         10 my (@salt, $iter, $cipher, @hash);
367 5         101 (@salt[0..7], $iter, $cipher, @hash[0..7])
368             = unpack $unpackstr, $appinfo->{other};
369              
370 5         70 $appinfo->{salt} = sprintf "%02x" x 8, @salt;
371 5         17 $appinfo->{iter} = $iter;
372 5         16 $appinfo->{cipher} = $cipher;
373 5         27 $appinfo->{masterhash} = sprintf "%02x" x 8, @hash;
374 5         17 delete $appinfo->{other};
375              
376 5         37 return $appinfo
377             }
378              
379             # PackAppInfoBlock
380              
381             sub PackAppInfoBlock
382             {
383 2     2 1 353 my $self = shift;
384 2         2 my $retval;
385              
386 2 100       12 if ($self->{version} == 4) {
    50          
387             # Nothing to do for v4
388              
389             } elsif ($self->{version} == 5) {
390 1         4 _pack_appinfo_v5($self->{appinfo});
391             } else {
392 0         0 croak "Unsupported Version $self->{version}";
393             }
394 2         21 return &Palm::StdAppInfo::pack_StdAppInfo($self->{appinfo});
395             }
396              
397             sub _pack_appinfo_v5
398             {
399 1     1   2 my $appinfo = shift;
400              
401 1         3 my $packstr
402             = ("C1" x 8) # 8 uint8s in an array for the salt
403             . ("n1" x 2) # the iter (uint16) and the cipher (uint16)
404             . ("C1" x 8); # and finally 8 more uint8s for the hash
405              
406 1         9 my @salt = map { hex $_ } $appinfo->{salt} =~ /../gxm;
  8         16  
407 1         8 my @hash = map { hex $_ } $appinfo->{masterhash} =~ /../gxm;
  8         14  
408              
409 1         6 my $packed = pack($packstr,
410             @salt,
411             $appinfo->{iter},
412             $appinfo->{cipher},
413             @hash
414             );
415              
416 1         4 $appinfo->{other} = $packed;
417              
418 1         4 return $appinfo
419             }
420              
421             # Encrypt
422              
423             sub Encrypt
424             {
425 10     10 1 3469 my $self = shift;
426 10         18 my $rec = shift;
427 10   100     38 my $pass = shift || $self->{password};
428 10   66     50 my $data = shift || $rec->{plaintext};
429 10         18 my $ivec = shift;
430              
431 10 50 66     34 if ( ! $pass && ! $self->{appinfo}->{key}) {
432 0         0 croak("password not set!\n");
433             }
434              
435 10 50       43 if ( ! $rec) {
436 0         0 croak("Needed parameter 'record' not passed!\n");
437             }
438              
439 10 50       27 if ( ! $data) {
440 0         0 croak("Needed 'plaintext' not passed!\n");
441             }
442              
443 10 50 66     43 if ( $pass && ! $self->Password($pass)) {
444 0         0 croak("Incorrect Password!\n");
445             }
446              
447 10         14 my $acct;
448 10 100       35 if ($rec->{encrypted}) {
449 2         9 $acct = $self->Decrypt($rec, $pass);
450             }
451              
452 10         15 my $encrypted;
453 10 100       58 if ($self->{version} == 4) {
    50          
454 3   33     19 $self->{digest} ||= _calc_keys( $pass );
455 3         38 my $datav4 = {
456             name => $data->{0}->{data},
457             account => $data->{1}->{data},
458             password => $data->{2}->{data},
459             lastchange => $data->{3}->{data},
460             notes => $data->{255}->{data},
461             };
462 3         46 my $acctv4 = {
463             name => $acct->{0}->{data},
464             account => $acct->{1}->{data},
465             password => $acct->{2}->{data},
466             lastchange => $acct->{3}->{data},
467             notes => $acct->{255}->{data},
468             };
469 3         11 $encrypted = _encrypt_v4($datav4, $acctv4, $self->{digest});
470              
471             } elsif ($self->{version} == 5) {
472 7         33 ($encrypted, $ivec) = _encrypt_v5(
473             $data, $acct,
474             $self->{appinfo}->{key},
475             $self->{appinfo}->{cipher},
476             $ivec,
477             );
478 7 50       23 if (defined $ivec) {
479 7         18 $rec->{ivec} = $ivec;
480             }
481              
482             } else {
483 0         0 croak "Unsupported Version $self->{version}";
484             }
485              
486 10         30 $rec->{plaintext}->{0} = $data->{0};
487              
488 10 50       31 if ($encrypted) {
489 10 50       31 if ($encrypted eq '1') {
490 0         0 return 1;
491             }
492              
493 10         26 $rec->{attributes}{Dirty} = 1;
494 10         22 $rec->{attributes}{dirty} = 1;
495 10         22 $rec->{encrypted} = $encrypted;
496              
497 10         59 return 1;
498             } else {
499 0         0 return;
500             }
501             }
502              
503             sub _encrypt_v4
504             {
505 3     3   5 my $new = shift;
506 3         4 my $old = shift;
507 3         10 my $digest = shift;
508              
509 3   33     12 $new->{account} ||= $EMPTY;
510 3   33     10 $new->{password} ||= $EMPTY;
511 3   33     11 $new->{notes} ||= $EMPTY;
512              
513 3         4 my $changed = 0;
514 3         6 my $need_newdate = 0;
515 3 50 33     12 if ($old && %{ $old }) {
  3         20  
516 3     3   39591 no warnings 'uninitialized';
  3         7  
  3         22971  
517 3         4 foreach my $key (keys %{ $new }) {
  3         12  
518 8 100       21 next if $key eq 'lastchange';
519 6 100       34 if ($new->{$key} ne $old->{$key}) {
520 3         4 $changed = 1;
521 3         6 last;
522             }
523             }
524 3 100 33     55 if ( exists $new->{lastchange} && exists $old->{lastchange} && (
      66        
      33        
525             $new->{lastchange}->{day} != $old->{lastchange}->{day} ||
526             $new->{lastchange}->{month} != $old->{lastchange}->{month} ||
527             $new->{lastchange}->{year} != $old->{lastchange}->{year}
528             )) {
529 2         5 $changed = 1;
530 2         20 $need_newdate = 0;
531             } else {
532 1         3 $need_newdate = 1;
533             }
534              
535             } else {
536 0         0 $changed = 1;
537             }
538              
539             # no need to re-encrypt if it has not changed.
540 3 50       11 return 1 if ! $changed;
541              
542 3         5 my ($day, $month, $year);
543              
544 3 100 66     30 if ($new->{lastchange} && ! $need_newdate ) {
545 2   50     10 $day = $new->{lastchange}->{day} || 1;
546 2   50     10 $month = $new->{lastchange}->{month} || 0;
547 2   50     8 $year = $new->{lastchange}->{year} || 0;
548              
549             # XXX Need to actually validate the above information somehow
550 2 50       8 if ($year >= 1900) {
551 0         0 $year -= 1900;
552             }
553             } else {
554 1         3 $need_newdate = 1;
555             }
556              
557 3 100       9 if ($need_newdate) {
558 1         172 ($day, $month, $year) = (localtime)[3,4,5];
559             }
560              
561 3         21 my $packed_date = _pack_keyring_date( {
562             year => $year,
563             month => $month,
564             day => $day,
565             });
566              
567 3         20 my $plaintext = join $NULL,
568             $new->{account}, $new->{password}, $new->{notes}, $packed_date;
569              
570 3         10 return _crypt3des( $plaintext, $digest, $ENCRYPT );
571             }
572              
573             sub _encrypt_v5
574             {
575 7     7   12 my $new = shift;
576 7         10 my $old = shift;
577 7         13 my $key = shift;
578 7         13 my $cipher = shift;
579 7         13 my $ivec = shift;
580 7 50       16 my $c = crypts($cipher) or croak('Unknown cipher ' . $cipher);
581              
582 7 50       24 if (! defined $ivec) {
583 7         20 $ivec = pack("C*",map {rand(256)} 1..$c->{blocksize});
  7         40  
584             }
585              
586 7         10 my $changed = 0;
587 7         11 my $need_newdate = 1;
588 7 50       25 if ($new->{3}->{data}) {
589 7         13 $need_newdate = 0;
590             }
591 7         11 foreach my $k (keys %{ $new }) {
  7         38  
592 24 100       51 if (! $old) {
    100          
593 20         38 $changed = 1;
594             } elsif ($k == 3) {
595 1 50 33     26 if ($old && (
      33        
      33        
596             $new->{$k}{data}{day} == $old->{$k}{data}{day} &&
597             $new->{$k}{data}{month} == $old->{$k}{data}{month} &&
598             $new->{$k}{data}{year} == $old->{$k}{data}{year}
599             )) {
600 1         12 $changed = 1;
601 1         4 $need_newdate = 1;
602             }
603              
604             } else {
605 3         5 my $n = join ':', sort %{ $new->{$k} };
  3         36  
606 3         7 my $o = join ':', sort %{ $old->{$k} };
  3         37  
607 3 100       26 if ($n ne $o) {
608 1         5 $changed = 1;
609             }
610             }
611             }
612              
613 7 50       23 return 1, 0 if $changed == 0;
614              
615 7 100       14 if ($need_newdate) {
616 1         208 my ($day, $month, $year) = (localtime)[3,4,5];
617 1         9 $new->{3} = {
618             label => 'lastchange',
619             label_id => 3,
620             font => 0,
621             data => {
622             year => $year,
623             month => $month,
624             day => $day,
625             },
626             };
627             } else {
628             # XXX Need to actually validate the above information somehow
629 6 50       27 if ($new->{3}->{data}->{year} >= 1900) {
630 0         0 $new->{3}->{data}->{year} -= 1900;
631             }
632             }
633              
634 7         12 my $plaintext;
635 7         10 foreach my $k (keys %{ $new }) {
  7         23  
636 24 100       70 next if $new->{$k}->{label_id} == 0;
637 17         51 $plaintext .= _pack_field($new->{$k});
638             }
639 7         17 $plaintext .= chr(0xff) x 2;
640              
641 7         43 my $encrypted;
642 7 50 0     23 if ($c->{name} eq 'None') {
    0          
643             # do nothing
644 7         11 $encrypted = $plaintext;
645              
646             } elsif ($c->{name} eq 'DES_EDE3' or $c->{name} eq 'Rijndael') {
647 0         0 require Crypt::CBC;
648 0         0 my $cbc = Crypt::CBC->new(
649             -key => $key,
650             -literal_key => 1,
651             -iv => $ivec,
652             -cipher => $c->{name},
653             -keysize => $c->{keylen},
654             -blocksize => $c->{blocksize},
655             -header => 'none',
656             -padding => 'oneandzeroes',
657             );
658              
659 0 0       0 if (! $c) {
660 0         0 croak("Unable to set up encryption!");
661             }
662              
663 0         0 $encrypted = $cbc->encrypt($plaintext);
664              
665             } else {
666 0         0 croak "Unsupported Crypt $c->{name}";
667             }
668              
669 7         35 return $encrypted, $ivec;
670             }
671              
672             # Decrypt
673              
674             sub Decrypt
675             {
676 26     26 1 608 my $self = shift;
677 26         42 my $rec = shift;
678 26   100     116 my $pass = shift || $self->{password};
679              
680 26 100 66     120 if ( ! $pass && ! $self->{appinfo}->{key}) {
681 2         473 croak("password not set!\n");
682             }
683              
684 24 50       68 if ( ! $rec) {
685 0         0 croak("Needed parameter 'record' not passed!\n");
686             }
687              
688 24 50 66     109 if ( $pass && ! $self->Password($pass)) {
689 0         0 croak("Invalid Password!\n");
690             }
691              
692 24 50       98 if ( ! $rec->{encrypted} ) {
693 0         0 croak("No encrypted content!");
694             }
695              
696 24         37 my $plaintext;
697 24 100       106 if ($self->{version} == 4) {
    50          
698 6   33     18 $self->{digest} ||= _calc_keys( $pass );
699 6         19 my $acct = _decrypt_v4($rec->{encrypted}, $self->{digest});
700 6         98 $plaintext = {
701             0 => $rec->{plaintext}->{0},
702             1 => {
703             label => 'account',
704             label_id => 1,
705             font => 0,
706             data => $acct->{account},
707             },
708             2 => {
709             label => 'password',
710             label_id => 2,
711             font => 0,
712             data => $acct->{password},
713             },
714             3 => {
715             label => 'lastchange',
716             label_id => 3,
717             font => 0,
718             data => $acct->{lastchange},
719             },
720             255 => {
721             label => 'notes',
722             label_id => 255,
723             font => 0,
724             data => $acct->{notes},
725             },
726             };
727              
728             } elsif ($self->{version} == 5) {
729 18         80 $plaintext = _decrypt_v5(
730             $rec->{encrypted}, $self->{appinfo}->{key},
731             $self->{appinfo}->{cipher}, $rec->{ivec},
732             );
733 18   33     107 $plaintext->{0} ||= $rec->{plaintext}->{0};
734              
735             } else {
736 0         0 croak "Unsupported Version $self->{version}";
737             }
738              
739 24 50       63 if ($plaintext) {
740 24         46 $rec->{plaintext} = $plaintext;
741 24         146 return $plaintext;
742             }
743 0         0 return;
744             }
745              
746             sub _decrypt_v4
747             {
748 6     6   13 my $encrypted = shift;
749 6         10 my $digest = shift;
750              
751 6         15 my $plaintext = _crypt3des( $encrypted, $digest, $DECRYPT );
752 6         65 my ( $account, $password, $notes, $packed_date )
753             = split /$NULL/xm, $plaintext, 4;
754              
755 6         15 my $modified;
756 6 50       17 if ($packed_date) {
757 6         17 $modified = _parse_keyring_date($packed_date);
758             }
759              
760             return {
761 6         52 account => $account,
762             password => $password,
763             notes => $notes,
764             lastchange => $modified,
765             };
766             }
767              
768             sub _decrypt_v5
769             {
770              
771 18     18   45 my $encrypted = shift;
772 18         34 my $key = shift;
773 18         33 my $cipher = shift;
774 18         41 my $ivec = shift;
775              
776 18 50       45 my $c = crypts($cipher) or croak('Unknown cipher ' . $cipher);
777              
778 18         27 my $plaintext;
779              
780 18 50 0     69 if ($c->{name} eq 'None') {
    0          
781             # do nothing
782 18         39 $plaintext = $encrypted;
783              
784             } elsif ($c->{name} eq 'DES_EDE3' or $c->{name} eq 'Rijndael') {
785 0         0 require Crypt::CBC;
786 0         0 my $cbc = Crypt::CBC->new(
787             -key => $key,
788             -literal_key => 1,
789             -iv => $ivec,
790             -cipher => $c->{name},
791             -keysize => $c->{keylen},
792             -blocksize => $c->{blocksize},
793             -header => 'none',
794             -padding => 'oneandzeroes',
795             );
796              
797 0 0       0 if (! $c) {
798 0         0 croak("Unable to set up encryption!");
799             }
800 0         0 my $len = $c->{blocksize} - length($encrypted) % $c->{blocksize};
801 0         0 $encrypted .= $NULL x $len;
802 0         0 $plaintext = $cbc->decrypt($encrypted);
803              
804             } else {
805 0         0 croak "Unsupported Crypt $c->{name}";
806             }
807              
808 18         25 my %fields;
809 18         54 while ($plaintext) {
810 63         109 my $field;
811 63         129 ($field, $plaintext) = _parse_field($plaintext);
812 63 100       203 if (! $field) {
813 18         37 last;
814             }
815 45         230 $fields{ $field->{label_id} } = $field;
816             }
817              
818 18         61 return \%fields;
819             }
820              
821             # Password
822              
823             sub Password
824             {
825 31     31 1 10631 my $self = shift;
826 31         47 my $pass = shift;
827 31         48 my $new_pass = shift;
828              
829 31 100       78 if (! $pass) {
830 4         11 delete $self->{password};
831 4         15 delete $self->{appinfo}->{key};
832 4         16 return 1;
833             }
834              
835 27 100 100     310 if (
      100        
      66        
836             ($self->{version} == 4 && ! exists $self->{encpassword}) ||
837             ($self->{version} == 5 && ! exists $self->{appinfo}->{masterhash})
838             ) {
839 2         9 return $self->_password_update($pass);
840             }
841              
842 25 100       76 if ($new_pass) {
843 2         6 my @accts = ();
844 2         4 foreach my $rec (@{ $self->{records} }) {
  2         8  
845 4         16 my $acct = $self->Decrypt($rec, $pass);
846 4 50       16 if ( ! $acct ) {
847 0         0 croak("Couldn't decrypt $rec->{plaintext}->{0}->{data}");
848             }
849 4         14 push @accts, $acct;
850             }
851              
852 2 50       15 if ( ! $self->_password_update($new_pass)) {
853 0         0 croak("Couldn't set new password!");
854             }
855 2         7 $pass = $new_pass;
856              
857 2         10 foreach my $i (0..$#accts) {
858 4         15 delete $self->{records}->[$i]->{encrypted};
859 4         12 $self->{records}->[$i]->{plaintext} = $accts[$i];
860 4         18 $self->Encrypt($self->{records}->[$i], $pass);
861             }
862             }
863              
864 25 100 66     121 if (defined $self->{password} && $pass eq $self->{password}) {
865             # already verified this password
866 10         58 return 1;
867             }
868              
869 15 100       80 if ($self->{version} == 4) {
    50          
870 2         11 my $valid = _password_verify_v4($pass, $self->{encpassword});
871              
872             # May as well generate the keys we need now,
873             # since we know the password is right
874 2 50       7 if ($valid) {
875 2         7 $self->{digest} = _calc_keys($pass);
876 2 50       8 if ($self->{digest} ) {
877 2         5 $self->{password} = $pass;
878 2         14 return 1;
879             }
880             }
881             } elsif ($self->{version} == 5) {
882 13         46 return _password_verify_v5($self->{appinfo}, $pass);
883             } else {
884 0         0 croak "Unsupported version $self->{version}";
885             }
886              
887 0         0 return;
888             }
889              
890             sub _password_verify_v4
891             {
892 2     2   18 require Digest::MD5;
893 2         75 import Digest::MD5 qw(md5);
894              
895 2         4 my $pass = shift;
896 2         4 my $data = shift;
897              
898 2 50       10 if (! $pass) { croak('No password specified!'); };
  0         0  
899              
900             # XXX die "No encrypted password in file!" unless defined $data;
901 2 50       8 if ( ! defined $data) { return; };
  0         0  
902              
903 2         26 $data =~ s/$NULL$//xm;
904              
905 2         9 my $salt = substr $data, 0, $kSalt_Size;
906              
907 2         118 my $msg = $salt . $pass;
908 2         11 $msg .= "\0" x ( $MD5_CBLOCK - length $msg );
909              
910 2         16 my $digest = md5($msg);
911              
912 2 50       18 if ($data ne $salt . $digest ) {
913 0         0 return;
914             }
915              
916 2         9 return 1;
917             }
918              
919             sub _password_verify_v5
920             {
921 13     13   22 my $appinfo = shift;
922 13         21 my $pass = shift;
923              
924 13         54 my $salt = pack("H*", $appinfo->{salt});
925              
926 13 50       39 my $c = crypts($appinfo->{cipher})
927             or croak('Unknown cipher ' . $appinfo->{cipher});
928 13         68 my ($key, $hash) = _calc_key_v5(
929             $pass, $salt, $appinfo->{iter},
930             $c->{keylen},
931             $c->{DES_odd_parity},
932             );
933              
934             #print "Iter: '" . $appinfo->{iter} . "'\n";
935             #print "Key: '". unpack("H*", $key) . "'\n";
936             #print "Salt: '". unpack("H*", $salt) . "'\n";
937             #print "Hash: '". $hash . "'\n";
938             #print "Hash: '". $appinfo->{masterhash} . "'\n";
939              
940 13 50       76 if ($appinfo->{masterhash} eq $hash) {
941 13         36 $appinfo->{key} = $key;
942             } else {
943 0         0 return;
944             }
945              
946 13         110 return $key;
947             }
948              
949              
950             sub _password_update
951             {
952             # It is very important to Encrypt after calling this
953             # (Although it is generally only called by Encrypt)
954             # because otherwise the data will be out of sync with the
955             # password, and that would suck!
956 4     4   56 my $self = shift;
957 4         9 my $pass = shift;
958              
959 4 100       26 if ($self->{version} == 4) {
    50          
960 2         9 my $data = _password_update_v4($pass, @_);
961              
962 2 50       7 if (! $data) {
963 0         0 carp("Failed to update password!");
964 0         0 return;
965             }
966              
967             # AFAIK the thing we use to test the password is
968             # always in the first entry
969 2         11 $self->{encpassword} = $data;
970 2         7 $self->{password} = $pass;
971 2         8 $self->{digest} = _calc_keys( $self->{password} );
972              
973 2         10 return 1;
974              
975             } elsif ($self->{version} == 5) {
976 2   33     14 my $cipher = shift || $self->{appinfo}->{cipher};
977 2   33     20 my $iter = shift || $self->{appinfo}->{iter};
978 2   50     11 my $salt = shift || 0;
979              
980 2         9 my $hash = _password_update_v5(
981             $self->{appinfo}, $pass, $cipher, $iter, $salt
982             );
983              
984 2 50       7 if (! $hash) {
985 0         0 carp("Failed to update password!");
986 0         0 return;
987             }
988              
989 2         10 return 1;
990             } else {
991 0         0 croak("Unsupported version ($self->{version})");
992             }
993              
994 0         0 return;
995             }
996              
997             sub _password_update_v4
998             {
999 2     2   14 require Digest::MD5;
1000 2         71 import Digest::MD5 qw(md5);
1001              
1002 2         4 my $pass = shift;
1003            
1004 2 50       7 if (! defined $pass) { croak('No password specified!'); };
  0         0  
1005              
1006 2         3 my $salt;
1007 2         7 for ( 1 .. $kSalt_Size ) {
1008 8         22 $salt .= chr int rand 255;
1009             }
1010              
1011 2         5 my $msg = $salt . $pass;
1012              
1013 2         9 $msg .= "\0" x ( $MD5_CBLOCK - length $msg );
1014              
1015 2         11 my $digest = md5($msg);
1016              
1017 2         5 my $data = $salt . $digest; # . "\0";
1018              
1019 2         5 return $data;
1020             }
1021              
1022             sub _password_update_v5
1023             {
1024 2     2   4 my $appinfo = shift;
1025 2         4 my $pass = shift;
1026 2         4 my $cipher = shift;
1027 2         4 my $iter = shift;
1028              
1029             # I thought this needed to be 'blocksize', but apparently not.
1030             #my $length = $CRYPTS[ $cipher ]{blocksize};
1031 2         4 my $length = 8;
1032 2   33     18 my $salt = shift || pack("C*",map {rand(256)} 1..$length);
1033              
1034 2 50       7 my $c = crypts($cipher) or croak('Unknown cipher ' . $cipher);
1035 2         21 my ($key, $hash) = _calc_key_v5(
1036             $pass, $salt, $iter,
1037             $c->{keylen},
1038             $c->{DES_odd_parity},
1039             );
1040              
1041 2         12 $appinfo->{salt} = unpack "H*", $salt;
1042 2         5 $appinfo->{iter} = $iter;
1043 2         5 $appinfo->{cipher} = $cipher;
1044 2         7 $appinfo->{masterhash} = $hash;
1045 2         3 $appinfo->{key} = $key;
1046              
1047 2         8 return $key;
1048             }
1049              
1050             sub Unlock
1051             {
1052 2     2 1 1828 my $self = shift;
1053 2         6 my ($pass) = @_;
1054 2   33     9 $pass ||= $self->{password};
1055              
1056 2 50 33     13 if ( $pass && ! $self->Password($pass)) {
1057 0         0 croak("Invalid Password!\n");
1058             }
1059              
1060 2         6 foreach my $rec (@{ $self->{records} }) {
  2         11  
1061 4         14 $self->Decrypt($rec);
1062             }
1063              
1064 2         16 return 1;
1065              
1066             }
1067              
1068             sub Lock
1069             {
1070 2     2 1 6 my $self = shift;
1071              
1072 2         11 $self->Password();
1073              
1074 2         3 foreach my $rec (@{ $self->{records} }) {
  2         9  
1075 4         8 my $name = $rec->{plaintext}->{0};
1076 4         9 delete $rec->{plaintext};
1077 4         15 $rec->{plaintext}->{0} = $name;
1078             }
1079              
1080 2         11 return 1;
1081             }
1082              
1083             # Helpers
1084              
1085             sub _calc_keys
1086             {
1087 4     4   28 my $pass = shift;
1088 4 50       17 if (! defined $pass) { croak('No password defined!'); };
  0         0  
1089              
1090 4         14 my $digest = md5($pass);
1091              
1092 4         21 my ( $key1, $key2 ) = unpack 'a8a8', $digest;
1093              
1094             #--------------------------------------------------
1095             # print "key1: $key1: ", length $key1, "\n";
1096             # print "key2: $key2: ", length $key2, "\n";
1097             #--------------------------------------------------
1098              
1099 4         18 $digest = unpack 'H*', $key1 . $key2 . $key1;
1100              
1101             #--------------------------------------------------
1102             # print "Digest: ", $digest, "\n";
1103             # print length $digest, "\n";
1104             #--------------------------------------------------
1105              
1106 4         13 return $digest;
1107             }
1108              
1109             sub _calc_key_v5
1110             {
1111 15     15   50 my ($pass, $salt, $iter, $keylen, $dop) = @_;
1112              
1113 15         139 require Digest::HMAC_SHA1;
1114 15         437 import Digest::HMAC_SHA1 qw(hmac_sha1);
1115 15         1905 require Digest::SHA1;
1116 15         2561 import Digest::SHA1 qw(sha1);
1117              
1118 15         55 my $key = _pbkdf2( $pass, $salt, $iter, $keylen, \&hmac_sha1 );
1119 15 50       50 if ($dop) { $key = _DES_odd_parity($key); }
  0         0  
1120              
1121 15         155 my $hash = unpack("H*", substr(sha1($key.$salt),0, 8));
1122              
1123 15         66 return $key, $hash;
1124             }
1125              
1126             sub _crypt3des
1127             {
1128 9     9   53 require Crypt::DES;
1129              
1130 9         30 my ( $plaintext, $passphrase, $flag ) = @_;
1131              
1132 9         21 $passphrase .= $SPACE x ( 16 * 3 );
1133 9         13 my $cyphertext = $EMPTY;
1134              
1135 9         20 my $size = length $plaintext;
1136              
1137             #print "STRING: '$plaintext' - Length: " . (length $plaintext) . "\n";
1138              
1139 9         15 my @C;
1140 9         22 for ( 0 .. 2 ) {
1141 27         368 $C[$_] =
1142             new Crypt::DES( pack 'H*', ( substr $passphrase, 16 * $_, 16 ));
1143             }
1144              
1145 9         147 for ( 0 .. ( ($size) / 8 ) ) {
1146 60         200 my $pt = substr $plaintext, $_ * 8, 8;
1147              
1148             #print "PT: '$pt' - Length: " . length($pt) . "\n";
1149 60 100       193 if (! length $pt) { next; };
  6         20  
1150 54 100       150 if ( (length $pt) < 8 ) {
1151 3 50       9 if ($flag == $DECRYPT) { croak('record not 8 byte padded'); };
  0         0  
1152 3         8 my $len = 8 - (length $pt);
1153 3         9 $pt .= ($NULL x $len);
1154             }
1155 54 100       119 if ( $flag == $ENCRYPT ) {
1156 18         54 $pt = $C[0]->encrypt($pt);
1157 18         213 $pt = $C[1]->decrypt($pt);
1158 18         160 $pt = $C[2]->encrypt($pt);
1159             }
1160             else {
1161 36         106 $pt = $C[0]->decrypt($pt);
1162 36         369 $pt = $C[1]->encrypt($pt);
1163 36         345 $pt = $C[2]->decrypt($pt);
1164             }
1165              
1166             #print "PT: '$pt' - Length: " . length($pt) . "\n";
1167 54         516 $cyphertext .= $pt;
1168             }
1169              
1170 9         92 $cyphertext =~ s/$NULL+$//xm;
1171              
1172             #print "CT: '$cyphertext' - Length: " . length($cyphertext) . "\n";
1173              
1174 9         81 return $cyphertext;
1175             }
1176              
1177             sub _parse_field
1178             {
1179 78     78   174 my $field = shift;
1180              
1181 78         270 my ($len) = unpack "n", $field;
1182 78 100       353 if ($len + 4 > length $field) {
1183 18         103 return undef, $field;
1184             }
1185 60         273 my $unpackstr = "x2 C1 C1 A$len";
1186 60         135 my $offset = 2 +1 +1 +$len;
1187 60 100       203 if ($len % 2) {
1188             # trim the 0/1 byte padding for next even address.
1189 15         25 $offset++;
1190 15         32 $unpackstr .= ' x'
1191             }
1192              
1193 60         327 my ($label, $font, $data) = unpack $unpackstr, $field;
1194 60         205 my $leftover = substr $field, $offset;
1195              
1196 60         108 my $label_id = $label;
1197 60         123 my $l = labels($label);
1198 60 50       145 if ($l) {
1199 60   33     193 $label = $l->{name} || $l->{id};
1200 60         113 $label_id = $l->{id};
1201             }
1202              
1203 60 100 100     251 if ($label_id && $label_id == 3) {
1204 18         48 ($data) = substr $field, 4, $len;
1205 18         42 $data = _parse_keyring_date($data);
1206             }
1207             return {
1208             #len => $len,
1209 60         627 label => $label,
1210             label_id => $label_id,
1211             font => $font,
1212             data => $data,
1213             }, $leftover;
1214             }
1215              
1216             sub _pack_field
1217             {
1218 20     20   32 my $field = shift;
1219              
1220 20         21 my $packed;
1221 20 50       38 if (defined $field) {
1222 20   100     62 my $label = $field->{label_id} || 0;
1223 20 100 66     91 if (defined $field->{label} && ! $label) {
1224 3         6 $label = $field->{label};
1225             }
1226              
1227 20         44 my $l = labels($field->{label});
1228 20 50       58 if ($l) {
1229 20         34 $label = $l->{id};
1230             }
1231              
1232 20   50     84 my $font = $field->{font} || 0;
1233 20 50       64 my $data = defined $field->{data} ? $field->{data} : $EMPTY;
1234              
1235 20 100 100     94 if ($label && $label == 3) {
1236 7         16 $data = _pack_keyring_date($data);
1237             }
1238 20         38 my $len = length $data;
1239 20         27 my $packstr = "n1 C1 C1 A*";
1240              
1241 20         67 $packed = pack $packstr, ($len, $label, $font, $data);
1242              
1243 20 100       93 if ($len % 2) {
1244             # add byte padding for next even address.
1245 6         19 $packed .= $NULL;
1246             }
1247             } else {
1248 0         0 my $packstr = "n1 C1 C1 x1";
1249 0         0 $packed = pack $packstr, 0, 0, 0;
1250             }
1251              
1252 20         75 return $packed;
1253             }
1254              
1255             sub _parse_keyring_date
1256             {
1257 24     24   50 my $data = shift;
1258              
1259 24         78 my $u = unpack 'n', $data;
1260 24         94 my $year = (($u & 0xFE00) >> 9) + 4; # since 1900
1261 24         88 my $month = (($u & 0x01E0) >> 5) - 1; # 0-11
1262 24         257 my $day = (($u & 0x001F) >> 0); # 1-31
1263              
1264             return {
1265 24   50     298 year => $year,
      50        
1266             month => $month || 0,
1267             day => $day || 1,
1268             };
1269             }
1270              
1271             sub _pack_keyring_date
1272             {
1273 10     10   17 my $d = shift;
1274 10         22 my $year = $d->{year};
1275 10         21 my $month = $d->{month};
1276 10         22 my $day = $d->{day};
1277              
1278 10         18 $year -= 4;
1279 10         16 $month++;
1280              
1281 10         72 return pack 'n*', $day | ($month << 5) | ($year << 9);
1282             }
1283              
1284              
1285             sub _hexdump
1286             {
1287 0     0   0 my $prefix = shift; # What to print in front of each line
1288 0         0 my $data = shift; # The data to dump
1289 0         0 my $maxlines = shift; # Max # of lines to dump
1290 0         0 my $offset; # Offset of current chunk
1291              
1292 0         0 for ($offset = 0; $offset < length($data); $offset += 16)
1293             {
1294 0         0 my $hex; # Hex values of the data
1295             my $ascii; # ASCII values of the data
1296 0         0 my $chunk; # Current chunk of data
1297              
1298 0 0 0     0 last if defined($maxlines) && ($offset >= ($maxlines * 16));
1299              
1300 0         0 $chunk = substr($data, $offset, 16);
1301              
1302 0         0 ($hex = $chunk) =~ s/./sprintf "%02x ", ord($&)/ges;
  0         0  
1303              
1304 0         0 ($ascii = $chunk) =~ y/\040-\176/./c;
1305              
1306 0         0 printf "%s %-48s|%-16s|\n", $prefix, $hex, $ascii;
1307             }
1308             }
1309              
1310             sub _bindump
1311             {
1312 0     0   0 my $prefix = shift; # What to print in front of each line
1313 0         0 my $data = shift; # The data to dump
1314 0         0 my $maxlines = shift; # Max # of lines to dump
1315 0         0 my $offset; # Offset of current chunk
1316              
1317 0         0 for ($offset = 0; $offset < length($data); $offset += 8)
1318             {
1319 0         0 my $bin; # binary values of the data
1320             my $ascii; # ASCII values of the data
1321 0         0 my $chunk; # Current chunk of data
1322              
1323 0 0 0     0 last if defined($maxlines) && ($offset >= ($maxlines * 8));
1324              
1325 0         0 $chunk = substr($data, $offset, 8);
1326              
1327 0         0 ($bin = $chunk) =~ s/./sprintf "%08b ", ord($&)/ges;
  0         0  
1328              
1329 0         0 ($ascii = $chunk) =~ y/\040-\176/./c;
1330              
1331 0         0 printf "%s %-72s|%-8s|\n", $prefix, $bin, $ascii;
1332             }
1333             }
1334              
1335             # Thanks to Jochen Hoenicke
1336             # (one of the authors of Palm Keyring)
1337             # for these next two subs.
1338              
1339             # Usage pbkdf2(password, salt, iter, keylen, prf)
1340             # iter is number of iterations
1341             # keylen is length of generated key in bytes
1342             # prf is the pseudo random function (e.g. hmac_sha1)
1343             # returns the key.
1344             sub _pbkdf2($$$$$)
1345             {
1346 15     15   37 my ($password, $salt, $iter, $keylen, $prf) = @_;
1347 15         24 my ($k, $t, $u, $ui, $i);
1348 15         23 $t = "";
1349 15         50 for ($k = 1; length($t) < $keylen; $k++) {
1350 15         77 $u = $ui = &$prf($salt.pack('N', $k), $password);
1351 15         433 for ($i = 1; $i < $iter; $i++) {
1352 7485         18168 $ui = &$prf($ui, $password);
1353 7485         147994 $u ^= $ui;
1354             }
1355 15         71 $t .= $u;
1356             }
1357 15         64 return substr($t, 0, $keylen);
1358             }
1359              
1360             sub _DES_odd_parity($) {
1361 0     0     my $key = $_[0];
1362 0           my ($r, $i);
1363 0           my @odd_parity = (
1364             1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14,
1365             16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31,
1366             32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47,
1367             49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62,
1368             64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79,
1369             81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94,
1370             97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110,
1371             112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127,
1372             128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143,
1373             145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158,
1374             161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174,
1375             176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191,
1376             193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206,
1377             208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223,
1378             224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239,
1379             241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254);
1380 0           for ($i = 0; $i< length($key); $i++) {
1381 0           $r .= chr($odd_parity[ord(substr($key, $i, 1))]);
1382             }
1383 0           return $r;
1384             }
1385              
1386             1;
1387             __END__