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

File: [local] / palm / Palm-Keyring / lib / Palm / Keyring.pm (download)

Revision 1.14, Sun Jan 28 22:24:17 2007 UTC (17 years, 6 months ago) by andrew
Branch: MAIN
Changes since 1.13: +383 -361 lines

LOTS of changes for Perl::Critic.  Dunno if it was worth it, but hey, it's a start.

package Palm::Keyring;

# $RedRiver: Keyring.pm,v 1.13 2007/01/28 18:13:28 andrew Exp $
#
# Perl class for dealing with Keyring for Palm OS databases.
#
#   This started as Memo.pm, I just made it work for Keyring.

use strict;
use warnings;
use Carp;

use base qw/ Palm::StdAppInfo /;

use Digest::MD5 qw(md5);
use Crypt::DES;
use Readonly;

Readonly my $ENCRYPT    => 1;
Readonly my $DECRYPT    => 0;
Readonly my $MD5_CBLOCK => 64;
Readonly my $kSalt_Size => 4;
Readonly my $EMPTY      => q{};
Readonly my $SPACE      => q{ };
Readonly my $NULL       => chr 0;

# One liner, to allow MakeMaker to work.
our ($VERSION) = q$Revision: 1.14 $ =~ m{ Revision: \s+ (\S+) }xm;

#@ISA = qw( Palm::StdAppInfo Palm::Raw );

sub new {
    my $classname = shift;
    my $pass      = shift;

    # Create a generic PDB. No need to rebless it, though.
    my $self = $classname->SUPER::new(@_);

    $self->{'name'}    = 'Keys-Gtkr';    # Default
    $self->{'creator'} = 'Gtkr';
    $self->{'type'}    = 'Gkyr';

    # 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;

    # Initialize the AppInfo block
    $self->{'appinfo'} = {};

    # Add the standard AppInfo block stuff
    Palm::StdAppInfo::seed_StdAppInfo( $self->{'appinfo'} );

    # Set the version
    $self->{'version'} = 4;

    # Give the PDB the first record that will hold the encrypted password
    $self->{'records'} = [ $self->new_Record ];

    if ( defined $pass ) {
        $self->Encrypt($pass);
    }

    return $self;
}

sub import {
    Palm::PDB::RegisterPDBHandlers( __PACKAGE__, [ 'Gtkr', 'Gkyr' ], );
    return 1;
}

sub Load {
    my $self     = shift;
    my $filename = shift;
    my $password = shift;

    $self->{'appinfo'} = {};
    $self->{'records'} = [];
    $self->SUPER::Load($filename);

    foreach my $rec ( @{ $self->{'records'} } ) {
        if ( ! exists $rec->{'data'}) { next; };
        my ( $name, $encrypted ) = split /$NULL/xm, $rec->{'data'}, 2;
        if ( ! $encrypted ) { next };
        $rec->{'plaintext'}->{'name'} = $name;
        $rec->{'encrypted'} = $encrypted;
    }

    return $self->Decrypt($password) if defined $password;

    return 1;
}

sub Write {
    my $self     = shift;
    my $filename = shift;
    my $password = shift;

    $self->Encrypt($password) || return;
    return $self->SUPER::Write($filename);
}

