]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Loader/V4.pm
fa8d21d867e220a321efe81c06bff1b30d1b4278
[chaz/p5-File-KDBX] / lib / File / KDBX / Loader / V4.pm
1 package File::KDBX::Loader::V4;
2 # ABSTRACT: Load KDBX4 files
3
4 # magic
5 # headers
6 # headers checksum
7 # headers hmac
8 # body
9 # HMAC(
10 # CRYPT(
11 # COMPRESS(
12 # xml
13 # )
14 # )
15 # )
16
17 use warnings;
18 use strict;
19
20 use Crypt::Digest qw(digest_data);
21 use Crypt::Mac::HMAC qw(hmac);
22 use Encode qw(decode);
23 use File::KDBX::Constants qw(:header :inner_header :variant_map :compression);
24 use File::KDBX::Error;
25 use File::KDBX::Util qw(:io assert_64bit erase_scoped);
26 use File::KDBX::IO::Crypt;
27 use File::KDBX::IO::HmacBlock;
28 use boolean;
29 use namespace::clean;
30
31 use parent 'File::KDBX::Loader';
32
33 our $VERSION = '999.999'; # VERSION
34
35 sub _read_header {
36 my $self = shift;
37 my $fh = shift;
38
39 read_all $fh, my $buf, 5 or throw 'Malformed header field, expected header type and size';
40 my ($type, $size) = unpack('C L<', $buf);
41
42 my $val;
43 if (0 < $size) {
44 read_all $fh, $val, $size or throw 'Expected header value', type => $type, size => $size;
45 $buf .= $val;
46 }
47
48 $type = KDBX_HEADER($type);
49 if ($type == HEADER_END) {
50 # done
51 }
52 elsif ($type == HEADER_COMMENT) {
53 $val = decode('UTF-8', $val);
54 }
55 elsif ($type == HEADER_CIPHER_ID) {
56 $size == 16 or throw 'Invalid cipher UUID length', got => $size, expected => $size;
57 }
58 elsif ($type == HEADER_COMPRESSION_FLAGS) {
59 $val = unpack('L<', $val);
60 }
61 elsif ($type == HEADER_MASTER_SEED) {
62 $size == 32 or throw 'Invalid master seed length', got => $size, expected => $size;
63 }
64 elsif ($type == HEADER_ENCRYPTION_IV) {
65 # nothing
66 }
67 elsif ($type == HEADER_KDF_PARAMETERS) {
68 open(my $dict_fh, '<', \$val);
69 $val = $self->_read_variant_dictionary($dict_fh);
70 }
71 elsif ($type == HEADER_PUBLIC_CUSTOM_DATA) {
72 open(my $dict_fh, '<', \$val);
73 $val = $self->_read_variant_dictionary($dict_fh);
74 }
75 elsif ($type == HEADER_INNER_RANDOM_STREAM_ID ||
76 $type == HEADER_INNER_RANDOM_STREAM_KEY ||
77 $type == HEADER_TRANSFORM_SEED ||
78 $type == HEADER_TRANSFORM_ROUNDS ||
79 $type == HEADER_STREAM_START_BYTES) {
80 throw "Unexpected KDBX3 header: $type", type => $type;
81 }
82 else {
83 alert "Unknown header: $type", type => $type;
84 }
85
86 return wantarray ? ($type => $val, $buf) : $buf;
87 }
88
89 sub _read_variant_dictionary {
90 my $self = shift;
91 my $fh = shift;
92
93 read_all $fh, my $buf, 2 or throw 'Failed to read variant dictionary version';
94 my ($version) = unpack('S<', $buf);
95 VMAP_VERSION == ($version & VMAP_VERSION_MAJOR_MASK)
96 or throw 'Unsupported variant dictionary version', version => $version;
97
98 my %dict;
99
100 while (1) {
101 read_all $fh, $buf, 1 or throw 'Failed to read variant type';
102 my ($type) = unpack('C', $buf);
103 last if $type == VMAP_TYPE_END; # terminating null
104
105 read_all $fh, $buf, 4 or throw 'Failed to read variant key size';
106 my ($klen) = unpack('L<', $buf);
107
108 read_all $fh, my $key, $klen or throw 'Failed to read variant key';
109
110 read_all $fh, $buf, 4 or throw 'Failed to read variant size';
111 my ($vlen) = unpack('L<', $buf);
112
113 read_all $fh, my $val, $vlen or throw 'Failed to read variant';
114
115 if ($type == VMAP_TYPE_UINT32) {
116 ($val) = unpack('L<', $val);
117 }
118 elsif ($type == VMAP_TYPE_UINT64) {
119 assert_64bit;
120 ($val) = unpack('Q<', $val);
121 }
122 elsif ($type == VMAP_TYPE_BOOL) {
123 ($val) = unpack('C', $val);
124 $val = boolean($val);
125 }
126 elsif ($type == VMAP_TYPE_INT32) {
127 ($val) = unpack('l<', $val);
128 }
129 elsif ($type == VMAP_TYPE_INT64) {
130 assert_64bit;
131 ($val) = unpack('q<', $val);
132 }
133 elsif ($type == VMAP_TYPE_STRING) {
134 $val = decode('UTF-8', $val);
135 }
136 elsif ($type == VMAP_TYPE_BYTEARRAY) {
137 # nothing
138 }
139 else {
140 throw 'Unknown variant type', type => $type;
141 }
142 $dict{$key} = $val;
143 }
144
145 return \%dict;
146 }
147
148 sub _read_body {
149 my $self = shift;
150 my $fh = shift;
151 my $key = shift;
152 my $header_data = shift;
153 my $kdbx = $self->kdbx;
154
155 # assert all required headers present
156 for my $field (
157 HEADER_CIPHER_ID,
158 HEADER_ENCRYPTION_IV,
159 HEADER_MASTER_SEED,
160 ) {
161 defined $kdbx->headers->{$field} or throw "Missing $field";
162 }
163
164 my @cleanup;
165
166 # checksum check
167 read_all $fh, my $header_hash, 32 or throw 'Failed to read header hash';
168 my $got_header_hash = digest_data('SHA256', $header_data);
169 $got_header_hash eq $header_hash
170 or throw 'Data is corrupt (header checksum mismatch)',
171 got => $got_header_hash, expected => $header_hash;
172
173 $key = $kdbx->composite_key($key);
174 my $transformed_key = $kdbx->kdf->transform($key);
175 push @cleanup, erase_scoped $transformed_key;
176
177 # authentication check
178 read_all $fh, my $header_hmac, 32 or throw 'Failed to read header HMAC';
179 my $hmac_key = digest_data('SHA512', $kdbx->headers->{master_seed}, $transformed_key, "\x01");
180 push @cleanup, erase_scoped $hmac_key;
181 my $got_header_hmac = hmac('SHA256',
182 digest_data('SHA512', "\xff\xff\xff\xff\xff\xff\xff\xff", $hmac_key),
183 $header_data,
184 );
185 $got_header_hmac eq $header_hmac
186 or throw "Invalid credentials or data is corrupt (header HMAC mismatch)\n",
187 got => $got_header_hmac, expected => $header_hmac;
188
189 $kdbx->key($key);
190
191 $fh = File::KDBX::IO::HmacBlock->new($fh, key => $hmac_key);
192
193 my $final_key = digest_data('SHA256', $kdbx->headers->{master_seed}, $transformed_key);
194 push @cleanup, erase_scoped $final_key;
195
196 my $cipher = $kdbx->cipher(key => $final_key);
197 $fh = File::KDBX::IO::Crypt->new($fh, cipher => $cipher);
198
199 my $compress = $kdbx->headers->{+HEADER_COMPRESSION_FLAGS};
200 if ($compress == COMPRESSION_GZIP) {
201 require IO::Uncompress::Gunzip;
202 $fh = IO::Uncompress::Gunzip->new($fh)
203 or throw "Failed to initialize compression library: $IO::Uncompress::Gunzip::GunzipError",
204 error => $IO::Uncompress::Gunzip::GunzipError;
205 }
206 elsif ($compress != COMPRESSION_NONE) {
207 throw "Unsupported compression ($compress)\n", compression_flags => $compress;
208 }
209
210 $self->_read_inner_headers($fh);
211 $self->_read_inner_body($fh);
212 }
213
214 sub _read_inner_headers {
215 my $self = shift;
216 my $fh = shift;
217
218 while (my ($type, $val) = $self->_read_inner_header($fh)) {
219 last if $type == INNER_HEADER_END;
220 }
221 }
222
223 sub _read_inner_header {
224 my $self = shift;
225 my $fh = shift;
226 my $kdbx = $self->kdbx;
227
228 read_all $fh, my $buf, 5 or throw 'Expected inner header type and size',
229 compression_error => $IO::Uncompress::Gunzip::GunzipError,
230 crypt_error => $File::KDBX::IO::Crypt::ERROR,
231 hmac_error => $File::KDBX::IO::HmacBLock::ERROR;
232 my ($type, $size) = unpack('C L<', $buf);
233
234 my $val;
235 if (0 < $size) {
236 read_all $fh, $val, $size or throw 'Expected inner header value', type => $type, size => $size;
237 }
238
239 my $dualtype = KDBX_INNER_HEADER($type);
240
241 if (!defined $dualtype) {
242 alert "Ignoring unknown inner header type ($type)", type => $type, size => $size, value => $val;
243 return wantarray ? ($type => $val) : $type;
244 }
245 elsif ($dualtype == INNER_HEADER_END) {
246 # nothing
247 }
248 elsif ($dualtype == INNER_HEADER_INNER_RANDOM_STREAM_ID) {
249 $val = unpack('L<', $val);
250 $kdbx->inner_headers->{$dualtype} = $val;
251 }
252 elsif ($dualtype == INNER_HEADER_INNER_RANDOM_STREAM_KEY) {
253 $kdbx->inner_headers->{$dualtype} = $val;
254 }
255 elsif ($dualtype == INNER_HEADER_BINARY) {
256 my $msize = $size - 1;
257 my ($flags, $data) = unpack("C a$msize", $val);
258 my $id = scalar keys %{$kdbx->binaries};
259 $kdbx->binaries->{$id} = {
260 value => $data,
261 $flags & INNER_HEADER_BINARY_FLAG_PROTECT ? (protect => true) : (),
262 };
263 }
264
265 return wantarray ? ($dualtype => $val) : $dualtype;
266 }
267
268 1;
This page took 0.046736 seconds and 3 git commands to generate.