[BACK]Return to Keyring.pm CVS log [TXT][DIR] Up to [local] / palm / Palm-Keyring / lib / Palm

Diff for /palm/Palm-Keyring/lib/Palm/Keyring.pm between version 1.44 and 1.52

version 1.44, 2007/02/23 22:11:33 version 1.52, 2007/12/04 03:33:34
Line 1 
Line 1 
 package Palm::Keyring;  package Palm::Keyring;
 # $RedRiver: Keyring.pm,v 1.43 2007/02/23 22:05:17 andrew Exp $  # $RedRiver: Keyring.pm,v 1.51 2007/09/13 15:46:09 andrew Exp $
 ########################################################################  ########################################################################
 # Keyring.pm *** Perl class for Keyring for Palm OS databases.  # Keyring.pm *** Perl class for Keyring for Palm OS databases.
 #  #
Line 59 
Line 59 
     },      },
 );  );
   
   my %LABELS = (
       0 => {
           id   => 0,
           name => 'name',
       },
       1 => {
           id   => 1,
           name => 'account',
       },
       2 => {
           id   => 2,
           name => 'password',
       },
       3 => {
           id   => 3,
           name => 'lastchange',
       },
       255 => {
           id   => 255,
           name => 'notes',
       },
   );
   
 our $VERSION = 0.95;  
   
   our $VERSION = '0.96_06';
   
 sub new  sub new
 {  {
     my $classname = shift;      my $classname = shift;
     my $options = {};      my $options = {};
   
     # hashref arguments      if (@_) {
     if (ref $_[0] eq 'HASH') {          # hashref arguments
       $options = shift;          if (ref $_[0] eq 'HASH') {
     }            $options = shift;
           }
   
     # CGI style arguments          # CGI style arguments
     elsif ($_[0] =~ /^-[a-zA-Z0-9_]{1,20}$/) {          elsif ($_[0] =~ /^-[a-zA-Z0-9_]{1,20}$/) {
       my %tmp = @_;            my %tmp = @_;
       while ( my($key,$value) = each %tmp) {            while ( my($key,$value) = each %tmp) {
         $key =~ s/^-//;              $key =~ s/^-//;
         $options->{lc $key} = $value;              $options->{lc $key} = $value;
       }            }
     }          }
   
     else {          else {
         $options->{password} = shift;              $options->{password} = shift;
         $options->{version}  = shift;              $options->{version}  = shift;
           }
     }      }
   
     # Create a generic PDB. No need to rebless it, though.      # Create a generic PDB. No need to rebless it, though.
Line 132 
Line 157 
 sub crypts  sub crypts
 {  {
     my $crypt = shift;      my $crypt = shift;
     if (! defined $crypt || ! length $crypt) {      if ((! defined $crypt) || (! length $crypt)) {
         return;          return;
     } elsif ($crypt =~ /\D/) {      } elsif ($crypt =~ /\D/) {
         foreach my $c (@CRYPTS) {          foreach my $c (@CRYPTS) {
Line 147 
Line 172 
     }      }
 }  }
   
   sub labels
   {
       my $label = shift;
   
       if ((! defined $label) || (! length $label)) {
           return;
       } elsif (exists $LABELS{$label}) {
           return $LABELS{$label};
       } else {
           foreach my $l (keys %LABELS) {
               if ($LABELS{$l}{name} eq $label) {
                   return $LABELS{$l};
               }
           }
   
           # didn't find it, make one.
           if ($label =~ /^\d+$/) {
               return {
                   id => $label,
                   name => undef,
               };
           } else {
               return;
           }
       }
   }
   
   # Write
   
   sub Write
   {
       my $self = shift;
   
       if ($self->{version} == 4) {
          # Give the PDB the first record that will hold the encrypted password
           my $rec = $self->new_Record;
           $rec->{data} = $self->{encpassword};
   
           if (ref $self->{records} eq 'ARRAY') {
               unshift @{ $self->{records} }, $rec;
           } else {
               $self->{records} = [ $rec ];
           }
       }
   
       my $rc = $self->SUPER::Write(@_);
   
       if ($self->{version} == 4) {
           shift @{ $self->{records} };
       }
   
       return $rc;
   }
   
 # ParseRecord  # ParseRecord
   
 sub ParseRecord  sub ParseRecord
Line 158 
Line 237 
   
     if ($self->{version} == 4) {      if ($self->{version} == 4) {
         # skip the first record because it contains the password.          # skip the first record because it contains the password.
         return $rec if ! exists $self->{records};          if (! exists $self->{records}) {
               $self->{encpassword} = $rec->{data};
               return '__DELETE_ME__';
           }
   
           if ($self->{records}->[0] eq '__DELETE_ME__') {
               shift @{ $self->{records} };
           }
   
         my ( $name, $encrypted ) = split /$NULL/xm, $rec->{data}, 2;          my ( $name, $encrypted ) = split /$NULL/xm, $rec->{data}, 2;
   
         return $rec if ! $encrypted;          return $rec if ! $encrypted;
         $rec->{name}      = $name;          $rec->{plaintext}->{0} = {
               label => 'name',
               label_id => 0,
               data  => $name,
               font  => 0,
           };
         $rec->{encrypted} = $encrypted;          $rec->{encrypted} = $encrypted;
         delete $rec->{data};          delete $rec->{data};
   
Line 174 
Line 265 
         my ($field, $extra) = _parse_field($rec->{data});          my ($field, $extra) = _parse_field($rec->{data});
         delete $rec->{data};          delete $rec->{data};
   
         $rec->{name}      = $field->{data};          $rec->{plaintext}->{0} = $field;
         $rec->{ivec}      = substr $extra, 0, $blocksize;          $rec->{ivec}      = substr $extra, 0, $blocksize;
         $rec->{encrypted} = substr $extra, $blocksize;          $rec->{encrypted} = substr $extra, $blocksize;
   
     } else {      } else {
         die 'Unsupported Version';          croak "Unsupported Version $self->{version}";
         return;          return;
     }      }
   
Line 195 
Line 286 
   
     if ($self->{version} == 4) {      if ($self->{version} == 4) {
         if ($rec->{encrypted}) {          if ($rec->{encrypted}) {
             if (! defined $rec->{name}) {              my $name = $rec->{plaintext}->{0}->{data} || $EMPTY;
                 $rec->{name} = $EMPTY;              $rec->{data} = join $NULL, $name, $rec->{encrypted};
             }              delete $rec->{plaintext};
             $rec->{data} = join $NULL, $rec->{name}, $rec->{encrypted};  
             delete $rec->{name};  
             delete $rec->{encrypted};              delete $rec->{encrypted};
         }          }
   
     } elsif ($self->{version} == 5) {      } elsif ($self->{version} == 5) {
         my $field;          my $field;
         if ($rec->{name}) {          if ($rec->{plaintext}->{0}) {
             $field = {              $field = $rec->{plaintext}->{0};
                 'label_id' => 1,  
                 'data'     => $rec->{name},  
                 'font'     => 0,  
             };  
         } else {          } else {
             $field = {              $field = {
                 'label_id' => $EMPTY,                  'label'    => 'name',
                   'label_id' => 0,
                 'data'     => $EMPTY,                  'data'     => $EMPTY,
                 'font'     => 0,                  'font'     => 0,
             };              };
         }          }
         my $packed = _pack_field($field);          my $packed = _pack_field($field);
   
         $rec->{data} = join '', $packed, $rec->{ivec}, $rec->{encrypted};          $rec->{data} = join $EMPTY, $packed, $rec->{ivec}, $rec->{encrypted};
   
     } else {      } else {
         die 'Unsupported Version';          croak "Unsupported Version $self->{version}";
     }      }
   
     return $self->SUPER::PackRecord($rec, @_);      return $self->SUPER::PackRecord($rec, @_);
Line 255 
Line 341 
         _parse_appinfo_v5($appinfo) || return;          _parse_appinfo_v5($appinfo) || return;
   
     } else {      } else {
         die "Unsupported Version";          croak "Unsupported Version $self->{version}";
         return;  
     }      }
   
     return $appinfo;      return $appinfo;
Line 302 
Line 387 
     } elsif ($self->{version} == 5) {      } elsif ($self->{version} == 5) {
         _pack_appinfo_v5($self->{appinfo});          _pack_appinfo_v5($self->{appinfo});
     } else {      } else {
         die "Unsupported Version";          croak "Unsupported Version $self->{version}";
         return;  
     }      }
     return &Palm::StdAppInfo::pack_StdAppInfo($self->{appinfo});      return &Palm::StdAppInfo::pack_StdAppInfo($self->{appinfo});
 }  }
