X-Git-Url: https://git.dogcows.com/gitweb?a=blobdiff_plain;f=lib%2FFile%2FKDBX%2FLoader%2FV3.pm;fp=lib%2FFile%2FKDBX%2FLoader%2FV3.pm;h=68d7f9ce0c35389592889415ae8178aa3819419d;hb=f63182fc62b25269b1c38588dca2b3535ed1a1a2;hp=0000000000000000000000000000000000000000;hpb=e2deca75a6040911441e0d7c4430aeae9be69e40;p=chaz%2Fp5-File-KDBX diff --git a/lib/File/KDBX/Loader/V3.pm b/lib/File/KDBX/Loader/V3.pm new file mode 100644 index 0000000..68d7f9c --- /dev/null +++ b/lib/File/KDBX/Loader/V3.pm @@ -0,0 +1,164 @@ +package File::KDBX::Loader::V3; +# ABSTRACT: Load KDBX3 files + +# magic +# headers +# body +# CRYPT( +# start bytes +# HASH( +# COMPRESS( +# xml +# ) +# ) +# ) + +use warnings; +use strict; + +use Crypt::Digest qw(digest_data); +use Encode qw(decode); +use File::KDBX::Constants qw(:header :compression :kdf); +use File::KDBX::Error; +use File::KDBX::Util qw(:io assert_64bit erase_scoped); +use PerlIO::via::File::KDBX::Crypt; +use PerlIO::via::File::KDBX::HashBlock; +use namespace::clean; + +use parent 'File::KDBX::Loader'; + +our $VERSION = '999.999'; # VERSION + +sub _read_header { + my $self = shift; + my $fh = shift; + + read_all $fh, my $buf, 3 or throw 'Malformed header field, expected header type and size'; + my ($type, $size) = unpack('C S<', $buf); + + my $val; + if (0 < $size) { + read_all $fh, $val, $size or throw 'Expected header value', type => $type, size => $size; + $buf .= $val; + } + + $type = KDBX_HEADER($type); + if ($type == HEADER_END) { + # done + } + elsif ($type == HEADER_COMMENT) { + $val = decode('UTF-8', $val); + } + elsif ($type == HEADER_CIPHER_ID) { + $size == 16 or throw 'Invalid cipher UUID length', got => $size, expected => $size; + } + elsif ($type == HEADER_COMPRESSION_FLAGS) { + $val = unpack('L<', $val); + } + elsif ($type == HEADER_MASTER_SEED) { + $size == 32 or throw 'Invalid master seed length', got => $size, expected => $size; + } + elsif ($type == HEADER_TRANSFORM_SEED) { + # nothing + } + elsif ($type == HEADER_TRANSFORM_ROUNDS) { + assert_64bit; + $val = unpack('Q<', $val); + } + elsif ($type == HEADER_ENCRYPTION_IV) { + # nothing + } + elsif ($type == HEADER_INNER_RANDOM_STREAM_KEY) { + # nothing + } + elsif ($type == HEADER_STREAM_START_BYTES) { + # nothing + } + elsif ($type == HEADER_INNER_RANDOM_STREAM_ID) { + $val = unpack('L<', $val); + } + elsif ($type == HEADER_KDF_PARAMETERS || + $type == HEADER_PUBLIC_CUSTOM_DATA) { + throw "Unexpected KDBX4 header: $type", type => $type; + } + else { + alert "Unknown header: $type", type => $type; + } + + return wantarray ? ($type => $val, $buf) : $buf; +} + +sub _read_body { + my $self = shift; + my $fh = shift; + my $key = shift; + my $header_data = shift; + my $kdbx = $self->kdbx; + + # assert all required headers present + for my $field ( + HEADER_CIPHER_ID, + HEADER_ENCRYPTION_IV, + HEADER_MASTER_SEED, + HEADER_INNER_RANDOM_STREAM_KEY, + HEADER_STREAM_START_BYTES, + ) { + defined $kdbx->headers->{$field} or throw "Missing $field"; + } + + $kdbx->kdf_parameters({ + KDF_PARAM_UUID() => KDF_UUID_AES, + KDF_PARAM_AES_ROUNDS() => delete $kdbx->headers->{+HEADER_TRANSFORM_ROUNDS}, + KDF_PARAM_AES_SEED() => delete $kdbx->headers->{+HEADER_TRANSFORM_SEED}, + }); + + my $master_seed = $kdbx->headers->{+HEADER_MASTER_SEED}; + + my @cleanup; + $key = $kdbx->composite_key($key); + + my $response = $key->challenge($master_seed); + push @cleanup, erase_scoped $response; + + my $transformed_key = $kdbx->kdf->transform($key); + push @cleanup, erase_scoped $transformed_key; + + my $final_key = digest_data('SHA256', $master_seed, $response, $transformed_key); + push @cleanup, erase_scoped $final_key; + + my $cipher = $kdbx->cipher(key => $final_key); + PerlIO::via::File::KDBX::Crypt->push($fh, $cipher); + + read_all $fh, my $start_bytes, 32 or throw 'Failed to read starting bytes'; + + my $expected_start_bytes = $kdbx->headers->{stream_start_bytes}; + $start_bytes eq $expected_start_bytes + or throw "Invalid credentials or data is corrupt (wrong starting bytes)\n", + got => $start_bytes, expected => $expected_start_bytes, headers => $kdbx->headers; + + $kdbx->key($key); + + PerlIO::via::File::KDBX::HashBlock->push($fh); + + my $compress = $kdbx->headers->{+HEADER_COMPRESSION_FLAGS}; + if ($compress == COMPRESSION_GZIP) { + require PerlIO::via::File::KDBX::Compression; + PerlIO::via::File::KDBX::Compression->push($fh); + } + elsif ($compress != COMPRESSION_NONE) { + throw "Unsupported compression ($compress)\n", compression_flags => $compress; + } + + $self->_read_inner_body($fh); + + binmode($fh, ':pop') if $compress; + binmode($fh, ':pop:pop'); + + if (my $header_hash = $kdbx->meta->{header_hash}) { + my $got_header_hash = digest_data('SHA256', $header_data); + $header_hash eq $got_header_hash + or throw 'Header hash does not match', got => $got_header_hash, expected => $header_hash; + } +} + +1;