File Coverage

lib/Crypt/EAMessage.pm
Criterion Covered Total %
statement 155 161 96.2
branch 32 38 84.2
condition 17 21 80.9
subroutine 25 25 100.0
pod 7 7 100.0
total 236 252 93.6


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