]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Loader/V3.pm
77ad479635abea9c3ea2ea56764f6161619bfb56
[chaz/p5-File-KDBX] / lib / File / KDBX / Loader / V3.pm
1 package File::KDBX::Loader::V3;
2 # ABSTRACT: Load KDBX3 files
3
4 # magic
5 # headers
6 # body
7 # CRYPT(
8 # start bytes
9 # HASH(
10 # COMPRESS(
11 # xml
12 # )
13 # )
14 # )
15
16 use warnings;
17 use strict;
18
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(:io assert_64bit erase_scoped);
26 use namespace::clean;
27
28 use parent 'File::KDBX::Loader';
29
30 our $VERSION = '999.999'; # VERSION
31
32 sub _read_header {
33 my $self = shift;
34 my $fh = shift;
35
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);
38
39 my $val;
40 if (0 < $size) {
41 read_all $fh, $val, $size or throw 'Expected header value', type => $type, size => $size;
42 $buf .= $val;
43 }
44
45 $type = KDBX_HEADER($type);
46 if ($type == HEADER_END) {
47 # done
48 }
49 elsif ($type == HEADER_COMMENT) {
50 $val = decode('UTF-8', $val);
51 }
52 elsif ($type == HEADER_CIPHER_ID) {
53 $size == 16 or throw 'Invalid cipher UUID length', got => $size, expected => $size;
54 }
55 elsif ($type == HEADER_COMPRESSION_FLAGS) {
56 $val = unpack('L<', $val);
57 }
58 elsif ($type == HEADER_MASTER_SEED) {
59 $size == 32 or throw 'Invalid master seed length', got => $size, expected => $size;
60 }
61 elsif ($type == HEADER_TRANSFORM_SEED) {
62 # nothing
63 }
64 elsif ($type == HEADER_TRANSFORM_ROUNDS) {
65 assert_64bit;
66 $val = unpack('Q<', $val);
67 }
68 elsif ($type == HEADER_ENCRYPTION_IV) {
69 # nothing
70 }
71 elsif ($type == HEADER_INNER_RANDOM_STREAM_KEY) {
72 # nothing
73 }
74 elsif ($type == HEADER_STREAM_START_BYTES) {
75 # nothing
76 }
77 elsif ($type == HEADER_INNER_RANDOM_STREAM_ID) {
78 $val = unpack('L<', $val);
79 }
80 elsif ($type == HEADER_KDF_PARAMETERS ||
81 $type == HEADER_PUBLIC_CUSTOM_DATA) {
82 throw "Unexpected KDBX4 header: $type", type => $type;
83 }
84 else {
85 alert "Unknown header: $type", type => $type;
86 }
87
88 return wantarray ? ($type => $val, $buf) : $buf;
89 }
90
91 sub _read_body {
92 my $self = shift;
93 my $fh = shift;
94 my $key = shift;
95 my $header_data = shift;
96 my $kdbx = $self->kdbx;
97
98 # assert all required headers present
99 for my $field (
100 HEADER_CIPHER_ID,
101 HEADER_ENCRYPTION_IV,
102 HEADER_MASTER_SEED,
103 HEADER_INNER_RANDOM_STREAM_KEY,
104 HEADER_STREAM_START_BYTES,
105 ) {
106 defined $kdbx->headers->{$field} or throw "Missing $field";
107 }
108
109 $kdbx->kdf_parameters({
110 KDF_PARAM_UUID() => KDF_UUID_AES,
111 KDF_PARAM_AES_ROUNDS() => delete $kdbx->headers->{+HEADER_TRANSFORM_ROUNDS},
112 KDF_PARAM_AES_SEED() => delete $kdbx->headers->{+HEADER_TRANSFORM_SEED},
113 });
114
115 my $master_seed = $kdbx->headers->{+HEADER_MASTER_SEED};
116
117 my @cleanup;
118 $key = $kdbx->composite_key($key);
119
120 my $response = $key->challenge($master_seed);
121 push @cleanup, erase_scoped $response;
122
123 my $transformed_key = $kdbx->kdf->transform($key);
124 push @cleanup, erase_scoped $transformed_key;
125
126 my $final_key = digest_data('SHA256', $master_seed, $response, $transformed_key);
127 push @cleanup, erase_scoped $final_key;
128
129 my $cipher = $kdbx->cipher(key => $final_key);
130 $fh = File::KDBX::IO::Crypt->new($fh, cipher => $cipher);
131
132 read_all $fh, my $start_bytes, 32 or throw 'Failed to read starting bytes';
133
134 my $expected_start_bytes = $kdbx->headers->{stream_start_bytes};
135 $start_bytes eq $expected_start_bytes
136 or throw "Invalid credentials or data is corrupt (wrong starting bytes)\n",
137 got => $start_bytes, expected => $expected_start_bytes, headers => $kdbx->headers;
138
139 $kdbx->key($key);
140
141 $fh = File::KDBX::IO::HashBlock->new($fh);
142
143 my $compress = $kdbx->headers->{+HEADER_COMPRESSION_FLAGS};
144 if ($compress == COMPRESSION_GZIP) {
145 require IO::Uncompress::Gunzip;
146 $fh = IO::Uncompress::Gunzip->new($fh)
147 or throw "Failed to initialize compression library: $IO::Uncompress::Gunzip::GunzipError",
148 error => $IO::Uncompress::Gunzip::GunzipError;
149 }
150 elsif ($compress != COMPRESSION_NONE) {
151 throw "Unsupported compression ($compress)\n", compression_flags => $compress;
152 }
153
154 $self->_read_inner_body($fh);
155 close($fh);
156
157 if (my $header_hash = $kdbx->meta->{header_hash}) {
158 my $got_header_hash = digest_data('SHA256', $header_data);
159 $header_hash eq $got_header_hash
160 or throw 'Header hash does not match', got => $got_header_hash, expected => $header_hash;
161 }
162 }
163
164 1;
This page took 0.040508 seconds and 3 git commands to generate.