sub Encrypt {
    my $self = shift;
    my $pass = shift;

    if ($pass) {
        if (
            !( 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;
            $self->_keyring_verify($pass) || return;
        }
    }

    $self->{'digest'} ||= _calc_keys( $self->{'password'} );

    foreach my $rec ( @{ $self->{'records'} } ) {
        if (!defined $rec->{'plaintext'}) { next; };

        my $name =
          defined $rec->{'plaintext'}->{'name'}
          ? $rec->{'plaintext'}->{'name'}
          : $EMPTY;
        my $account =
          defined $rec->{'plaintext'}->{'account'}
          ? $rec->{'plaintext'}->{'account'}
          : $EMPTY;
        my $password =
          defined $rec->{'plaintext'}->{'password'}
          ? $rec->{'plaintext'}->{'password'}
          : $EMPTY;
        my $description =
          defined $rec->{'plaintext'}->{'description'}
          ? $rec->{'plaintext'}->{'description'}
          : $EMPTY;
        my $extra = $EMPTY;

        my $plaintext = join "$NULL", $account, $password, $description, $extra;

        my $encrypted = _crypt3des( $plaintext, $self->{'digest'}, $ENCRYPT );

        $rec->{'data'} = join "$NULL", $name, $encrypted;
    }

    return 1;
}

sub Decrypt {
    my $self = shift;
    my $pass = shift;

    if ($pass) {
        $self->_keyring_verify($pass) || return;
    }

    $self->{'digest'} ||= _calc_keys( $self->{'password'} );

    my $reccount = 0;
    foreach my $rec ( @{ $self->{'records'} } ) {
        $reccount++;

        # always skip the first record that has the password in it.
        next if $reccount <= 1;
        if ( ! defined $rec->{'data'} ) {
            warn 'Invalid record ' . ( $reccount - 1 ) . "\n";
            next;
        }

        my ( $name, $encrypted ) = split /$NULL/xm, $rec->{'data'}, 2;
        if (! $encrypted) { next; };

        $rec->{'plaintext'}->{'name'} = $name;

        my $decrypted = _crypt3des( $encrypted, $self->{'digest'}, $DECRYPT );
        my ( $account, $password, $description, $extra ) = split /$NULL/xm,
          $decrypted, 4;

        $rec->{'plaintext'}->{'account'} = defined $account ? $account : $EMPTY;
        $rec->{'plaintext'}->{'password'} =
          defined $password ? $password : $EMPTY;
        $rec->{'plaintext'}->{'description'} =
          defined $description ? $description : $EMPTY;

        #print "Name:      '$name'\n";
        #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";
        #--------------------------------------------------

    }

    return 1;
}

sub _calc_keys {
    my $pass = shift;
    if (! defined $pass) { croak('No password defined!'); };

    my $digest = md5($pass);

    my ( $key1, $key2 ) = unpack 'a8a8', $digest;

    #--------------------------------------------------
    # print "key1: $key1: ", length $key1, "\n";
    # print "key2: $key2: ", length $key2, "\n";
    #--------------------------------------------------

    $digest = unpack 'H*', $key1 . $key2 . $key1;

    #--------------------------------------------------
    # print "Digest: ", $digest, "\n";
    # print length $digest, "\n";
    #--------------------------------------------------

    return $digest;
}

sub _keyring_verify {
    my $self = shift;
    my $pass = shift;

    if (! $pass) { croak('No password specified!'); };

    # AFAIK the thing we use to test the password is
    #     always in the first entry
    my $data = $self->{'records'}->[0]->{'data'};

    #die "No encrypted password in file!" unless defined $data;
    if (! defined $data) { return; };

    $data =~ s/$NULL$//xm;

    my $salt = substr $data, 0, $kSalt_Size;

    my $msg = $salt . $pass;

    $msg .= "\0" x ( $MD5_CBLOCK - length $msg );

    my $digest = md5($msg);

    if ( $data eq $salt . $digest ) {

# 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;
}

sub _keyring_update {

    # 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;

    if (! $pass) { croak('No password specified!'); };

    # if the database already has a password in it
    if ( $self->{'records'}->[0]->{'data'} ) {

        # Make sure everything is decrypted before we update the keyring
        $self->Decrypt() || return;
    }

    my $salt;
    for ( 1 .. $kSalt_Size ) {
        $salt .= chr int rand 255;
    }

    my $msg = $salt . $pass;

    $msg .= "\0" x ( $MD5_CBLOCK - length $msg );

    my $digest = md5($msg);

    my $data = $salt . $digest;    # . "\0";

    # AFAIK the thing we use to test the password is
    #     always in the first entry
    $self->{'records'}->[0]->{'data'} = $data;

    $self->{'password'} = $pass;
    $self->{'digest'}   = _calc_keys( $self->{'password'} );

    return 1;
}

sub _crypt3des {
    my ( $plaintext, $passphrase, $flag ) = @_;

    $passphrase   .= $SPACE x ( 16 * 3 );
    my $cyphertext = $EMPTY;

    my $size = length $plaintext;

    #print "STRING: '$plaintext' - Length: " . (length $plaintext) . "\n";

    my @C;
    for ( 0 .. 2 ) {
        $C[$_] =
          new Crypt::DES( pack 'H*', ( substr $passphrase, 16 * $_, 16 ));
    }

    for ( 0 .. ( ($size) / 8 ) ) {
        my $pt = substr $plaintext, $_ * 8, 8;

        #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);

            #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+$//xm;

    #print "CT: '$cyphertext' - Length: " . length($cyphertext) . "\n";

    return $cyphertext;
}

1;
__END__

=head1 NAME

Palm::Keyring - Handler for Palm Keyring databases.

=head1 DESCRIPTION

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/>.

It has the standard Palm::PDB methods with 2 additional public methods.  
Decrypt and Encrypt.

It currently supports the v4 Keyring databases.  The v5 databases from the pre-release keyring-2.0 are not supported.

=head1 SYNOPSIS

	use Palm::PDB;
	use Palm::Keyring;
	my $pdb = new Palm::PDB;
	$pdb->Load($file);
	foreach my $rec (@{ $pdb->{'records'} }) {
		print "$rec->{'plaintext'}->{'name'}\n";
	}
	$pdb->Decrypt($password);
	# do something with the decrypted parts

=head1 SUBROUTINES/METHODS

=head2 new

	$pdb = new Palm::Keyring([$password]);

Create a new PDB, initialized with the various Palm::Keyring fields
and an empty record list.

Use this method if you're creating a Keyring PDB from scratch otherwise you 
can just use Palm::PDB::new().

=head2 Load

	$pdb->Load($filename[, $password]);

Overrides the standard Palm::Raw Load() to add 
$rec->{'plaintext'}->{'name'} and 
$rec->{'encrypted'} fields.  
$rec->{'plaintext'}->{'name'} holds the name of the record,  
$rec->{'encrypted'} is the encrypted information in the PDB.

It also takes an additional optional parameter, which is the password to use to
decrypt the database.  

See Decrypt() for the additional fields that are available after decryption.

=head2 Write

	$pdb->Write($filename[, $password]);

Just like the Palm::Raw::Write() but encrypts everything before saving.

Also takes an optional password to encrypt with a new password, not needed
unless you are changing the password.

=head2 Encrypt

	$pdb->Encrypt([$password]);

Encrypts the PDB, either with the password used to decrypt or create it, or
optionally with a password that is passed.

See Decrypt() for an what plaintext fields are available to be encrypted.

=head2 Decrypt

	$pdb->Decrypt([$password]);

Decrypts the PDB and fills out the rest of the fields available in 
$rec->{'plaintext'}.

The plaintext should now be this, before encryption or after decryption:

	$rec->{'plaintext'} = {
		name        => $name,
		account     => $account, 
		password    => $account_password,
		description => $description,
	};

=head1 DEPENDENCIES

Palm::StdAppInfo

Digest::MD5

Crypt::DES

Readonly

=head1 BUGS AND LIMITATIONS

Once this module is uploaded, you can 
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

Andrew Fresh E<lt>andrew@mad-techies.orgE<gt>

=head1 LICENSE AND COPYRIGHT

You may distribute this file under the terms of perl itself
as specified in the LICENSE file.

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

Palm::PDB(3)

Palm::StdAppInfo(3)

The Keyring for Palm OS website: 
L<http://gnukeyring.sourceforge.net/>