File Coverage

blib/lib/File/KDBX/Loader.pm
Criterion Covered Total %
statement 130 148 87.8
branch 34 70 48.5
condition 12 35 34.2
subroutine 25 29 86.2
pod 11 11 100.0
total 212 293 72.3


line stmt bran cond sub pod time code
1             package File::KDBX::Loader;
2             # ABSTRACT: Load KDBX files
3              
4 5     5   28 use warnings;
  5         8  
  5         156  
5 5     5   23 use strict;
  5         7  
  5         115  
6              
7 5     5   21 use File::KDBX::Constants qw(:magic :header :version);
  5         9  
  5         996  
8 5     5   33 use File::KDBX::Error;
  5         9  
  5         243  
9 5     5   25 use File::KDBX::Util qw(:class :io);
  5         8  
  5         491  
10 5     5   32 use File::KDBX;
  5         6  
  5         137  
11 5     5   1343 use IO::Handle;
  5         15541  
  5         179  
12 5     5   28 use Module::Load ();
  5         9  
  5         91  
13 5     5   21 use Ref::Util qw(is_ref is_scalarref);
  5         8  
  5         199  
14 5     5   26 use Scalar::Util qw(looks_like_number openhandle);
  5         11  
  5         191  
15 5     5   29 use namespace::clean;
  5         10  
  5         26  
