File Coverage

blib/lib/File/KDBX/Dumper/V4.pm
Criterion Covered Total %
statement 168 200 84.0
branch 58 108 53.7
condition 15 39 38.4
subroutine 23 24 95.8
pod n/a
total 264 371 71.1


line stmt bran cond sub pod time code
1             package File::KDBX::Dumper::V4;
2             # ABSTRACT: Dump KDBX4 files
3              
4 1     1   614 use warnings;
  1         3  
  1         35  
5 1     1   6 use strict;
  1         3  
  1         26  
6              
7 1     1   4 use Crypt::Digest qw(digest_data);
  1         2  
  1         48  
8 1     1   6 use Crypt::Mac::HMAC qw(hmac);
  1         2  
  1         37  
9 1     1   4 use Encode qw(encode is_utf8);
  1         2  
  1         37  
10 1     1   5 use File::KDBX::Constants qw(:header :inner_header :compression :kdf :variant_map);
  1         2  
  1         294  
11 1     1   7 use File::KDBX::Error;
  1         1  
  1         42  
12 1     1   5 use File::KDBX::IO::Crypt;
  1         2  
  1         25  
13 1     1   5 use File::KDBX::IO::HmacBlock;
  1         1  
  1         37  
14 1     1   4 use File::KDBX::Util qw(:class :empty :int :load erase_scoped);
  1         2  
  1         142  
15 1     1   5 use IO::Handle;
  1         4  
  1         24  
16 1     1   4 use Scalar::Util qw(looks_like_number);
  1         1  
  1         47  
17 1     1   32 use boolean qw(:all);
  1         2  
  1         7  
18 1     1   123 use namespace::clean;
  1         4  
  1         14  
