File Coverage

lib/Crypt/EAMessage.pm
Criterion Covered Total %
statement 153 158 96.8
branch 29 34 85.2
condition 17 21 80.9
subroutine 25 25 100.0
pod 7 7 100.0
total 231 245 94.2


line stmt bran cond sub pod time code
1             #
2             # Copyright (C) 2015-2022 Joelle Maslak
3             # All Rights Reserved - See License
4             #
5              
6             package Crypt::EAMessage;
7             $Crypt::EAMessage::VERSION = '1.220391';
8 8     8   1298796 use v5.22;
  8         56  
9              
10             # ABSTRACT: Simple-to-use Abstraction of Encrypted Authenticated Messages
11              
12 8     8   40 use strict;
  8         12  
  8         139  
13 8     8   29 use warnings;
  8         11  
  8         153  
14 8     8   4254 use autodie;
  8         103712  
  8         37  
15              
16 8     8   46112 use feature "signatures";
  8         13  
  8         1129  
17              
18 8     8   52 use Carp;
  8         15  
  8         557  
19              
20 8     8   4724 use Moose;
  8         3204158  
  8         63  
21 8     8   51459 use Moose::Util::TypeConstraints;
  8         25  
  8         71  
22              
23 8     8   15231 no warnings "experimental::signatures";
  8         19  
  8         398  
24              
25 8     8   6321 use Bytes::Random::Secure;
  8         73239  
  8         455  
26 8     8   3194 use Crypt::AuthEnc::CCM qw(ccm_encrypt_authenticate ccm_decrypt_verify);
  8         20742  
  8         475  
27 8     8   49 use MIME::Base64 qw(encode_base64 decode_base64);
  8         15  
  8         336  
28 8     8   5169 use Storable qw(nfreeze thaw);
  8         23457  
  8         490  
29              
30 8     8   3455 use namespace::autoclean;
  8         58952  
  8         31  
