| version 1.11, 2007/01/27 23:59:29 | version 1.27, 2007/02/10 16:21:28 | 
|  |  | 
| # Palm::Keyring.pm | package Palm::Keyring; | 
|  | # $RedRiver: Keyring.pm,v 1.26 2007/02/06 02:58:50 andrew Exp $ | 
|  | ######################################################################## | 
|  | # Keyring.pm *** Perl class for Keyring for Palm OS databases. | 
| # | # | 
| # Perl class for dealing with Keyring for Palm OS databases. |  | 
| # |  | 
| #       Copyright (C) 2004, Andrew Fresh |  | 
| #       You may distribute this file under the terms of the Artistic |  | 
| #       License, as specified in the README file distributed with the p5-Palm distribution. |  | 
| # |  | 
| #   This started as Memo.pm, I just made it work for Keyring. | #   This started as Memo.pm, I just made it work for Keyring. | 
| # | # | 
| # $Id$ | # 2006.01.26 #*#*# andrew fresh <andrew@cpan.org> | 
| # $RedRiver: Keyring.pm,v 1.10 2006/12/06 18:45:42 andrew Exp $ | ######################################################################## | 
|  | # Copyright (C) 2006, 2007 by Andrew Fresh | 
|  | # | 
|  | # This program is free software; you can redistribute it and/or modify | 
|  | # it under the same terms as Perl itself. | 
|  | ######################################################################## | 
| use strict; | use strict; | 
| package Palm::Keyring; | use warnings; | 
| use Palm::Raw(); |  | 
| use Palm::StdAppInfo(); |  | 
| use vars qw( $VERSION @ISA ); |  | 
|  |  | 
|  | use Carp; | 
|  |  | 
|  | use base qw/ Palm::StdAppInfo /; | 
|  |  | 
| use Digest::MD5 qw(md5); | use Digest::MD5 qw(md5); | 
| use Crypt::DES; | use Crypt::DES; | 
|  |  | 
| use constant ENCRYPT    =>  1; | my $ENCRYPT    = 1; | 
| use constant DECRYPT    =>  0; | my $DECRYPT    = 0; | 
| use constant MD5_CBLOCK => 64; | my $MD5_CBLOCK = 64; | 
| my $kSaltSize = 4; | my $kSalt_Size = 4; | 
|  | my $EMPTY      = q{}; | 
|  | my $SPACE      = q{ }; | 
|  | my $NULL       = chr 0; | 
|  |  | 
|  | our $VERSION = 0.94; | 
|  |  | 
| # One liner, to allow MakeMaker to work. | sub new { | 
| $VERSION = do { my @r = (q$Revision$ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; | my $classname = shift; | 
|  | my $pass      = shift; | 
|  |  | 
| @ISA = qw( Palm::StdAppInfo Palm::Raw ); | # Create a generic PDB. No need to rebless it, though. | 
|  | my $self = $classname->SUPER::new(@_); | 
|  |  | 
| =head1 NAME | $self->{'name'}    = 'Keys-Gtkr';    # Default | 
|  | $self->{'creator'} = 'Gtkr'; | 
|  | $self->{'type'}    = 'Gkyr'; | 
|  |  | 
| Palm::Keyring - Handler for Palm Keyring databases. | # The PDB is not a resource database by | 
|  | # default, but it's worth emphasizing, | 
|  | # since MemoDB is explicitly not a PRC. | 
|  | $self->{'attributes'}{'resource'} = 0; | 
|  |  | 
| =head1 SYNOPSIS | # Initialize the AppInfo block | 
|  | $self->{'appinfo'} = {}; | 
|  |  | 
| use Palm::Keyring; | # Add the standard AppInfo block stuff | 
| $pdb->Load($file); | Palm::StdAppInfo::seed_StdAppInfo( $self->{'appinfo'} ); | 
| $pdb->Decrypt($assword); |  | 
|  |  | 
| =head1 DESCRIPTION | # Set the version | 
|  | $self->{'version'} = 4; | 
|  |  | 
| The Keyring PDB handler is a helper class for the Palm::PDB package. It | if ( defined $pass ) { | 
| parses Keyring for Palm OS databases.  See | $self->Password($pass); | 
| L<http://gnukeyring.sourceforge.net/>. | } | 
|  |  | 
| It has the standard Palm::Raw with 2 additional public methods. | return $self; | 
| Decrypt and Encrypt. | } | 
|  |  | 
| =cut | sub import { | 
|  | Palm::PDB::RegisterPDBHandlers( __PACKAGE__, [ 'Gtkr', 'Gkyr' ], ); | 
|  | return 1; | 
|  | } | 
|  |  | 
| =head2 new | sub ParseRecord { | 
|  | my $self     = shift; | 
|  |  | 
| $pdb = new Palm::Keyring($password); | my $rec = $self->SUPER::ParseRecord(@_); | 
|  |  | 
| Create a new PDB, initialized with the various Palm::Keyring fields | # skip the 0 record that holds the password | 
| and an empty record list. | return $rec if ! exists $self->{'records'}; | 
|  | return $rec if ! exists $rec->{'data'}; | 
|  |  | 
| Use this method if you're creating a Keyring PDB from scratch. | my ( $name, $encrypted ) = split /$NULL/xm, $rec->{'data'}, 2; | 
|  |  | 
| =cut | return $rec if ! $encrypted; | 
|  | delete $rec->{'data'}; | 
|  | $rec->{'name'} = $name; | 
|  | $rec->{'encrypted'} = $encrypted; | 
|  |  | 
| sub new | return $rec; | 
| { | } | 
| my $classname   = shift; |  | 
| my $pass = shift; |  | 
|  |  | 
| # Create a generic PDB. No need to rebless it, though. | sub PackRecord { | 
| my $self        = $classname->SUPER::new(@_); | my $self = shift; | 
|  | my $rec  = shift; | 
|  |  | 
| $self->{name} = "Keys-Gtkr";    # Default | if ($rec->{'encrypted'}) { | 
| $self->{creator} = "Gtkr"; | if (! defined $rec->{'name'}) { | 
| $self->{type} = "Gkyr"; | $rec->{'name'} = $EMPTY; | 
| # The PDB is not a resource database by | } | 
| # default, but it's worth emphasizing, | $rec->{'data'} = join $NULL, $rec->{'name'}, $rec->{'encrypted'}; | 
| # since MemoDB is explicitly not a PRC. | delete $rec->{'name'}; | 
| $self->{attributes}{resource} = 0; | delete $rec->{'encrypted'}; | 
|  | } | 
|  |  | 
| # Initialize the AppInfo block | return $self->SUPER::PackRecord($rec, @_); | 
| $self->{appinfo} = {}; | } | 
|  |  | 
| # Add the standard AppInfo block stuff | sub Encrypt { | 
| &Palm::StdAppInfo::seed_StdAppInfo($self->{appinfo}); | my $self = shift; | 
|  | my $rec  = shift; | 
|  | my $data = shift; | 
|  | my $pass = shift || $self->{'password'}; | 
|  |  | 
| # Set the version | if ( ! $pass) { | 
| $self->{version} = 4; | croak("'password' not set!\n"); | 
|  | } | 
|  |  | 
| # Give the PDB the first record that will hold the encrypted password | if ( ! $rec) { | 
| $self->{records} = [ $self->new_Record ]; | croak("Needed parameter 'record' not passed!\n"); | 
|  | } | 
|  |  | 
| if (defined $pass) { | if ( ! $data) { | 
| $self->Encrypt($pass); | croak("Needed parameter 'data' not passed!\n"); | 
| } | } | 
|  |  | 
| return $self; | if ( ! $self->Password($pass)) { | 
| } | croak("Incorrect Password!\n"); | 
|  | } | 
|  |  | 
| sub import | $self->{'digest'}   ||= _calc_keys( $pass ); | 
| { |  | 
| &Palm::PDB::RegisterPDBHandlers(__PACKAGE__, |  | 
| [ "Gtkr", "Gkyr" ], |  | 
| ); |  | 
| } |  | 
|  |  | 
| =pod | $data->{'account'}  ||= $EMPTY; | 
|  | $data->{'password'} ||= $EMPTY; | 
|  | $data->{'notes'}    ||= $EMPTY; | 
|  |  | 
| =head2 Load | my $changed      = 0; | 
|  | my $need_newdate = 0; | 
|  | my $acct = {}; | 
|  | if ($rec->{'encrypted'}) { | 
|  | $acct = $self->Decrypt($rec, $pass); | 
|  | foreach my $key (keys %{ $data }) { | 
|  | next if $key eq 'lastchange'; | 
|  | if ($data->{$key} ne $acct->{$key}) { | 
|  | $changed = 1; | 
|  | last; | 
|  | } | 
|  | } | 
|  | if ( exists $data->{'lastchange'} && exists $acct->{'lastchange'} && ( | 
|  | $data->{'lastchange'}->{day}   != $acct->{'lastchange'}->{day}   || | 
|  | $data->{'lastchange'}->{month} != $acct->{'lastchange'}->{month} || | 
|  | $data->{'lastchange'}->{year}  != $acct->{'lastchange'}->{year} | 
|  | )) { | 
|  | $changed = 1; | 
|  | $need_newdate = 0; | 
|  | } else { | 
|  | $need_newdate = 1; | 
|  | } | 
|  |  | 
| $pdb->Load($filename[, $password]); | } else { | 
|  | $changed = 1; | 
|  | } | 
|  |  | 
| Overrides the standard Palm::Raw Load() to add | # no need to re-encrypt if it has not changed. | 
| $record->{'plaintext'}->{'name'} and | return 1 if ! $changed; | 
| $record->{'encrypted'} fields. |  | 
| $record->{'plaintext'}->{'name'} holds the name of the record, |  | 
| $record->{'encrypted'} is the encrypted information in the PDB. |  | 
|  |  | 
| It also takes an additional optional parameter, which is the password to use to | my ($day, $month, $year); | 
| decrypt the database. |  | 
|  |  | 
| See Decrypt() for the additional fields that are available after decryption. | if ($data->{'lastchange'} && ! $need_newdate ) { | 
|  | $day   = $data->{'lastchange'}->{'day'}   || 1; | 
|  | $month = $data->{'lastchange'}->{'month'} || 0; | 
|  | $year  = $data->{'lastchange'}->{'year'}  || 0; | 
|  |  | 
| =cut | # XXX Need to actually validate the above information somehow | 
|  | if ($year >= 1900) { | 
|  | $year -= 1900; | 
|  | } | 
|  | } else { | 
|  | $need_newdate = 1; | 
|  | } | 
|  |  | 
| sub Load | if ($need_newdate) { | 
| { | ($day, $month, $year) = (localtime)[3,4,5]; | 
| my $self     = shift; | } | 
| my $filename = shift; | $year -= 4; | 
| my $password = shift; | $month++; | 
|  |  | 
| $self->{'appinfo'} = {}; |  | 
| $self->{'records'} = []; |  | 
| $self->SUPER::Load($filename); |  | 
|  |  | 
| foreach my $record (@{ $self->{records} }) { | my $p = $day | ($month << 5) | ($year << 9); | 
| next unless exists $record->{data}; | my $packeddate = pack 'n', $p; | 
| my ($name, $encrypted) = split /\000/, $record->{data}, 2; |  | 
| next unless $encrypted; |  | 
| $record->{plaintext}->{name} = $name; |  | 
| $record->{encrypted} = $encrypted; |  | 
| } |  | 
|  |  | 
| return $self->Decrypt($password) if defined $password; | my $plaintext = join $NULL, | 
|  | $data->{'account'}, $data->{'password'}, $data->{'notes'}, $packeddate; | 
|  |  | 
| 1; | my $encrypted = _crypt3des( $plaintext, $self->{'digest'}, $ENCRYPT ); | 
|  |  | 
|  | return if ! $encrypted; | 
|  |  | 
|  | $rec->{'attributes'}{'Dirty'} = 1; | 
|  | $rec->{'attributes'}{'dirty'} = 1; | 
|  | $rec->{'name'}    ||= $data->{'name'}; | 
|  | $rec->{'encrypted'} = $encrypted; | 
|  |  | 
|  | return 1; | 
| } | } | 
|  |  | 
| =pod | sub Decrypt { | 
|  | my $self = shift; | 
|  | my $rec  = shift; | 
|  | my $pass = shift || $self->{'password'}; | 
|  |  | 
| =head2 Write | if ( ! $pass) { | 
|  | croak("'password' not set!\n"); | 
|  | } | 
|  |  | 
| $pdb->Write($filename[, $password]); | if ( ! $rec) { | 
|  | croak("Needed parameter 'record' not passed!\n"); | 
|  | } | 
|  |  | 
| Just like the Palm::Raw::Write() but encrypts everything before saving. | if ( ! $self->Password($pass)) { | 
|  | croak("Invalid Password!\n"); | 
|  | } | 
|  |  | 
| Also takes an optional password to encrypt with a new password, not needed | if ( ! $rec->{'encrypted'} ) { | 
| unless you are changing the password. | croak("No encrypted content!"); | 
|  | } | 
|  |  | 
| =cut | $self->{'digest'} ||= _calc_keys( $pass ); | 
|  |  | 
| sub Write | my $decrypted = | 
| { | _crypt3des( $rec->{'encrypted'}, $self->{'digest'}, $DECRYPT ); | 
| my $self = shift; | my ( $account, $password, $notes, $packeddate ) = split /$NULL/xm, | 
| my $filename = shift; | $decrypted, 4; | 
| my $password = shift; |  | 
|  |  | 
| $self->Encrypt($password) || return undef; | my %Modified; | 
| return $self->SUPER::Write($filename); | if ($packeddate) { | 
|  | my $u = unpack 'n', $packeddate; | 
|  | my $year  = (($u & 0xFE00) >> 9) + 4; # since 1900 | 
|  | my $month = (($u & 0x01E0) >> 5) - 1; # 0-11 | 
|  | my $day   = (($u & 0x001F) >> 0);     # 1-31 | 
|  |  | 
|  | %Modified = ( | 
|  | year   => $year, | 
|  | month  => $month || 0, | 
|  | day    => $day   || 1, | 
|  | ); | 
|  | } | 
|  |  | 
|  | return { | 
|  | name       => $rec->{'name'}, | 
|  | account    => $account, | 
|  | password   => $password, | 
|  | notes      => $notes, | 
|  | lastchange => \%Modified, | 
|  | }; | 
| } | } | 
|  |  | 
| =pod | sub Password { | 
|  | my $self = shift; | 
|  | my $pass = shift; | 
|  | my $new_pass = shift; | 
|  |  | 
| =head2 Encrypt | if (! $pass) { | 
|  | delete $self->{password}; | 
|  | return 1; | 
|  | } | 
|  |  | 
| $pdb->Encrypt([$password]); | if (! exists $self->{'records'}) { | 
|  | # Give the PDB the first record that will hold the encrypted password | 
|  | $self->{'records'} = [ $self->new_Record ]; | 
|  |  | 
| Encrypts the PDB, either with the password used to decrypt or create it, or | return $self->_password_update($pass); | 
| optionally with a password that is passed. | } | 
|  |  | 
| See Decrypt() for an what plaintext fields are available to be encrypted. | if ($new_pass) { | 
|  | my @accts = (); | 
|  | foreach my $i (0..$#{ $self->{'records'} }) { | 
|  | if ($i == 0) { | 
|  | push @accts, undef; | 
|  | next; | 
|  | } | 
|  | my $acct = $self->Decrypt($self->{'records'}->[$i], $pass); | 
|  | if ( ! $acct ) { | 
|  | croak("Couldn't decrypt $self->{'records'}->[$i]->{'name'}"); | 
|  | } | 
|  | push @accts, $acct; | 
|  | } | 
|  |  | 
| =cut | if ( ! $self->_password_update($new_pass)) { | 
|  | croak("Couldn't set new password!"); | 
|  | } | 
|  | $pass = $new_pass; | 
|  |  | 
| sub Encrypt | foreach my $i (0..$#accts) { | 
| { | next if $i == 0; | 
| my $self = shift; | delete $self->{'records'}->[$i]->{'encrypted'}; | 
| my $pass = shift; | $self->Encrypt($self->{'records'}->[$i], $accts[$i], $pass); | 
|  | } | 
|  | } | 
|  |  | 
| if ($pass) { | return $self->_password_verify($pass); | 
| unless (exists $self->{'records'}->[0]->{'data'} && | } | 
| $self->_keyring_verify($pass) ) { |  | 
| # This would encrypt with a new password. |  | 
| # First decrypting everything with the old password of course. |  | 
| $self->_keyring_update($pass) || return undef; |  | 
| $self->_keyring_verify($pass) || return undef; |  | 
| } |  | 
| } |  | 
|  |  | 
| $self->{digest} ||= _calc_keys($self->{password}); | sub _calc_keys { | 
|  | my $pass = shift; | 
|  | if (! defined $pass) { croak('No password defined!'); }; | 
|  |  | 
| foreach my $record (@{ $self->{records} }) { | my $digest = md5($pass); | 
| next unless defined $record->{plaintext}; |  | 
|  |  | 
| my $name        = defined $record->{plaintext}->{name}        ? | my ( $key1, $key2 ) = unpack 'a8a8', $digest; | 
| $record->{plaintext}->{name}        : ''; |  | 
| my $account     = defined $record->{plaintext}->{account}     ? |  | 
| $record->{plaintext}->{account}     : ''; |  | 
| my $password    = defined $record->{plaintext}->{password}    ? |  | 
| $record->{plaintext}->{password}    : ''; |  | 
| my $description = defined $record->{plaintext}->{description} ? |  | 
| $record->{plaintext}->{description} : ''; |  | 
| my $extra       = ''; |  | 
|  |  | 
| my $plaintext = join("\000", $account, $password, $description, $extra); | #-------------------------------------------------- | 
|  | # print "key1: $key1: ", length $key1, "\n"; | 
|  | # print "key2: $key2: ", length $key2, "\n"; | 
|  | #-------------------------------------------------- | 
|  |  | 
| my $encrypted = _crypt3des($plaintext, $self->{digest}, ENCRYPT); | $digest = unpack 'H*', $key1 . $key2 . $key1; | 
|  |  | 
| $record->{data} = join("\000", $name, $encrypted); | #-------------------------------------------------- | 
| } | # print "Digest: ", $digest, "\n"; | 
|  | # print length $digest, "\n"; | 
|  | #-------------------------------------------------- | 
|  |  | 
| 1; | return $digest; | 
| } | } | 
|  |  | 
| =head2 Decrypt | sub _password_verify { | 
|  | my $self = shift; | 
|  | my $pass = shift; | 
|  |  | 
| $pdb->Decrypt([$password]); | if (! $pass) { croak('No password specified!'); }; | 
|  |  | 
| Decrypts the PDB and fills out the rest of the fields available in | if (defined $self->{'password'} && $pass eq $self->{'password'}) { | 
| $record->{'plaintext'}. | # already verified this password | 
|  | return 1; | 
|  | } | 
|  |  | 
| The plaintext should now be this, before encryption or after decryption: | # AFAIK the thing we use to test the password is | 
|  | #     always in the first entry | 
|  | my $data = $self->{'records'}->[0]->{'data'}; | 
|  |  | 
| $record->{'plaintext'} = { | #die "No encrypted password in file!" unless defined $data; | 
| name        => $name, | if ( ! defined $data) { return; }; | 
| account     => $account, |  | 
| password    => $account_password, |  | 
| description => $description, |  | 
| }; |  | 
|  |  | 
| =cut | $data =~ s/$NULL$//xm; | 
|  |  | 
| sub Decrypt | my $salt = substr $data, 0, $kSalt_Size; | 
| { |  | 
| my $self = shift; |  | 
| my $pass = shift; |  | 
|  |  | 
| if ($pass) { | my $msg = $salt . $pass; | 
| $self->_keyring_verify($pass) || return undef; |  | 
| } |  | 
|  |  | 
| $self->{digest} ||= _calc_keys($self->{password}); | $msg .= "\0" x ( $MD5_CBLOCK - length $msg ); | 
|  |  | 
| foreach my $record (@{ $self->{records} }) { | my $digest = md5($msg); | 
| next unless defined $record->{data}; |  | 
|  |  | 
| my ($name, $encrypted) = split /\000/, $record->{data}, 2; | if ( $data eq $salt . $digest ) { | 
| next unless $encrypted; |  | 
|  |  | 
| $record->{plaintext}->{name} = $name; | # May as well generate the keys we need now, since we know the password is right | 
|  | $self->{'digest'} = _calc_keys($pass); | 
|  | if ( $self->{'digest'} ) { | 
|  | $self->{'password'} = $pass; | 
|  | return 1; | 
|  | } | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
| my $decrypted = _crypt3des($encrypted, $self->{digest}, DECRYPT); | sub _password_update { | 
| my ($account, $password, $description, $extra) |  | 
| = split /\000/, $decrypted, 4; |  | 
|  |  | 
| $record->{plaintext}->{account}     = defined $account     ? | # It is very important to Encrypt after calling this | 
| $account     : ''; | #     (Although it is generally only called by Encrypt) | 
| $record->{plaintext}->{password}    = defined $password    ? | # because otherwise the data will be out of sync with the | 
| $password    : ''; | # password, and that would suck! | 
| $record->{plaintext}->{description} = defined $description ? | my $self = shift; | 
| $description : ''; | my $pass = shift; | 
|  |  | 
| #print "Name:      '$name'\n"; | if (! defined $pass) { croak('No password specified!'); }; | 
| #print "Encrypted: '$encrypted' - Length: " . length($encrypted) . "\n"; |  | 
| #print "    Hex:   '" . unpack("H*", $encrypted) . "'\n"; |  | 
| #print "    Binary:'" . unpack("b*", $encrypted) . "'\n"; |  | 
| #print "Decrypted: '$decrypted' - Length: " . length($decrypted) . "\n"; |  | 
| #print "    Hex:   '" . unpack("H*", $decrypted) . "'\n"; |  | 
| #print "    Binary:'" . unpack("b*", $decrypted) . "'\n"; |  | 
| #print "\n"; |  | 
| #print "Extra: $extra\n"; |  | 
| #exit; |  | 
| #-------------------------------------------------- |  | 
| # print "Account:     $account\n"; |  | 
| # print "Password:    $password\n"; |  | 
| # print "Description: $description\n"; |  | 
| #-------------------------------------------------- |  | 
|  |  | 
| } | my $salt; | 
|  | for ( 1 .. $kSalt_Size ) { | 
|  | $salt .= chr int rand 255; | 
|  | } | 
|  |  | 
| 1; | my $msg = $salt . $pass; | 
| } |  | 
|  |  | 
| sub _calc_keys | $msg .= "\0" x ( $MD5_CBLOCK - length $msg ); | 
| { |  | 
| my $pass = shift; |  | 
| die "No password defined!" unless defined $pass; |  | 
|  |  | 
| my $digest = md5($pass); | my $digest = md5($msg); | 
|  |  | 
| my ($key1, $key2) = unpack('a8a8', $digest); | my $data = $salt . $digest;    # . "\0"; | 
| #-------------------------------------------------- |  | 
| # print "key1: $key1: ", length $key1, "\n"; |  | 
| # print "key2: $key2: ", length $key2, "\n"; |  | 
| #-------------------------------------------------- |  | 
|  |  | 
| $digest = unpack('H*', $key1 . $key2 . $key1); | # AFAIK the thing we use to test the password is | 
| #-------------------------------------------------- | #     always in the first entry | 
| # print "Digest: ", $digest, "\n"; | $self->{'records'}->[0]->{'data'} = $data; | 
| # print length $digest, "\n"; |  | 
| #-------------------------------------------------- |  | 
|  |  | 
| return $digest; | $self->{'password'} = $pass; | 
|  | $self->{'digest'}   = _calc_keys( $self->{'password'} ); | 
|  |  | 
|  | return 1; | 
| } | } | 
|  |  | 
| sub _keyring_verify | sub _crypt3des { | 
| { | my ( $plaintext, $passphrase, $flag ) = @_; | 
| my $self = shift; |  | 
| my $pass = shift; |  | 
|  |  | 
| die "No password specified!" unless $pass; | $passphrase   .= $SPACE x ( 16 * 3 ); | 
|  | my $cyphertext = $EMPTY; | 
|  |  | 
| # AFAIK the thing we use to test the password is | my $size = length $plaintext; | 
| #     always in the first entry |  | 
| my $data = $self->{records}->[0]->{data}; |  | 
| #die "No encrypted password in file!" unless defined $data; |  | 
| return undef unless defined $data; |  | 
|  |  | 
| $data =~ s/\0$//; | #print "STRING: '$plaintext' - Length: " . (length $plaintext) . "\n"; | 
|  |  | 
| my $salt = substr($data, 0, $kSaltSize); | my @C; | 
|  | for ( 0 .. 2 ) { | 
|  | $C[$_] = | 
|  | new Crypt::DES( pack 'H*', ( substr $passphrase, 16 * $_, 16 )); | 
|  | } | 
|  |  | 
| my $msg = $salt . $pass; | for ( 0 .. ( ($size) / 8 ) ) { | 
|  | my $pt = substr $plaintext, $_ * 8, 8; | 
|  |  | 
| $msg .= "\0" x (MD5_CBLOCK - length($msg)); | #print "PT: '$pt' - Length: " . length($pt) . "\n"; | 
|  | if (! length $pt) { next; }; | 
|  | if ( (length $pt) < 8 ) { | 
|  | if ($flag == $DECRYPT) { croak('record not 8 byte padded'); }; | 
|  | my $len = 8 - (length $pt); | 
|  |  | 
| my $digest = md5($msg); | #print "LENGTH: $len\n"; | 
|  | #print "Binary:    '" . unpack("b*", $pt) . "'\n"; | 
|  | $pt .= ($NULL x $len); | 
|  |  | 
| if ($data eq $salt . $digest) { | #print "PT: '$pt' - Length: " . length($pt) . "\n"; | 
| # May as well generate the keys we need now, since we know the password is right | #print "Binary:    '" . unpack("b*", $pt) . "'\n"; | 
| $self->{digest} = _calc_keys($pass); | } | 
| if ($self->{digest}) { | if ( $flag == $ENCRYPT ) { | 
| $self->{password} = $pass; | $pt = $C[0]->encrypt($pt); | 
| return 1; | $pt = $C[1]->decrypt($pt); | 
| } else { | $pt = $C[2]->encrypt($pt); | 
| return undef; | } | 
| } | else { | 
| } else { | $pt = $C[0]->decrypt($pt); | 
| return undef; | $pt = $C[1]->encrypt($pt); | 
| } | $pt = $C[2]->decrypt($pt); | 
|  | } | 
|  |  | 
|  | #print "PT: '$pt' - Length: " . length($pt) . "\n"; | 
|  | $cyphertext .= $pt; | 
|  | } | 
|  |  | 
|  | $cyphertext =~ s/$NULL+$//xm; | 
|  |  | 
|  | #print "CT: '$cyphertext' - Length: " . length($cyphertext) . "\n"; | 
|  |  | 
|  | return $cyphertext; | 
| } | } | 
|  |  | 
| sub _keyring_update | 1; | 
| { | __END__ | 
| # It is very important to Encrypt after calling this |  | 
| #     (Although it is generally only called by Encrypt) |  | 
| # because otherwise the data will be out of sync with the |  | 
| # password, and that would suck! |  | 
| my $self = shift; |  | 
| my $pass = shift; |  | 
|  |  | 
| die "No password specified!" unless $pass; | =head1 NAME | 
|  |  | 
| # if the database already has a password in it | Palm::Keyring - Handler for Palm Keyring databases. | 
| if ($self->{records}->[0]->{data}) { |  | 
| # Make sure everything is decrypted before we update the keyring |  | 
| $self->Decrypt() || return undef; |  | 
| } |  | 
|  |  | 
| my $salt; | =head1 DESCRIPTION | 
| for (1..$kSaltSize) { |  | 
| $salt .= chr(int(rand(255))); |  | 
| } |  | 
|  |  | 
| my $msg = $salt . $pass; | The Keyring PDB handler is a helper class for the Palm::PDB package. It | 
|  | parses Keyring for Palm OS databases.  See | 
|  | L<http://gnukeyring.sourceforge.net/>. | 
|  |  | 
| $msg .= "\0" x (MD5_CBLOCK - length($msg)); | It has the standard Palm::PDB methods with 2 additional public methods. | 
|  | Decrypt and Encrypt. | 
|  |  | 
| my $digest = md5($msg); | It currently supports the v4 Keyring databases.  The v5 databases from | 
|  | the pre-release keyring-2.0 are not supported. | 
|  |  | 
| my $data = $salt . $digest;# . "\0"; | This module doesn't store the decrypted content.  It only keeps it until it | 
|  | returns it to you or encrypts it. | 
|  |  | 
| # AFAIK the thing we use to test the password is | =head1 SYNOPSIS | 
| #     always in the first entry |  | 
| $self->{records}->[0]->{data} = $data; |  | 
|  |  | 
| $self->{password} = $pass; | use Palm::PDB; | 
| $self->{digest}   = _calc_keys($self->{password}); | use Palm::Keyring; | 
|  |  | 
|  | my $pass = 'password'; | 
|  | my $file = 'Keys-Gtkr.pdb'; | 
|  | my $pdb  = new Palm::PDB; | 
|  | $pdb->Load($file); | 
|  |  | 
|  | foreach (0..$#{ $pdb->{'records'} }) { | 
|  | next if $_ = 0; # skip the password record | 
|  | my $rec  = $pdb->{'records'}->[$_]; | 
|  | my $acct = $pdb->Decrypt($rec, $pass); | 
|  | print $rec->{'name'}, ' - ', $acct->{'account'}, "\n"; | 
|  | } | 
|  |  | 
| return 1; | =head1 SUBROUTINES/METHODS | 
| } |  | 
|  |  | 
| sub _crypt3des { | =head2 new | 
| my ( $plaintext, $passphrase, $flag ) = @_; |  | 
| my $NULL = chr(0); |  | 
|  |  | 
| $passphrase .= ' ' x (16*3); | $pdb = new Palm::Keyring([$password]); | 
| my $cyphertext = ""; |  | 
|  |  | 
| my $size = length ( $plaintext ); | Create a new PDB, initialized with the various Palm::Keyring fields | 
| #print "STRING: '$plaintext' - Length: " . length($plaintext) . "\n"; | and an empty record list. | 
|  |  | 
| my @C; | Use this method if you're creating a Keyring PDB from scratch otherwise you | 
| for ( 0..2 ) { | can just use Palm::PDB::new() before calling Load(). | 
| $C[$_] = new Crypt::DES( pack( "H*", substr($passphrase, 16*$_, 16 ))); |  | 
| } |  | 
|  |  | 
| for ( 0 .. (($size)/8)) { | If you pass in a password, it will initalize the first record with the encrypted | 
| my $pt = substr( $plaintext, $_*8, 8 ); | password. | 
| #print "PT: '$pt' - Length: " . length($pt) . "\n"; |  | 
| next unless length($pt); |  | 
| if (length($pt) < 8) { |  | 
| die "record not 8 byte padded" if  $flag == DECRYPT; |  | 
| my $len = 8 - length($pt); |  | 
| #print "LENGTH: $len\n"; |  | 
| #print "Binary:    '" . unpack("b*", $pt) . "'\n"; |  | 
| $pt .= ($NULL x $len); |  | 
| #print "PT: '$pt' - Length: " . length($pt) . "\n"; |  | 
| #print "Binary:    '" . unpack("b*", $pt) . "'\n"; |  | 
| } |  | 
| if ($flag == ENCRYPT) { |  | 
| $pt = $C[0]->encrypt( $pt ); |  | 
| $pt = $C[1]->decrypt( $pt ); |  | 
| $pt = $C[2]->encrypt( $pt ); |  | 
| } else { |  | 
| $pt = $C[0]->decrypt( $pt ); |  | 
| $pt = $C[1]->encrypt( $pt ); |  | 
| $pt = $C[2]->decrypt( $pt ); |  | 
| } |  | 
| #print "PT: '$pt' - Length: " . length($pt) . "\n"; |  | 
| $cyphertext .= $pt; |  | 
| } |  | 
|  |  | 
| $cyphertext =~ s/$NULL+$//; | =head2 Encrypt | 
| #print "CT: '$cyphertext' - Length: " . length($cyphertext) . "\n"; |  | 
|  |  | 
| return $cyphertext; | $pdb->Encrypt($rec, $acct[, $password]); | 
| } |  | 
|  |  | 
| 1; | Encrypts an account into a record, either with the password previously | 
| __END__ | used, or with a password that is passed. | 
|  |  | 
|  | $rec is a record from $pdb->{'records'} or a new_Record(). | 
|  | $acct is a hashref in the format below. | 
|  |  | 
|  | my $acct = { | 
|  | name       => $rec->{'name'}, | 
|  | account    => $account, | 
|  | password   => $password, | 
|  | notes      => $notes, | 
|  | lastchange => { | 
|  | year  => 107, # years since 1900 | 
|  | month =>   0, # 0-11, 0 = January, 11 = December | 
|  | day   =>  30, # 1-31, same as localtime | 
|  | }, | 
|  | }; | 
|  |  | 
|  | 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. | 
|  |  | 
|  | If you pass in a lastchange field that is different than the one in the | 
|  | record, it will honor what you passed in. | 
|  |  | 
|  | Encrypt() only uses the $acct->{'name'} if there is not already a $rec->{'name'}. | 
|  |  | 
|  | =head2 Decrypt | 
|  |  | 
|  | my $acct = $pdb->Decrypt($rec[, $password]); | 
|  |  | 
|  | Decrypts the record and returns a hashref for the account as described | 
|  | under Encrypt(). | 
|  |  | 
|  | foreach (0..$#{ $pdb->{'records'}) { | 
|  | next if $_ == 0; | 
|  | my $rec = $pdb->{'records'}->[$_]; | 
|  | my $acct = $pdb->Decrypt($rec[, $password]); | 
|  | # do something with $acct | 
|  | } | 
|  |  | 
|  | =head2 Password | 
|  |  | 
|  | $pdb->Password([$password[, $new_password]]); | 
|  |  | 
|  | Either sets the password to be used to crypt, or if you pass $new_password, | 
|  | changes the password on the database. | 
|  |  | 
|  | If you have created a new $pdb, and you didn't set a password when you | 
|  | called new(), you only need to pass one password and it will set that as | 
|  | the password. | 
|  |  | 
|  | If nothing is passed, it forgets the password that it was remembering. | 
|  |  | 
|  | =head1 DEPENDENCIES | 
|  |  | 
|  | Palm::StdAppInfo | 
|  |  | 
|  | Digest::MD5 | 
|  |  | 
|  | Crypt::DES | 
|  |  | 
|  | Readonly | 
|  |  | 
|  | =head1 THANKS | 
|  |  | 
|  | I would like to thank the helpful Perlmonk shigetsu who gave me some great advice | 
|  | and helped me get my first module posted.  L<http://perlmonks.org/?node_id=596998> | 
|  |  | 
|  | I would also like to thank | 
|  | Johan Vromans | 
|  | E<lt>jvromans@squirrel.nlE<gt> -- | 
|  | L<http://www.squirrel.nl/people/jvromans>. | 
|  | He had his own Palm::KeyRing module that he posted a couple of days before | 
|  | mine was ready and he was kind enough to let me have the namespace as well | 
|  | as giving me some very helpful hints about doing a few things that I was | 
|  | unsure of.  He is really great. | 
|  |  | 
|  | =head1 BUGS AND LIMITATIONS | 
|  |  | 
|  | Please report any bugs or feature requests to | 
|  | C<bug-palm-keyring at rt.cpan.org>, or through the web interface at | 
|  | L<http://rt.cpan.org>.  I will be notified, and then you'll automatically be | 
|  | notified of progress on your bug as I make changes. | 
|  |  | 
| =head1 AUTHOR | =head1 AUTHOR | 
|  |  | 
| Andrew Fresh E<lt>andrew@mad-techies.org<gt> | Andrew Fresh E<lt>andrew@cpan.orgE<gt> | 
|  |  | 
|  | =head1 LICENSE AND COPYRIGHT | 
|  |  | 
|  | Copyright 2004, 2005, 2006, 2007 Andrew Fresh, All Rights Reserved. | 
|  |  | 
|  | This program is free software; you can redistribute it and/or | 
|  | modify it under the same terms as Perl itself. | 
|  |  | 
| =head1 SEE ALSO | =head1 SEE ALSO | 
|  |  | 
| Palm::PDB(3) | Palm::PDB(3) | 
|  |  | 
| The Keyring for Palm OS website: | The Keyring for Palm OS website: | 
| L<http://gnukeyring.sourceforge.net/> | L<http://gnukeyring.sourceforge.net/> | 
|  |  | 
| =cut | Johan Vromans also has a wxkeyring app that now uses this module, available | 
|  | from his website at L<http://www.vromans.org/johan/software/sw_palmkeyring.html> |