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 )