]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Key/File.pm
be9abd283538e0095cbb398eae73f5490991e3ba
[chaz/p5-File-KDBX] / lib / File / KDBX / Key / File.pm
1 package File::KDBX::Key::File;
2 # ABSTRACT: A file key
3
4 use warnings;
5 use strict;
6
7 use Crypt::Digest qw(digest_data);
8 use Crypt::Misc 0.029 qw(decode_b64);
9 use File::KDBX::Constants qw(:key_file);
10 use File::KDBX::Error;
11 use File::KDBX::Util qw(:erase trim);
12 use Ref::Util qw(is_ref is_scalarref);
13 use Scalar::Util qw(openhandle);
14 use XML::LibXML::Reader;
15 use namespace::clean;
16
17 use parent 'File::KDBX::Key';
18
19 our $VERSION = '999.999'; # VERSION
20
21 sub init {
22 my $self = shift;
23 my $primitive = shift // throw 'Missing key primitive';
24
25 my $data;
26 my $cleanup;
27
28 if (openhandle($primitive)) {
29 seek $primitive, 0, 0; # not using ->seek method so it works on perl 5.10
30 my $buf = do { local $/; <$primitive> };
31 $data = \$buf;
32 $cleanup = erase_scoped $data;
33 }
34 elsif (is_scalarref($primitive)) {
35 $data = $primitive;
36 }
37 elsif (defined $primitive && !is_ref($primitive)) {
38 open(my $fh, '<:raw', $primitive)
39 or throw "Failed to open key file ($primitive)", filepath => $primitive;
40 my $buf = do { local $/; <$fh> };
41 $data = \$buf;
42 $cleanup = erase_scoped $data;
43 $self->{filepath} = $primitive;
44 }
45 else {
46 throw 'Unexpected primitive type', type => ref $primitive;
47 }
48
49 my $raw_key;
50 if (substr($$data, 0, 120) =~ /<KeyFile>/
51 and my ($type, $version) = $self->_load_xml($data, \$raw_key)) {
52 $self->{type} = $type;
53 $self->{version} = $version;
54 $self->_set_raw_key($raw_key);
55 }
56 elsif (length($$data) == 32) {
57 $self->{type} = KEY_FILE_TYPE_BINARY;
58 $self->_set_raw_key($$data);
59 }
60 elsif ($$data =~ /^[A-Fa-f0-9]{64}$/) {
61 $self->{type} = KEY_FILE_TYPE_HEX;
62 $self->_set_raw_key(pack('H64', $$data));
63 }
64 else {
65 $self->{type} = KEY_FILE_TYPE_HASHED;
66 $self->_set_raw_key(digest_data('SHA256', $$data));
67 }
68
69 return $self->hide;
70 }
71
72 =method reload
73
74 $key->reload;
75
76 Re-read the key file, if possible, and update the raw key if the key changed.
77
78 =cut
79
80 sub reload {
81 my $self = shift;
82 $self->init($self->{filepath}) if defined $self->{filepath};
83 return $self;
84 }
85
86 =attr type
87
88 $type = $key->type;
89
90 Get the type of key file. Can be one of:
91
92 =for :list
93 * C<KEY_FILE_TYPE_BINARY>
94 * C<KEY_FILE_TYPE_HEX>
95 * C<KEY_FILE_TYPE_XML>
96 * C<KEY_FILE_TYPE_HASHED>
97
98 =cut
99
100 sub type { $_[0]->{type} }
101
102 =attr version
103
104 $version = $key->version;
105
106 Get the file version. Only applies to XML key files.
107
108 =cut
109
110 sub version { $_[0]->{version} }
111
112 =attr filepath
113
114 $filepath = $key->filepath;
115
116 Get the filepath to the key file, if known.
117
118 =cut
119
120 sub filepath { $_[0]->{filepath} }
121
122 ##############################################################################
123
124 sub _load_xml {
125 my $self = shift;
126 my $buf = shift;
127 my $out = shift;
128
129 my ($version, $hash, $data);
130
131 my $reader = XML::LibXML::Reader->new(string => $$buf);
132 my $pattern = XML::LibXML::Pattern->new('/KeyFile/Meta/Version|/KeyFile/Key/Data');
133
134 while ($reader->nextPatternMatch($pattern) == 1) {
135 next if $reader->nodeType != XML_READER_TYPE_ELEMENT;
136 my $name = $reader->localName;
137 if ($name eq 'Version') {
138 $reader->read if !$reader->isEmptyElement;
139 $reader->nodeType == XML_READER_TYPE_TEXT
140 or alert 'Expected text node with version', line => $reader->lineNumber;
141 my $val = trim($reader->value);
142 defined $version
143 and alert 'Overwriting version', previous => $version, new => $val, line => $reader->lineNumber;
144 $version = $val;
145 }
146 elsif ($name eq 'Data') {
147 $hash = trim($reader->getAttribute('Hash')) if $reader->hasAttributes;
148 $reader->read if !$reader->isEmptyElement;
149 $reader->nodeType == XML_READER_TYPE_TEXT
150 or alert 'Expected text node with data', line => $reader->lineNumber;
151 $data = $reader->value;
152 $data =~ s/\s+//g if defined $data;
153 }
154 }
155
156 return if !defined $version || !defined $data;
157
158 if ($version =~ /^1\.0/ && $data =~ /^[A-Za-z0-9+\/=]+$/) {
159 $$out = eval { decode_b64($data) };
160 if (my $err = $@) {
161 throw 'Failed to decode key in key file', version => $version, data => $data, error => $err;
162 }
163 return (KEY_FILE_TYPE_XML, $version);
164 }
165 elsif ($version =~ /^2\.0/ && $data =~ /^[A-Fa-f0-9]+$/ && defined $hash && $hash =~ /^[A-Fa-f0-9]+$/) {
166 $$out = pack('H*', $data);
167 $hash = pack('H*', $hash);
168 my $got_hash = digest_data('SHA256', $$out);
169 $hash eq substr($got_hash, 0, 4)
170 or throw 'Checksum mismatch', got => $got_hash, expected => $hash;
171 return (KEY_FILE_TYPE_XML, $version);
172 }
173
174 throw 'Unexpected data in key file', version => $version, data => $data;
175 }
176
177 1;
This page took 0.038537 seconds and 3 git commands to generate.