Line 338 
Line 422 
 {  {
     my $self = shift;      my $self = shift;
     my $rec  = shift;      my $rec  = shift;
     my $data = shift;  
     my $pass = shift || $self->{password};      my $pass = shift || $self->{password};
       my $data = shift || $rec->{plaintext};
     my $ivec = shift;      my $ivec = shift;
   
     if ( ! $pass && ! $self->{appinfo}->{key}) {      if ( ! $pass && ! $self->{appinfo}->{key}) {
Line 351 
Line 435 
     }      }
   
     if ( ! $data) {      if ( ! $data) {
         croak("Needed parameter 'data' not passed!\n");          croak("Needed 'plaintext' not passed!\n");
     }      }
   
     if ( $pass && ! $self->Password($pass)) {      if ( $pass && ! $self->Password($pass)) {
Line 366 
Line 450 
     my $encrypted;      my $encrypted;
     if ($self->{version} == 4) {      if ($self->{version} == 4) {
         $self->{digest} ||= _calc_keys( $pass );          $self->{digest} ||= _calc_keys( $pass );
         $encrypted = _encrypt_v4($data, $acct, $self->{digest});          my $datav4 = {
         $rec->{name}    ||= $data->{name};              name       => $data->{0}->{data},
               account    => $data->{1}->{data},
               password   => $data->{2}->{data},
               lastchange => $data->{3}->{data},
               notes      => $data->{255}->{data},
           };
           my $acctv4 = {
               name       => $acct->{0}->{data},
               account    => $acct->{1}->{data},
               password   => $acct->{2}->{data},
               lastchange => $acct->{3}->{data},
               notes      => $acct->{255}->{data},
           };
           $encrypted = _encrypt_v4($datav4, $acctv4, $self->{digest});
   
     } elsif ($self->{version} == 5) {      } elsif ($self->{version} == 5) {
         my @accts = ($data, $acct);  
         if ($self->{options}->{v4compatible}) {  
             $rec->{name} ||= $data->{name};  
             foreach my $a (@accts) {  
                 my @fields;  
                 foreach my $k (sort keys %{ $a }) {  
                     my $field = {  
                         label    => $k,  
                         font     => 0,  
                         data     => $a->{$k},  
                     };  
                     push @fields, $field;  
                 }  
                 $a = \@fields;  
             }  
         }  
   
         ($encrypted, $ivec) = _encrypt_v5(          ($encrypted, $ivec) = _encrypt_v5(
             @accts,              $data, $acct,
             $self->{appinfo}->{key},              $self->{appinfo}->{key},
             $self->{appinfo}->{cipher},              $self->{appinfo}->{cipher},
             $ivec,              $ivec,
Line 398 
Line 478 
         }          }
   
     } else {      } else {
         die "Unsupported Version";          croak "Unsupported Version $self->{version}";
     }      }
   
       $rec->{plaintext}->{0} = $data->{0};
   
     if ($encrypted) {      if ($encrypted) {
         if ($encrypted eq '1') {          if ($encrypted eq '1') {
             return 1;              return 1;
Line 429 
Line 511 
     my $changed      = 0;      my $changed      = 0;
     my $need_newdate = 0;      my $need_newdate = 0;
     if ($old && %{ $old }) {      if ($old && %{ $old }) {
           no warnings 'uninitialized';
         foreach my $key (keys %{ $new }) {          foreach my $key (keys %{ $new }) {
             next if $key eq 'lastchange';              next if $key eq 'lastchange';
             if ($new->{$key} ne $old->{$key}) {              if ($new->{$key} ne $old->{$key}) {
Line 500 
Line 583 
   
     my $changed = 0;      my $changed = 0;
     my $need_newdate = 1;      my $need_newdate = 1;
     my $date_index;      if ($new->{3}->{data}) {
     for (my $i = 0; $i < @{ $new }; $i++) {          $need_newdate = 0;
         if (      }
             ($new->[$i]->{label_id} && $new->[$i]->{label_id} == 3) ||      foreach my $k (keys %{ $new }) {
             ($new->[$i]->{label} && $new->[$i]->{label} eq 'lastchange')          if (! $old) {
         ) {              $changed = 1;
             $date_index   = $i;          } elsif ($k == 3) {
             if ( $old && $#{ $new } == $#{ $old } && (              if ($old && (
                     $new->[$i]{data}{day}   != $old->[$i]{data}{day}   ||                      $new->{$k}{data}{day}   == $old->{$k}{data}{day}   &&
                     $new->[$i]{data}{month} != $old->[$i]{data}{month} ||                      $new->{$k}{data}{month} == $old->{$k}{data}{month} &&
                     $new->[$i]{data}{year}  != $old->[$i]{data}{year}                      $new->{$k}{data}{year}  == $old->{$k}{data}{year}
                 )) {                  )) {
                 $changed      = 1;                  $changed      = 1;
                 $need_newdate = 0;                  $need_newdate = 1;
             }              }
   
         } elsif ($old && $#{ $new } == $#{ $old }) {          } else {
             my $n = join ':', %{ $new->[$i] };              my $n = join ':', sort %{ $new->{$k} };
             my $o = join ':', %{ $old->[$i] };              my $o = join ':', sort %{ $old->{$k} };
             if ($n ne $o) {              if ($n ne $o) {
                 $changed = 1;                  $changed = 1;
             }              }
         } elsif ($#{ $new } != $#{ $old }) {  
             $changed = 1;  
         }          }
     }      }
     if ($old && (! @{ $old }) && $date_index) {  
         $need_newdate = 0;  
     }  
   
     return 1, 0 if $changed == 0;      return 1, 0 if $changed == 0;
   
     if ($need_newdate || ! defined $date_index) {      if ($need_newdate) {
         my ($day, $month, $year) = (localtime)[3,4,5];          my ($day, $month, $year) = (localtime)[3,4,5];
         my $date = {          $new->{3} = {
             year  => $year,              label => 'lastchange',
             month => $month,              label_id => 3,
             day   => $day,              font  => 0,
               data => {
                   year  => $year,
                   month => $month,
                   day   => $day,
              },
         };          };
         if (defined $date_index) {  
             $new->[$date_index]->{data} = $date;  
         } else {  
             push @{ $new }, {  
                 label => 'lastchange',  
                 font  => 0,  
                 data  => $date,  
             };  
         }  
     } else {      } else {
         # XXX Need to actually validate the above information somehow          # XXX Need to actually validate the above information somehow
         if ($new->[$date_index]->{data}->{year} >= 1900) {          if ($new->{3}->{data}->{year} >= 1900) {
             $new->[$date_index]->{data}->{year} -= 1900;              $new->{3}->{data}->{year} -= 1900;
         }          }
     }      }
   
     my $decrypted;      my $plaintext;
     foreach my $field (@{ $new }) {      foreach my $k (keys %{ $new }) {
         $decrypted .= _pack_field($field);          next if $new->{$k}->{label_id} == 0;
           $plaintext .= _pack_field($new->{$k});
     }      }
           #$plaintext .= chr(0xff) x 2;
   
     my $encrypted;      my $encrypted;
     if ($c->{name} eq 'None') {      if ($c->{name} eq 'None') {
         # do nothing          # do nothing
         $encrypted = $decrypted;          $encrypted = $plaintext;
   
     } elsif ($c->{name} eq 'DES_EDE3' or $c->{name} eq 'Rijndael') {      } elsif ($c->{name} eq 'DES_EDE3' or $c->{name} eq 'Rijndael') {
         require Crypt::CBC;          require Crypt::CBC;
Line 581 
Line 658 
             croak("Unable to set up encryption!");              croak("Unable to set up encryption!");
         }          }
   
         $encrypted = $cbc->encrypt($decrypted);          $encrypted = $cbc->encrypt($plaintext);
   
     } else {      } else {
         die "Unsupported Version";          croak "Unsupported Crypt $c->{name}";
     }      }
   
     return $encrypted, $ivec;      return $encrypted, $ivec;
Line 614 
Line 691 
         croak("No encrypted content!");          croak("No encrypted content!");
     }      }
   
       my $plaintext;
     if ($self->{version} == 4) {      if ($self->{version} == 4) {
         $self->{digest} ||= _calc_keys( $pass );          $self->{digest} ||= _calc_keys( $pass );
         my $acct = _decrypt_v4($rec->{encrypted}, $self->{digest});          my $acct = _decrypt_v4($rec->{encrypted}, $self->{digest});
         $acct->{name} ||= $rec->{name};          $plaintext = {
         return $acct;              0 => $rec->{plaintext}->{0},
               1 => {
                   label    => 'account',
                   label_id => 1,
                   font     => 0,
                   data     => $acct->{account},
               },
               2 => {
                   label    => 'password',
                   label_id => 2,
                   font     => 0,
                   data     => $acct->{password},
               },
               3 => {
                   label    => 'lastchange',
                   label_id => 3,
                   font     => 0,
                   data     => $acct->{lastchange},
               },
               255 => {
                   label    => 'notes',
                   label_id => 255,
                   font     => 0,
                   data     => $acct->{notes},
               },
           };
   
     } elsif ($self->{version} == 5) {      } elsif ($self->{version} == 5) {
         my $fields = _decrypt_v5(          $plaintext = _decrypt_v5(
             $rec->{encrypted}, $self->{appinfo}->{key},              $rec->{encrypted}, $self->{appinfo}->{key},
             $self->{appinfo}->{cipher}, $rec->{ivec},              $self->{appinfo}->{cipher}, $rec->{ivec},
         );          );
         if ($self->{options}->{v4compatible}) {          $plaintext->{0} ||= $rec->{plaintext}->{0};
             my %acct;  
             foreach my $f (@{ $fields }) {  
                 $acct{ $f->{label} } = $f->{data};  
             }  
             $acct{name} ||= $rec->{name};  
             return \%acct;  
         } else {  
             return $fields;  
         }  
   
     } else {      } else {
         die "Unsupported Version";          croak "Unsupported Version $self->{version}";
     }      }
   
       if ($plaintext) {
           $rec->{plaintext} = $plaintext;
           return $plaintext;
       }
     return;      return;
 }  }
   
Line 647 
Line 746 
     my $encrypted = shift;      my $encrypted = shift;
     my $digest    = shift;      my $digest    = shift;
   
     my $decrypted = _crypt3des( $encrypted, $digest, $DECRYPT );      my $plaintext = _crypt3des( $encrypted, $digest, $DECRYPT );
     my ( $account, $password, $notes, $packed_date )      my ( $account, $password, $notes, $packed_date )
         = split /$NULL/xm, $decrypted, 4;          = split /$NULL/xm, $plaintext, 4;
   
     my $modified;      my $modified;
     if ($packed_date) {      if ($packed_date) {
Line 674 
Line 773 
   
     my $c = crypts($cipher) or croak('Unknown cipher ' . $cipher);      my $c = crypts($cipher) or croak('Unknown cipher ' . $cipher);
   
     my $decrypted;      my $plaintext;
   
     if ($c->{name} eq 'None') {      if ($c->{name} eq 'None') {
         # do nothing          # do nothing
         $decrypted = $encrypted;          $plaintext = $encrypted;
   
     } elsif ($c->{name} eq 'DES_EDE3' or $c->{name} eq 'Rijndael') {      } elsif ($c->{name} eq 'DES_EDE3' or $c->{name} eq 'Rijndael') {
         require Crypt::CBC;          require Crypt::CBC;
Line 698 
Line 797 
         }          }
         my $len = $c->{blocksize} - length($encrypted) % $c->{blocksize};          my $len = $c->{blocksize} - length($encrypted) % $c->{blocksize};
         $encrypted .= $NULL x $len;          $encrypted .= $NULL x $len;
         $decrypted  = $cbc->decrypt($encrypted);          $plaintext  = $cbc->decrypt($encrypted);
   
     } else {      } else {
         die "Unsupported Version";          croak "Unsupported Crypt $c->{name}";
         return;  
     }      }
   
     my @fields;      my %fields;
     while ($decrypted) {      while ($plaintext) {
         my $field;          my $field;
         ($field, $decrypted) = _parse_field($decrypted);          ($field, $plaintext) = _parse_field($plaintext);
         if (! $field) {          if (! $field) {
             last;              last;
         }          }
         push @fields, $field;          $fields{ $field->{label_id} } = $field;
     }      }
   
     return \@fields;      return \%fields;
 }  }
   
 # Password  # Password
Line 733 
Line 831 
     }      }
   
     if (      if (
         ($self->{version} == 4 && ! exists $self->{records}) ||          ($self->{version} == 4 && ! exists $self->{encpassword}) ||
         ($self->{version} == 5 && ! exists $self->{appinfo}->{masterhash})          ($self->{version} == 5 && ! exists $self->{appinfo}->{masterhash})
     ) {      ) {
         if ($self->{version} == 4) {  
             # Give the PDB the first record that will hold the encrypted password  
             $self->{records} = [ $self->new_Record ];  
         }  
   
         return $self->_password_update($pass);          return $self->_password_update($pass);
     }      }
   
     if ($new_pass) {      if ($new_pass) {
         my $v4compat = $self->{options}->{v4compatible};  
         $self->{options}->{v4compatible} = 0;  
   
         my @accts = ();          my @accts = ();
         foreach my $i (0..$#{ $self->{records} }) {          foreach my $rec (@{ $self->{records} }) {
             if ($self->{version} == 4 && $i == 0) {              my $acct = $self->Decrypt($rec, $pass);
                 push @accts, undef;  
                 next;  
             }  
             my $acct = $self->Decrypt($self->{records}->[$i], $pass);  
             if ( ! $acct ) {              if ( ! $acct ) {
                 croak("Couldn't decrypt $self->{records}->[$i]->{name}");                  croak("Couldn't decrypt $rec->{plaintext}->{0}->{data}");
             }              }
             push @accts, $acct;              push @accts, $acct;
         }          }
Line 767 
Line 853 
         $pass = $new_pass;          $pass = $new_pass;
   
         foreach my $i (0..$#accts) {          foreach my $i (0..$#accts) {
             if ($self->{version} == 4 && $i == 0) {  
                 next;  
             }  
             delete $self->{records}->[$i]->{encrypted};              delete $self->{records}->[$i]->{encrypted};
             $self->Encrypt($self->{records}->[$i], $accts[$i], $pass);              $self->{records}->[$i]->{plaintext} = $accts[$i];
               $self->Encrypt($self->{records}->[$i], $pass);
         }          }
   
         $self->{options}->{v4compatible} = $v4compat;  
     }      }
   
     if (defined $self->{password} && $pass eq $self->{password}) {      if (defined $self->{password} && $pass eq $self->{password}) {
Line 783 
Line 865 
     }      }
   
     if ($self->{version} == 4) {      if ($self->{version} == 4) {
         # AFAIK the thing we use to test the password is          my $valid = _password_verify_v4($pass, $self->{encpassword});
         #     always in the first entry  
         my $valid = _password_verify_v4($pass, $self->{records}->[0]->{data});  
   
         # May as well generate the keys we need now, since we know the password is right          # May as well generate the keys we need now,
           # since we know the password is right
         if ($valid) {          if ($valid) {
             $self->{digest} = _calc_keys($pass);              $self->{digest} = _calc_keys($pass);
             if ($self->{digest} ) {              if ($self->{digest} ) {
Line 798 
Line 879 
     } elsif ($self->{version} == 5) {      } elsif ($self->{version} == 5) {
         return _password_verify_v5($self->{appinfo}, $pass);          return _password_verify_v5($self->{appinfo}, $pass);
     } else {      } else {
         # XXX unsupported version          croak "Unsupported version $self->{version}";
     }      }
   
     return;      return;
Line 883 
Line 964 
   
         # AFAIK the thing we use to test the password is          # AFAIK the thing we use to test the password is
         #     always in the first entry          #     always in the first entry
         $self->{records}->[0]->{data} = $data;          $self->{encpassword} = $data;
         $self->{password} = $pass;          $self->{password} = $pass;
         $self->{digest}   = _calc_keys( $self->{password} );          $self->{digest}   = _calc_keys( $self->{password} );
   
Line 964 
Line 1045 
     return $key;      return $key;
 }  }
   
   sub Unlock
   {
       my $self = shift;
       my ($pass) = @_;
       $pass ||= $self->{password};
   
       if ( $pass && ! $self->Password($pass)) {
           croak("Invalid Password!\n");
       }
   
       foreach my $rec (@{ $self->{records} }) {
           $self->Decrypt($rec);
       }
   
       return 1;
   
   }
   
   sub Lock
   {
       my $self = shift;
   
       $self->Password();
   
       foreach my $rec (@{ $self->{records} }) {
           my $name = $rec->{plaintext}->{0};
           delete $rec->{plaintext};
           $rec->{plaintext}->{0} = $name;
       }
   
       return 1;
   }
   
 # Helpers  # Helpers
   
 sub _calc_keys  sub _calc_keys
Line 1062 
Line 1176 
 {  {
     my $field = shift;      my $field = shift;
   
     my @labels;      my ($len) = unpack "n", $field;
     $labels[0]   = 'name';  
     $labels[1]   = 'account';  
     $labels[2]   = 'password';  
     $labels[3]   = 'lastchange';  
     $labels[255] = 'notes';  
   
     my ($len) = unpack "n1", $field;  
     if ($len + 4 > length $field) {      if ($len + 4 > length $field) {
         return undef, $field;          return undef, $field;
     }      }
     my $unpackstr = "x2 C1 C1 A$len";      my $unpackstr = "x2 C1 C1 A$len";
     my $offset    =   2 +1 +1 +$len;      my $offset    =   2 +1 +1 +$len;
     if ($len % 2) { # && $len + 4 < length $field) {      if ($len % 2) {
         # trim the 0/1 byte padding for next even address.          # trim the 0/1 byte padding for next even address.
         $offset++;          $offset++;
         $unpackstr .= ' x'          $unpackstr .= ' x'
Line 1084 
Line 1191 
     my ($label, $font, $data) = unpack $unpackstr, $field;      my ($label, $font, $data) = unpack $unpackstr, $field;
     my $leftover = substr $field, $offset;      my $leftover = substr $field, $offset;
   
     if ($label && $label == 3) {      my $label_id = $label;
       my $l = labels($label);
       if ($l) {
           $label = $l->{name} || $l->{id};
           $label_id = $l->{id};
       }
   
       if ($label_id && $label_id == 3) {
           ($data) = substr $field, 4, $len;
         $data = _parse_keyring_date($data);          $data = _parse_keyring_date($data);
     }      }
     return {      return {
         #len      => $len,          #len      => $len,
         label    => $labels[ $label ] || $label,          label    => $label,
         label_id => $label,          label_id => $label_id,
         font     => $font,          font     => $font,
         data     => $data,          data     => $data,
     }, $leftover;      }, $leftover;
Line 1100 
Line 1215 
 {  {
     my $field = shift;      my $field = shift;
   
     my %labels = (  
         name       =>   0,  
         account    =>   1,  
         password   =>   2,  
         lastchange =>   3,  
         notes      => 255,  
     );  
   
     my $packed;      my $packed;
     if (defined $field) {      if (defined $field) {
         my $label = $field->{label_id} || 0;          my $label = $field->{label_id} || 0;
         if (defined $field->{label} && ! $label) {          if (defined $field->{label} && ! $label) {
             $label = $labels{ $field->{label} };              $label = $field->{label};
         }          }
   
           my $l = labels($field->{label});
           if ($l) {
               $label = $l->{id};
           }
   
         my $font  = $field->{font} || 0;          my $font  = $field->{font} || 0;
         my $data  = defined $field->{data} ? $field->{data} : $EMPTY;          my $data  = defined $field->{data} ? $field->{data} : $EMPTY;
   
Line 1163 
Line 1276 
     $year -= 4;      $year -= 4;
     $month++;      $month++;
   
     return pack 'n', $day | ($month << 5) | ($year << 9);      return pack 'n*', $day | ($month << 5) | ($year << 9);
 }  }
   
   
Line 1280 
Line 1393 
 parses Keyring for Palm OS databases.  See  parses Keyring for Palm OS databases.  See
 L<http://gnukeyring.sourceforge.net/>.  L<http://gnukeyring.sourceforge.net/>.
   
 It has the standard Palm::PDB methods with 2 additional public methods.  It has the standard Palm::PDB methods with 4 additional public methods.
 Decrypt and Encrypt.  Unlock, Lock, Decrypt and Encrypt.
   
 It currently supports the v4 Keyring databases as well as  It currently supports the v4 Keyring databases as well as
 the pre-release v5 databases.  I am not completely happy with the interface  the pre-release v5 databases.
 for accessing v5 databases, so any suggestions on improvements on  
 the interface are appreciated.  
   
 This module doesn't store the decrypted content.  It only keeps it until it  
 returns it to you or encrypts it.  
   
 =head1 SYNOPSIS  =head1 SYNOPSIS
   
     use Palm::PDB;      use Palm::PDB;
Line 1301 
Line 1409 
     my $pdb  = new Palm::PDB;      my $pdb  = new Palm::PDB;
     $pdb->Load($file);      $pdb->Load($file);
   
     foreach (0..$#{ $pdb->{records} }) {      $pdb->Unlock($pass);
         # skip the password record for version 4 databases      foreach my $rec (@{ $pdb->{records} }) {
         next if $_ == 0 && $pdb->{version} == 4;          print $rec->{plaintext}->{0}->{data}, ' - ',
         my $rec  = $pdb->{records}->[$_];                $rec->{plaintext}->{1}->{data}, "\n";
         my $acct = $pdb->Decrypt($rec, $pass);  
         print $rec->{name}, ' - ';  
         if ($pdb->{version} == 4 || $pdb->{options}->{v4compatible}) {  
             print ' - ', $acct->{account};  
         } else {  
             foreach my $a (@{ $acct }) {  
                 if ($a->{label} eq 'account') {  
                     print ' - ',  $a->{data};  
                     last;  
                 }  
             }  
         }  
         print "\n";  
     }      }
       $pdb->Lock();
   
 =head1 SUBROUTINES/METHODS  =head1 SUBROUTINES/METHODS
   
Line 1332 
Line 1428 
 Use this method if you're creating a Keyring PDB from scratch otherwise you  Use this method if you're creating a Keyring PDB from scratch otherwise you
 can just use Palm::PDB::new() before calling Load().  can just use Palm::PDB::new() before calling Load().
   
 If you pass in a password, it will initalize the first record with the encrypted  If you pass in a password, it will initalize the database with the encrypted
 password.  password.
   
 new() now also takes options in other formats  new() now also takes options in other formats
Line 1354 
Line 1450 
   
 The version of database to create.  Accepts either 4 or 5.  Currently defaults to 4.  The version of database to create.  Accepts either 4 or 5.  Currently defaults to 4.
   
 =item v4compatible  
   
 The format of the fields passed to Encrypt and returned from Decrypt have changed.  
 This allows programs to use the newer databases with few changes but with less features.  
   
 =item cipher  =item cipher
   
 The cipher to use.  Either the number or the name.  The cipher to use.  Either the number or the name.  Only used by v5 datbases.
   
     0 => None      0 => None
     1 => DES_EDE3      1 => DES_EDE3
Line 1370 
Line 1461 
   
 =item iterations  =item iterations
   
 The number of iterations to encrypt with.  The number of iterations to encrypt with.  Only used by somy crypts in v5 databases.
   
 =item options  
   
 A hashref of the options that are set  
   
 =back  =back
   
 =back  =back
Line 1411 
Line 1498 
         default_iter => <default iterations for the cipher>,          default_iter => <default iterations for the cipher>,
     };      };
   
   If it is unable to find the crypt it will return undef.
   
   =head2 labels
   
   Pass in the id or the name of the label.  The label id is used as a key
   to the different parts of the records.
   See Encrypt() for details on where the label is used.
   
   This is a function, not a method.
   
       my $l = Palm::Keyring::labels($label);
   
   $l is now:
   
       $l = {
           id => 0,
           name => 'name',
       };
   
   If what you passed in was a number that doesn't have a name, it will return:
   
       $l => {
           id => $num_passed_in,
           name => undef,
       }
   
   If you pass in a name that it can't find, then it returns undef.
   
 =head2 Encrypt  =head2 Encrypt
   
     $pdb->Encrypt($rec, $acct[, $password[, $ivec]]);  =head3 B<!!! IMPORTANT !!!>  The order of the arguments to Encrypt has
   changed.  $password and $plaintext used to be swapped.  They changed
   because you can now set $rec->{plaintext} and not pass in $plaintext so
   $password is more important.
   
       $pdb->Encrypt($rec[, $password[, $plaintext[, $ivec]]]);
   
 Encrypts an account into a record, either with the password previously  Encrypts an account into a record, either with the password previously
 used, or with a password that is passed.  used, or with a password that is passed.
   
Line 1423 
Line 1543 
 randomly.  randomly.
   
 $rec is a record from $pdb->{records} or a new_Record().  $rec is a record from $pdb->{records} or a new_Record().
 The v4 $acct is a hashref in the format below.  $rec->{plaintext} is a hashref in the format below.
   
     my $v4acct = {      $plaintext = {
         name       => $rec->{name},          0 => {
         account    => $account,              label    => 'name',
         password   => $password,              label_id => 0,
         notes      => $notes,              font     => 0,
         lastchange => {              data     => $name,
             year  => 107, # years since 1900          1 => {
             month =>   0, # 0-11, 0 = January, 11 = December              label    => 'account',
             day   =>  30, # 1-31, same as localtime              label_id => 1,
               font     => 0,
               data     => $account,
         },          },
     };          2 => {
               label    => 'password',
 The v5 $acct is an arrayref full of hashrefs that contain each encrypted field.              label_id => 2,
               font     => 0,
     my $v5acct = [              data     => $password,
         {  
             'label_id' => 2,  
             'data' => 'abcd1234',  
             'label' => 'password',  
             'font' => 0  
         },          },
         {          3 => {
             'label_id' => 3,              label    => 'lastchange',
             'data' => {              label_id => 3,
                 'month' => 1,              font     => 0,
                 'day' => 11,              data     => {
                 'year' => 107                  year => $year, # usually the year - 1900
                   mon  => $mon,  # range 0-11
                   day  => $day,  # range 1-31
             },              },
             'label' => 'lastchange',  
             'font' => 0  
         },          },
         {          255 => {
             'label_id' => 255,              label    => 'notes',
             'data' => 'This is a short note.',              label_id => 255,
             'label' => 'notes',              font     => 0,
             'font' => 0              data     => $notes,
         }          },
     ];      };
   
   The account name is stored in $rec->{plaintext}->{0}->{data} for both v4
   and v5 databases even when the record has not been Decrypt()ed.
   
 The account name is stored in $rec->{name} for both v4 and v5 databases.      $rec->{plaintext}->{0} => {
 It is not returned in the decrypted information for v5.          label    => 'name',
           label_id => 0,
           font     => 0,
           data     => 'account name',
       };
   
     $rec->{name} = 'account name';  
   
 If you have changed anything other than the lastchange, or don't pass in a  If you have changed anything other than the lastchange, or don't pass in a
 lastchange key, Encrypt() will generate a new lastchange date for you.  lastchange key, Encrypt() will generate a new lastchange date for you.
   
 If you pass in a lastchange field that is different than the one in the  If you pass in a lastchange field that is different than the one in the
 record, it will honor what you passed in.  record, it will honor what you passed in.
   
 Encrypt() only uses the $acct->{name} if there is not already a $rec->{name}.  You can either set $rec->{plaintext} or pass in $plaintext.  $plaintext
   is used over anything in $rec->{plaintext}.
   
   
 =head2 Decrypt  =head2 Decrypt
   
     my $acct = $pdb->Decrypt($rec[, $password]);      my $plaintext = $pdb->Decrypt($rec[, $password]);
   
 Decrypts the record and returns a reference for the account as described  Decrypts the record and returns a reference for the plaintext account as
 under Encrypt().  described under Encrypt().
   Also sets $rec->{plaintext} with the same information as $plaintext as
   described in Encrypt().
   
     foreach (0..$#{ $pdb->{records} }) {      foreach my $rec (@{ $pdb->{records} }) {
         next if $_ == 0 && $pdb->{version} == 4;          my $plaintext = $pdb->Decrypt($rec);
         my $rec = $pdb->{records}->[$_];          # do something with $plaintext
         my $acct = $pdb->Decrypt($rec);  
         # do something with $acct  
     }      }
   
   
Line 1512 
Line 1635 
   
     $pdb->{digest}   = the calculated digest used from the key;      $pdb->{digest}   = the calculated digest used from the key;
     $pdb->{password} = the password that was passed in;      $pdb->{password} = the password that was passed in;
       $pdb->{encpassword} = the password as stored in the pdb;
   
 For v5  For v5
   
Line 1528 
Line 1652 
                       or calculated when setting a new password.                        or calculated when setting a new password.
     };      };
   
   =head2 Unlock
   
       $pdb->Unlock([$password]);
   
   Decrypts all the records.  Sets $rec->{plaintext} for all records.
   
   This makes it easy to show all decrypted information.
   
      my $pdb = Palm::KeyRing->new();
      $pdb->Load($keyring_file);
      $pdb->Unlock($password);
      foreach my $plaintext (map { $_->{plaintext} } @{ $pdb->{records} }) {
          # Do something like display the account.
      }
      $pdb->Lock();
   
   =head2 Lock
   
       $pdb->Lock();
   
   Unsets $rec->{plaintext} for all records and unsets the saved password.
   
   This does NOT Encrypt() any of the records before clearing them, so if
   you are not careful you will lose information.
   
   B<CAVEAT!> This only does "delete $rec->{plaintext}" and the same for the
   password.  If someone knows of a cross platform reliable way to make
   sure that the information is actually cleared from memory I would
   appreciate it.  Also, if someone knows how to make sure that the stuff
   in $rec->{plaintext} is not written to swap, that would be very handy as
   well.
   
 =head2 Other overridden subroutines/methods  =head2 Other overridden subroutines/methods
   
 =over  =over
Line 1557 
Line 1713 
         encrypted  => the encrypted information          encrypted  => the encrypted information
     };      };
   
   For v4 databases it also removes record 0 and moves the encrypted password
   to $self->{encpassword}.
   
 =item PackRecord  =item PackRecord
   
 Reverses ParseRecord and then sends it through Palm::StdAppInfo::PackRecord()  Reverses ParseRecord and then sends it through Palm::StdAppInfo::PackRecord()
   
   =item Write
   
   For v4 databases it puts back the record 0 for the encrypted password before
   writing it.
   
 =back  =back
   
 =head1 DEPENDENCIES  =head1 DEPENDENCIES
