commit 5c28e1f95437342b07561246110a158101669e1c Author: TLINDEN Date: Fri Jul 20 12:58:07 2012 +0200 first commit diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..f3cd7ea --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,18 @@ +1.03 + after saving we do not mv the tmp file but copying + it, because mv sometimes doesn't work with files the + current user is not the owner but has write permissions + while cp works on such files. so now we cp and unlink + the tmpfile after saving. + +1.02 + doc fix in ::Record (group separator is . not /) + added Shell.pm to Makefile.PL dependencies + + +1.01 + bug fix in t/run.t + + +1.00 + initial version diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..cdfa9e8 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,12 @@ +lib/Crypt/PWSafe3/Databaseformat.pm +lib/Crypt/PWSafe3/Field.pm +lib/Crypt/PWSafe3/HeaderField.pm +lib/Crypt/PWSafe3/Record.pm +lib/Crypt/PWSafe3/SHA256.pm +lib/Crypt/PWSafe3.pm +Makefile.PL +MANIFEST +README +t/run.t +t/tom.psafe3 +CHANGELOG diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..1c98889 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,22 @@ +require 5.004; +use ExtUtils::MakeMaker; + +WriteMakefile( + 'NAME' => 'Crypt::PWSafe3', + 'VERSION_FROM' => 'lib/Crypt/PWSafe3.pm', + 'PREREQ_PM' => { 'Digest::HMAC' => 1.00, + 'Digest::SHA' => 1.00, + 'Crypt::CBC' => 2.30, + 'Crypt::ECB' => 1.45, + 'Crypt::Twofish' => 2.14, + 'Crypt::Random' => 1.25, + 'Data::UUID' => 1.217, + 'Shell' => 0.5, + }, + 'AUTHOR' => 'Thomas Linden ', + 'clean' => { + FILES => 't/*.out *~ */*~ */*/*~ */*/*/*~' + }, + +); + diff --git a/README b/README new file mode 100644 index 0000000..6ab2f84 --- /dev/null +++ b/README @@ -0,0 +1,52 @@ +NAME + Crypt::PWSafe3 - Read and write Passwordsafe v3 files + +SYNOPSIS + + use Crypt::PWSafe3; + my $vault = new Crypt::PWSafe3(file => 'filename.psafe3', + password => 'somesecret'); + + + +DESCRIPTION + Crypt::PWSafe3 provides read and write access to password + database files created by Password Safe V3 (and up) available + at http://passwordsafe.sf.net. + + + + +INSTALLATION + + to install, type: + perl Makefile.PL + make + make test + make install + + to read the complete documentation, type: + perldoc Crypt::PWSafe3 + perldoc Crypt::PWSafe3::Record + + +COPYRIGHT + Crypt::PWSafe3 + Copyright (c) 2011 by T. Linden + + This library is free software; you can redistribute it + and/or modify it under the same terms as Perl itself. + +HOMEPAGE + + The homepage of Config::General is located at: + + http://www.daemon.de/crypt-pwsafe3/. + + +AUTHOR + T. Linden + + +VERSION + 1.03 diff --git a/lib/Crypt/PWSafe3.pm b/lib/Crypt/PWSafe3.pm new file mode 100644 index 0000000..e8bdc93 --- /dev/null +++ b/lib/Crypt/PWSafe3.pm @@ -0,0 +1,912 @@ + +# http://passwordsafe.svn.sourceforge.net/viewvc/passwordsafe/trunk/pwsafe/pwsafe/docs/formatV3.txt?revision=2139 + +package Crypt::PWSafe3; + +use strict; + +use Carp::Heavy; +use Carp; + +use Crypt::CBC; +use Crypt::ECB; +use Crypt::Twofish; +use Digest::HMAC; +use Digest::SHA; +use Crypt::Random qw( makerandom ); +use Data::UUID; +use Shell qw(mv cp); +use File::Spec; +use FileHandle; +use Data::Dumper; +use Exporter (); +use vars qw(@ISA @EXPORT); + +$Crypt::PWSafe3::VERSION = '1.03'; + +use Crypt::PWSafe3::Field; +use Crypt::PWSafe3::HeaderField; +use Crypt::PWSafe3::Record; +use Crypt::PWSafe3::SHA256; + +my @fields = qw(tag salt iter shaps b1 b2 b3 b4 keyk file program + keyl iv hmac header strechedpw password whoami); +foreach my $field (@fields) { + eval qq( + *Crypt::PWSafe3::$field = sub { + my(\$this, \$arg) = \@_; + if (\$arg) { + return \$this->{$field} = \$arg; + } + else { + return \$this->{$field}; + } + } + ); +} + +sub new { + # + # new vault object + my($this, %param) = @_; + my $class = ref($this) || $this; + my $self = \%param; # file, password, whoami, program + bless($self, $class); + + # sanity checks + if (! exists $self->{whoami}) { + $self->{whoami} = $ENV{USER}; + } + + if (! exists $self->{program}) { + $self->{program} = $0; + } + + if (! exists $self->{password}) { + croak 'Parameter password is required'; + } + + if (! exists $self->{file}) { + $self->{file} = ''; + $self->create(); + } + else { + if (! -s $self->{file}) { + $self->create(); + } + else { + $self->read(); + } + } + + $self->{modified} = 0; + + return $self; +} + +sub stretchpw { + # + # generate the streched password hash + # + # algorithm is described here: + # [KEYSTRETCH Section 4.1] http://www.schneier.com/paper-low-entropy.pdf + my ($this, $passwd) = @_; + my $sha = new Digest::SHA('SHA-256'); + $sha->reset(); + $sha->add( ( $passwd, $this->salt) ); + my $stretched = $sha->digest(); + foreach (1 .. $this->iter) { + $sha->reset(); + $sha->add( ( $stretched) ); + $stretched = $sha->digest(); + } + $passwd = $this->random(64); + return $stretched; +} + +sub create { + # + # create an empty vault without writing to disk + my($this) = @_; + + # default header fields + $this->tag('PWS3'); + $this->salt($this->random(32)); + $this->iter(2048); + + # the streched pw + $this->strechedpw($this->stretchpw($this->password())); + + # generate hash of the streched pw + my $sha = new Digest::SHA('SHA-256'); + $sha->reset(); + $sha->add( ( $this->strechedpw() ) ); + $this->shaps( $sha->digest() ); + + # encrypt b1 .. b4 + my $crypt = Crypt::ECB->new; + #$crypt->padding(PADDING_AUTO); + $crypt->cipher('Twofish'); + $crypt->key( $this->strechedpw() ); + $this->b1( $crypt->encrypt( $this->random(16) ) ); + $this->b2( $crypt->encrypt( $this->random(16) ) ); + $this->b3( $crypt->encrypt( $this->random(16) ) ); + $this->b4( $crypt->encrypt( $this->random(16) ) ); + + # create key k + l + $this->keyk( $crypt->decrypt( $this->b1() ) . $crypt->decrypt( $this->b2() )); + $this->keyl( $crypt->decrypt( $this->b3() ) . $crypt->decrypt( $this->b4() )); + + # create IV + $this->iv( $this->random(16) ); + + # create hmac'er and cipher for actual encryption + $this->{hmacer} = new Digest::HMAC($this->keyl, "Crypt::PWSafe3::SHA256"); + $this->{cipher} = new Crypt::CBC( + -key => $this->keyk, + -iv => $this->iv, + -cipher => 'Twofish', + -header => 'none', + -padding => 'null', + -literal_key => 1, + -keysize => 32, + -blocksize => 16 + ); + + # empty for now + $this->hmac( $this->{hmacer}->digest() ); +} + +sub read { + # + # read and decrypt an existing vault file + my($this) = @_; + + my $fd = new FileHandle($this->file, 'r'); + $fd->binmode(); + $this->{fd} = $fd; + + $this->tag( $this->readbytes(4) ); + if ($this->tag ne 'PWS3') { + croak "Not a PasswordSave V3 file!"; + } + + $this->salt( $this->readbytes(32) ); + $this->iter( unpack("V", $this->readbytes(4) ) ); + + $this->strechedpw($this->stretchpw($this->password())); + + my $sha = new Digest::SHA(256); + $sha->reset(); + $sha->add( ( $this->strechedpw() ) ); + $this->shaps( $sha->digest() ); + + my $fileshaps = $this->readbytes(32); + #print "sha1: <" . unpack('H*', $fileshaps) . ">\nsha2: <" . unpack('H*', $this->shaps) . ">\n"; + if ($fileshaps ne $this->shaps) { + croak "Wrong password!"; + } + + $this->b1( $this->readbytes(16) ); + $this->b2( $this->readbytes(16) ); + $this->b3( $this->readbytes(16) ); + $this->b4( $this->readbytes(16) ); + + my $crypt = Crypt::ECB->new; + $crypt->cipher('Twofish') || die $crypt->errstring; + $crypt->key( $this->strechedpw() ); + + $this->keyk($crypt->decrypt($this->b1) . $crypt->decrypt($this->b2)); + $this->keyl($crypt->decrypt($this->b3) . $crypt->decrypt($this->b4)); + + #print "keyk:<" . unpack('H*', $this->keyk) . ">\n"; + + $this->iv( $this->readbytes(16) ); + + # create hmac'er and cipher for actual encryption + $this->{hmacer} = new Digest::HMAC($this->keyl, "Crypt::PWSafe3::SHA256"); + #print "keyk len: " . length($this->keyk) . "\n"; + $this->{cipher} = new Crypt::CBC( + -key => $this->keyk, + -iv => $this->iv, + -cipher => 'Twofish', + -header => 'none', + -padding => 'null', + -literal_key => 1, + -keysize => 32, + -blocksize => 16 + ); + + # read db header fields + $this->{header} = {}; + while (1) { + my $field = $this->readfield('header'); + if (! $field) { + last; + } + if ($field->type == 0xff) { + last; + } + $this->addheader($field); + $this->hmacer($field->raw); + } + + # read db records + my $record = new Crypt::PWSafe3::Record(); + $this->{record} = {}; + while (1) { + my $field = $this->readfield(); + if (! $field) { + last; + } + if ($field->type == 0xff) { + $this->addrecord($record); + #print "--- record added (uuid:" . $record->uuid . ")\n"; + $record = new Crypt::PWSafe3::Record(); + } + else { + $record->addfield($field); + $this->hmacer($field->raw); + } + } + + # read and check file hmac + $this->hmac( $this->readbytes(32) ); + my $calcmac = $this->{hmacer}->digest(); + if ($calcmac ne $this->hmac) { + croak "File integrity check failed"; + } + + $this->{fd}->close(); +} + + +sub save { + # + # write data to the vault file + my($this, %param) = @_; + my($file, $passwd); + + if (! exists $param{file}) { + $file = $this->file; + } + else { + $file = $param{file} + } + if (! exists $param{passwd}) { + $passwd = $this->password; + } + else { + $passwd = $param{passwd} + } + + if (! $this->{modified}) { + return; + } + + my $lastsave = new Crypt::PWSafe3::HeaderField(type => 0x04, value => time); + my $whatsaved = new Crypt::PWSafe3::HeaderField(type => 0x06, value => $this->{program}); + my $whosaved = new Crypt::PWSafe3::HeaderField(type => 0x05, value => $this->{whoami}); + $this->addheader($lastsave); + $this->addheader($whatsaved); + $this->addheader($whosaved); + + my $tmpfile = File::Spec->catfile(File::Spec->tmpdir(), + ".vault-" . unpack("H*", $this->random(16))); + unlink $tmpfile; + my $fd = new FileHandle($tmpfile, 'w') or croak "Could not open tmpfile $tmpfile: $!\n"; + $fd->binmode(); + $this->{fd} = $fd; + + $this->writebytes($this->tag); + $this->writebytes($this->salt); + $this->writebytes(pack("V", $this->iter)); + + $this->strechedpw($this->stretchpw($passwd)); + + # line 472 + my $sha = new Digest::SHA(256); + $sha->reset(); + $sha->add( ( $this->strechedpw() ) ); + $this->shaps( $sha->digest() ); + + $this->writebytes($this->shaps); + $this->writebytes($this->b1); + $this->writebytes($this->b2); + $this->writebytes($this->b3); + $this->writebytes($this->b4); + + my $crypt = Crypt::ECB->new; + $crypt->cipher('Twofish'); + $crypt->key( $this->strechedpw() ); + + $this->keyk($crypt->decrypt($this->b1) . $crypt->decrypt($this->b2)); + $this->keyl($crypt->decrypt($this->b3) . $crypt->decrypt($this->b4)); + + $this->writebytes($this->iv); + + $this->{hmacer} = new Digest::HMAC($this->keyl, "Crypt::PWSafe3::SHA256"); + $this->{cipher} = new Crypt::CBC( + -key => $this->keyk, + -iv => $this->iv, + -cipher => 'Twofish', + -header => 'none', + -padding => 'null', + -literal_key => 1, + -keysize => 32, + -blocksize => 16 + ); + + my $eof = new Crypt::PWSafe3::HeaderField(type => 0xff, value => ''); + + foreach my $type (keys %{$this->{header}}) { + $this->writefield($this->{header}->{$type}); + $this->hmacer($this->{header}->{$type}->{raw}); + } + $this->writefield($eof); + $this->hmacer($eof->{raw}); + + $eof = new Crypt::PWSafe3::Field(type => 0xff, value => ''); + + foreach my $uuid (keys %{$this->{record}}) { + my $record = $this->{record}->{$uuid}; + foreach my $type (keys %{$record->{field}}) { + $this->writefield($record->{field}->{$type}); + $this->hmacer($record->{field}->{$type}->{raw}); + } + $this->writefield($eof); + $this->hmacer($eof->{raw}); + } + + $this->writefield(new Crypt::PWSafe3::Field(type => 'none', raw => 0)); + + $this->hmac( $this->{hmacer}->digest() ); + $this->writebytes($this->hmac); + $this->{fd}->close(); + + # now try to read it in again to check if it + # is valid what we created + eval { + my $vault = new Crypt::PWSafe3(file => $tmpfile, password => $passwd); + }; + if ($@) { + unlink $tmpfile; + croak "File integrity check failed ($@)"; + } + else { + # well, seems to be ok :) + cp($tmpfile, $file); + unlink $tmpfile; + } +} + +sub writefield { + # + # write a field to vault file + my($this, $field) = @_; + + #print "write field " . $field->name . "\n"; + + if ($field->type eq 'none') { + $this->writebytes("PWS3-EOFPWS3-EOF"); + return; + } + + my $len = pack("V", $field->len); + my $type = pack("C", $field->type); + my $raw = $field->raw; + + # Assemble TLV block and pad to 16-byte boundary + my $data = $len . $type . $raw; + + if (length($data) % 16 != 0) { + # too small or too large, padding required + my $padcount = 16 - (length($data) % 16); + $data .= $this->random($padcount); + } + + if (length($data) > 16) { + my $crypt; + while (1) { + #print "processing part\n"; + my $part = substr($data, 0, 16); + $crypt .= $this->encrypt($part); + if (length($data) <= 16) { + #print " this was the last one\n"; + last; + } + else { + #print " getting next\n"; + $data = substr($data, 16); + } + } + #print " len: " . length($crypt) . "\n"; + $this->writebytes($crypt); + } + else { + $this->writebytes($this->encrypt($data)); + } +} + +sub getrecord { + # + # return the given record + my($this, $uuid) = @_; + if (exists $this->{record}->{$uuid}) { + return $this->{record}->{$uuid}; + } + else { + return 0; + } +} + +sub getrecords { + # + # return all records we've got as a copy + my ($this) = @_; + return map { $this->{record}->{$_} } keys %{$this->{record}}; +} + +sub looprecord { + # + # return a list of uuid's of all records + my ($this) = @_; + return keys %{$this->{record}}; +} + +sub modifyrecord { + # + # modify a record identified by the given uuid + my($this, $uuid, %fields) = @_; + + if (! exists $this->{record}->{$uuid}) { + croak "No record with uuid $uuid found!"; + } + + foreach my $field (keys %fields) { + $this->{record}->{$uuid}->modifyfield($field, $fields{$field}); + } + + # mark vault as modified + $this->markmodified(); +} + +sub markmodified { + # + # mark the vault as modified by setting the appropriate header fields + my($this) = @_; + my $lastmod = new Crypt::PWSafe3::HeaderField( + name => "lastsavetime", + value => time + ); + my $who = new Crypt::PWSafe3::HeaderField( + name => "wholastsaved", + value => $this->{whoami} + ); + $this->addheader($lastmod); + $this->addheader($who); + $this->{modified} = 1; +} + +sub newrecord { + # + # add a new record to an existing vault + my($this, %fields) = @_; + my $record = new Crypt::PWSafe3::Record(); + foreach my $field (keys %fields) { + $record->modifyfield($field, $fields{$field}); + } + $this->markmodified(); + $this->addrecord($record); + return $record->uuid; +} + +sub addrecord { + # + # add a record object to record hash + my($this, $record) = @_; + $this->{record}->{ $record->uuid } = $record; +} + +sub addheader { + # + # add a header field to header hash + my($this, $field) = @_; + $this->{header}->{ $field->name } = $field; +} + + +sub readfield { + # + # read and return a field object of the vault + my($this, $header) = @_; + my $data = $this->readbytes(16); + if (! $data or length($data) < 16) { + croak "EOF encountered when parsing record field"; + } + if ($data eq "PWS3-EOFPWS3-EOF") { + return 0; + } + + #print "\n raw: <" . unpack('H*', $data) . ">\n"; + + $data = $this->decrypt($data); + + #print "clear: <" . unpack('H*', $data) . ">\n"; + + my $len = unpack("V", substr($data, 0, 4)); + my $type = unpack("C", substr($data, 4, 1)); + my $raw = substr($data, 5); + + #print "readfield: len: $len, type: $type\n"; + + if ($len > 11) { + my $step = int(($len+4) / 16); + for (1 .. $step) { + my $data = $this->readbytes(16); + if (! $data or length($data) < 16) { + croak "EOF encountered when parsing record field"; + } + $raw .= $this->decrypt($data); + } + } + $raw = substr($raw, 0, $len); + if ($header) { + return new Crypt::PWSafe3::HeaderField(type => $type, raw => $raw); + } + else { + return new Crypt::PWSafe3::Field(type => $type, raw => $raw); + } +} + +sub decrypt { + # + # helper, decrypt a string + my ($this, $data) = @_; + my $clear = $this->{cipher}->decrypt($data); + $this->{cipher}->iv($data); + return $clear; +} + +sub encrypt { + # + # helper, encrypt a string + my ($this, $data) = @_; + my $raw = $this->{cipher}->encrypt($data); + if (length($raw) > 16) { + # we use only the last 16byte block as next iv + # if data is more than 1 blocks than Crypt::CBC + # has already updated the iv for the inner blocks + $raw = substr($raw, -16, 16); + } + $this->{cipher}->iv($raw); + return $raw; +} + +sub hmacer { + # + # helper, hmac generator + my($this, $data) = @_; + + $this->{hmacer}->add($data); +} + +sub readbytes { + # + # helper, reads number of bytes + my ($this, $size) = @_; + my $buffer; + my ($package, $filename, $line) = caller; + + my $got = $this->{fd}->sysread($buffer, $size); + if ($got == $size) { + $this->{sum} += $got; + #print "Got $got bytes (read so far: $this->{sum} bytes) $package line $line\n"; + return $buffer; + } + else { + return 0; + } +} + +sub writebytes { + # + # helper, reads number of bytes + my ($this, $bytes) = @_; + my $got = $this->{fd}->syswrite($bytes); + if ($got) { + return $got; + } + else { + croak "Could not write to $this->{file}: $!"; + } +} + +sub random { + # + # helper, return some secure random bytes + my($this, $len) = @_; + my $bits = makerandom(Size => 256, Strength => 1); + return substr($bits, 0, $len); +} + +sub getheader { + # + # return a header object + my($this, $name) = @_; + # $this->{header}->{ $field->name } = $field; + if (exists $this->{header}->{$name}) { + return $this->{header}->{$name}; + } + else { + croak "Unknown header $name"; + } +} + + + + +=head1 NAME + +Crypt::PWSafe3 - Read and write Passwordsafe v3 files + +=head1 SYNOPSIS + + use Crypt::PWSafe3; + my $vault = new Crypt::PWSafe3(file => 'filename.psafe3', password => 'somesecret'); + + # fetch all database records + my @records = $vault->getrecords(); + foreach my $record (@records) { + print $record->uuid; + print $record->title; + print $record->passwd; + # see Crypt::PWSafe3::Record for more details on accessing records + } + + # same as above but don't detach records from vault + foreach my $uuid ($vault->looprecord) { + # either change a record + $vault->modifyrecord($uuid, passwd => 'p1'); + + # or just access it directly + print $vault->{record}->{$uuid}->title; + } + + # add a new record + $vault->newrecord(user => 'u1', passwd => 'p1', title => 't1'); + + # modify an existing record + $vault->modifyrecord($uuid, passwd => 'p1'); + + # replace a record (aka edit it) + my $record = $vault->getrecord($uuid); + $record->title('t2'); + $record->passwd('foobar'); + $vault->addrecord($record); + + # mark the vault as modified (not required if + # changes were done with ::modifyrecord() + $vault->markmodified(); + + # save the vault + $vault->save(); + + # save it under another name using another password + $vault->save(file => 'another.pwsafe3', passwd => 'blah'); + + # access database headers + print $vault->getheader('wholastsaved')->value(); + print scalar localtime($vault->getheader('lastsavetime')->value()); + + # add/replace a database header + my $h = new Crypt::PWSafe3::HeaderField(name => 'savedonhost', value => 'localhost'); + $vault->addheader($h); + +=head1 DESCRIPTION + +Crypt::PWSafe3 provides read and write access to password +database files created by Password Safe V3 (and up) available at +http://passwordsafe.sf.net. + +=head1 METHODS + +=head2 B + +The new() method creates a new Crypt::PWSafe3 object. Any parameters +must be given as hash parameters. + + my $vault = new Crypt::PWSafe3( + file => 'vault.psafe3', + password => 'secret', + whoami => 'user1', + program => 'mypwtool v1' + ); + +Mandatory parameters: + +=over + +=item B + +Specifies the password safe (v3) file. If it exists +it will be read in. Otherwise it will be created +if you call B. + +=item B + +The password required to decrypt the password safe file. + +=back + +Optional parameters: + +=over + +=item B + +Specifies the user who saves the password safe file. +If omitted the environment variable USER will be used +when calling B. + +=item B + +Specifies which program saved the password safe file. +If omitted, the content of the perl variable $0 will +be used, which contains the name of the current running +script. + +=back + +The optional parameters will become header fields of +the password safe file. You can manually set/override +more headers. See section L for +more details. + +=head2 B + +Returns a list of all records found in the password +safe file. Each element is an B +object. + +A record object is identified by its B value, +which is a unique identifier. You can access the uuid by: + + $record->uuid + +Accessing other record properties works the same. For +more details, refer to L. + +Please note that record objects accessed this way are +copies. If you change such a record object and save the +database, nothing will in fact change. In this case you +need to put the changed record back into the record +list of the Crypt::PWSafe3 object by: + + $vault->addrecord($record): + +See section L for more details on this. + + +=head2 B + +Returns a list of UUIDs of all known records. You can +use this list to iterate over the records without +copying them and optionally changing them in place. + +Example: + + foreach my $uuid ($vault->looprecord) { + # either change a record + $vault->modifyrecord($uuid, passwd => 'p1'); + + # or just access it directly + print $vault->{record}->{$uuid}->title; + } + + +=head2 B + +Modifies the record identified by the given UUID using +the values of the supplied parameter hash. + +Example: + + $vault->modifyrecord($uuid, passwd => 'p1'); + +The parameter hash may contain any valid record field +type with according values. Refer to L +for details about available fields. + + +=head2 B + +Save the current password safe vault back to disk. + +If not otherwise specified, use the same file and +password as we used to open it initially. If the +file doesn't exist it will be created. + +You may specify another filename and password here +by using a parameter hash. + +Example: + + $vault->save(file => 'anotherfile.psafe3', passwd => 'foo'); + +Please note, that the vault will be written to a +temporary file first, then this temporary file +will be read in and if that works, it will be +moved over the destination file. This way the original +file persists if the written database gets corrupted +by some unknown reason (a bug for instance). + + +=head2 B + +Returns a raw B object. +Refer to L for details +how to access it. + +=head2 B + +Adds a header field to the password safe database. The +object parameter must be an B +object. + +If the header already exists it will be replaced. + +Refer to L for details +how to create new ones +. + +=head1 AUTHOR + +T. Linden + +=head1 BUGS + +Report bugs to +http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Crypt-PWSafe3. + +=head1 VERSION + +Crypt::PWSafe3 Version 1.03. + +=head1 SEE ALSO + +Subclasses: + +L +L +L + +Password Safe Homepage: +L + +Another (read-only) perl module: +L + +A python port of Password Safe: +L +Many thanks to Christoph Sommer, his python library +inspired me a lot and in fact most of the concepts +in this module are his ideas ported to perl. + +=head1 COPYRIGHT + +Copyright (c) 2011 by T.Linden . +All rights reserved. + +=head1 LICENSE + +This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + +=cut + + + + +1; + diff --git a/lib/Crypt/PWSafe3/Databaseformat.pm b/lib/Crypt/PWSafe3/Databaseformat.pm new file mode 100644 index 0000000..faac3e5 --- /dev/null +++ b/lib/Crypt/PWSafe3/Databaseformat.pm @@ -0,0 +1,425 @@ +=head1 NAME + +PasswordSafe database format description version 3.03 + + +=head1 LICENSE + +Copyright (c) 2003-2008 Rony Shapiro . +All rights reserved. Use of the code is allowed under the Artistic +License terms, as specified in the LICENSE file distributed with this +code, or available from +http://www.opensource.org/licenses/artistic-license-2.0.php + +=head1 1. Introduction + +This document defines a file format for the secure +storage of passwords and related data. The format is designed +according to current cryptographic best practices, and is beleived to +be secure, in the sense that without knowledge of the master +passphrase, only a brute-force attack or a flaw in the underlying +cryptographic algorithm will result in unauthorized access to the +data. + +1.1 Design Goals: The PasswordSafe database format is designed to be +secure, extensible and platform-independent. + +1.2 History: This specification is an evolution of previous +formats. The main differences between version 3 of the format and +previous versions are: +1.2.1. This version addresses a minor design flaw in previous versions +of the PasswordSafe database format. +1.2.3. This version replaces the underlying cryptographic functions +with more advanced versions. +1.2.4. This version allows the detection of a truncated or +corrupted/tampered database. + +Meeting these goals is impossible without breaking compatibility: This +format is NOT compatible with previous (major) versions. Note, +however, that since the data stored in previous versions is a proper +subset of the data described here, implementers may read a database +written in an older version and store the result in the format +described here. + +=head1 2. Format + +A V3 format PasswordSafe is structured as follows: + + TAG|SALT|ITER|H(P')|B1|B2|B3|B4|IV|HDR|R1|R2|...|Rn|EOF|HMAC + +Where: + +2.1 TAG is the sequence of 4 ASCII characters "PWS3". This is to serve as a +quick way for the application to identify the database as a PasswordSafe +version 3 file. This tag has no cryptographic value. + +2.1 SALT is a 256 bit random value, generated at file creation time. + +2.3 P' is the "stretched key" generated from the user's passphrase and +the SALT, as defined in by the hash-function-based key stretching +algorithm in [KEYSTRETCH] (Section 4.1), with SHA-256 [SHA256] as the +hash function, and ITER iterations (at least 2048, i.e., t = 11). + +2.4 ITER is the number of iterations on the hash function to calculate P', +stored as a 32 bit little-endian value. This value is stored here in order +to future-proof the file format against increases in processing power. + +2.5 H(P') is SHA-256(P'), and is used to verify that the user has the +correct passphrase. + +2.6 B1 and B2 are two 128-bit blocks encrypted with Twofish [TWOFISH] +using P' as the key, in ECB mode. These blocks contain the 256 bit +random key K that is used to encrypt the actual records. (This has the +property that there is no known or guessable information on the +plaintext encrypted with the passphrase-derived key that allows an +attacker to mount an attack that bypasses the key stretching +algorithm.) + +2.7 B3 and B4 are two 128-bit blocks encrypted with Twofish using P' as the +key, in ECB mode. These blocks contain the 256 bit random key L that is +used to calculate the HMAC (keyed-hash message authentication code) of the +encrypted data. See description of EOF field below for more details. +Implementation Note: K and L must NOT be related. + +2.8 IV is the 128-bit random Initial Value for CBC mode. + +2.9 All following records are encrypted using Twofish in CBC mode, with K +as the encryption key. + +2.9.1 HDR: The database header. The header consists of one or more typed +fields (as defined in section 3.2), terminated by the 'END' type field. The +version number field is mandatory. Aside from the 'END' field, no +order is assumed on the field types. + +2.9.2 R1..Rn: The actual database records. Each record consists of one or +more typed fields (as defined in Section 3.2), terminated by the 'END' type +field. The UUID, Title, and Password fields are mandatory. All non- +mandatory fields may either be absent or have zero length. When a field is +absent or zero-length, its default value shall be used. Aside from the +'END' field, no order is assumed on the field types. + +2.10 EOF: The ASCII characters "PWS3-EOFPWS3-EOF" (note that this is +exactly one block long), unencrypted. This is an implementation convenience +to inform the application that the following bytes are to be processed +differently. + +2.11 HMAC: The 256-bit keyed-hash MAC, as described in RFC2104, with SHA- +256 as the underlying hash function. The value is calculated over all of +the plaintext fields, that is, over all the data stored in all fields +(starting from the version number in the header, ending with the last field +of the last record). The key L as stored in B3 and B4 is used as the hash +key value. + +3. Fields: Data in PasswordSafe is stored in typed fields. Each field +consists of one or more blocks. The blocks are the blocks of the underlying +encryption algorithm - 16 bytes long for Twofish. The first block contains +the field length in the first 4 bytes (little-endian), followed by a one- +byte type identifier. The rest of the block contains up to 11 bytes of +record data. If the record has less than 11 bytes of data, the extra bytes +are filled with random values. The type of a field also defines the data +representation. + +=head1 3.1 Data representations + +=head2 3.1.1 UUID + + The UUID data type is 16 bytes long, as defined in RFC4122. Microsoft + Windows has functions for this, and the RFC has a sample + implementation. + +=head2 3.1.2 Text + + Text is represented in UTF-8 encoding (as defined in RFC3629), with + no byte order marker (BOM) and no end-of-string mark (e.g., null + byte). Note that the latter isn't neccessary since the length of the + field is provided explicitly. Note that ALL fields described as + "text" are UTF-8 encoded unless explicitly stated otherwise. + +=head2 3.1.3 Time + + Timestamps are stored as 32 bit, little endian, unsigned integers, + representing the number of seconds since Midnight, January 1, 1970, GMT. + (This is equivalent to the time_t type on Windows and POSIX. On the + Macintosh, the value needs to be adjusted by the constant value 2082844800 + to account for the different epoch of its time_t type.) + Note that future versions of this format may allow time to be + specifed in 64 bits as well. + +=head2 3.2 Field types for the PasswordSafe database header: + + Currently + Name Value Type Implemented Comments + -------------------------------------------------------------------------- + Version 0x00 2 bytes Y [1] + UUID 0x01 UUID Y [2] + Non-default preferences 0x02 Text Y [3] + Tree Display Status 0x03 Text Y [4] + Timestamp of last save 0x04 time_t Y [5] + Who performed last save 0x05 Text Y [DEPRECATED 6] + What performed last save 0x06 Text Y [7] + Last saved by user 0x07 Text Y [8] + Last saved on host 0x08 Text Y [9] + Database Name 0x09 Text Y [10] + Database Description 0x0a Text Y [11] + Database Filters 0x0b Text Y [12] + End of Entry 0xff [empty] Y [13] + +[1] The version number of the database format. For this version, the value +is 0x0305 (stored in little-endian format, that is, 0x05, 0x03). + +PasswordSafe V3.01 introduced Format 0x0300 +PasswordSafe V3.03 introduced Format 0x0301 +PasswordSafe V3.09 introduced Format 0x0302 +PasswordSafe V3.12 introduced Format 0x0303 +PasswordSafe V3.13 introduced Format 0x0304 +PasswordSafe V3.14 introduced Format 0x0305 + +[2] A universally unique identifier is needed in order to synchronize +databases, e.g., between a handheld pocketPC device and a +PC. Representation is as described in Section 3.1.1. + +[3] Non-default preferences are encoded in a string as follows: The string +is of the form "X nn vv X nn vv..." Where X=[BIS] for binary, integer and +string respectively, nn is the numeric value of the enum, and vv is the +value, {1 or 0} for bool, unsigned integer for int, and a delimited string +for String. Only non-default values are stored. See PWSprefs.cpp for more +details. Note: normally strings are delimited by the doublequote character. +However, if this character is in the string value, an arbitrary character +will be chosen to delimit the string. + +[4] If requested to be saved, this is a string of 1s and 0s indicating the +expanded state of the tree display when the database was saved. This can +be applied at database open time, if the user wishes, so that the tree is +displayed as it was. Alternatively, it can be ignored and the tree +displayed completely expanded or collapsed. Note that the mapping of +the string to the display state is implementation-specific. Introduced +in format 0x0301. + +[5] Representation is as described in Section 3.1.3. Note that prior +to PasswordSafe 3.09, this field was mistakenly represented as an +eight-byte hexadecimal ASCII string. Implementations SHOULD attempt to +parse 8-byte long timestamps as a hexadecimal ASCII string +representation of the timestamp value. + +[6] Text saved in the format: nnnnu..uh..h, where: + nnnn = 4 hexadecimal digits giving length of following user name field + u..u = user name + h..h = host computer name + Note: As of format 0x0302, this field is deprecated, and should be + replaced by fields 0x07 and 0x08. In databases prior to format + 0x0302, this field should be maintained. 0x0302 and later may + either maintain this field in addition to fields 0x07 and 0x08, + for backwards compatability, or not write this field. If both this + field and 0x07, 0x08 exist, they MUST represent the same values. + +[7] Free form text giving the application that saved the database. +For example, the Windows PasswordSafe application will use the text +"Password Safe Vnn.mm", where nn and mm are the major and minor +version numbers. The major version will contain only the significant +digits whereas the minor version will be padded to the left with +zeroes e.g. "Password Safe V3.02". + +[8] Text containing the username (e.g., login, userid, etc.) of the +user who last saved the database, as determined by the appropriate +operating-system dependent function. This field was introduced in +format version 0x0302, as a replacement for field 0x05. See Comment +[6]. + +[9] Text containing the hostname (e.g., machine name, hostid, etc.) of the +machine on which the database was last saved, as determined by the +appropriate operating-system dependent function. This field was +introduced in format version 0x0302, as a replacement for field +0x05. See Comment [6]. + +[10] Database name. A logical name for a database which can be used by +applications in place of the possibly lengthy filepath notion. Note +that this field SHOULD be limited to what can be displayed in a single +line. This field was introduced in format version 0x0302. + +[11] Database Description. A purely informative description concerning +the purpose or other practical use of the database. This field was +introduced in format version 0x0302. + +[12] Specfic filters for this database. This is the text equivalent to +the XML export of the filters as defined by the filter schema. The text +'image' has no 'print formatting' e.g. tabs and carraige return/line feeds, +since XML processing does not require this. This field was introduced in +format version 0x0305. + +[13] An explicit end of entry field is useful for supporting new fields +without breaking backwards compatability. + +=head2 3.3 Field types for database Records: + + Currently + Name Value Type Implemented Comments + -------------------------------------------------------------------------- + UUID 0x01 UUID Y [1] + Group 0x02 Text Y [2] + Title 0x03 Text Y + Username 0x04 Text Y + Notes 0x05 Text Y + Password 0x06 Text Y [3,4] + Creation Time 0x07 time_t Y [5] + Password Modification Time 0x08 time_t Y [5] + Last Access Time 0x09 time_t Y [5,6] + Password Expiry Time 0x0a time_t Y [5,7] + *RESERVED* 0x0b 4 bytes - [8] + Last Modification Time 0x0c time_t Y [5,9] + URL 0x0d Text Y [10] + Autotype 0x0e Text Y [11] + Password History 0x0f Text Y [12] + Password Policy 0x10 Text Y [13] + Password Expiry Interval 0x11 2 bytes Y [14] + End of Entry 0xff [empty] Y [15] + +[1] Per-record UUID to assist in sync, merge, etc. Representation is +as described in Section 3.1.1. + +[2] The "Group" supports displaying the entries in a tree-like manner. +Groups can be hierarchical, with elements separated by a period, supporting +groups such as "Finance.credit cards.Visa", "Finance.credit +cards.Mastercard", Finance.bank.web access", etc. Dots entered by the user +should be "escaped" by the application. + +[3] If the entry is an alias, the password will be saved in a special form +of "[[uuidstr]]", where "uuidstr" is a 32-character representation of the +alias' associated base entry's UUID (field type 0x01). This representation +is the same as the standard 36-character string representation as defined in +RFC4122 but with the four hyphens removed. If an entry with this UUID is not +in the database, this is treated just as an 'unusual' password. The alias +will only use its base's password entry when copying it to the clipboard or +during Autotype. + +[4] If the entry is a shortcut, the password will be saved in a special form +of "[~uuidstr~]", where "uuidstr" is a 32-character representation of the +shortcut's associated base entry's UUID (field type 0x01). This representation +is the same as the standard 36-character string representation as defined in +RFC4122 but with the four hyphens removed. If an entry with this UUID is not +in the database, this is treated just as an 'unusual' password. The shortcut +will use all its base's data when used in any action. It has no fields of +its own. + +[5] Representation is as described in Section 3.1.3. + +[6] This will be updated whenever any part of this entry is accessed +i.e., to copy its username, password or notes to the clipboard; to +perform autotype or to browse to url. + +[7] This will allow the user to enter an expiry date for an entry. The +application can then prompt the user about passwords that need to be +changed. A value of zero means "forever". + +[8] Although earmarked for Password Policy, the coding in versions prior +to V3.12 does not correctly handle the presence of this field. For this +reason, this value cannot be used for any future V3 field without causing +a potential issue when a user opens a V3.12 or later database with program +version V3.11 or earlier. See note [14]. + +[9] This is the time that any field of the record was modified, useful for +merging databases. + +[10] The URL will be passed to the shell when the user chooses the "Browse +to" action for this entry. In version 2 of the format, this was extracted +from the Notes field. By placing it in a separate field, we are no longer +restricted to a URL - any action that may be executed by the shell may be +specified here. + +[11] The text to be 'typed' by PasswordSafe upon the "Perform Autotype" +action maybe specified here. If unspecified, the default value of +'username, tab, password, tab, enter' is used. In version 2 of the format, +this was extracted from the Notes field. Several codes are recognized here, +e.g, '%p' is replaced by the record's password. See the user documentation +for the complete list of codes. The replacement is done by the application +at runtime, and is not stored in the database. + +[12] Password History is an optional record. If it exists, it stores the +creation times and values of the last few passwords used in the current +entry, in the following format: + "fmmnnTLPTLP...TLP" +where: + f = {0,1} if password history is on/off + mm = 2 hexadecimal digits max size of history list (i.e. max = 255) + nn = 2 hexadecimal digits current size of history list + T = Time password was set (time_t written out in %08x) + L = 4 hexadecimal digit password length (in TCHAR) + P = Password +No history being kept for a record can be represented either by the lack of +the PWH field (preferred), or by a header of _T("00000"): + flag = 0, max = 00, num = 00 +Note that 0aabb, where bb <= aa, is possible if password history was enabled +in the past and has then been disabled but the history hasn't been cleared. + +[13] This field allows a specific Password Policy per entry. The format is: + + "ffffnnnllluuudddsss" + +where: + + ffff = 4 hexadecimal digits representing the following flags + UseLowercase = 0x8000 - can have a minimum length + UseUppercase = 0x4000 - can have a minimum length + UseDigits = 0x2000 - can have a minimum length + UseSymbols = 0x1000 - can have a minimum length + UseHexDigits = 0x0800 (if set, then no other flags can be set) + UseEasyVision = 0x0400 + MakePronounceable = 0x0200 + Unused 0x01ff + nnn = 3 hexadecimal digits password total length + lll = 3 hexadecimal digits password minimum number of lowercase characters + uuu = 3 hexadecimal digits password minimum number of uppercase characters + ddd = 3 hexadecimal digits password minimum number of digit characters + sss = 3 hexadecimal digits password minimum number of symbol characters + +[14] Password Expiry Interval, in days, before this password expires. Once set, +this value is used when the password is first generated and thereafter whenever +the password is changed, until this value is unset. Valid values are 1-3650 +corresponding to up to approximately 10 years. A value of zero is equivalent to +this field not being set. + +[15] An explicit end of entry field is useful for supporting new fields +without breaking backwards compatability. + +=head1 4. Extensibility + +4.1 Forward compatability: Implementations of this format SHOULD NOT +discard or report an error when encountering a filed of an unknown +type. Rather, the field(s) type and data should be read, and perserved +when the database is saved. + +4.2 Field type identifiers: This document specifies the field type +identifiers for the current version of the format. Compliant +implementations MUST support the mandatory fields, and SHOULD support +the other fields described herein. Future versions of the format may +specify other type identifiers. +4.2.1 Application-unique type identifiers: The type identifiers +0xc0-0xdf are available for application developers on a first-come +first-serve basis. Application developers interested in reserving a +type identifier for their application should contact the maintainer of +this document (Currently the PasswordSafe project administrator at +SourceForge). +4.2.2 Application-specific type identifiers: The type identifiers +0xe0-0xfe are reserved for implementation-specific purposes, and will +NOT be specified in this or future versions of the format +description. +4.2.3 All unassigned identifiers except as listed in the previous two +subsections are reserved, and should not be used by other +implementations of this format specification in the interest of +interoperablity. + +=head1 5. References: + +[TWOFISH] http://www.schneier.com/paper-twofish-paper.html +[SHA256] +http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf +[KEYSTRETCH] http://www.schneier.com/paper-low-entropy.pdf + +End of Format description. + +=head1 SEE ALSO + +Original source of this file: + +http://passwordsafe.svn.sourceforge.net/viewvc/passwordsafe/trunk/pwsafe/pwsafe/docs/formatV3.txt?revision=2139 + +=cut diff --git a/lib/Crypt/PWSafe3/Field.pm b/lib/Crypt/PWSafe3/Field.pm new file mode 100644 index 0000000..e6441e8 --- /dev/null +++ b/lib/Crypt/PWSafe3/Field.pm @@ -0,0 +1,184 @@ +package Crypt::PWSafe3::Field; + + +use Carp::Heavy; +use Carp; +use Exporter (); +use vars qw(@ISA @EXPORT); +use utf8; + +$Crypt::PWSafe3::Field::VERSION = '1.01'; + +%Crypt::PWSafe3::Field::map2type = ( + uuid => 0x01, + group => 0x02, + title => 0x03, + user => 0x04, + passwd => 0x06, + notes => 0x05, + ctime => 0x07, + mtime => 0x08, + atime => 0x09, + reserve => 0x0b, + lastmod => 0x0c, + url => 0x0d, + autotype => 0x0e, + pwhist => 0x0f, + pwpol => 0x10, + pwexp => 0x11, + eof => 0xff + ); +%Crypt::PWSafe3::Field::map2name = map { $Crypt::PWSafe3::Field::map2type{$_} => $_ } keys %Crypt::PWSafe3::Field::map2type; + +my @fields = qw(raw len value type name); +foreach my $field (@fields) { + eval qq( + *Crypt::PWSafe3::Field::$field = sub { + my(\$this, \$arg) = \@_; + if (\$arg) { + return \$this->{$field} = \$arg; + } + else { + return \$this->{$field}; + } + } + ); +} + +sub new { + # + # new field object + my($this, %param) = @_; + my $class = ref($this) || $this; + my $self = \%param; + bless($self, $class); + + + + if (! exists $param{type}) { + if (exists $param{name}) { + $param{type} = $Crypt::PWSafe3::Field::map2type{$param{name}}; + } + else { + croak "HeaderField needs to have a type/name parameter!"; + } + } + + my @convtime = (0x07, 0x08, 0x09, 0x0a, 0x0c); + my @convhex = (0x01); + my @convbyte = (0x00, 0x11); + + if (exists $param{raw}) { + if (grep { $_ eq $param{type} } @convtime) { + $self->{value} = unpack("V", $param{raw}); + } + elsif (grep { $_ eq $param{type} } @convhex) { + $self->{value} = unpack('H*', $param{raw}); + } + elsif (grep { $_ eq $param{type} } @convbyte) { + $self->{value} = unpack('W*', $param{raw}); + } + else { + $self->{value} = $param{raw}; + utf8::decode($self->{value}); + } + $self->{len} = length($param{raw}); + } + else { + if (exists $param{value}) { + if (grep { $_ eq $param{type} } @convtime) { + $self->{raw} = pack("V", $param{value}); + } + elsif (grep { $_ eq $param{type} } @convhex) { + $self->{raw} = pack('H*', $param{value}); + } + elsif (grep { $_ eq $param{type} } @convbyte) { + $self->{raw} = pack('W*', $param{value}); + } + else { + $self->{raw} = $param{value}; + utf8::encode($param{raw}); + } + } + else { + croak "Either raw or value must be given to Crypt::PWSafe3::Field->new()"; + } + } + + $self->{len} = length($param{raw}); + + if (exists $Crypt::PWSafe3::Field::map2name{$self->{type}}) { + $self->{name} = $Crypt::PWSafe3::Field::map2name{$self->{type}}; + } + else { + $self->{name} = $self->{type}; + } + + #print "New Field of type $self->{name}\n"; + #print "Field Value: $self->{value}\n"; + + return $self; +} + +sub eq { + # + # compare this field with the given one + my ($this, $field) = @_; + return $this->type == $field->type and $this->value eq $field->value; +} + +=head1 NAME + +Crypt::PWSafe3::Field - represent a passwordsafe v3 record field. + +=head1 SYNOPSIS + + use Crypt::PWSafe3; + my $record = $vault->getrecord($uuid); + print $record-{field}->{user}->raw(); + print $record-{field}->{user}->len(); + +=head1 DESCRIPTION + +B represents a record field. This is the +raw implementation and you normally don't have to cope with it. + +However, if you ever do, you can do it this way: + + my $field = new Crypt::PWSafe3::Field( + value => 'testing', + name => 'title + ); + $record->addfield($field); + +This is the preferred way to do it, Crypt::PWSafe3 does +it internaly exactly like this. + +If there already exists a record field of this type, it will +be overwritten. + +The better way to handle fields is the method B +of the class L. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +T. Linden + +=head1 COPYRIGHT + +Copyright (c) 2011 by T.Linden . +All rights reserved. + +=head1 LICENSE + +This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + + +=cut + +1; diff --git a/lib/Crypt/PWSafe3/HeaderField.pm b/lib/Crypt/PWSafe3/HeaderField.pm new file mode 100644 index 0000000..e3624c1 --- /dev/null +++ b/lib/Crypt/PWSafe3/HeaderField.pm @@ -0,0 +1,209 @@ +package Crypt::PWSafe3::HeaderField; + +use Carp::Heavy; +use Carp; +use Exporter (); +use vars qw(@ISA @EXPORT); +use utf8; + +$Crypt::PWSafe3::HeaderField::VERSION = '1.01'; + +%Crypt::PWSafe3::HeaderField::map2name = ( + 0x00 => "version", + 0x01 => "uuid", + 0x02 => "preferences", + 0x03 => "treedisplaystatus", + 0x04 => "lastsavetime", + 0x05 => "wholastsaved", + 0x06 => "whatlastsaved", + 0x07 => "savedbyuser", + 0x08 => "savedonhost", + 0x09 => "databasename", + 0x0a => "databasedescr", + 0x0b => "databasefilters", + 0xff => "eof" + ); + +%Crypt::PWSafe3::HeaderField::map2type = map { $Crypt::PWSafe3::HeaderField::map2name{$_} => $_ } keys %Crypt::PWSafe3::HeaderField::map2name; + +my @fields = qw(raw len value type name); +foreach my $field (@fields) { + eval qq( + *Crypt::PWSafe3::HeaderField::$field = sub { + my(\$this, \$arg) = \@_; + if (\$arg) { + return \$this->{$field} = \$arg; + } + else { + return \$this->{$field}; + } + } + ); +} + +sub new { + # + # new header field object + my($this, %param) = @_; + my $class = ref($this) || $this; + my $self = \%param; + bless($self, $class); + + if (! exists $param{type}) { + if (exists $param{name}) { + if (exists $Crypt::PWSafe3::HeaderField::map2type{$param{name}}) { + $param{type} = $Crypt::PWSafe3::HeaderField::map2type{$param{name}}; + } + else { + croak "Unknown header type $param{name}"; + } + } + else { + croak "HeaderField needs to have a type/name parameter!"; + } + } + + if (exists $param{raw}) { + if ($param{type} == 0x00) { + $self->{value} = unpack('H*', $param{raw});# maybe WW or CC ? + } + elsif ($param{type} == 0x01) { + $self->{value} = unpack('H*', $param{raw}); + } + elsif ($param{type} == 0x04) { + $self->{value} = unpack('V', $param{raw}); + } + else { + $self->{value} = $param{raw}; + } + $self->{len} = length($param{raw}); + } + else { + if (exists $param{value}) { + if ($param{type} == 0x00) { + $self->{raw} = pack("H*", $param{value}); + } + elsif ($param{type} == 0x01) { + $self->{raw} = pack('H*', $param{value}); + } + elsif ($param{type} == 0x04) { + $self->{raw} = pack('V', $param{value}); + } + else { + $self->{raw} = $param{value}; + } + } + else { + croak "Either raw or value must be given to Crypt::PWSafe3::Field->new()"; + } + } + + $self->{len} = length($param{raw}); + + if (exists $Crypt::PWSafe3::HeaderField::map2name{$self->{type}}) { + $self->{name} = $Crypt::PWSafe3::HeaderField::map2name{$self->{type}}; + } + else { + $self->{name} = $self->{type}; + } + + return $self; +} + + +sub eq { + # + # compare this field with the given one + my ($this, $field) = @_; + return $this->type == $field->type and $this->value eq $field->value; +} + +=head1 NAME + +Crypt::PWSafe3::HeaderField - represent a passwordsafe v3 header field. + +=head1 SYNOPSIS + + use Crypt::PWSafe3; + my $who = $vault->getheader('wholastsaved'); + print $who->value; + + my $h = new Crypt::PWSafe3::HeaderField(name => 'savedonhost', + value => 'localhost'); + $vault->addheader($h); + +=head1 DESCRIPTION + +B represents a header field. This is the +raw implementation and you normally don't have to cope with it. + +However, if you ever do, you can add/replace any field type +this way: + + my $field = new Crypt::PWSafe3::HeaderField( + value => 'localhost', + name => 'savedonhost' + ); + $record->addheader($field); + +This is the preferred way to do it, Crypt::PWSafe3 does +it internaly exactly like this. + +If there already exists a field of this type, it will +be overwritten. + +=head1 HEADER FIELDS + +A password safe v3 database supports the following header fields: + +version + +uuid + +preferences + +treedisplaystatus + +lastsavetime + +wholastsaved + +whatlastsaved + +savedbyuser + +savedonhost + +databasename + +databasedescr + +databasefilters + +eof + +Refer to L for details on those +header fields. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +T. Linden + +=head1 COPYRIGHT + +Copyright (c) 2011 by T.Linden . +All rights reserved. + +=head1 LICENSE + +This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + + +=cut + +1; diff --git a/lib/Crypt/PWSafe3/Record.pm b/lib/Crypt/PWSafe3/Record.pm new file mode 100644 index 0000000..8d9426a --- /dev/null +++ b/lib/Crypt/PWSafe3/Record.pm @@ -0,0 +1,296 @@ +package Crypt::PWSafe3::Record; + +use Carp::Heavy; +use Carp; +use Exporter (); +use vars qw(@ISA @EXPORT %map2name %map2type); + +my %map2type = %Crypt::PWSafe3::Field::map2type; + +my %map2name = %Crypt::PWSafe3::Field::map2name; + +$Crypt::PWSafe3::Record::VERSION = '1.02'; + +foreach my $field (keys %map2type ) { + eval qq( + *Crypt::PWSafe3::Record::$field = sub { + my(\$this, \$arg) = \@_; + if (\$arg) { + return \$this->modifyfield("$field", \$arg); + } + else { + return \$this->{field}->{$field}->{value}; + } + } + ); +} + +sub new { + # + # new record object + my($this) = @_; + my $class = ref($this) || $this; + my $self = { }; + bless($self, $class); + $self->{field} = (); + + # just in case this is a record to be filled by the user, + # initialize it properly + my $newuuid = $self->genuuid(); + $self->addfield(new Crypt::PWSafe3::Field( + name => 'uuid', + raw => $newuuid, + )); + + $self->addfield(new Crypt::PWSafe3::Field( + name => 'ctime', + value => time, + )); + + $self->addfield(new Crypt::PWSafe3::Field( + name => 'mtime', + value => time + )); + + $self->addfield(new Crypt::PWSafe3::Field( + name => 'lastmod', + value => time + )); + + $self->addfield(new Crypt::PWSafe3::Field( + name => 'passwd', + value => '' + )); + + $self->addfield(new Crypt::PWSafe3::Field( + name => 'user', + value => '' + )); + + $self->addfield(new Crypt::PWSafe3::Field( + name => 'title', + value => '' + )); + + return $self; +} + +sub modifyfield { + # + # add or modify a record field + my($this, $name, $value) = @_; + if (exists $map2type{$name}) { + my $type = $map2type{$name}; + my $field = new Crypt::PWSafe3::Field( + type => $type, + value => $value + ); + # we are in fact just overwriting an eventually + # existing field with a new one, instead of modifying + # it, so we are using the conversion automatism in + # Field::new() + $this->addfield($field); + + # mark the record as modified + $this->addfield(new Crypt::PWSafe3::Field( + name => 'mtime', + value => time + )); + + $this->addfield(new Crypt::PWSafe3::Field( + name => "lastmod", + value => time + )); + return $field; + } + else { + croak "Unknown field $name"; + } +} + +sub genuuid { + # + # generate a v4 uuid string + my($this) = @_; + my $ug = new Data::UUID; + my $uuid = $ug->create(); + return $uuid; +} + +sub addfield { + # + # add a field to the record + my ($this, $field) = @_; + $this->{field}->{ $map2name{$field->type} } = $field; +} + +=head1 NAME + +Crypt::PWSafe3::Record - Represents a Passwordsafe v3 data record + +=head1 SYNOPSIS + + use Crypt::PWSafe3; + my $record = $vault->getrecord($uuid); + $record->title('t2'); + $record->passwd('foobar'); + print $record->notes; + +=head1 DESCRIPTION + +B represents a Passwordsafe v3 data record. +Each record consists of a number of fields of type B. +The class provides get/set methods to access the values of those +fields. + +It is also possible to access the raw unencoded values of the fields +by accessing them directly, refer to L for more +details on this. + +=head1 METHODS + +=head2 B + +Returns the UUID without argument. Sets the UUID if an argument +is given. Must be a hex representation of an L object. + +This will be generated automatically for new records, so you +normally don't have to cope with. + +=head2 B + +Returns the username without argument. Sets the username +if an argument is given. + +=head2 B + +Returns the title without argument. Sets the title +if an argument is given. + +=head2 B + +Returns the password without argument. Sets the password +if an argument is given. + +=head2 B + +Returns the notes without argument. Sets the notes +if an argument is given. + +=head2 B + +Returns the group without argument. Sets the group +if an argument is given. + +Group hierarchy can be done by separating subgroups +by dot, eg: + + $record->group('accounts.banking'); + +=head2 B + +Returns the creation time without argument. Sets the creation time +if an argument is given. Argument must be an integer timestamp +as returned by L. + +This will be generated automatically for new records, so you +normally don't have to cope with. + +=head2 B + +Returns the access time without argument. Sets the access time +if an argument is given. Argument must be an integer timestamp +as returned by L. + +B doesn't update the atime field currently. So if +you mind, do it yourself. + +=head2 B + +Returns the modification time without argument. Sets the modification time +if an argument is given. Argument must be an integer timestamp +as returned by L. + +This will be generated automatically for modified records, so you +normally don't have to cope with. + +=head2 B + +Returns the modification time without argument. Sets the modification time +if an argument is given. Argument must be an integer timestamp +as returned by L. + +This will be generated automatically for modified records, so you +normally don't have to cope with. + +I + +=head2 B + +Returns the url without argument. Sets the url +if an argument is given. The url must be in the well +known notation as: + + proto://host/path + +=head2 B + +Returns the password history without argument. Sets the password history +if an argument is given. + +B doesn't update the pwhist field currently. So if +you mind, do it yourself. Refer to L +for more details. + +=head2 B + +Returns the password policy without argument. Sets the password policy +if an argument is given. + +B doesn't update the pwpol field currently. So if +you mind, do it yourself. Refer to L +for more details. + +=head2 B + +Returns the password expire time without argument. Sets the password expire time +if an argument is given. + +B doesn't update the pwexp field currently. So if +you mind, do it yourself. Refer to L +for more details. + +=head1 MANDATORY FIELDS + +B creates the following fields automatically +on creation, because those fields are mandatory: + +B will be generated using L. + +B will be set to the empty string. + +B will be set to current +time of creation time. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +T. Linden + +=head1 COPYRIGHT + +Copyright (c) 2011 by T.Linden . +All rights reserved. + +=head1 LICENSE + +This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + +=cut + +1; diff --git a/lib/Crypt/PWSafe3/SHA256.pm b/lib/Crypt/PWSafe3/SHA256.pm new file mode 100644 index 0000000..eb1db82 --- /dev/null +++ b/lib/Crypt/PWSafe3/SHA256.pm @@ -0,0 +1,60 @@ +# +# helper class to provide SHA-256 to HMAC class + +package Crypt::PWSafe3::SHA256; + +$Crypt::PWSafe3::SHA256::VERSION = '1.01'; + +use Digest::SHA; + +sub new { + my($this) = @_; + my $class = ref($this) || $this; + my $self = { }; + bless($self, $class); + my $sha = new Digest::SHA('SHA-256'); + return $sha; +} + +=head1 NAME + +Crypt::PWSafe3::SHA256 - HMAC Helper Class + +=head1 DESCRIPTION + +This is a small helper class used to work with +SHA256 in Digest::HMAC module. This is because the +Digest::HMAC module requires a module as parameter +for the algorithm but Digest::SHA256 doesn't exist +as a module. + +This module here is just a wrapper, it doesn't return +an instance of its own but an instance of Digest::SHA('SHA-256') +instead. + +=head1 AUTHOR + +T. Linden + +=head1 SEE ALSO + +L +L +L + +=head1 COPYRIGHT + +Copyright (c) 2011 by T.Linden . +All rights reserved. + +=head1 LICENSE + +This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + +=cut + + + + +1; diff --git a/t/run.t b/t/run.t new file mode 100644 index 0000000..c59b340 --- /dev/null +++ b/t/run.t @@ -0,0 +1,108 @@ +# -*-perl-*- +# testscript for Crypt::PWSafe3 Classes by Thomas Linden +# +# needs to be invoked using the command "make test" from +# the Crypt::PWSafe3 source directory. +# +# Under normal circumstances every test should succeed. + + +use Data::Dumper; +#use Test::More tests => 57; +use Test::More qw(no_plan); + + +### 1 +# load module +BEGIN { use_ok "Crypt::PWSafe3"}; +require_ok( 'Crypt::PWSafe3' ); + +### 2 +# open vault and read in all records +eval { + my $vault = new Crypt::PWSafe3(file => 't/tom.psafe3', password => 'tom'); + my @r = $vault->getrecords; + my $got = 0; + foreach my $rec (@r) { + if ($rec->uuid) { + $got++; + } + } + if (! $got) { + die "No records found in test database"; + } +}; +ok(!$@, "open a pwsafe3 database"); + +### 3 +# modify an existing record +my $uuid3; +my %rdata3; +my $rec3; +my %data3 = ( + user => 'u3', + passwd => 'p3', + group => 'g3', + title => 't3', + notes => 'n3' + ); +eval { + my $vault3 = new Crypt::PWSafe3(file => 't/tom.psafe3', password => 'tom'); + foreach my $uuid ($vault3->looprecord) { + $uuid3 = $uuid; + $vault3->modifyrecord($uuid3, %data3); + last; + } + $vault3->save(file=>'t/3.out'); + + my $rvault3 = new Crypt::PWSafe3(file => 't/3.out', password => 'tom'); + $rec3 = $rvault3->getrecord($uuid3); + + foreach my $name (keys %data3) { + $rdata3{$name} = $rec3->$name(); + } +}; +ok(!$@, "read a pwsafe3 database and change a record ($@)"); +is_deeply(\%data3, \%rdata3, "Change a record an check if changes persist after saving"); + + +### 4 +# re-use $rec3 and change it the oop way +my $rec4; +eval { + my $vault4 = new Crypt::PWSafe3(file => 't/3.out', password => 'tom'); + $rec4 = $vault4->getrecord($uuid3); + + $rec4->user("u4"); + $rec4->passwd("p4"); + + $vault4->addrecord($rec4); + $vault4->markmodified(); + $vault4->save(file=>'t/4.out'); + + my $rvault4 = new Crypt::PWSafe3(file => 't/4.out', password => 'tom'); + $rec4 = $rvault4->getrecord($uuid3); + if ($rec4->user ne 'u4') { + die "oop way record change failed"; + } +}; +ok(!$@, "re-use record and change it the oop way\n" . $@ . "\n"); + + +### 5 modify some header fields +eval { + my $vault5 = new Crypt::PWSafe3(file => 't/tom.psafe3', password => 'tom'); + + my $h3 = new Crypt::PWSafe3::HeaderField(name => 'savedonhost', value => 'localhost'); + + $vault5->addheader($h3); + $vault5->markmodified(); + $vault5->save(file=>'t/5.out'); + + my $rvault5 = new Crypt::PWSafe3(file => 't/5.out', password => 'tom'); + + if ($rvault5->getheader('savedonhost')->value() ne 'localhost') { + die "header savedonhost not correct"; + } +}; +ok(!$@, "modify some header fields ($@)"); diff --git a/t/tom.psafe3 b/t/tom.psafe3 new file mode 100644 index 0000000..6ad60b5 Binary files /dev/null and b/t/tom.psafe3 differ