1 package File
::KDBX
::Dumper
::V3
;
2 # ABSTRACT: Dump KDBX3 files
7 use Crypt
::Digest
qw(digest_data);
9 use File
::KDBX
::Constants
qw(:header :compression);
10 use File
::KDBX
::Error
;
11 use File
::KDBX
::IO
::Crypt
;
12 use File
::KDBX
::IO
::HashBlock
;
13 use File
::KDBX
::Util
qw(:class :empty :int :load erase_scoped);
17 extends
'File::KDBX::Dumper';
19 our $VERSION = '999.999'; # VERSION
25 my $kdbx = $self->kdbx;
26 my $headers = $kdbx->headers;
29 # FIXME kinda janky - maybe add a "prepare" hook to massage the KDBX into the correct shape before we get
31 local $headers->{+HEADER_TRANSFORM_SEED
} = $kdbx->transform_seed;
32 local $headers->{+HEADER_TRANSFORM_ROUNDS
} = $kdbx->transform_rounds;
34 my $got_iv_size = length($headers->{+HEADER_ENCRYPTION_IV
});
35 alert
'Encryption IV should be exactly 16 bytes long',
37 expected
=> 16 if $got_iv_size != 16;
39 if (nonempty
(my $comment = $headers->{+HEADER_COMMENT
})) {
40 $buf .= $self->_write_header($fh, HEADER_COMMENT
, $comment);
44 HEADER_COMPRESSION_FLAGS
,
46 HEADER_TRANSFORM_SEED
,
47 HEADER_TRANSFORM_ROUNDS
,
49 HEADER_INNER_RANDOM_STREAM_KEY
,
50 HEADER_STREAM_START_BYTES
,
51 HEADER_INNER_RANDOM_STREAM_ID
,
53 defined $headers->{$type} or throw
"Missing value for required header: $type", type
=> $type;
54 $buf .= $self->_write_header($fh, $type, $headers->{$type});
56 $buf .= $self->_write_header($fh, HEADER_END
);
65 my $val = shift // '';
67 $type = to_header_constant
($type);
68 if ($type == HEADER_END
) {
71 elsif ($type == HEADER_COMMENT
) {
72 $val = encode
('UTF-8', $val);
74 elsif ($type == HEADER_CIPHER_ID
) {
75 my $size = length($val);
76 $size == 16 or throw
'Invalid cipher UUID length', got
=> $size, expected
=> $size;
78 elsif ($type == HEADER_COMPRESSION_FLAGS
) {
79 $val = pack('L<', $val);
81 elsif ($type == HEADER_MASTER_SEED
) {
82 my $size = length($val);
83 $size == 32 or throw
'Invalid master seed length', got
=> $size, expected
=> $size;
85 elsif ($type == HEADER_TRANSFORM_SEED
) {
88 elsif ($type == HEADER_TRANSFORM_ROUNDS
) {
91 elsif ($type == HEADER_ENCRYPTION_IV
) {
94 elsif ($type == HEADER_INNER_RANDOM_STREAM_KEY
) {
97 elsif ($type == HEADER_STREAM_START_BYTES
) {
100 elsif ($type == HEADER_INNER_RANDOM_STREAM_ID
) {
101 $val = pack('L<', $val);
103 elsif ($type == HEADER_KDF_PARAMETERS
||
104 $type == HEADER_PUBLIC_CUSTOM_DATA
) {
105 throw
"Unexpected KDBX4 header: $type", type
=> $type;
107 elsif ($type == HEADER_COMMENT
) {
108 throw
"Unexpected KDB header: $type", type
=> $type;
111 alert
"Unknown header: $type", type
=> $type;
114 my $size = length($val);
115 my $buf = pack('C S<', 0+$type, $size);
117 $fh->print($buf, $val) or throw
'Failed to write header';
126 my $header_data = shift;
127 my $kdbx = $self->kdbx;
129 # assert all required headers present
132 HEADER_ENCRYPTION_IV
,
134 HEADER_INNER_RANDOM_STREAM_KEY
,
135 HEADER_STREAM_START_BYTES
,
137 defined $kdbx->headers->{$field} or throw
"Missing $field";
140 my $master_seed = $kdbx->headers->{+HEADER_MASTER_SEED
};
143 $key = $kdbx->composite_key($key);
145 my $response = $key->challenge($master_seed);
146 push @cleanup, erase_scoped
$response;
148 my $transformed_key = $kdbx->kdf->transform($key);
149 push @cleanup, erase_scoped
$transformed_key;
151 my $final_key = digest_data
('SHA256', $master_seed, $response, $transformed_key);
152 push @cleanup, erase_scoped
$final_key;
154 my $cipher = $kdbx->cipher(key
=> $final_key);
155 $fh = File
::KDBX
::IO
::Crypt-
>new($fh, cipher
=> $cipher);
157 $fh->print($kdbx->headers->{+HEADER_STREAM_START_BYTES
})
158 or throw
'Failed to write start bytes';
162 $fh = File
::KDBX
::IO
::HashBlock-
>new($fh);
164 my $compress = $kdbx->headers->{+HEADER_COMPRESSION_FLAGS
};
165 if ($compress == COMPRESSION_GZIP
) {
166 load_optional
('IO::Compress::Gzip');
167 $fh = IO
::Compress
::Gzip-
>new($fh,
168 -Level
=> IO
::Compress
::Gzip
::Z_BEST_COMPRESSION
(),
170 ) or throw
"Failed to initialize compression library: $IO::Compress::Gzip::GzipError",
171 error
=> $IO::Compress
::Gzip
::GzipError
;
173 elsif ($compress != COMPRESSION_NONE
) {
174 throw
"Unsupported compression ($compress)\n", compression_flags
=> $compress;
177 my $header_hash = digest_data
('SHA256', $header_data);
178 $self->_write_inner_body($fh, $header_hash);