31              
32              
33              
34             around 'BUILDARGS', sub ( $orig, $class, %args ) {
35             my (@only_one) = qw(raw_key hex_key);
36             my $cnt = 0;
37             foreach my $a (@only_one) {
38             if ( exists( $args{$a} ) ) {
39             $cnt++;
40             }
41             }
42             if ( $cnt > 1 ) { die("Must not have multiple *_key arguments"); }
43              
44             if ( exists( $args{hex_key} ) ) {
45             my $hex = $args{hex_key};
46             delete( $args{hex_key} );
47              
48             $args{raw_key} = _hex_to_raw($hex);
49             }
50              
51             $class->$orig(%args);
52             };
53              
54 35     35   44 sub _hex_to_raw ($hex) {
  35         42  
  35         64  
55 35         62 $hex =~ s/^0x//; # Remove 0x leader if it is present
56              
57 35 100       108 if ( $hex =~ /[^0-9A-Fa-f]/s ) { die("Non-hex characters present in hex_key"); }
  1         11  
58              
59 34         54 my $l = length($hex);
60 34 100 100     108 if ( ( $l != 32 ) && ( $l != 48 ) && ( $l != 64 ) ) {
      100        
61 2         17 die("hex_key is the wrong length");
62             }
63              
64 32         279 return pack( 'H*', $hex );
65             }
66              
67             subtype 'Crypt::EAMessage::Key', as 'Str',
68             where { _valid_key($_) },
69             message { "AES key lengths must be 16, 24, or 32 bytes long" };
70              
71 42     42   63 sub _valid_key ($key) {
  42         62  
  42         48  
72 42         56 my $l = length($_);
73              
74 42 100 100     143 if ( ( $l != 16 ) && ( $l != 24 ) && ( $l != 32 ) ) { return; }
  2   100     7  
75 40 100       104 if ( utf8::is_utf8($key) ) {
76 1         30 die("Key must not be UTF-8 encoded");
77             }
78              
79 39         70 return 1;
80             }
81              
82              
83             has 'raw_key' => (
84             is => 'rw',
85             isa => 'Crypt::EAMessage::Key',
86             required => 1,
87             );
88              
89              
90             sub hex_key {
91 18 50 33 18 1 3096 if ( ( scalar(@_) < 1 ) || ( scalar(@_) > 2 ) ) {
92 0         0 confess("Invalid call");
93             }
94              
95 18         28 my $self = shift;
96              
97 18 100       34 if ( scalar(@_) == 1 ) {
98             # Setter
99 5         11 $self->raw_key( _hex_to_raw(shift) );
100             }
101              
102 18         448 return unpack( 'H*', $self->raw_key() );
103             }
104              
105              
106 10     10 1 7527 sub encrypt_auth ( $self, $input ) {
  8         10  
  8         10  
  8         8  
107 8         18 my $ct = $self->_encrypt_auth_internal($input);
108 8         22 return "1$ct"; # Type 1 = Binary Format
109             }
110              
111              
112 14     14 1 499 sub encrypt_auth_ascii ( $self, $input, $eol = undef ) {
  12         18  
  12         14  
  12         15  
  12         12  
113 12         21 my $ct = $self->_encrypt_auth_internal($input);
114 12         37 my $base64 = encode_base64( $ct, $eol );
115 12         29 return "2$base64"; # Type 2 = Base 64
116             }
117              
118 26     26   30 sub _encrypt_auth_internal ( $self, $input, $opts = {} ) {
  26         29  
  26         28  
  26         37  
  26         27  
119 26         37 state $random = Bytes::Random::Secure->new( Bits => 1024, NonBlocking => 1 );
120              
121 26         167 for my $opt ( sort keys %$opts ) {
122 2 50       5 if ( $opt eq 'text' ) { next; }
  2         5  
123              
124 0         0 die("Unknown option to encrypt: $opt");
125             }
126              
127 26         61 my $nonce = $random->bytes(16);
128              
129 26         1408 my $data;
130 26 100 66     88 if ( ( !exists( $opts->{text} ) ) && ( !$opts->{text} ) ) {
131             # Any type of input
132 24         57 $data = nfreeze( \$input );
133             } else {
134             # Text only input
135 2         4 $data = $input;
136             }
137              
138 26         1369 my ( $enc, $tag ) =
139             ccm_encrypt_authenticate( 'AES', $self->raw_key(), $nonce, '', 128, $data );
140              
141 26         55 my $ct = $nonce . $tag . $enc;
142 26         56 return $ct;
143             }
144              
145              
146 6     6 1 465 sub encrypt_auth_urlsafe ( $self, $input ) {
  4         5  
  4         7  
  4         5  
147 4         6 my $ct = $self->_encrypt_auth_internal($input);
148              
149 4         11 my $urltext = encode_base64( $ct, "" );
150 4         9 $urltext =~ tr|\+/|-_|;
151              
152 4         12 return "3$urltext"; # Type 3 = Modified Base 64
153             }
154              
155              
156 4     4 1 481 sub encrypt_auth_portable ( $self, $input ) {
  2         3  
  2         3  
  2         2  
157 2         6 my $ct = $self->_encrypt_auth_internal( $input, { text => 1 } );
158              
159 2         8 my $urltext = encode_base64( $ct, "" );
160 2         4 $urltext =~ tr|\+/|-_|;
161              
162 2         6 return "4$urltext"; # Type 3 = Modified Base 64
163             }
164              
165              
166 46     46 1 25475 sub decrypt_auth ( $self, $ct ) {
  44         55  
  44         83  
  44         50  
167 44 50       94 if ( length($ct) < 34 ) { die("Message too short to be valid") }
  0         0  
168              
169 44         73 my $type = substr( $ct, 0, 1 );
170 44         67 my $enc = substr( $ct, 1 );
171              
172 44 100       119 if ( $type eq '1' ) {
    100          
    100          
    100          
173 17         32 return $self->_decrypt_auth_internal($enc);
174             } elsif ( $type eq '2' ) {
175 15         46 my $ascii = decode_base64($enc); # It's okay if this ignores bad base64,
176             # since we'll fail decryption.
177 15         33 return $self->_decrypt_auth_internal($ascii);
178             } elsif ( $type eq '3' ) {
179 6         17 $enc =~ tr|-_|+/|;
180 6         19 my $ascii = decode_base64($enc); # It's okay if this ignores bad base64,
181             # since we'll fail decryption.
182 6         15 return $self->_decrypt_auth_internal($ascii);
183             } elsif ( $type eq '4' ) {
184 2         4 $enc =~ tr|-_|+/|;
185 2         7 my $ascii = decode_base64($enc); # It's okay if this ignores bad base64,
186             # since we'll fail decryption.
187 2         8 return $self->_decrypt_auth_internal( $ascii, { text => 1 } );
188             } else {
189 4         27 die("Unsupported encoding type");
190             }
191             }
192              
193 40     40   49 sub _decrypt_auth_internal ( $self, $ct, $opts = {} ) {
  40         45  
  40         48  
  40         54  
  40         65  
194 40 50       68 if ( length($ct) < 32 ) { die("Message too short to be valid") }
  0         0  
195              
196 40         116 for my $opt ( sort keys %$opts ) {
197 2 50       15 if ( $opt eq 'text' ) { next; }
  2         5  
198              
199 0         0 die("Unknown option to decrypt: $opt");
200             }
201              
202 40         76 my $nonce = substr( $ct, 0, 16 );
203 40         53 my $tag = substr( $ct, 16, 16 );
204 40         55 my $enc = substr( $ct, 32 );
205              
206 40         1094 my $frozen = ccm_decrypt_verify( 'AES', $self->raw_key(), $nonce, '', $enc, $tag );
207 40 100       114 if ( !defined($frozen) ) { die("Could not decrypt message") }
  12         85  
208              
209 28 100 66     105 if ( ( !exists( $opts->{text} ) ) && ( !$opts->{text} ) ) {
210             # Perl 5 data structure
211 26         74 my $plaintext = thaw($frozen);
212 26         459 return $$plaintext;
213             } else {
214             # Plain text
215 2         8 return $frozen;
216             }
217             }
218              
219              
220 5     5 1 821 sub generate_key ($self) {
  4         7  
  4         5  
221 4         14 return Bytes::Random::Secure::random_bytes_hex(32);
222             }
223              
224             __PACKAGE__->meta->make_immutable;
225              
226             1;
227              
228             __END__
229              
230             =pod
231              
232             =encoding UTF-8
233              
234             =head1 NAME
235              
236             Crypt::EAMessage - Simple-to-use Abstraction of Encrypted Authenticated Messages
237              
238             =head1 VERSION
239              
240             version 1.220391
241              
242             =head1 SYNOPSIS
243              
244             use Crypt::EAMessage;
245              
246             my $eamsg = Crypt::EAMessage->new( hex_key => $hex );
247              
248             $encrypted = $eamsg->encrypt_auth($input);
249             $enc_ascii = $eamsg->encrypt_auth_ascii($input);
250             $enc_url = $eamsg->encrypt_auth_urlsafe($input);
251             $enc_portable = $eamsg->encrypt_auth_portable($input); # Input must be text
252              
253             $decrypted = $eamsg->decrypt_auth($encrypted);
254              
255             =head1 DESCRIPTION
256              
257             This module provides an easy-to-use method to create encrypted and
258             authenticated messages from arbitrary Perl objects (anything compatible
259             with L<Storable>).
260              
261             While there are many modules that encrypt text, there are many less that
262             provide encryption and authentication without a complex interface. This
263             module uses AES encryption in CCM mode. This allows two parties to
264             communicate securely, provided they both use the same secret key. In
265             addition to providing privacy, this module also ensures that the message
266             was created by someone who had knowledge of the private key - in otherwords
267             the message was also not tampered with in-transit.
268              
269             When encrypting, this module produces a message that contains the
270             message's nonce (a unique value that changes the results of the encryption
271             so two identical messages will be encrypted differently), the authentication
272             tag (used to authenticate the message), and the cipher text. It can be
273             formatted in either a "printable" base 64 encoding or in raw binary form.
274              
275             =head1 ATTRIBUTES
276              
277             =head2 raw_key
278              
279             This is the key used for encryption/decryption (a string of 16, 24, or 32
280             bytes). Note that the size of the key determines the strength of the AES
281             encryption - a 16 byte string uses AES-128, 24 uses AES-192, 32 uses
282             AES-256.
283              
284             =head2 hex_key
285              
286             This is the hex version of the key. This should consist of a string
287             of 32, 48, or 64 hex digits (creating a 16, 24, or 32 byte key).
288              
289             =head1 METHODS
290              
291             =head2 new
292              
293             my $eamsg = Crypt::EAMessage->new( raw_key => $key );
294              
295             or
296              
297             my $eamsg = Crypt::EAMessage->new( hex_key => $hex );
298              
299             Create a new workunit class. It takes either a C<raw_key> or a C<hex_key>
300             parameter. See the C<raw_key> and C<hex_key> attributes.
301              
302             =head2 encrypt_auth
303              
304             my $ciphertext = $ea->encrypt_auth( $plaintext );
305              
306             Encrypts the plain text (or any other Perl object that C<Storable> can
307             freeze and thaw) passed as a parameter, generating a binary (non-printable)
308             cipher text output.
309              
310             =head2 encrypt_auth_ascii
311              
312             my $ciphertext = $ea->encrypt_auth_ascii( $plaintext );
313             my $ciphertext = $ea->encrypt_auth_ascii( $plaintext, "" );
314              
315             Encrypts the plain text (or any other Perl object that C<Storable> can
316             freeze and thaw) passed as a parameter, generating an ASCII (base64)
317             cipher text output.
318              
319             Starting in version 1.004, a second, optional, argument is allowed.
320             If an argument after C<$plaintext> is supplied, that becomes the line ending
321             for the output text. If no argument is provided, a standard newline
322             appropriate to the platform is used. Otherwise, the value of that string
323             is used as the line ending, in the same way as it would be if passed as
324             the L<MIME::Base64::encode_base64> function's second argument.
325              
326             Note that when using line endings other than a blank ending (no line ending)
327             or a standard newline, you should strip the new line identifier from the
328             cypertext before calling the L<decrypt_auth_ascii> method.
329              
330             =head2 encrypt_auth_urlsafe
331              
332             my $ciphertext = $ea->encrypt_auth_urlsafe( $plaintext );
333              
334             Added in version 1.006.
335              
336             Encrypts the plain text (or any other Perl object that C<Storable> can
337             freeze and thaw) passed as a parameter, generating an ASCII (modified
338             base64) cipher text output. This output is safe to pass as part of a
339             query string or URL. Namely, it doesn't use the standard Base 64
340             characters C<+> or C</>, replacing them with C<-> and C<_> respectively.
341             In addition, the cyphertext output will start with a "3" rather than the
342             "2" that the base 64 variant starts with.
343              
344             =head2 encrypt_auth_portable
345              
346             my $ciphertext = $ea->encrypt_auth_portable( $plaintext );
347              
348             Added in version 1.190900
349              
350             Encrypts the plain text (or byte string) passed as a parameter, generating
351             an ASCII (modified base64) cipher text output. This output is safe to pass
352             as part of a query string or URL. Namely, it doesn't use the standard Base 64
353             characters C<+> or C</>, replacing them with C<-> and C<_> respectively.
354             In addition, the cyphertext output will start with a "4".
355              
356             This is intended for cross-language compatibility, so it does not utilize
357             store/thaw.
358              
359             SECURITY NOTE: The contents of a zero length string can be determined from
360             the length of the encrypted portable message.
361              
362             =head2 decrypt_auth
363              
364             my $plaintext = $ea->decrypt_auth( $ciphertext );
365              
366             Decrypts the cipher text into the object that was frozen during encryption.
367              
368             If the authentication or decryption fails, an exception is thrown. Otherwise
369             it returns the plaintext/object.
370              
371             =head2 generate_key
372              
373             say "Hex key: " . Crypt::EAMessage->generate_key()
374              
375             Added in version 1.220390
376              
377             This is a class method (I.E. you do not need to instantiate the
378             C<Crypt::EAMessage> class to use this).
379              
380             Returns a randomly generated key suitable to use with AES256 as a hex number.
381              
382             =head1 GENERATING AES256 KEYS
383              
384             To generate a key, a simple Perl program can accomplish this - note that you
385             should NOT use standard C<rand()> to do this.
386              
387             use feature 'say';
388             use Crypt::EAMessage;
389              
390             my $hexkey = Crypt::EAMessage->generate_key()
391             say "Key is: $hexkey";
392              
393             Alternative, you can do this with a one-liner to return a hex key, and the
394             L<Crypt::EAMessage::Keygen> module:
395              
396             perl -MCrypt::EAMessage::Keygen -e 1
397              
398             This will output a random key in hex format suitable for use as an AES256 key.
399              
400             =head1 SECURITY
401              
402             Note that this module use L<Storable>. Thus this module should only be used
403             when the endpoint is trusted. This module will ensure that the stored
404             object is received without tampering by an intermediary (and is secure even
405             when an untrusted third party can modify the encrypted message in transit),
406             because C<thaw> is not called unless the message passes authentication
407             checks. But if an endpoint can create a malicious message using a valid
408             key, it is possible that this message could exploit some vulnerability in
409             the L<Storable> module.
410              
411             This module does not protect against replay attacks.
412              
413             This module is not protected against timing attacks.
414              
415             =head1 ALTERNATIVES
416              
417             This module implements a tiny subset of the functionality in L<Crypt::Util>
418             which may be a better choice for more complex use cases.
419              
420             =head1 BUGS
421              
422             None known, however it is certainly possible that I am less than perfect!
423             If you find any bug you believe has security implications, I would
424             greatly appreciate being notified via email sent to jmaslak@antelope.net
425             prior to public disclosure. In the event of such notification, I will
426             attempt to work with you to develop a plan for fixing the bug.
427              
428             All other bugs can be reported via email to jmaslak@antelope.net or by
429             using the Git Hub issue tracker
430             at L<https://github.com/jmaslak/Crypt-EAMessage/issues>
431              
432             =head1 AUTHOR
433              
434             Joelle Maslak <jmaslak@antelope.net>
435              
436             =head1 COPYRIGHT AND LICENSE
437              
438             This software is copyright (c) 2019-2022 by Joelle Maslak.
439              
440             This is free software; you can redistribute it and/or modify it under
441             the same terms as the Perl 5 programming language system itself.
442              
443             =cut