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   30 use warnings;
  5         9  
  5         155  
5 5     5   21 use strict;
  5         9  
  5         117  
6              
7 5     5   22 use File::KDBX::Constants qw(:magic :header :version);
  5         9  
  5         965  
8 5     5   30 use File::KDBX::Error;
  5         8  
  5         236  
9 5     5   24 use File::KDBX::Util qw(:class :io);
  5         9  
  5         460  
10 5     5   34 use File::KDBX;
  5         6  
  5         94  
11 5     5   1294 use IO::Handle;
  5         15082  
  5         228  
12 5     5   34 use Module::Load ();
  5         9  
  5         86  
13 5     5   20 use Ref::Util qw(is_ref is_scalarref);
  5         15  
  5         224  
14 5     5   52 use Scalar::Util qw(looks_like_number openhandle);
  5         10  
  5         192  
15 5     5   25 use namespace::clean;
  5         8  
  5         36  
16              
17             our $VERSION = '0.904'; # VERSION
18              
19              
20             sub new {
21 30     30 1 61 my $class = shift;
22 30         72 my $self = bless {}, $class;
23 30         93 $self->init(@_);
24             }
25              
26              
27             sub init {
28 60     60 1 100 my $self = shift;
29 60         160 my %args = @_;
30              
31 60         194 @$self{keys %args} = values %args;
32              
33 60         269 return $self;
34             }
35              
36             sub _rebless {
37 51     51   89 my $self = shift;
38 51   66     184 my $format = shift // $self->format;
39              
40 51         132 my $sig2 = $self->kdbx->sig2;
41 51         124 my $version = $self->kdbx->version;
42              
43 51         92 my $subclass;
44              
45 51 100 33     301 if (defined $format) {
    50          
    50          
46 21         271 $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         80 my $major = $version & KDBX_VERSION_MAJOR_MASK;
53 30         130 my %subclasses = (
54             KDBX_VERSION_2_0() => 'V3',
55             KDBX_VERSION_3_0() => 'V3',
56             KDBX_VERSION_4_0() => 'V4',
57             );
58 30 50       128 $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         301 Module::Load::load "File::KDBX::Loader::$subclass";
66 51         2709 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 15 my $self = shift;
79 9         18 my $src = shift;
80 9 50 33     48 return $self->load_handle($src, @_) if openhandle($src) || $src eq '-';
81 9 50       21 return $self->load_string($src, @_) if is_scalarref($src);
82 9 50 33     55 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 46 my $self = shift;
89 21 50       119 my $str = shift or throw 'Expected string to load';
90 21 50       119 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
91              
92 21         47 my $key = delete $args{key};
93 21   33     122 $args{kdbx} //= $self->kdbx;
94              
95 21 100       67 my $ref = is_scalarref($str) ? $str : \$str;
96              
97 21 50       482 open(my $fh, '<', $ref) or throw "Failed to open string buffer: $!";
98              
99 21 50       79 $self = $self->new if !ref $self;
100 21         76 $self->init(%args, fh => $fh)->_read($fh, $key);
101 12         83 return $args{kdbx};
102             }
103              
104              
105             sub load_file {
106 9     9 1 16 my $self = shift;
107 9         13 my $filepath = shift;
108 9 50       37 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
109              
110 9         19 my $key = delete $args{key};
111 9   33     38 $args{kdbx} //= $self->kdbx;
112              
113 9 50       443 open(my $fh, '<:raw', $filepath) or throw 'Open file failed', filepath => $filepath;
114              
115 9 50       43 $self = $self->new if !ref $self;
116 9         37 $self->init(%args, fh => $fh, filepath => $filepath)->_read($fh, $key);
117 8         48 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 458 my $self = shift;
139 346 50       659 return File::KDBX->new if !ref $self;
140 346 50       581 $self->{kdbx} = shift if @_;
141 346   33     1272 $self->{kdbx} //= File::KDBX->new;
142 30 50   30 1 114 }
143 21 50   21 1 113  
144 30   50     163  
145 21   33     174 has format => undef, is => 'ro';
146             has inner_format => 'XML', is => 'ro';
147              
148              
149             sub read_magic_numbers {
150 30     30 1 56 my $self = shift;
151 30         48 my $fh = shift;
152 30   33     74 my $kdbx = shift // $self->kdbx;
153              
154 30 50       81 read_all $fh, my $magic, 12 or throw 'Failed to read file signature';
155              
156 30         143 my ($sig1, $sig2, $version) = unpack('L<3', $magic);
157              
158 30 50       74 if ($kdbx) {
159 30         125 $kdbx->sig1($sig1);
160 30         94 $kdbx->sig2($sig2);
161 30         85 $kdbx->version($version);
162 30 50       139 $self->_rebless if ref $self;
163             }
164              
165 30 50       162 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   56 my $self = shift;
172 30         46 my $fh = shift;
173 30         50 my $key = shift;
174              
175 30         63 my $kdbx = $self->kdbx;
176 30 0 33     67 $key //= $kdbx->key ? $kdbx->key->reload : undef;
177 30         119 $kdbx->reset;
178              
179 30 50       122 read_all $fh, my $buf, 1 or throw 'Failed to read the first byte', type => 'parser';
180 30         68 my $first = ord($buf);
181 30         321 $fh->ungetc($first);
182 30 50       85 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         111 my $magic = $self->read_magic_numbers($fh, $kdbx);
188 30 50       110 $kdbx->sig1 == KDBX_SIG1 or throw 'Invalid file signature', type => 'parser', sig1 => $kdbx->sig1;
189              
190 30 50       244 if (ref($self) =~ /::(?:KDB|V[34])$/) {
191 30 50       82 defined $key or throw 'Must provide a master key', type => 'key.missing';
192             }
193              
194 30         133 my $headers = $self->_read_headers($fh);
195              
196 30         48 eval {
197 30         132 $self->_read_body($fh, $key, "$magic$headers");
198             };
199 30 100       179 if (my $err = $@) {
200 10         36 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   48 my $self = shift;
211 30         45 my $fh = shift;
212              
213 30         76 my $headers = $self->kdbx->headers;
214 30         60 my $all_raw = '';
215              
216 30         121 while (my ($type, $val, $raw) = $self->_read_header($fh)) {
217 247         386 $all_raw .= $raw;
218 247 100       410 last if $type == HEADER_END;
219 217         669 $headers->{$type} = $val;
220             }
221              
222 30         94 return $all_raw;
223             }
224              
225 0     0   0 sub _read_body { die "Not implemented" }
226              
227             sub _read_inner_body {
228 21     21   52 my $self = shift;
229              
230 21         43 my $current_pkg = ref $self;
231 21         128 require Scope::Guard;
232 21     21   205 my $guard = Scope::Guard->new(sub { bless $self, $current_pkg });
  21         323  
233              
234 21         324 $self->_rebless($self->inner_format);
235 21         105 $self->_read_inner_body(@_);
236             }
237              
238             1;
239              
240             __END__