File Coverage

blib/lib/Crypt/NamedKeys.pm
Criterion Covered Total %
statement 33 89 37.0
branch 0 32 0.0
condition 0 11 0.0
subroutine 11 20 55.0
pod 6 6 100.0
total 50 158 31.6


line stmt bran cond sub pod time code
1             package Crypt::NamedKeys;
2              
3 2     2   143948 use strict;
  2         4  
  2         43  
4 2     2   8 use warnings;
  2         2  
  2         38  
5 2     2   966 use Moo;
  2         17074  
  2         10  
6              
7             =head1 NAME
8              
9             Crypt::NamedKeys - A Crypt::CBC wrapper with key rotation support
10              
11             =head1 SYNOPSYS
12              
13             use Crypt::NamedKeys;
14             my $crypt = Crypt::NamedKeys->new(keyname => 'href');
15             my $encrypted = $crypt->encrypt_data(data => $href);
16             my $restored_href = $crypt->decrypt_data(
17             data => $encrypted->{data},
18             mac => $encrypted->{mac},
19             );
20              
21             =head1 DESCRIPTION
22              
23             This module provides functions to serialize data for transfer via non-protected
24             channels with encryption and data integrity protection. The module tracks key
25             number used to encrypt information so that keys can be rotated without making
26             data unreadable.
27              
28             =head1 CONFIGURATION AND KEY ROTATION
29              
30             The keys are stored in the keyfile, configurable as below. Keys are numbered
31             starting at 1. Numbers must never be reused. Typically key rotation will be
32             done in several steps, each with its own rollout. These steps MUST be done as
33             separate releases because otherwise keys may not be available to decrypt data,
34             and so things may not work.
35              
36             =head2 keyfile location
37              
38             The keyfile can be set using the keyfile($path) function. There is no default.
39              
40             =head2 keyfile format
41              
42             The format of the keyfile is YAML, following a basic structure of
43              
44             keyname:
45             [keyhashdef]
46              
47             so for example:
48              
49             cryptedfeed:
50             default_keynum: 9
51             none: queith7eeTh0teejaichoodobooX9ceechee9Sai9gauChiengaeraew3aDiehei
52             1: aePh8ahBaNg1bee6ohj3er5cuzeepoophai1oogohpoixothah4AuYiongu4ahta
53             2: oht1eep8uxoo1eeshaSaemee9aem5chahqueu0Aedaa7eeXae9aeghe5umoNah6a
54             3: chigh4veifoofe0Vohphee4ohkaef9giz2iaje2ahF4ohboSh6ifaiNgohwohchi
55             4: Ahphahmisaingo5Ietheangeegi5ia1uuF9taerooShaitoh1Eophig3ohziejet
56             5: oe5wi2equee6FeiZohjah2peas6Ahquohniefeimai0beip2waxeizoo1OhthohN
57             6: eigaezee3CeuC8phae4giph6Miqu6piy3Eideipahticesheij7se9eecai9fiez
58             7: DuuGhohViGh0Sheihahr6ce4Phuin7ahpaiSa5jaiphie3eiz8oa3dohrohghuow
59             8: ahfoniemah4boemeN8seJ7hohhualeetei7aegohhai5ohwahlohnah2Ee2Ewal1
60             9: Ceixei4shelohxee1ohdoochuliebael1kae8eit0Geeth1so9fohZi0cohs8go4
61             10: boreiDe0shueNgie7shai7ooc1yaeveiKeihuox0xahp1hai8phe7aephiel2oob
62              
63             In general we assume key spefications to use numeric keys within the named
64             key hash. This makes key rotation a lot easier and prevents reusing key
65             numbers.
66              
67             Key names may not contain = or -.
68              
69             All keys listed can be used for decryption (with the special 'none' key used if
70             no key number is specified in the cyphertex), but by default only the default
71             keynumber (default_keynum, in this case 9) is used for encrypting.
72              
73             The keynumber is specified in the resulting cyphertext so we know which key
74             to use for decrypting the cyphertext even if we don't try to decrypt it. This
75             allows:
76              
77             =over
78              
79             =item Key checking
80              
81             If you store cyphertext in your rdbms, you can check which keys are used before
82             you remove decryption support for a key.
83              
84             =item Orderly key rotation
85              
86             You can add a key, and later depricate it, managing the transition (and perhaps
87             even using logging to know when the old key is no longer needed).
88              
89             =back
90              
91             =head2 Step 1: Adding a New Key
92              
93             In many cases you need to be able to add and remove keys without requiring that
94             everything gets the new keys at the same time. For example if you have multiple
95             production systems, they are likely to get updated in series, and if you expect
96             that everyone gets the keys at the same time, timing issues may occur.
97              
98             For this reason, we recommend breaking up the encryption key rollout into a
99             number of steps. The first one is making sure that everyone can use the
100             new key to decrypt before anyone uses it to encrypt.
101              
102             The first release is by adding a new key so that it is available for decryption.
103              
104             For example, in the keyfile suppose one has:
105              
106             mykey:
107             default_keynum: 1
108             none: rsdfagtiaueIUPOIUYHH
109             1: rsdfagtiaueIUPOIUYHH
110              
111             We might add another line
112              
113             2: IRvswqerituq-HPIOHJHGdeewrwyugfrGRSe3eyy6te
114              
115             Once this file is released, the key number 2 will be available globally for
116             decryption purposes, but everything will still be encrypted using key number 1.
117              
118             This means it is safe then to go onto the second step.
119              
120             =head2 Step 2: Setting the new key as default
121              
122             Once the new keys have been released, the next step is to change the default
123             keynumber. Data encrypted in this way will be available even to servers waiting
124             to be updated because the keys have previously been rolled out. To do this,
125             simply change the default_keynum:
126              
127             mykey:
128             default_keynum: 1
129             1: rsdfagtiaueIUPOIUYHH
130             2: IRvswqerituq-HPIOHJHGdeewrwyugfrGRSe3eyy6te
131              
132             becomes:
133              
134             mykey:
135             default_keynum: 2
136             1: rsdfagtiaueIUPOIUYHH
137             2: IRvswqerituq-HPIOHJHGdeewrwyugfrGRSe3eyy6te
138              
139             Now all new data will be encrypted using keynumber 2.
140              
141             =head2 Step 3: Retiring the old key
142              
143             Once the old key is no longer being used, it can be retired by deleting the
144             row.
145              
146             =head2 The Special 'none' keynum
147              
148             For aes keys before the key versioning was introduced, there is no keynum
149             associated with the cyphertext, so we use this key.
150              
151             =cut
152              
153 2     2   2157 use Carp;
  2         3  
  2         91  
