File-KeePass

 view release on metacpan or  search on metacpan

lib/File/KeePass.pm  view on Meta::CPAN

    $buffer = $self->compress($buffer) if $head->{'compression'} eq '1';
    $buffer = $self->chunksum($buffer);

    substr $buffer, 0, 0, $head->{'start_bytes'};

    return $header . $self->encrypt_rijndael_cbc($buffer, $key, $head->{'enc_iv'});
}

sub _gen_v2_date {
    my ($self, $date) = @_;
    $date = $self->now($date) if !$date || $date =~ /^\d+$/;
    my ($year, $mon, $day, $hour, $min, $sec) = $date =~ $qr_date ? ($1,$2,$3,$4,$5,$6) : die "Invalid date ($date)";
    return "${year}-${mon}-${day}T${hour}:${min}:${sec}Z";
}

sub _gen_v2_header {
    my ($self, $head) = @_;
    $head->{'sig1'}        = DB_SIG_1;
    $head->{'sig2'}        = DB_SIG_2_v2;
    $head->{'ver'}         = DB_VER_DW_V2;
    $head->{'comment'}     = '' if ! defined $head->{'comment'};
    $head->{'compression'} = (!defined($head->{'compression'}) || $head->{'compression'} eq '1') ? 1 : 0;
    $head->{'0'}           ||= "\r\n\r\n";
    $head->{'protected_stream_key'} ||= join '', map {chr rand 256} 1..32;
    die "Missing start_bytes\n" if ! $head->{'start_bytes'};
    die "Length of $_ was not 32 (".length($head->{$_}).")\n" for grep {length($head->{$_}) != 32} qw(seed_rand seed_key protected_stream_key start_bytes);
    die "Length of enc_iv was not 16\n" if length($head->{'enc_iv'}) != 16;

    my $buffer = pack 'L3', @$head{qw(sig1 sig2 ver)};

    my $pack = sub { my ($type, $str) = @_; $buffer .= pack('C S', $type, length($str)) . $str };
    $pack->(1, $head->{'comment'}) if defined($head->{'comment'}) && length($head->{'comment'});
    $pack->(2, "\x31\xc1\xf2\xe6\xbf\x71\x43\x50\xbe\x58\x05\x21\x6a\xfc\x5a\xff"); # aes cipher
    $pack->(3, pack 'V', $head->{'compression'} ? 1 : 0);
    $pack->(4, $head->{'seed_rand'});
    $pack->(5, $head->{'seed_key'});
    $pack->(6, pack 'LL', $head->{'rounds'}, 0); # a little odd to be double the length but not used
    $pack->(7, $head->{'enc_iv'});
    $pack->(8, $head->{'protected_stream_key'});
    $pack->(9, $head->{'start_bytes'});
    $pack->(10, pack('V', 2)); # salsa20 protection
    $pack->(0, $head->{'0'});
    return $buffer;
}

###----------------------------------------------------------------###

sub slurp {
    my ($self, $file) = @_;
    open my $fh, '<', $file or die "Could not open $file: $!\n";
    my $size = -s $file || die "File $file appears to be empty.\n";
    binmode $fh;
    read($fh, my $buffer, $size);
    close $fh;
    die "Could not read entire file contents of $file.\n" if length($buffer) != $size;
    return $buffer;
}

sub decrypt_rijndael_cbc {
    my ($self, $buffer, $key, $enc_iv) = @_;
    #use Crypt::CBC; return Crypt::CBC->new(-cipher => 'Rijndael', -key => $key, -iv => $enc_iv, -regenerate_key => 0, -prepend_iv => 0)->decrypt($buffer);
    my $cipher = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_CBC());
    $cipher->set_iv($enc_iv);
    $buffer = $cipher->decrypt($buffer);
    my $extra = ord(substr $buffer, -1, 1);
    substr($buffer, length($buffer) - $extra, $extra, '');
    return $buffer;
}

sub encrypt_rijndael_cbc {
    my ($self, $buffer, $key, $enc_iv) = @_;
    #use Crypt::CBC; return Crypt::CBC->new(-cipher => 'Rijndael', -key => $key, -iv => $enc_iv, -regenerate_key => 0, -prepend_iv => 0)->encrypt($buffer);
    my $cipher = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_CBC());
    $cipher->set_iv($enc_iv);
    my $extra = (16 - length($buffer) % 16) || 16; # always pad so we can always trim
    $buffer .= chr($extra) for 1 .. $extra;
    return $cipher->encrypt($buffer);
}

sub unchunksum {
    my ($self, $buffer) = @_;
    my ($new, $pos) = ('', 0);
    while ($pos < length($buffer)) {
        my ($index, $hash, $size) = unpack "\@$pos L a32 i", $buffer;
        $pos += 40;
        if ($size == 0) {
            warn "Found mismatch for 0 chunksize\n" if $hash ne "\0"x32;
            last;
        }
        #print "$index $hash $size\n";
        my $chunk = substr $buffer, $pos, $size;
        die "Chunk hash of index $index did not match\n" if $hash ne sha256($chunk);
        $pos += $size;
        $new .= $chunk;
    }
    return $new;
}

sub chunksum {
    my ($self, $buffer) = @_;
    my $new;
    my $index = 0;
    my $chunk_size = 8192;
    my $pos = 0;
    while ($pos < length($buffer)) {
        my $chunk = substr($buffer, $pos, $chunk_size);
        $new .= pack "L a32 i", $index++, sha256($chunk), length($chunk);
        $new .= $chunk;
        $pos += length($chunk);
    }
    $new .= pack "L a32 i", $index++, "\0"x32, 0;
    return $new;
}

sub decompress {
    my ($self, $buffer) = @_;
    eval { require Compress::Raw::Zlib } or die "Cannot load compression library to decompress database: $@";
    my ($i, $status) = Compress::Raw::Zlib::Inflate->new(-WindowBits => 31);
    die "Failed to initialize inflator ($status)\n" if $status != Compress::Raw::Zlib::Z_OK();
    $status = $i->inflate($buffer, my $out);
    die "Failed to uncompress buffer ($status)\n" if $status != Compress::Raw::Zlib::Z_STREAM_END();
    return $out;
}

sub compress {
    my ($self, $buffer) = @_;
    eval { require Compress::Raw::Zlib } or die "Cannot load compression library to compress database: $@";
    my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1);
    die "Failed to initialize inflator ($status)\n" if $status != Compress::Raw::Zlib::Z_OK();
    $status = $d->deflate($buffer, my $out);
    die "Failed to compress buffer ($status)\n" if $status != Compress::Raw::Zlib::Z_OK();
    $status = $d->flush($out);
    die "Failed to compress buffer ($status).\n" if $status != Compress::Raw::Zlib::Z_OK();



( run in 1.181 second using v1.01-cache-2.11-cpan-e1769b4cff6 )