File Coverage

blib/lib/WWW/KeePassHttp.pm
Criterion Covered Total %
statement 118 118 100.0
branch 44 44 100.0
condition 19 19 100.0
subroutine 22 22 100.0
pod 11 11 100.0
total 214 214 100.0


line stmt bran cond sub pod time code
1             package WWW::KeePassHttp;
2 8     8   453932 use 5.012; # //, strict, s//r
  8         68  
3 8     8   39 use warnings;
  8         11  
  8         211  
4            
5 8     8   832 use MIME::Base64;
  8         1263  
  8         357  
6 8     8   2945 use Crypt::Mode::CBC;
  8         72904  
  8         211  
7 8     8   3061 use HTTP::Tiny;
  8         211485  
  8         229  
8 8     8   51 use JSON; # will use JSON::XS when available
  8         15  
  8         49  
9 8     8   5541 use Time::HiRes qw/gettimeofday sleep/;
  8         9398  
  8         29  
10 8     8   1140 use MIME::Base64;
  8         18  
  8         328  
11 8     8   48 use Carp;
  8         15  
  8         303  
12 8     8   3132 use WWW::KeePassHttp::Entry;
  8         17  
  8         543  
13            
14             our $VERSION = '0.020'; # rrr.mmmsss : rrr is major revision; mmm is minor revision; sss is sub-revision (new feature path or bugfix); optionally use _sss instead, for alpha sub-releases
15            
16             my $dumpfn;
17             BEGIN {
18 7         160 $dumpfn = sub { JSON->new->utf8->pretty->encode($_[0]) } # hidden from podcheckers and external namespace
19 8     8   9964 }
20            
21             =pod
22            
23             =head1 NAME
24            
25             WWW::KeePassHttp - Interface with KeePass PasswordSafe through the KeePassHttp plugin
26            
27             =head1 SYNOPSIS
28            
29             use WWW::KeePassHttp;
30            
31             my $kph = WWW::KeePassHttp->new(Key => $key);
32             $kph->associate() unless $kph->test_associate();
33             my @entries = @${ $kph->get_logins($search_string) };
34             print $entry[0]->url;
35             print $entry[0]->login;
36             print $entry[0]->password;
37            
38             =head1 DESCRIPTION
39            
40             Interface with KeePass PasswordSafe through the KeePassHttp plugin. Allows reading entries based on URL or TITLE, and creating a new entry as well.
41            
42             =head2 REQUIREMENTS
43            
44             You need to have KeePass (or compatible) on your system, with the KeePassHttp plugin installed.
45            
46             =head1 INTERFACE
47            
48             =head2 CONSTRUCTOR AND CONFIGURATION
49            
50             =over
51            
52             =item new
53            
54             my $kph = WWW::KeePassHttp->new( Key => $key, %options);
55             my $kph = WWW::KeePassHttp->new( Key => $key, keep_alive => 0, %options);
56            
57             Creates a new KeePassHttp connection, and sets up the AES encryption.
58            
59             The C $key> is required; pass in a string of 32 octets that
60             represent a 256-bit key value. If you have your key as 64 hex nibbles,
61             then use C<$key = pack 'H*', $hexnibbles;> to convert it to the value.
62             If you have your key as a Base64 string, use
63             C<$key = decode_base64($base64string);> to convert it to the value.
64            
65             There is also a C option, which will tell the HTTP user
66             agent to keep the connection alive when the option is set to C<1> (or
67             when it's not specified); setting the option to a C<0> will disable
68             that feature of the user agent.
69            
70             The C<%options> share the same name and purposes with the
71             configuration methods that follow, and can be individually specified in
72             the constructor as key/value pairs, or passing in an C<%options> hash.
73            
74             =cut
75            
76             sub new
77             {
78 21     21 1 11225 my ($class, %opts) = @_;
79 21         48 my $self = bless {}, $class;
80            
81             # user agent and URL
82 21   100     102 $opts{keep_alive} //= 1; # default to keep_alive
83 21         81 $self->{ua} = HTTP::Tiny->new(keep_alive => $opts{keep_alive} );
84            
85 21   100     1320 $self->{request_base} = $opts{request_base} // 'http://localhost'; # default to localhost
86 21   100     62 $self->{request_port} = $opts{request_port} // 19455; # default to 19455
87 21         61 $self->{request_url} = $self->{request_base} . ':' . $self->{request_port};
88            
89             # encryption object
90 21         324 $self->{cbc} = Crypt::Mode::CBC->new('AES');
91 21         49 $self->{key} = $opts{Key};
92 21         70 for($self->{key}) {
93 21 100       75 croak "256-bit AES key is required" unless defined $_;
94 19 100       53 last if length($_) == 32; # a 32-octet string is assumed to be a valid key
95 5         8 chomp;
96 5 100       32 croak "256-bit AES key must be in octets, not hex nibbles"
97             if /^(0x)?[[:xdigit:]]{64}$/;
98 3 100       23 croak "256-bit AES key must be in octets, not in base64"
99             if length($_) == 44;
100 1         7 croak "Key not recognized as 256-bit AES";
101             }
102 14         54 $self->{key64} = encode_base64($self->{key}, '');
103            
104             # appid
105 14   100     58 $self->{appid} = $opts{appid} // 'WWW::KeePassHttp';
106            
107 14         87 return $self;
108             }
109            
110             =item appid
111            
112             %options = ( ..., appid => 'name of your app', ... );
113             or
114             $kph->appid('name of your app');
115            
116             Changes the appid, which is the name that is used to map your
117             application with the stored key in the KeePassHttp settings in
118             KeePass.
119            
120             If not defined in the initial options or via this method,
121             the module will use a default appid of C.
122            
123             =cut
124            
125             sub appid
126             {
127 2     2 1 9 my ($self, $val) = @_;
128 2 100       6 $self->{appid} = $val if defined $val;
129 2         6 return $self->{appid};
130             }
131            
132             =item request_base
133            
134             %options = ( ..., request_base => 'localhost', ... );
135             or
136             $kph->request_base('127.0.0.1');
137            
138             Changes the protocol and host: the KeePassHttp plugin defaults to C, but can be configured differently, so you will need to make your object match your plugin settings.
139            
140             =cut
141            
142             sub request_base
143             {
144 2     2 1 8 my ($self, $val) = @_;
145 2 100       5 $self->{request_base} = $val if defined $val;
146 2         4 $self->{request_url} = $self->{request_base} . ':' . $self->{request_port};
147 2         5 return $self->{request_base};
148             }
149            
150             =item request_port
151            
152             %options = ( ..., request_port => 19455, ... );
153             or
154             $kph->request_port(19455);
155            
156             Changes the port: the KeePassHttp plugin defaults to port 19455, but can be configured differently, so you will need to make your object match your plugin settings.
157            
158             =cut
159            
160             sub request_port
161             {
162 2     2 1 8 my ($self, $val) = @_;
163 2 100       6 $self->{request_port} = $val if defined $val;
164 2         5 $self->{request_url} = $self->{request_base} . ':' . $self->{request_port};
165 2         5 return $self->{request_port};
166             }
167            
168             =back
169            
170             =for comment END OF CONSTRUCTOR AND CONFIGURATION
171            
172             =head2 USER INTERFACE
173            
174             These methods implement the L, with one method for each RequestType.
175            
176             =over
177            
178             =item test_associate
179            
180             $kph->associate unless $kph->test_associate();
181            
182             Sends the C request to the KeePassHttp server,
183             which is used to see whether or not your application has been
184             associated with the KeePassHttp plugin or not. Returns a true
185             value if your application is already associated, or a false
186             value otherwise.
187            
188             =cut
189            
190             sub test_associate
191             {
192 2     2 1 1089 my ($self, %args) = @_;
193 2         6 my $content = $self->request('test-associate', %args);
194 2         7 return $content->{Success};
195             }
196            
197             =item associate
198            
199             $kph->associate unless $kph->test_associate();
200            
201             Sends the C request to the KeePassHttp server,
202             which is used to give your application's key to the KeePassHttp
203             plugin.
204            
205             When this request is received, KeePass will pop up a dialog
206             asking for a name -- this name should match the C value
207             that you defined for the C<$kph> instance. All requests sent
208             to the plugin will include this C so that KeePassHttp can
209             look up your application's key, so it must match exactly.
210             As per the C,
211             the server saves your application's key in the C
212             entry, in the B String Fields> with a name of
213             C, where C is the name you type in the dialog
214             box (which needs to match your C).
215            
216             B: this C communication is insecure,
217             since KeePassHttp plugin is not using HTTPS. Every other
218             communication between your application and the plugin uses the
219             key (which both your application and the plugin know) to
220             encrypt the critical data (usernames, passwords, titles, etc),
221             and is thus secure;
222             but the C interaction, because it happens before
223             the plugin has your key, by its nature cannot be encrypted by
224             that key, so it sends the encoded key I. If this
225             worries you, I suggest that you manually insert the key: do an
226             C once with a dummy key, then manually overwrite the
227             encoded key that it stores with the encoded version of your real
228             key. (This limitation is due to the design of the KeePassHttp
229             plugin and its protocol for the C command, not due
230             to the wrapper around that protocol that this module implements.)
231            
232             =cut
233            
234             sub associate
235             {
236 3     3 1 946 my ($self, %args) = @_;
237 3         12 my $content = $self->request('associate', Key64 => $self->{key64}, %args);
238 3 100 100     16 croak ("Wrong ID: ", $dumpfn->( { wrong_id => $content } )) unless $self->{appid} eq ($content->{Id}//'');
239 1         2 return $content;
240             }
241            
242             =item get_logins
243            
244             my @entries = @${ $kph->get_logins($search_string) };
245             print $entry[0]->url;
246             print $entry[0]->login;
247             print $entry[0]->password;
248            
249             Sends the C request, which returns the Name,
250             Login, and Password for each of the matching entries.
251            
252             C<$entries> is an array reference containing
253             L objects, from which you can
254             extract the url/name, login, and password for each matched
255             entry.
256            
257             The rules for the matching of the search string are defined in the
258             L.
259             But, in brief, it will do a fuzzy match on the URL, and an exact match
260             on the entry title. (The plugin was designed to be used for browser plugins
261             to request passwords for URLs from KeePass, hence its focus on URLs.)
262            
263             =cut
264            
265             sub get_logins
266             {
267 3     3 1 5390 my ($self, $search_term, %args) = @_;
268 3         6 $args{Url} = $search_term;
269 3 100       11 $args{SubmitUrl} = $self->{appid} unless exists $args{SubmitUrl}; # "SubmitUrl" is actually the name of the requestor; in the browser->keePassHttp interface, the requestor is the website requesting the password; but here, I am using it as the app identifier
270 3         13 my $content = $self->request('get-logins', Url => $search_term, %args);
271 3 100       12 return [] unless $content->{Count};
272 2         3 my $entries = $content->{Entries};
273 2         7 for my $entry ( @$entries ) {
274 2         10 for my $k ( sort keys %$entry ) {
275 8         82 $entry->{$k} = $self->{cbc}->decrypt( decode_base64($entry->{$k}), $self->{key}, decode_base64($content->{Nonce}));
276             }
277 2         23 $entry->{Url} = $entry->{Name};
278 2         14 $entry = WWW::KeePassHttp::Entry->new( %$entry );
279             }
280             #$dumpfn->( { Entries => $entries } );
281 2         11 return $entries;
282             }
283            
284             =item get_logins_count
285            
286             my $count = $kph->get_logins_count($search_string);
287            
288             Sends the C request, which returns a count of
289             the number of matches for the search string.
290            
291             The rules for the matching of the search string are defined in the
292             L.
293             But, in brief, it will do a fuzzy match on the URL, and an exact match
294             on the entry title. (The plugin was designed to be used for browser plugins
295             to request passwords for URLs from KeePass, hence its focus on URLs.)
296            
297             This method is useful when the fuzzy-URL-match might match a large
298             number of entries in the database; if after seeing this count, you
299             would rather refine your search instead of requesting that many entries,
300             this method enables knowing that right away, rather than after you
301             accidentally matched virtually every entry in your database by searching
302             for C.
303            
304             =cut
305            
306             sub get_logins_count
307             {
308 2     2 1 3550 my ($self, $search_term, %args) = @_;
309 2         5 $args{Url} = $search_term;
310 2 100       8 $args{SubmitUrl} = $self->{appid} unless exists $args{SubmitUrl}; # "SubmitUrl" is actually the name of the requestor; in the browser->keePassHttp interface, the requestor is the website requesting the password; but here, I am using it as the app identifier
311 2         8 my $content = $self->request('get-logins-count', Url => $search_term, %args);
312 2         8 return $content->{Count};
313             }
314            
315            
316             =item set_login
317            
318             $kph->set_login( Login => $username, Url => $url_and_title, Password => $password );
319             # or
320             $kph->set_login( $entry );
321            
322             Sends the C request, which adds a new entry to your
323             KeePass database, in the "KeePassHttp Passwords" group (folder).
324            
325             As far as I know, the plugin doesn't allow choosing a different group
326             for your entry. The plugin uses the URL that you supply as both the
327             entry title and the URL field in that entry. (Once again, the plugin
328             was designed around browser password needs, and thus is URL-focused).
329             I don't know if that's a deficiency in the plugin's implementation,
330             or just its documentation, or my interpretation of that documentation.
331            
332             The arguments to the method define the C (username), C (for
333             entry title and URL field), and C (secret value) for the new
334             entry. All three of those parameters are required by the protocol, and
335             thus by this method. Alernately, you can just pass it a L
336             object as the single argument.
337            
338             If you would prefer not to give one or more of those parameters a value,
339             just pass an empty string. You could afterword then manually access
340             your KeePass database and edit the entry yourself.
341            
342             =cut
343            
344             sub set_login
345             {
346 5     5 1 4509 my ($self, @rest) = @_;
347 5 100       33 my %args = UNIVERSAL::isa($rest[0], 'WWW::KeePassHttp::Entry') ? (%{$rest[0]}) : (@rest);
  1         5  
348 5 100       25 croak "set_login(): missing Login parameter" unless defined $args{Login};
349 4 100       15 croak "set_login(): missing Url parameter" unless defined $args{Url};
350 3 100       16 croak "set_login(): missing Password parameter" unless defined $args{Password};
351 2         7 my $content = $self->request('set-login', %args);
352 2         16 return $content->{Success};
353             }
354            
355            
356             =item request
357            
358             my $results = $kph->request( $type, %options );
359            
360             This is the generic method for making a request of the
361             KeePassHttp plugin. In general, other methods should handle
362             most requests. However, maybe a new method has been exposed
363             in the plugin but not yet implemented here, so you can use
364             this method for handling that.
365            
366             The C<$type> indicates the RequestType, which include
367             C, C, C,
368             C, and C.
369            
370             This method automatically fills out the RequestType, TriggerUnlock, Id, Nonce, and Verifier parameters. If your RequestType requires
371             any other parameters, add them to the C<%options>.
372            
373             It then encodes the request into the JSON payload, and
374             sends that request to the KeePassHttp plugin, and gets the response,
375             decoding the JSON content back into a Perl hashref. It verifies that
376             the response's Nonce and Verifier parameters are appropriate for the
377             communication channel, to make sure communications from the plugin
378             are properly encrypted.
379            
380             Returns the hashref decoded from the JSON
381            
382             =cut
383            
384             sub request {
385 19     19 1 3411 my ($self, $type, %params) = @_;
386 19         44 my ($iv, $nonce) = generate_nonce();
387            
388             #print STDERR "request($type):\n";
389            
390             # these are required in every request
391             my %request = (
392             RequestType => $type,
393             TriggerUnlock => JSON::true, # was intended for TRUE to request that KeePass unlock, but that doesn't actually happen
394             Id => $self->{appid},
395             Nonce => $nonce,
396 19         53 Verifier => encode_base64($self->{cbc}->encrypt($nonce, $self->{key}, $iv), ''),
397             );
398            
399             # don't want to encrypt the key during an association request
400 19         525 delete $params{Key}; # only allow Key64
401 19 100       48 $request{Key} = delete $params{Key64} if( exists $params{Key64} );
402            
403             # encrypt all remaining parameter values
404 19         58 while(my ($k,$v) = each %params) {
405 17         182 $request{$k} = encode_base64($self->{cbc}->encrypt($v, $self->{key}, $iv), '');
406             }
407             #$dumpfn->({final_request => \%request});
408            
409             # send the request
410 19         275 my $response = $self->{ua}->get($self->{request_url}, {content=> encode_json \%request});
411            
412             # error checking
413 19 100       1221 croak $dumpfn->( { request_error => $response } ) unless $response->{success};
414 18 100       39 croak $dumpfn->( { no_json => $response } ) unless exists $response->{content};
415            
416             # get the JSON
417 17         144 my $content = decode_json $response->{content};
418             #$dumpfn->( { their_response => $response, their_content => $content } );
419            
420             # verification before returning the content -- if their verifier doesn't match their nonce,
421             # then we don't have secure communication
422             # Don't need to check on test-associate/associate if verifier is missing, because there can
423             # reasonably be no verifier on those (ie, when test-associate returns false, or when the associate fails)
424 16 100 100     62 if(exists $content->{Verifier} or ($type ne 'test-associate' and $type ne 'associate')) {
      100        
425 14 100 100     55 croak $dumpfn->( { missing_verifier => $content } ) unless exists $content->{Nonce} and exists $content->{Verifier};
426 12         44 my $their_iv = decode_base64($content->{Nonce});
427 12         48 my $decode_their_verifier = $self->{cbc}->decrypt( decode_base64($content->{Verifier}), $self->{key}, $their_iv );
428 12 100       200 if( $decode_their_verifier ne $content->{Nonce} ) {
429 1         4 croak $dumpfn->( { "Decoded Verifier $decode_their_verifier" => $content } );
430             }
431             }
432            
433             # If it made it to here, it's safe to return the content
434 13         45 return $content;
435             }
436            
437             =back
438            
439             =for comment END OF USER INTERFACE
440            
441             =head2 HELPER METHODS
442            
443             In general, most users won't need these. But maybe I will.
444            
445             =over
446            
447             =item generate_nonce
448            
449             my ($iv, $base64) = $kph->generate_nonce();
450            
451             This is used by the L method to generate the IV nonce
452             for communication. I don't think you need to use it yourself, but
453             it's available to you, if you find a need for it.
454            
455             The C<$iv> is the string of octets (the actual 128 IV nonce value).
456            
457             The C<$base64> is the base64 representation of the C<$iv>.
458            
459             =cut
460            
461             sub generate_nonce
462             {
463             # generate 10 bytes of random numbers, 2 bytes of microsecond time, and 4 bytes of seconds
464             # this gives randomness from two sources (rand and usecond),
465             # plus a deterministic counter that won't repeat for 2^31 seconds (almost 70 years)
466             # so as long as you aren't using the same key for 70 years, the nonce should be unique
467 22     22 1 2003240 my $hex = '';
468 22         397 $hex .= sprintf '%02X', rand(256) for 1..10;
469 22         90 my ($s,$us) = gettimeofday();
470 22         72 $hex .= sprintf '%04X%08X', $us&0xFFFF, $s&0xFFFFFFFF;
471 22         104 my $iv = pack 'H*', $hex;
472 22         103 my $nonce = encode_base64($iv, '');
473 22 100       113 return wantarray ? ($iv, $nonce) : $iv;
474             }
475            
476             =back
477            
478             =for comment END OF HELPER METHODS
479            
480            
481             =head1 SEE ALSO
482            
483             =over
484            
485             =item * L
486            
487             =item * L
488            
489             =item * L = A similar interface which uses the KeePassRest plugin to interface with KeePass
490            
491             =back
492            
493             =head1 ACKNOWLEDGEMENTS
494            
495             Thank you to L for providing a free
496             password manager with plugin capability.
497            
498             Thank you to the L
499             for providing a free and open source plugin which allows for easy
500             communication between an external application and the KeePass application,
501             enabling the existence of this module (and the ability for it to give
502             applications access to the passwords stored in KeePass).
503            
504             This module and author are not affiliated with either KeePass or KeePassHttp
505             except as a user of those fine products.
506            
507             =head1 TODO
508            
509             The entries should be full-fledged objects, with method-based access to
510             the underlying Login, Url, and Password values.
511            
512             =head1 AUTHOR
513            
514             Peter C. Jones Cpetercj AT cpan DOT orgE>
515            
516             Please report any bugs or feature requests
517             thru the repository's interface at L.
518            
519             =begin html
520            
521            
522            
523            
524            
525             Coverage Status
526             github perl-ci
527            
528             =end html
529            
530             =head1 COPYRIGHT
531            
532             Copyright (C) 2021 Peter C. Jones
533            
534             =head1 LICENSE
535            
536             This program is free software; you can redistribute it and/or modify it
537             under the terms of either: the GNU General Public License as published
538             by the Free Software Foundation; or the Artistic License.
539             See L for more information.
540            
541             =cut
542            
543             1;