]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Loader.pm
209a2a6976aaa4da24e30108d08ddbdaecb7561c
[chaz/p5-File-KDBX] / lib / File / KDBX / Loader.pm
1 package File::KDBX::Loader;
2 # ABSTRACT: Load KDBX files
3
4 use warnings;
5 use strict;
6
7 use File::KDBX::Constants qw(:magic :header :version);
8 use File::KDBX::Error;
9 use File::KDBX::Util qw(:class :io);
10 use File::KDBX;
11 use IO::Handle;
12 use Module::Load ();
13 use Ref::Util qw(is_ref is_scalarref);
14 use Scalar::Util qw(looks_like_number openhandle);
15 use namespace::clean;
16
17 our $VERSION = '0.800'; # VERSION
18
19
20 sub new {
21 my $class = shift;
22 my $self = bless {}, $class;
23 $self->init(@_);
24 }
25
26
27 sub init {
28 my $self = shift;
29 my %args = @_;
30
31 @$self{keys %args} = values %args;
32
33 return $self;
34 }
35
36 sub _rebless {
37 my $self = shift;
38 my $format = shift // $self->format;
39
40 my $sig2 = $self->kdbx->sig2;
41 my $version = $self->kdbx->version;
42
43 my $subclass;
44
45 if (defined $format) {
46 $subclass = $format;
47 }
48 elsif (defined $sig2 && $sig2 == KDBX_SIG2_1) {
49 $subclass = 'KDB';
50 }
51 elsif (looks_like_number($version)) {
52 my $major = $version & KDBX_VERSION_MAJOR_MASK;
53 my %subclasses = (
54 KDBX_VERSION_2_0() => 'V3',
55 KDBX_VERSION_3_0() => 'V3',
56 KDBX_VERSION_4_0() => 'V4',
57 );
58 $subclass = $subclasses{$major}
59 or throw sprintf('Unsupported KDBX file version: %x', $version), version => $version;
60 }
61 else {
62 throw sprintf('Unknown file version: %s', $version), version => $version;
63 }
64
65 Module::Load::load "File::KDBX::Loader::$subclass";
66 bless $self, "File::KDBX::Loader::$subclass";
67 }
68
69
70 sub reset {
71 my $self = shift;
72 %$self = ();
73 return $self;
74 }
75
76
77 sub load {
78 my $self = shift;
79 my $src = shift;
80 return $self->load_handle($src, @_) if openhandle($src) || $src eq '-';
81 return $self->load_string($src, @_) if is_scalarref($src);
82 return $self->load_file($src, @_) if !is_ref($src) && defined $src;
83 throw 'Programmer error: Must pass a stringref, filepath or IO handle to read';
84 }
85
86
87 sub load_string {
88 my $self = shift;
89 my $str = shift or throw 'Expected string to load';
90 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
91
92 my $key = delete $args{key};
93 $args{kdbx} //= $self->kdbx;
94
95 my $ref = is_scalarref($str) ? $str : \$str;
96
97 open(my $fh, '<', $ref) or throw "Failed to open string buffer: $!";
98
99 $self = $self->new if !ref $self;
100 $self->init(%args, fh => $fh)->_read($fh, $key);
101 return $args{kdbx};
102 }
103
104
105 sub load_file {
106 my $self = shift;
107 my $filepath = shift;
108 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
109
110 my $key = delete $args{key};
111 $args{kdbx} //= $self->kdbx;
112
113 open(my $fh, '<:raw', $filepath) or throw 'Open file failed', filepath => $filepath;
114
115 $self = $self->new if !ref $self;
116 $self->init(%args, fh => $fh, filepath => $filepath)->_read($fh, $key);
117 return $args{kdbx};
118 }
119
120
121 sub load_handle {
122 my $self = shift;
123 my $fh = shift;
124 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
125
126 $fh = *STDIN if $fh eq '-';
127
128 my $key = delete $args{key};
129 $args{kdbx} //= $self->kdbx;
130
131 $self = $self->new if !ref $self;
132 $self->init(%args, fh => $fh)->_read($fh, $key);
133 return $args{kdbx};
134 }
135
136
137 sub kdbx {
138 my $self = shift;
139 return File::KDBX->new if !ref $self;
140 $self->{kdbx} = shift if @_;
141 $self->{kdbx} //= File::KDBX->new;
142 }
143
144
145 has format => undef, is => 'ro';
146 has inner_format => 'XML', is => 'ro';
147
148
149 sub min_version { KDBX_VERSION_OLDEST }
150
151
152 sub read_magic_numbers {
153 my $self = shift;
154 my $fh = shift;
155 my $kdbx = shift // $self->kdbx;
156
157 read_all $fh, my $magic, 12 or throw 'Failed to read file signature';
158
159 my ($sig1, $sig2, $version) = unpack('L<3', $magic);
160
161 if ($kdbx) {
162 $kdbx->sig1($sig1);
163 $kdbx->sig2($sig2);
164 $kdbx->version($version);
165 $self->_rebless if ref $self;
166 }
167
168 return wantarray ? ($sig1, $sig2, $version, $magic) : $magic;
169 }
170
171 sub _fh { $_[0]->{fh} or throw 'IO handle not set' }
172
173 sub _read {
174 my $self = shift;
175 my $fh = shift;
176 my $key = shift;
177
178 my $kdbx = $self->kdbx;
179 $key //= $kdbx->key ? $kdbx->key->reload : undef;
180 $kdbx->reset;
181
182 read_all $fh, my $buf, 1 or throw 'Failed to read the first byte', type => 'parser';
183 my $first = ord($buf);
184 $fh->ungetc($first);
185 if ($first != KDBX_SIG1_FIRST_BYTE) {
186 # not a KDBX file... try skipping the outer layer
187 return $self->_read_inner_body($fh);
188 }
189
190 my $magic = $self->read_magic_numbers($fh, $kdbx);
191 $kdbx->sig1 == KDBX_SIG1 or throw 'Invalid file signature', type => 'parser', sig1 => $kdbx->sig1;
192
193 if (ref($self) =~ /::(?:KDB|V[34])$/) {
194 defined $key or throw 'Must provide a master key', type => 'key.missing';
195 }
196
197 my $headers = $self->_read_headers($fh);
198
199 eval {
200 $self->_read_body($fh, $key, "$magic$headers");
201 };
202 if (my $err = $@) {
203 throw "Failed to load KDBX file: $err",
204 error => $err,
205 compression_error => $IO::Uncompress::Gunzip::GunzipError,
206 crypt_error => $File::KDBX::IO::Crypt::ERROR,
207 hash_error => $File::KDBX::IO::HashBLock::ERROR,
208 hmac_error => $File::KDBX::IO::HmacBLock::ERROR;
209 }
210 }
211
212 sub _read_headers {
213 my $self = shift;
214 my $fh = shift;
215
216 my $headers = $self->kdbx->headers;
217 my $all_raw = '';
218
219 while (my ($type, $val, $raw) = $self->_read_header($fh)) {
220 $all_raw .= $raw;
221 last if $type == HEADER_END;
222 $headers->{$type} = $val;
223 }
224
225 return $all_raw;
226 }
227
228 sub _read_body { die "Not implemented" }
229
230 sub _read_inner_body {
231 my $self = shift;
232
233 my $current_pkg = ref $self;
234 require Scope::Guard;
235 my $guard = Scope::Guard->new(sub { bless $self, $current_pkg });
236
237 $self->_rebless($self->inner_format);
238 $self->_read_inner_body(@_);
239 }
240
241 1;
242
243 __END__
244
245 =pod
246
247 =encoding UTF-8
248
249 =head1 NAME
250
251 File::KDBX::Loader - Load KDBX files
252
253 =head1 VERSION
254
255 version 0.800
256
257 =head1 DESCRIPTION
258
259 =head1 ATTRIBUTES
260
261 =head2 kdbx
262
263 $kdbx = $loader->kdbx;
264 $loader->kdbx($kdbx);
265
266 Get or set the L<File::KDBX> instance for storing the loaded data into.
267
268 =head2 format
269
270 Get the file format used for reading the database. Normally the format is auto-detected from the data stream.
271 This auto-detection works well, so there's not really a good reason to explicitly specify the format.
272 Possible formats:
273
274 =over 4
275
276 =item *
277
278 C<V3>
279
280 =item *
281
282 C<V4>
283
284 =item *
285
286 C<KDB>
287
288 =item *
289
290 C<XML>
291
292 =item *
293
294 C<Raw>
295
296 =back
297
298 =head2 inner_format
299
300 Get the format of the data inside the KDBX envelope. This only applies to C<V3> and C<V4> formats. Possible
301 formats:
302
303 =over 4
304
305 =item *
306
307 C<XML> - Read the database groups and entries as XML (default)
308
309 =item *
310
311 C<Raw> - Read parsing and store the result in L<File::KDBX/raw>
312
313 =back
314
315 =head1 METHODS
316
317 =head2 new
318
319 $loader = File::KDBX::Loader->new(%attributes);
320
321 Construct a new L<File::KDBX::Loader>.
322
323 =head2 init
324
325 $loader = $loader->init(%attributes);
326
327 Initialize a L<File::KDBX::Loader> with a new set of attributes.
328
329 This is called by L</new>.
330
331 =head2 reset
332
333 $loader = $loader->reset;
334
335 Set a L<File::KDBX::Loader> to a blank state, ready to load another KDBX file.
336
337 =head2 load
338
339 $kdbx = File::KDBX::Loader->load(\$string, $key);
340 $kdbx = File::KDBX::Loader->load(*IO, $key);
341 $kdbx = File::KDBX::Loader->load($filepath, $key);
342 $kdbx = $loader->load(...); # also instance method
343
344 Load a KDBX file.
345
346 The C<$key> is either a L<File::KDBX::Key> or a primitive that can be converted to a Key object.
347
348 =head2 load_string
349
350 $kdbx = File::KDBX::Loader->load_string($string, $key);
351 $kdbx = File::KDBX::Loader->load_string(\$string, $key);
352 $kdbx = $loader->load_string(...); # also instance method
353
354 Load a KDBX file from a string / memory buffer.
355
356 =head2 load_file
357
358 $kdbx = File::KDBX::Loader->load_file($filepath, $key);
359 $kdbx = $loader->load_file(...); # also instance method
360
361 Read a KDBX file from a filesystem.
362
363 =head2 load_handle
364
365 $kdbx = File::KDBX::Loader->load_handle($fh, $key);
366 $kdbx = File::KDBX::Loader->load_handle(*IO, $key);
367 $kdbx->load_handle(...); # also instance method
368
369 Read a KDBX file from an input stream / file handle.
370
371 =head2 min_version
372
373 $min_version = File::KDBX::Loader->min_version;
374
375 Get the minimum KDBX file version supported, which is 3.0 or C<0x00030000> as
376 it is encoded.
377
378 To read older KDBX files unsupported by this module, try L<File::KeePass>.
379
380 =head2 read_magic_numbers
381
382 $magic = File::KDBX::Loader->read_magic_numbers($fh);
383 ($sig1, $sig2, $version, $magic) = File::KDBX::Loader->read_magic_numbers($fh);
384
385 $magic = $loader->read_magic_numbers($fh);
386 ($sig1, $sig2, $version, $magic) = $loader->read_magic_numbers($fh);
387
388 Read exactly 12 bytes from an IO handle and parse them into the three magic numbers that begin
389 a KDBX file. This is a quick way to determine if a file is actually a KDBX file.
390
391 C<$sig1> should always be C<KDBX_SIG1> if reading an actual KDB or KDBX file.
392
393 C<$sig2> should be C<KDBX_SIG2_1> for KeePass 1 files and C<KDBX_SIG2_2> for KeePass 2 files.
394
395 C<$version> is the file version (e.g. C<0x00040001>).
396
397 C<$magic> is the raw 12 bytes read from the IO handle.
398
399 If called on an instance, the C<sig1>, C<sig2> and C<version> attributes will be set in the L</kdbx>
400 and the instance will be blessed into the correct loader subclass.
401
402 =head1 BUGS
403
404 Please report any bugs or feature requests on the bugtracker website
405 L<https://github.com/chazmcgarvey/File-KDBX/issues>
406
407 When submitting a bug or request, please include a test-file or a
408 patch to an existing test-file that illustrates the bug or desired
409 feature.
410
411 =head1 AUTHOR
412
413 Charles McGarvey <ccm@cpan.org>
414
415 =head1 COPYRIGHT AND LICENSE
416
417 This software is copyright (c) 2022 by Charles McGarvey.
418
419 This is free software; you can redistribute it and/or modify it under
420 the same terms as the Perl 5 programming language system itself.
421
422 =cut
This page took 0.055223 seconds and 3 git commands to generate.