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   32 use warnings;
  5         10  
  5         149  
5 5     5   23 use strict;
  5         9  
  5         110  
6              
7 5     5   23 use File::KDBX::Constants qw(:magic :header :version);
  5         9  
  5         973  
8 5     5   31 use File::KDBX::Error;
  5         8  
  5         251  
9 5     5   27 use File::KDBX::Util qw(:class :io);
  5         14  
  5         462  
10 5     5   32 use File::KDBX;
  5         9  
  5         115  
11 5     5   1346 use IO::Handle;
  5         15447  
  5         159  
12 5     5   28 use Module::Load ();
  5         10  
  5         103  
13 5     5   25 use Ref::Util qw(is_ref is_scalarref);
  5         9  
  5         203  
14 5     5   27 use Scalar::Util qw(looks_like_number openhandle);
  5         9  
  5         184  
15 5     5   25 use namespace::clean;
  5         9  
  5         27  
16              
17             our $VERSION = '0.906'; # VERSION
18              
19              
20             sub new {
21 30     30 1 68 my $class = shift;
22 30         94 my $self = bless {}, $class;
23 30         93 $self->init(@_);
24             }
25              
26              
27             sub init {
28 60     60 1 82 my $self = shift;
29 60         162 my %args = @_;
30              
31 60         219 @$self{keys %args} = values %args;
32              
33 60         243 return $self;
34             }
35              
36             sub _rebless {
37 51     51   80 my $self = shift;
38 51   66     182 my $format = shift // $self->format;
39              
40 51         121 my $sig2 = $self->kdbx->sig2;
41 51         118 my $version = $self->kdbx->version;
42              
43 51         81 my $subclass;
44              
45 51 100 33     232 if (defined $format) {
    50          
    50          
46 21         166 $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         66 my $major = $version & KDBX_VERSION_MAJOR_MASK;
53 30         117 my %subclasses = (
54             KDBX_VERSION_2_0() => 'V3',
55             KDBX_VERSION_3_0() => 'V3',
56             KDBX_VERSION_4_0() => 'V4',
57             );
58 30 50       108 $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         232 Module::Load::load "File::KDBX::Loader::$subclass";
66 51         2567 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         15 my $src = shift;
80 9 50 33     54 return $self->load_handle($src, @_) if openhandle($src) || $src eq '-';
81 9 50       24 return $self->load_string($src, @_) if is_scalarref($src);
82 9 50 33     51 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 32 my $self = shift;
89 21 50       85 my $str = shift or throw 'Expected string to load';
90 21 50       75 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
91              
92 21         45 my $key = delete $args{key};
93 21   33     90 $args{kdbx} //= $self->kdbx;
94              
95 21 100       59 my $ref = is_scalarref($str) ? $str : \$str;
96              
97 21 50       294 open(my $fh, '<', $ref) or throw "Failed to open string buffer: $!";
98              
99 21 50       64 $self = $self->new if !ref $self;
100 21         55 $self->init(%args, fh => $fh)->_read($fh, $key);
101 12         66 return $args{kdbx};
102             }
103              
104              
105             sub load_file {
106 9     9 1 15 my $self = shift;
107 9         12 my $filepath = shift;
108 9 50       39 my %args = @_ % 2 == 0 ? @_ : (key => shift, @_);
109              
110 9         17 my $key = delete $args{key};
111 9   33     37 $args{kdbx} //= $self->kdbx;
112              
113 9 50       438 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         38 $self->init(%args, fh => $fh, filepath => $filepath)->_read($fh, $key);
117 8         55 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 428 my $self = shift;
139 346 50       644 return File::KDBX->new if !ref $self;
140 346 50       595 $self->{kdbx} = shift if @_;
141 346   33     1153 $self->{kdbx} //= File::KDBX->new;
142 30 50   30 1 97 }
143 21 50   21 1 92  
144 30   50     137  
145 21   33     142 has format => undef, is => 'ro';
146             has inner_format => 'XML', is => 'ro';
147              
148              
149             sub read_magic_numbers {
150 30     30 1 49 my $self = shift;
151 30         50 my $fh = shift;
152 30   33     65 my $kdbx = shift // $self->kdbx;
153              
154 30 50       74 read_all $fh, my $magic, 12 or throw 'Failed to read file signature';
155              
156 30         117 my ($sig1, $sig2, $version) = unpack('L<3', $magic);
157              
158 30 50       75 if ($kdbx) {
159 30         102 $kdbx->sig1($sig1);
160 30         89 $kdbx->sig2($sig2);
161 30         88 $kdbx->version($version);
162 30 50       117 $self->_rebless if ref $self;
163             }
164              
165 30 50       102 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         41 my $fh = shift;
173 30         47 my $key = shift;
174              
175 30         86 my $kdbx = $self->kdbx;
176 30 0 33     63 $key //= $kdbx->key ? $kdbx->key->reload : undef;
177 30         108 $kdbx->reset;
178              
179 30 50       116 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         319 $fh->ungetc($first);
182 30 50       92 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         91 my $magic = $self->read_magic_numbers($fh, $kdbx);
188 30 50       112 $kdbx->sig1 == KDBX_SIG1 or throw 'Invalid file signature', type => 'parser', sig1 => $kdbx->sig1;
189              
190 30 50       197 if (ref($self) =~ /::(?:KDB|V[34])$/) {
191 30 50       69 defined $key or throw 'Must provide a master key', type => 'key.missing';
192             }
193              
194 30         106 my $headers = $self->_read_headers($fh);
195              
196 30         55 eval {
197 30         112 $self->_read_body($fh, $key, "$magic$headers");
198             };
199 30 100       137 if (my $err = $@) {
200 10         31 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         72 my $headers = $self->kdbx->headers;
214 30         66 my $all_raw = '';
215              
216 30         99 while (my ($type, $val, $raw) = $self->_read_header($fh)) {
217 247         418 $all_raw .= $raw;
218 247 100       394 last if $type == HEADER_END;
219 217         635 $headers->{$type} = $val;
220             }
221              
222 30         75 return $all_raw;
223             }
224              
225 0     0   0 sub _read_body { die "Not implemented" }
226              
227             sub _read_inner_body {
228 21     21   47 my $self = shift;
229              
230 21         44 my $current_pkg = ref $self;
231 21         113 require Scope::Guard;
232 21     21   169 my $guard = Scope::Guard->new(sub { bless $self, $current_pkg });
  21         287  
233              
234 21         311 $self->_rebless($self->inner_format);
235 21         140 $self->_read_inner_body(@_);
236             }
237              
238             1;
239              
240             __END__