154 2     2   940 use Crypt::CBC;
  2         6617  
  2         56  
155 2     2   991 use Digest::SHA qw(hmac_sha256_base64 sha256);
  2         4399  
  2         130  
156 2     2   1132 use JSON;
  2         22210  
  2         10  
157 2     2   1441 use MIME::Base64;
  2         1108  
  2         130  
158 2     2   1050 use String::Compare::ConstantTime;
  2         812  
  2         87  
159 2     2   1068 use Try::Tiny;
  2         2341  
  2         112  
160 2     2   939 use YAML::XS;
  2         4616  
  2         2509  
161              
162             our $VERSION = '1.1.2';
163              
164             =head1 CONFIGURATION PARAMETERS
165              
166             =head2 $Crypt::NamedKeys::Escape_Eq;
167              
168             Set to true, using local or not, if you want to encode with - instead of =
169              
170             Note that on decryption both are handled.
171              
172             =cut
173              
174             our $Escape_Eq = 0;
175              
176             =head1 PROPERTIES
177              
178             =head2 keynum
179              
180             Defaults to the default keynumber specified in the keyfile (for encryption)
181              
182             =cut
183              
184             has keynum => (
185             is => 'ro',
186             lazy => 1,
187             builder => '_default_keynum',
188             );
189              
190             =head2 keyname
191              
192             The name of the key in the keyfile.
193              
194             =cut
195              
196             has keyname => (
197             is => 'ro',
198             required => 1,
199             );
200              
201             my $keyfile;
202              
203             =head1 METHODS AND FUNCTIONS
204              
205             =cut
206              
207             =head2 Crypt::NamedKeys->keyfile($path)
208              
209             Can also be called as Crypt::NamedKeys::keyfile($path)
210              
211             Sets the path of the keyfile. It does not load or reload it (that is done on
212             demand or by reload_keyfile() below
213              
214             =cut
215              
216             sub keyfile {
217 0     0 1   my $file = shift;
218 0 0         $file = shift if $file eq __PACKAGE__;
219 0 0         return $keyfile unless $file;
220 0           $keyfile = $file;
221 0           return $keyfile;
222             }
223              
224             my $keyhash;
225              
226             my $get_keyhash = sub {
227             return $keyhash if $keyhash;
228             reload_keyhash();
229             return $keyhash;
230             };
231              
232             =head2 reload_keyhash
233              
234             Can be called as an object method or function (i.e.
235             Crypt::NamedKeys::reload_keyhash()
236              
237             Loads or reloads the keyfile. Can be used via event handlers to reload
238             confguration as needed
239              
240             =cut
241              
242             sub reload_keyhash {
243 0 0   0 1   croak 'No keyfile defined (use keyfile() to set)' unless $keyfile;
244 0           $keyhash = YAML::XS::LoadFile($keyfile);
245 0           return scalar keys %$keyhash;
246             }
247              
248             my $get_secret = sub {
249             my %args = @_;
250             croak 'No key name specified' unless $args{keyname};
251             croak 'No key number specified' unless $args{keynum};
252             my $keytab = &$get_keyhash()->{$args{keyname}};
253             return $keytab->{$args{keynum}};
254             };
255              
256             sub _default_keynum {
257 0     0     my $self = shift;
258 0           my $keytab = &$get_keyhash()->{$self->keyname};
259             warn 'No default key found for ' . $self->keyname
260 0 0         unless $keytab->{default_keynum};
261 0           return $keytab->{default_keynum};
262             }
263              
264             my $mac_secret = sub {
265             my %args = @_;
266             return sha256(&$get_secret(@_)); ## nocritic
267             };
268              
269             =head2 $self->encrypt_data(data => $data)
270              
271             Serialize I<$data> to JSON, encrypt it, and encode as base64. Also compute HMAC
272             code for the encrypted data. Returns hash reference with 'data' and 'mac'
273             elements.
274              
275             Args include
276              
277             =over
278              
279             =item data
280              
281             Data structure reference to be encrypted
282              
283             =item cypher
284              
285             Cypher to use (default: Rijndael)
286              
287             =back
288              
289             =cut
290              
291             sub encrypt_data {
292 0     0 1   my ($self, %args) = @_;
293 0 0 0       croak "data argument is required and must be a reference" unless $args{data} and ref $args{data};
294 0           my $json_data = encode_json($args{data});
295 0   0       my $cypher = $args{cypher} || 'Rijndael';
296             # Crypt::CBC generates random 8 bytes salt that it uses to
297             # derive IV and encryption key from $args{secret}. It uses
298             # the same algorythm as OpenSSL, the output is identical to
299             # openssl enc -e -aes-256-cbc -k $args{secret} -salt
300 0           my $cbc = Crypt::CBC->new(
301             -key => &$get_secret(
302             keyname => $self->keyname,
303             keynum => $self->keynum,
304             ),
305             -cipher => $cypher,
306             -salt => 1,
307             );
308 0           my $data = encode_base64($cbc->encrypt($json_data), '');
309 0           my $mac = hmac_sha256_base64(
310             $data,
311             &$mac_secret(
312             keyname => $self->keyname,
313             keynum => $self->keynum
314             ));
315 0 0         $data =~ s/=/-/g if $Escape_Eq;
316 0 0         $mac =~ s/=/-/g if $Escape_Eq;
317             return {
318 0           data => $self->keynum . '*' . $data,
319             mac => $mac,
320             };
321             }
322              
323             =head2 $self->decrypt_data(data => $data, mac => $mac)
324              
325             Decrypt data encrypted using I. First checks HMAC code for data.
326             If data was not tampered, decrypts it and decodes from JSON. Returns data, or
327             undef if decryption failed.
328              
329             =cut
330              
331             sub decrypt_data {
332 0     0 1   my ($self, %args) = @_;
333 0 0 0       croak "method requires data and mac arguments" unless $args{data} and $args{mac};
334             # if the data was tampered do not try to decrypt it
335              
336 0           $args{data} =~ s/-/=/g;
337 0           $args{mac} =~ s/-/=/g;
338 0           my ($keynum, $cyphertext) = split /\*/, $args{data}, 2;
339              
340 0 0         if (!$cyphertext) {
341 0           $cyphertext = $keynum;
342 0           $keynum = 'none';
343             }
344 0           my $secret = &$get_secret(
345             keynum => $keynum,
346             keyname => $self->keyname
347             );
348 0 0 0       return unless ($cyphertext and $secret);
349 0           my $msg_mac = hmac_sha256_base64(
350             $cyphertext,
351             &$mac_secret(
352             keynum => $keynum,
353             keyname => $self->keyname,
354             ));
355 0 0         return unless String::Compare::ConstantTime::equals($msg_mac, $args{mac});
356              
357 0           my $cbc = Crypt::CBC->new(
358             -key => &$get_secret(
359             keynum => $keynum,
360             keyname => $self->keyname
361             ),
362             -cipher => 'Rijndael',
363             -salt => 1,
364             );
365 0           my $decrypted = $cbc->decrypt(decode_base64($cyphertext));
366 0 0         warn "Unable to decrypt $args{data} with keynum $keynum and keyname " . $self->keyname unless defined $decrypted;
367 0           my $data = decode_json($decrypted);
368 0           return $data;
369             }
370              
371             =head2 $self->encrypt_payload(data => $data)
372              
373             Encrypts data using I and returns result as a string including
374             both cyphertext and hmac in base-64 format. This can work on arbitrary data
375             structures, scalars, and references provided that the data can be serialized
376             as an attribute on a JSON document.
377              
378             =cut
379              
380             sub _to_payload {
381 0     0     my ($data) = @_;
382             return {
383 0           crypt_json_payload => $data,
384             crypt_json_version => $VERSION,
385             };
386             }
387              
388             sub _from_payload {
389 0     0     my ($payload) = @_;
390 0 0         return unless defined $payload;
391 0 0         return $payload->{crypt_json_payload} if exists $payload->{crypt_json_payload};
392 0           return $payload;
393             }
394              
395             sub encrypt_payload {
396 0     0 1   my ($self, %args) = @_;
397 0           $args{data} = _to_payload($args{data});
398 0           my $enc = $self->encrypt_data(%args);
399 0           my $res = $enc->{data};
400 0           $res .= '.' . $enc->{mac};
401 0           return $res;
402             }
403              
404             =head2 $self->decrypt_payload(value => $value)
405              
406             Accepts payload encrypted with I, checks HMAC and decrypts
407             the value. Returns decripted value or undef if check or decryption has failed.
408              
409             =cut
410              
411             sub decrypt_payload {
412 0     0 1   my ($self, %args) = @_;
413 0 0         return unless $args{value}; # nothing to decrypt
414 0 0         $args{value} =~ /^([A-Za-z0-9+\/*]+[=-]*)\.([A-Za-z0-9+\/-=]+)$/ or return;
415 0           my ($data, $mac) = ($1, $2);
416 0           return _from_payload(
417             $self->decrypt_data(
418             data => $data,
419             mac => $mac,
420             ));
421             }
422              
423             1;