Line 1589 
Line 1753 
   
 =head1 THANKS  =head1 THANKS
   
 I would like to thank the helpful Perlmonk shigetsu who gave me some great advice  I would like to thank the helpful Perlmonk shigetsu who gave me some great
 and helped me get my first module posted.  L<http://perlmonks.org/?node_id=596998>  advice and helped me get my first module posted.
   L<http://perlmonks.org/?node_id=596998>
   
 I would also like to thank  I would also like to thank
 Johan Vromans  Johan Vromans
Line 1612 
Line 1777 
 I am sure there are problems with this module.  For example, I have  I am sure there are problems with this module.  For example, I have
 not done very extensive testing of the v5 databases.  not done very extensive testing of the v5 databases.
   
 I am not very happy with the data structures used by Encrypt() and  I am not sure I am 'require module' the best way, but I don't want to
 Decrypt() for v5 databases, but I am not sure of a better way.  depend on modules that you don't need to use.
   
 The v4 compatibility mode does not insert a fake record 0 where  
 normally the encrypted password is stored.  
   
 The date validation for packing new dates is very poor.  The date validation for packing new dates is very poor.
   
 I have not gone through and standardized on how the module fails.  Some  I have not gone through and standardized on how the module fails.  Some
 things fail with croak, some return undef, some may even fail silently.  things fail with croak, some return undef, some may even fail silently.
 Nothing initializes a lasterr method or anything like that.  I need  Nothing initializes a lasterr method or anything like that.
 to fix all that before it is a 1.0 candidate.  
   This module does not do anything special with the plaintext data.  It SHOULD
   treat it somehow special so that it can't be found in RAM or in a swap file
   anywhere.  I don't have a clue how to do this.
   
   I need to fix all this before it is a 1.0 candidate.
   
 Please report any bugs or feature requests to  Please report any bugs or feature requests to
 C<bug-palm-keyring at rt.cpan.org>, or through the web interface at  C<bug-palm-keyring at rt.cpan.org>, or through the web interface at

Legend:
Removed from v.1.44  
changed lines
  Added in v.1.52

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>