Crypt-PWSafe3
view release on metacpan or search on metacpan
lib/Crypt/PWSafe3.pm view on Meta::CPAN
#
# Copyright (c) 2011-2016 T.v.Dein <tlinden |AT| cpan.org>.
#
# Licensed under the terms of the Artistic License 2.0
# see: http://www.perlfoundation.org/artistic_license_2_0
#
# Implements:
# http://passwordsafe.svn.sourceforge.net/viewvc/passwordsafe/trunk/pwsafe/pwsafe/docs/formatV3.txt?revision=2139
package Crypt::PWSafe3;
use strict;
use Config;
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 File::Copy qw(copy move);
use File::Temp;
use File::Spec;
use FileHandle;
use Data::Dumper;
use Exporter ();
use vars qw(@ISA @EXPORT);
$Crypt::PWSafe3::VERSION = '1.22';
use Crypt::PWSafe3::Field;
use Crypt::PWSafe3::HeaderField;
use Crypt::PWSafe3::Record;
use Crypt::PWSafe3::SHA256;
use Crypt::PWSafe3::PasswordPolicy;
require 5.10.0;
#
# check which random source to use.
# install a wrapper closure around the
# one we found.
BEGIN {
eval {
require Bytes::Random::Secure;
Bytes::Random::Secure->import("random_bytes");
};
if ($@) {
# well, didn' work, use slow function
eval { require Crypt::Random; };# qw( makerandom ); };
if ($@) {
croak "Could not find either Crypt::Random or Bytes::Random::Secure. Install one of them and retry!";
}
else {
*Crypt::PWSafe3::random = sub {
my($this, $len) = @_;
my $bits = makerandom(Size => 256, Strength => 1);
return substr($bits, 0, $len);
};
}
}
else {
# good. use the faster one
*Crypt::PWSafe3::random = sub {
my($this, $len) = @_;
return random_bytes($len);
};
}
}
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(
lib/Crypt/PWSafe3.pm view on Meta::CPAN
}
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 = Digest::SHA->new('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 = 0 x 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 = Digest::SHA->new('SHA-256');
$sha->reset();
$sha->add( ( $this->strechedpw() ) );
$this->shaps( $sha->digest() );
# encrypt b1 .. b4
my $crypt = Crypt::ECB->new;
$crypt->padding('none');
$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} = Digest::HMAC->new($this->keyl, "Crypt::PWSafe3::SHA256");
$this->{cipher} = Crypt::CBC->new(
-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 $file = $this->file();
my $fd = FileHandle->new($file, 'r') or croak "Could not open $file for reading: $!";
$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("L<", $this->readbytes(4) ) );
$this->strechedpw($this->stretchpw($this->password()));
my $sha = Digest::SHA->new(256);
$sha->reset();
$sha->add( ( $this->strechedpw() ) );
$this->shaps( $sha->digest() );
my $fileshaps = $this->readbytes(32);
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->padding('none');
$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));
$this->iv( $this->readbytes(16) );
# create hmac'er and cipher for actual encryption
$this->{hmacer} = Digest::HMAC->new($this->keyl, "Crypt::PWSafe3::SHA256");
$this->{cipher} = Crypt::CBC->new(
-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 = Crypt::PWSafe3::Record->new(super => $this);
$this->{record} = {};
while (1) {
my $field = $this->readfield();
if (! $field) {
last;
}
if ($field->type == 0xff) {
$this->addrecord($record);
$record = Crypt::PWSafe3::Record->new(super => $this);
}
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, invalid HMAC";
}
$this->{fd}->close() or croak "Could not close $file: $!";
}
sub untaint {
#
# untaint path's
my ($this, $path) = @_;
if($path =~ /([\w\-\/\\\.:]+\z)/) {
return $1;
}
else {
lib/Crypt/PWSafe3.pm view on Meta::CPAN
my $lastsave = Crypt::PWSafe3::HeaderField->new(type => 0x04, value => time);
my $whatsaved = Crypt::PWSafe3::HeaderField->new(type => 0x06, value => $this->{program});
my $whosaved = Crypt::PWSafe3::HeaderField->new(type => 0x05, value => $this->{whoami});
$this->addheader($lastsave);
$this->addheader($whatsaved);
$this->addheader($whosaved);
# open a temp vault file, first we try it in the same directory as the target vault file
my $tmpdir;
if (File::Spec->file_name_is_absolute($file)) {
my ($volume, $directories, undef) = File::Spec->splitpath($file);
$tmpdir = File::Spec->catdir($volume, $directories);
}
else {
my ($volume, $directories, undef) = File::Spec->splitpath(File::Spec->rel2abs($file));
$tmpdir = File::Spec->abs2rel(File::Spec->catdir($volume, $directories));
}
my $fd;
if (-w $tmpdir) {
$fd = File::Temp->new(TEMPLATE => '.vaultXXXXXXXX', DIR => $tmpdir, EXLOCK => 0) or croak "Could not open tmpfile: $!\n";
}
else {
# well, then we'll use one in the tmp dir of the system
$fd = File::Temp->new(TEMPLATE => '.vaultXXXXXXXX', TMPDIR => 1, EXLOCK => 0) or croak "Could not open tmpfile: $!\n";
}
my $tmpfile = "$fd";
$this->{fd} = $fd;
$this->writebytes($this->tag);
$this->writebytes($this->salt);
$this->writebytes(pack("L<", $this->iter));
$this->strechedpw($this->stretchpw($passwd));
# line 472
my $sha = Digest::SHA->new(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->padding('none');
$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} = Digest::HMAC->new($this->keyl, "Crypt::PWSafe3::SHA256");
$this->{cipher} = Crypt::CBC->new(
-key => $this->keyk,
-iv => $this->iv,
-cipher => 'Twofish',
-header => 'none',
-padding => 'null',
-literal_key => 1,
-keysize => 32,
-blocksize => 16
);
my $eof = Crypt::PWSafe3::HeaderField->new(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 = Crypt::PWSafe3::Field->new(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(Crypt::PWSafe3::Field->new(type => 'none', raw => 0));
$this->hmac( $this->{hmacer}->digest() );
$this->writebytes($this->hmac);
if ($Config{d_fsync}) {
$this->{fd}->sync() or croak "Could not fsync: $!";
}
$this->{fd}->close() or croak "Could not close tmpfile: $!";
# now try to read it in again to check if it
# is valid what we created
eval {
my $vault = Crypt::PWSafe3->new(file => $tmpfile, create => 0, password => $passwd);
};
if ($@) {
unlink $tmpfile;
croak "File integrity check failed ($@)";
}
else {
# well, seems to be ok :)
move($tmpfile, $file) or croak "Could not move $tmpfile to $file: $!";
}
}
sub writefield {
#
# write a field to vault file
my($this, $field) = @_;
lib/Crypt/PWSafe3.pm view on Meta::CPAN
#
# 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;
}
$data = $this->decrypt($data);
my $len = unpack("L<", substr($data, 0, 4));
my $type = unpack("C", substr($data, 4, 1));
my $raw = substr($data, 5);
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 Crypt::PWSafe3::HeaderField->new(type => $type, raw => $raw);
}
else {
return Crypt::PWSafe3::Field->new(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 then 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;
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 getheader {
#
# return a header object
my($this, $name) = @_;
if (exists $this->{header}->{$name}) {
return $this->{header}->{$name};
}
else {
croak "Unknown header $name";
}
}
( run in 2.438 seconds using v1.01-cache-2.11-cpan-e1769b4cff6 )