16              
17             our $VERSION = '0.905'; # VERSION
18              
19              
20             sub new {
21 30     30 1 57 my $class = shift;
22 30         66 my $self = bless {}, $class;
23 30         78 $self->init(@_);
24             }
25              
26              
27             sub init {
28 60     60 1 84 my $self = shift;
29 60         151 my %args = @_;
30              
31 60         184 @$self{keys %args} = values %args;
32              
33 60         208 return $self;
34             }
35              
36             sub _rebless {
37 51     51   83 my $self = shift;
38 51   66     144 my $format = shift // $self->format;
39              
40 51         111 my $sig2 = $self->kdbx->sig2;
41 51         118 my $version = $self->kdbx->version;
42              
43 51         70 my $subclass;
44              
45 51 100 33     236 if (defined $format) {
    50          
    50          
46 21         35 $subclass = $format;
47             }
48             elsif (defined $sig2 && $sig2 == KDBX_SIG2_1) {
49 0         0 $subclass = 'KDB';
50             }
51             elsif (looks_like_number($version)) {
52 30         60 my $major = $version & KDBX_VERSION_MAJOR_MASK;
53 30         111 my %subclasses = (
54             KDBX_VERSION_2_0() => 'V3',
55             KDBX_VERSION_3_0() => 'V3',
56             KDBX_VERSION_4_0() => 'V4',
57             );
58 30 50       107 $subclass = $subclasses{$major}
59             or throw sprintf('Unsupported KDBX file version: %x', $version), version => $version;
60             }
61             else {
62 0         0 throw sprintf('Unknown file version: %s', $version), version => $version;
63             }
64              
65 51         219 Module::Load::load "File::KDBX::Loader::$subclass";
66 51         2501 bless $self, "File::KDBX::Loader::$subclass";
67             }
68              
69              
70             sub reset {
71 0     0 1 0 my $self = shift;
72 0         0 %$self = ();
73 0         0 return $self;
74             }
75              
76              
77             sub load {
78 9     9 1 16 my $self = shift;
79 9         18 my $src = shift;
80 9 50 33     67 return $self->load_handle($src, @_) if openhandle($src) || $src eq '-';
81 9 50       26 return $self->load_string($src, @_) if is_scalarref($src);
82 9 50 33     59 return $self->load_file($src, @_) if !is_ref($src) && defined $src;
83 0         0 throw 'Programmer error: Must pass a stringref, filepath or IO handle to read';
84             }
85              
86              
87             sub load_string {
88 21     21 1 33 my $self = shift;
89 21 50       73 my $str = shift or throw 'Expected string to load';
90 21 50       72 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
91              
92 21         35 my $key = delete $args{key};
93 21   33     77 $args{kdbx} //= $self->kdbx;
94              
95 21 100       53 my $ref = is_scalarref($str) ? $str : \$str;
96              
97 21 50       258 open(my $fh, '<', $ref) or throw "Failed to open string buffer: $!";
98              
99 21 50       64 $self = $self->new if !ref $self;
100 21         48 $self->init(%args, fh => $fh)->_read($fh, $key);
101 12         57 return $args{kdbx};
102             }
103              
104              
105             sub load_file {
106 9     9 1 13 my $self = shift;
107 9         14 my $filepath = shift;
108 9 50       62 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
109              
110 9         23 my $key = delete $args{key};
111 9   33     41 $args{kdbx} //= $self->kdbx;
112              
113 9 50       579 open(my $fh, '<:raw', $filepath) or throw 'Open file failed', filepath => $filepath;
114              
115 9 50       46 $self = $self->new if !ref $self;
116 9         38 $self->init(%args, fh => $fh, filepath => $filepath)->_read($fh, $key);
117 8         61 return $args{kdbx};
118             }
119              
120              
121             sub load_handle {
122 0     0 1 0 my $self = shift;
123 0         0 my $fh = shift;
124 0 0       0 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
125              
126 0 0       0 $fh = *STDIN if $fh eq '-';
127              
128 0         0 my $key = delete $args{key};
129 0   0     0 $args{kdbx} //= $self->kdbx;
130              
131 0 0       0 $self = $self->new if !ref $self;
132 0         0 $self->init(%args, fh => $fh)->_read($fh, $key);
133 0         0 return $args{kdbx};
134             }
135              
136              
137             sub kdbx {
138 346     346 1 440 my $self = shift;
139 346 50       641 return File::KDBX->new if !ref $self;
140 346 50       557 $self->{kdbx} = shift if @_;
141 346   33     1113 $self->{kdbx} //= File::KDBX->new;
142 30 50   30 1 96 }
143 21 50   21 1 92  
144 30   50     127  
145 21   33     314 has format => undef, is => 'ro';
146             has inner_format => 'XML', is => 'ro';
147              
148              
149             sub read_magic_numbers {
150 30     30 1 47 my $self = shift;
151 30         45 my $fh = shift;
152 30   33     71 my $kdbx = shift // $self->kdbx;
153              
154 30 50       64 read_all $fh, my $magic, 12 or throw 'Failed to read file signature';
155              
156 30         111 my ($sig1, $sig2, $version) = unpack('L<3', $magic);
157              
158 30 50       72 if ($kdbx) {
159 30         92 $kdbx->sig1($sig1);
160 30         84 $kdbx->sig2($sig2);
161 30         77 $kdbx->version($version);
162 30 50       116 $self->_rebless if ref $self;
163             }
164              
165 30 50       96 return wantarray ? ($sig1, $sig2, $version, $magic) : $magic;
166             }
167              
168 0 0   0   0 sub _fh { $_[0]->{fh} or throw 'IO handle not set' }
169              
170             sub _read {
171 30     30   45 my $self = shift;
172 30         42 my $fh = shift;
173 30         36 my $key = shift;
174              
175 30         87 my $kdbx = $self->kdbx;
176 30 0 33     70 $key //= $kdbx->key ? $kdbx->key->reload : undef;
177 30         103 $kdbx->reset;
178              
179 30 50       85 read_all $fh, my $buf, 1 or throw 'Failed to read the first byte', type => 'parser';
180 30         59 my $first = ord($buf);
181 30         253 $fh->ungetc($first);
182 30 50       69 if ($first != KDBX_SIG1_FIRST_BYTE) {
183             # not a KDBX file... try skipping the outer layer
184 0         0 return $self->_read_inner_body($fh);
185             }
186              
187 30         76 my $magic = $self->read_magic_numbers($fh, $kdbx);
188 30 50       106 $kdbx->sig1 == KDBX_SIG1 or throw 'Invalid file signature', type => 'parser', sig1 => $kdbx->sig1;
189              
190 30 50       186 if (ref($self) =~ /::(?:KDB|V[34])$/) {
191 30 50       68 defined $key or throw 'Must provide a master key', type => 'key.missing';
192             }
193              
194 30         95 my $headers = $self->_read_headers($fh);
195              
196 30         45 eval {
197 30         96 $self->_read_body($fh, $key, "$magic$headers");
198             };
199 30 100       132 if (my $err = $@) {
200 10         26 throw "Failed to load KDBX file: $err",
201             error => $err,
202             compression_error => $IO::Uncompress::Gunzip::GunzipError,
203             crypt_error => $File::KDBX::IO::Crypt::ERROR,
204             hash_error => $File::KDBX::IO::HashBLock::ERROR,
205             hmac_error => $File::KDBX::IO::HmacBLock::ERROR;
206             }
207             }
208              
209             sub _read_headers {
210 30     30   42 my $self = shift;
211 30         40 my $fh = shift;
212              
213 30         66 my $headers = $self->kdbx->headers;
214 30         55 my $all_raw = '';
215              
216 30         147 while (my ($type, $val, $raw) = $self->_read_header($fh)) {
217 247         383 $all_raw .= $raw;
218 247 100       419 last if $type == HEADER_END;
219 217         604 $headers->{$type} = $val;
220             }
221              
222 30         70 return $all_raw;
223             }
224              
225 0     0   0 sub _read_body { die "Not implemented" }
226              
227             sub _read_inner_body {
228 21     21   37 my $self = shift;
229              
230 21         42 my $current_pkg = ref $self;
231 21         111 require Scope::Guard;
232 21     21   181 my $guard = Scope::Guard->new(sub { bless $self, $current_pkg });
  21         266  
233              
234 21         277 $self->_rebless($self->inner_format);
235 21         87 $self->_read_inner_body(@_);
236             }
237              
238             1;
239              
240             __END__