1 package File
::KDBX
::Loader
::V3
;
2 # ABSTRACT: Load KDBX3 files
19 use Crypt
::Digest
qw(digest_data);
20 use Encode
qw(decode);
21 use File
::KDBX
::Constants
qw(:header :compression :kdf);
22 use File
::KDBX
::Error
;
23 use File
::KDBX
::IO
::Crypt
;
24 use File
::KDBX
::IO
::HashBlock
;
25 use File
::KDBX
::Util
qw(:class :int :io :load erase_scoped);
28 extends
'File::KDBX::Loader';
30 our $VERSION = '999.999'; # VERSION
36 read_all
$fh, my $buf, 3 or throw
'Malformed header field, expected header type and size';
37 my ($type, $size) = unpack('C S<', $buf);
41 read_all
$fh, $val, $size or throw
'Expected header value', type
=> $type, size
=> $size;
45 $type = to_header_constant
($type);
46 if ($type == HEADER_END
) {
49 elsif ($type == HEADER_COMMENT
) {
50 $val = decode
('UTF-8', $val);
52 elsif ($type == HEADER_CIPHER_ID
) {
53 $size == 16 or throw
'Invalid cipher UUID length', got
=> $size, expected
=> $size;
55 elsif ($type == HEADER_COMPRESSION_FLAGS
) {
56 $val = unpack('L<', $val);
58 elsif ($type == HEADER_MASTER_SEED
) {
59 $size == 32 or throw
'Invalid master seed length', got
=> $size, expected
=> $size;
61 elsif ($type == HEADER_TRANSFORM_SEED
) {
64 elsif ($type == HEADER_TRANSFORM_ROUNDS
) {
65 ($val) = unpack_Ql
($val);
67 elsif ($type == HEADER_ENCRYPTION_IV
) {
70 elsif ($type == HEADER_INNER_RANDOM_STREAM_KEY
) {
73 elsif ($type == HEADER_STREAM_START_BYTES
) {
76 elsif ($type == HEADER_INNER_RANDOM_STREAM_ID
) {
77 ($val) = unpack('L<', $val);
79 elsif ($type == HEADER_KDF_PARAMETERS
||
80 $type == HEADER_PUBLIC_CUSTOM_DATA
) {
81 throw
"Unexpected KDBX4 header: $type", type
=> $type;
84 alert
"Unknown header: $type", type
=> $type;
87 return wantarray ? ($type => $val, $buf) : $buf;
94 my $header_data = shift;
95 my $kdbx = $self->kdbx;
97 # assert all required headers present
100 HEADER_ENCRYPTION_IV
,
102 HEADER_INNER_RANDOM_STREAM_KEY
,
103 HEADER_STREAM_START_BYTES
,
105 defined $kdbx->headers->{$field} or throw
"Missing $field";
108 $kdbx->kdf_parameters({
109 KDF_PARAM_UUID
() => KDF_UUID_AES
,
110 KDF_PARAM_AES_ROUNDS
() => delete $kdbx->headers->{+HEADER_TRANSFORM_ROUNDS
},
111 KDF_PARAM_AES_SEED
() => delete $kdbx->headers->{+HEADER_TRANSFORM_SEED
},
114 my $master_seed = $kdbx->headers->{+HEADER_MASTER_SEED
};
117 $key = $kdbx->composite_key($key);
119 my $response = $key->challenge($master_seed);
120 push @cleanup, erase_scoped
$response;
122 my $transformed_key = $kdbx->kdf->transform($key);
123 push @cleanup, erase_scoped
$transformed_key;
125 my $final_key = digest_data
('SHA256', $master_seed, $response, $transformed_key);
126 push @cleanup, erase_scoped
$final_key;
128 my $cipher = $kdbx->cipher(key
=> $final_key);
129 $fh = File
::KDBX
::IO
::Crypt-
>new($fh, cipher
=> $cipher);
131 read_all
$fh, my $start_bytes, 32 or throw
'Failed to read starting bytes';
133 my $expected_start_bytes = $kdbx->headers->{stream_start_bytes
};
134 $start_bytes eq $expected_start_bytes
135 or throw
"Invalid credentials or data is corrupt (wrong starting bytes)\n",
136 got
=> $start_bytes, expected
=> $expected_start_bytes, headers
=> $kdbx->headers;
140 $fh = File
::KDBX
::IO
::HashBlock-
>new($fh);
142 my $compress = $kdbx->headers->{+HEADER_COMPRESSION_FLAGS
};
143 if ($compress == COMPRESSION_GZIP
) {
144 load_optional
('IO::Uncompress::Gunzip');
145 $fh = IO
::Uncompress
::Gunzip-
>new($fh)
146 or throw
"Failed to initialize compression library: $IO::Uncompress::Gunzip::GunzipError",
147 error
=> $IO::Uncompress
::Gunzip
::GunzipError
;
149 elsif ($compress != COMPRESSION_NONE
) {
150 throw
"Unsupported compression ($compress)\n", compression_flags
=> $compress;
153 $self->_read_inner_body($fh);
156 if (my $header_hash = $kdbx->meta->{header_hash
}) {
157 my $got_header_hash = digest_data
('SHA256', $header_data);
158 $header_hash eq $got_header_hash
159 or throw
'Header hash does not match', got
=> $got_header_hash, expected
=> $header_hash;