19              
20             extends 'File::KDBX::Dumper';
21 7 50   7   26  
22             our $VERSION = '0.906'; # VERSION
23 7   50     34  
24             has _binaries_written => {}, is => 'ro';
25              
26             sub _write_headers {
27 7     7   15 my $self = shift;
28 7         8 my $fh = shift;
29              
30 7         20 my $kdbx = $self->kdbx;
31 7         20 my $headers = $kdbx->headers;
32 7         14 my $buf = '';
33              
34             # Always write the standard AES KDF UUID, for compatibility
35             local $headers->{+HEADER_KDF_PARAMETERS}->{+KDF_PARAM_UUID} = KDF_UUID_AES
36 7 50       23 if $headers->{+HEADER_KDF_PARAMETERS}->{+KDF_PARAM_UUID} eq KDF_UUID_AES_CHALLENGE_RESPONSE;
37              
38 7 50       23 if (nonempty (my $comment = $headers->{+HEADER_COMMENT})) {
39 0         0 $buf .= $self->_write_header($fh, HEADER_COMMENT, $comment);
40             }
41 7         21 for my $type (
42             HEADER_CIPHER_ID,
43             HEADER_COMPRESSION_FLAGS,
44             HEADER_MASTER_SEED,
45             HEADER_ENCRYPTION_IV,
46             HEADER_KDF_PARAMETERS,
47             ) {
48 35 50       73 defined $headers->{$type} or throw "Missing value for required header: $type", type => $type;
49 35         71 $buf .= $self->_write_header($fh, $type, $headers->{$type});
50             }
51             $buf .= $self->_write_header($fh, HEADER_PUBLIC_CUSTOM_DATA, $headers->{+HEADER_PUBLIC_CUSTOM_DATA})
52 7 100 66     23 if defined $headers->{+HEADER_PUBLIC_CUSTOM_DATA} && keys %{$headers->{+HEADER_PUBLIC_CUSTOM_DATA}};
  7         29  
53 7         22 $buf .= $self->_write_header($fh, HEADER_END);
54              
55 7         20 return $buf;
56             }
57              
58             sub _write_header {
59 44     44   51 my $self = shift;
60 44         47 my $fh = shift;
61 44         46 my $type = shift;
62 44   100     103 my $val = shift // '';
63              
64 44         86 $type = to_header_constant($type);
65 44 100 0     147 if ($type == HEADER_END) {
    50 0        
    100 0        
    100 0        
    100          
    100          
    100          
    50          
    0          
    0          
66             # nothing
67             }
68             elsif ($type == HEADER_COMMENT) {
69 0         0 $val = encode('UTF-8', $val);
70             }
71             elsif ($type == HEADER_CIPHER_ID) {
72 7         12 my $size = length($val);
73 7 50       17 $size == 16 or throw 'Invalid cipher UUID length', got => $size, expected => $size;
74             }
75             elsif ($type == HEADER_COMPRESSION_FLAGS) {
76 7         18 $val = pack('L<', $val);
77             }
78             elsif ($type == HEADER_MASTER_SEED) {
79 7         11 my $size = length($val);
80 7 50       16 $size == 32 or throw 'Invalid master seed length', got => $size, expected => $size;
81             }
82             elsif ($type == HEADER_ENCRYPTION_IV) {
83             # nothing
84             }
85             elsif ($type == HEADER_KDF_PARAMETERS) {
86 7         61 $val = $self->_write_variant_dictionary($val, {
87             KDF_PARAM_UUID() => VMAP_TYPE_BYTEARRAY,
88             KDF_PARAM_AES_ROUNDS() => VMAP_TYPE_UINT64,
89             KDF_PARAM_AES_SEED() => VMAP_TYPE_BYTEARRAY,
90             KDF_PARAM_ARGON2_SALT() => VMAP_TYPE_BYTEARRAY,
91             KDF_PARAM_ARGON2_PARALLELISM() => VMAP_TYPE_UINT32,
92             KDF_PARAM_ARGON2_MEMORY() => VMAP_TYPE_UINT64,
93             KDF_PARAM_ARGON2_ITERATIONS() => VMAP_TYPE_UINT64,
94             KDF_PARAM_ARGON2_VERSION() => VMAP_TYPE_UINT32,
95             KDF_PARAM_ARGON2_SECRET() => VMAP_TYPE_BYTEARRAY,
96             KDF_PARAM_ARGON2_ASSOCDATA() => VMAP_TYPE_BYTEARRAY,
97             });
98             }
99             elsif ($type == HEADER_PUBLIC_CUSTOM_DATA) {
100 2         7 $val = $self->_write_variant_dictionary($val);
101             }
102             elsif ($type == HEADER_INNER_RANDOM_STREAM_ID ||
103             $type == HEADER_INNER_RANDOM_STREAM_KEY ||
104             $type == HEADER_TRANSFORM_SEED ||
105             $type == HEADER_TRANSFORM_ROUNDS ||
106             $type == HEADER_STREAM_START_BYTES) {
107 0         0 throw "Unexpected KDBX3 header: $type", type => $type;
108             }
109             elsif ($type == HEADER_COMMENT) {
110 0         0 throw "Unexpected KDB header: $type", type => $type;
111             }
112             else {
113 0         0 alert "Unknown header: $type", type => $type;
114             }
115              
116 44         66 my $size = length($val);
117 44         80 my $buf = pack('C L<', 0+$type, $size);
118              
119 44 50       94 $fh->print($buf, $val) or throw 'Failed to write header';
120              
121 44         310 return "$buf$val";
122             }
123              
124             sub _intuit_variant_type {
125 5     5   6 my $self = shift;
126 5         9 my $variant = shift;
127              
128 5 100 66     13 if (isBoolean($variant)) {
    100          
    100          
129 1         22 return VMAP_TYPE_BOOL;
130             }
131             elsif (looks_like_number($variant) && ($variant + 0) =~ /^\d+$/) {
132 1         21 my $neg = $variant < 0;
133 1         5 my @b = unpack('L>2', scalar reverse pack_Ql($variant));
134 1 50 33     5 return VMAP_TYPE_INT64 if $b[0] && $neg;
135 1 50       3 return VMAP_TYPE_UINT64 if $b[0];
136 1 50       2 return VMAP_TYPE_INT32 if $neg;
137 1         5 return VMAP_TYPE_UINT32;
138             }
139             elsif (is_utf8($variant)) {
140 1         17 return VMAP_TYPE_STRING;
141             }
142 2         44 return VMAP_TYPE_BYTEARRAY;
143             }
144              
145             sub _write_variant_dictionary {
146 9     9   16 my $self = shift;
147 9   50     26 my $dict = shift || {};
148 9   100     22 my $types = shift || {};
149              
150 9         13 my $buf = '';
151              
152 9         16 $buf .= pack('S<', VMAP_VERSION);
153              
154 9         47 for my $key (sort keys %$dict) {
155 32         49 my $val = $dict->{$key};
156              
157 32   66     55 my $type = $types->{$key} // $self->_intuit_variant_type($val);
158 32         55 $buf .= pack('C', $type);
159              
160 32 100       93 if ($type == VMAP_TYPE_UINT32) {
    100          
    100          
    50          
    50          
    100          
    50          
161 5         11 $val = pack('L<', $val);
162             }
163             elsif ($type == VMAP_TYPE_UINT64) {
164 9         25 $val = pack_Ql($val);
165             }
166             elsif ($type == VMAP_TYPE_BOOL) {
167 1         3 $val = pack('C', $val);
168             }
169             elsif ($type == VMAP_TYPE_INT32) {
170 0         0 $val = pack('l', $val);
171             }
172             elsif ($type == VMAP_TYPE_INT64) {
173 0         0 $val = pack_ql($val);
174             }
175             elsif ($type == VMAP_TYPE_STRING) {
176 1         3 $val = encode('UTF-8', $val);
177             }
178             elsif ($type == VMAP_TYPE_BYTEARRAY) {
179             # $val = substr($$buf, $pos, $vlen);
180             # $val = [split //, $val];
181             }
182             else {
183 0         0 throw 'Unknown variant dictionary value type', type => $type;
184             }
185              
186 32         99 my ($klen, $vlen) = (length($key), length($val));
187 32         110 $buf .= pack("L< a$klen L< a$vlen", $klen, $key, $vlen, $val);
188             }
189              
190 9         22 $buf .= pack('C', VMAP_TYPE_END);
191              
192 9         17 return $buf;
193             }
194              
195             sub _write_body {
196 7     7   12 my $self = shift;
197 7         10 my $fh = shift;
198 7         11 my $key = shift;
199 7         12 my $header_data = shift;
200 7         16 my $kdbx = $self->kdbx;
201              
202             # assert all required headers present
203 7         23 for my $field (
204             HEADER_CIPHER_ID,
205             HEADER_ENCRYPTION_IV,
206             HEADER_MASTER_SEED,
207             ) {
208 21 50       40 defined $kdbx->headers->{$field} or throw "Missing header: $field";
209             }
210              
211 7         15 my @cleanup;
212              
213             # write 32-byte checksum
214 7         61 my $header_hash = digest_data('SHA256', $header_data);
215 7 50       21 $fh->print($header_hash) or throw 'Failed to write header hash';
216              
217 7         56 $key = $kdbx->composite_key($key);
218 7         23 my $transformed_key = $kdbx->kdf->transform($key);
219 7         31 push @cleanup, erase_scoped $transformed_key;
220              
221             # write 32-byte HMAC for header
222 7         100 my $hmac_key = digest_data('SHA512', $kdbx->headers->{master_seed}, $transformed_key, "\x01");
223 7         22 push @cleanup, erase_scoped $hmac_key;
224 7         167 my $header_hmac = hmac('SHA256',
225             digest_data('SHA512', "\xff\xff\xff\xff\xff\xff\xff\xff", $hmac_key),
226             $header_data,
227             );
228 7 50       32 $fh->print($header_hmac) or throw 'Failed to write header HMAC';
229              
230 7         76 $kdbx->key($key);
231              
232             # HMAC-block the rest of the stream
233 7         52 $fh = File::KDBX::IO::HmacBlock->new($fh, key => $hmac_key);
234              
235 7         21 my $final_key = digest_data('SHA256', $kdbx->headers->{master_seed}, $transformed_key);
236 7         23 push @cleanup, erase_scoped $final_key;
237              
238 7         80 my $cipher = $kdbx->cipher(key => $final_key);
239 7         57 $fh = File::KDBX::IO::Crypt->new($fh, cipher => $cipher);
240              
241 7         28 my $got_iv_size = length($kdbx->headers->{+HEADER_ENCRYPTION_IV});
242 7         18 my $iv_size = $cipher->iv_size;
243 7 50       21 alert "Encryption IV should be $iv_size bytes long",
244             got => $got_iv_size,
245             expected => $iv_size if $got_iv_size != $iv_size;
246              
247 7         15 my $compress = $kdbx->headers->{+HEADER_COMPRESSION_FLAGS};
248 7 50       24 if ($compress == COMPRESSION_GZIP) {
    0          
249 7         28 load_optional('IO::Compress::Gzip');
250 7 50       36 $fh = IO::Compress::Gzip->new($fh,
251             -Level => IO::Compress::Gzip::Z_BEST_COMPRESSION(),
252             -TextFlag => 1,
253             ) or throw "Failed to initialize compression library: $IO::Compress::Gzip::GzipError",
254             error => $IO::Compress::Gzip::GzipError;
255             }
256             elsif ($compress != COMPRESSION_NONE) {
257 0         0 throw "Unsupported compression ($compress)\n", compression_flags => $compress;
258             }
259              
260 7         202 $self->_write_inner_headers($fh);
261              
262 7         143 local $self->{compress_datetimes} = 1;
263 7         31 $self->_write_inner_body($fh, $header_hash);
264             }
265              
266             sub _write_inner_headers {
267 7     7   13 my $self = shift;
268 7         11 my $fh = shift;
269              
270 7         20 my $kdbx = $self->kdbx;
271 7         24 my $headers = $kdbx->inner_headers;
272              
273 7         23 for my $type (
274             INNER_HEADER_INNER_RANDOM_STREAM_ID,
275             INNER_HEADER_INNER_RANDOM_STREAM_KEY,
276             ) {
277 14 50       410 defined $headers->{$type} or throw "Missing inner header: $type";
278 14         37 $self->_write_inner_header($fh, $type => $headers->{$type});
279             }
280              
281 7         439 $self->_write_binaries($fh);
282              
283 7         21 $self->_write_inner_header($fh, INNER_HEADER_END);
284             }
285              
286             sub _write_inner_header {
287 21     21   27 my $self = shift;
288 21         23 my $fh = shift;
289 21         31 my $type = shift;
290 21   100     57 my $val = shift // '';
291              
292 21         44 my $buf = pack('C', $type);
293 21 50       63 $fh->print($buf) or throw 'Failed to write inner header type';
294              
295 21         1379 $type = to_inner_header_constant($type);
296 21 100       63 if ($type == INNER_HEADER_END) {
    100          
    50          
    0          
297             # nothing
298             }
299             elsif ($type == INNER_HEADER_INNER_RANDOM_STREAM_ID) {
300 7         22 $val = pack('L<', $val);
301             }
302             elsif ($type == INNER_HEADER_INNER_RANDOM_STREAM_KEY) {
303             # nothing
304             }
305             elsif ($type == INNER_HEADER_BINARY) {
306             # nothing
307             }
308              
309 21         42 $buf = pack('L<', length($val));
310 21 50       44 $fh->print($buf) or throw 'Failed to write inner header value size';
311 21 50       1085 $fh->print($val) or throw 'Failed to write inner header value';
312             }
313              
314             sub _write_binaries {
315 7     7   19 my $self = shift;
316 7         15 my $fh = shift;
317              
318 7         16 my $kdbx = $self->kdbx;
319              
320 7         17 my $new_ref = 0;
321 7         21 my $written = $self->_binaries_written;
322              
323 7         28 my $entries = $kdbx->entries(history => 1);
324 7         54 while (my $entry = $entries->next) {
325 2         4 for my $key (keys %{$entry->binaries}) {
  2         5  
326 0           my $binary = $entry->binaries->{$key};
327 0 0 0       if (defined $binary->{ref} && defined $kdbx->binaries->{$binary->{ref}}) {
328 0           $binary = $kdbx->binaries->{$binary->{ref}};
329             }
330              
331 0 0         if (!defined $binary->{value}) {
332 0           alert "Skipping binary which has no value: $key", key => $key;
333 0           next;
334             }
335              
336 0           my $hash = digest_data('SHA256', $binary->{value});
337 0 0         if (defined $written->{$hash}) {
338             # nothing
339             }
340             else {
341 0           my $flags = 0;
342 0 0         $flags &= INNER_HEADER_BINARY_FLAG_PROTECT if $binary->{protect};
343              
344 0           $self->_write_binary($fh, \$binary->{value}, $flags);
345 0           $written->{$hash} = $new_ref++;
346             }
347             }
348             }
349             }
350              
351             sub _write_binary {
352 0     0     my $self = shift;
353 0           my $fh = shift;
354 0           my $data = shift;
355 0   0       my $flags = shift || 0;
356              
357 0           my $buf = pack('C', 0 + INNER_HEADER_BINARY);
358 0 0         $fh->print($buf) or throw 'Failed to write inner header type';
359              
360 0           $buf = pack('L<', 1 + length($$data));
361 0 0         $fh->print($buf) or throw 'Failed to write inner header value size';
362              
363 0           $buf = pack('C', $flags);
364 0 0         $fh->print($buf) or throw 'Failed to write inner header binary flags';
365              
366 0 0         $fh->print($$data) or throw 'Failed to write inner header value';
367             }
368              
369             1;
370              
371             __END__