File Coverage

blib/lib/Palm/Keyring.pm
Criterion Covered Total %
statement 136 574 23.6
branch 36 224 16.0
condition 14 173 8.0
subroutine 17 36 47.2
pod 9 9 100.0
total 212 1016 20.8


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