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.220390'; # TRIAL
8 8     8   1076292 use v5.22;
  8         51  
9              
10             # ABSTRACT: Simple-to-use Abstraction of Encrypted Authenticated Messages
11              
12 8     8   34 use strict;
  8         14  
  8         125  
13 8     8   29 use warnings;
  8         9  
  8         151  
14 8     8   2488 use autodie;
  8         77001  
  8         33  
15              
16 8     8   39321 use feature "signatures";
  8         18  
  8         924  
17              
18 8     8   46 use Carp;
  8         15  
  8         566  
19              
20 8     8   4408 use Moose;
  8         2902362  
  8         51  
21 8     8   45859 use Moose::Util::TypeConstraints;
  8         16  
  8         66  
22              
23 8     8   13795 no warnings "experimental::signatures";
  8         14  
  8         338  
24              
25 8     8   4817 use Bytes::Random::Secure;
  8         64425  
  8         424  
26 8     8   3071 use Crypt::AuthEnc::CCM qw(ccm_encrypt_authenticate ccm_decrypt_verify);
  8         18599  
  8         440  
27 8     8   53 use MIME::Base64 qw(encode_base64 decode_base64);
  8         12  
  8         348  
28 8     8   4229 use Storable qw(nfreeze thaw);
  8         21117  
  8         465  
29              
30 8     8   3269 use namespace::autoclean;
  8         50099  
  8         30  
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   41 sub _hex_to_raw ($hex) {
  35         44  
  35         40  
55 35         58 $hex =~ s/^0x//; # Remove 0x leader if it is present
56              
57 35 100       101 if ( $hex =~ /[^0-9A-Fa-f]/s ) { die("Non-hex characters present in hex_key"); }
  1         7  
58              
59 34         46 my $l = length($hex);
60 34 100 100     111 if ( ( $l != 32 ) && ( $l != 48 ) && ( $l != 64 ) ) {
      100        
61 2         18 die("hex_key is the wrong length");
62             }
63              
64 32         257 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   59 sub _valid_key ($key) {
  42         58  
  42         42  
72 42         56 my $l = length($_);
73              
74 42 100 100     118 if ( ( $l != 16 ) && ( $l != 24 ) && ( $l != 32 ) ) { return; }
  2   100     5  
75 40 100       94 if ( utf8::is_utf8($key) ) {
76 1         39 die("Key must not be UTF-8 encoded");
77             }
78              
79 39         63 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 2797 if ( ( scalar(@_) < 1 ) || ( scalar(@_) > 2 ) ) {
92 0         0 confess("Invalid call");
93             }
94              
95 18         29 my $self = shift;
96              
97 18 100       28 if ( scalar(@_) == 1 ) {
98             # Setter
99 5         11 $self->raw_key( _hex_to_raw(shift) );
100             }
101              
102 18         366 return unpack( 'H*', $self->raw_key() );
103             }
104              
105              
106 10     10 1 5735 sub encrypt_auth ( $self, $input ) {
  8         11  
  8         9  
  8         9  
107 8         21 my $ct = $self->_encrypt_auth_internal($input);
108 8         23 return "1$ct"; # Type 1 = Binary Format
109             }
110              
111              
112 14     14 1 438 sub encrypt_auth_ascii ( $self, $input, $eol = undef ) {
  12         13  
  12         13  
  12         15  
  12         12  
113 12         19 my $ct = $self->_encrypt_auth_internal($input);
114 12         37 my $base64 = encode_base64( $ct, $eol );
115 12         32 return "2$base64"; # Type 2 = Base 64
116             }
117              
118 26     26   55 sub _encrypt_auth_internal ( $self, $input, $opts = {} ) {
  26         27  
  26         28  
  26         30  
  26         25  
119 26         38 state $random = Bytes::Random::Secure->new( Bits => 1024, NonBlocking => 1 );
120              
121 26         165 for my $opt ( sort keys %$opts ) {
122 2 50       5 if ( $opt eq 'text' ) { next; }
  2         4  
123              
124 0         0 die("Unknown option to encrypt: $opt");
125             }
126              
127 26         69 my $nonce = $random->bytes(16);
128              
129 26         1369 my $data;
130 26 100 66     89 if ( ( !exists( $opts->{text} ) ) && ( !$opts->{text} ) ) {
131             # Any type of input
132 24         54 $data = nfreeze( \$input );
133             } else {
134             # Text only input
135 2         3 $data = $input;
136             }
137              
138 26         1521 my ( $enc, $tag ) =
139             ccm_encrypt_authenticate( 'AES', $self->raw_key(), $nonce, '', 128, $data );
140              
141 26         56 my $ct = $nonce . $tag . $enc;
142 26         56 return $ct;
143             }
144              
145              
146 6     6 1 442 sub encrypt_auth_urlsafe ( $self, $input ) {
  4         7  
  4         5  
  4         5  
147 4         8 my $ct = $self->_encrypt_auth_internal($input);
148              
149 4         12 my $urltext = encode_base64( $ct, "" );
150 4         10 $urltext =~ tr|\+/|-_|;
151              
152 4         11 return "3$urltext"; # Type 3 = Modified Base 64
153             }
154              
155              
156 4     4 1 421 sub encrypt_auth_portable ( $self, $input ) {
  2         4  
  2         3  
  2         2  
157 2         6 my $ct = $self->_encrypt_auth_internal( $input, { text => 1 } );
158              
159 2         7 my $urltext = encode_base64( $ct, "" );
160 2         5 $urltext =~ tr|\+/|-_|;
161              
162 2         8 return "4$urltext"; # Type 3 = Modified Base 64
163             }
164              
165              
166 46     46 1 23987 sub decrypt_auth ( $self, $ct ) {
  44         52  
  44         50  
  44         44  
167 44 50       87 if ( length($ct) < 34 ) { die("Message too short to be valid") }
  0         0  
168              
169 44         66 my $type = substr( $ct, 0, 1 );
170 44         71 my $enc = substr( $ct, 1 );
171              
172 44 100       117 if ( $type eq '1' ) {
    100          
    100          
    100          
173 17         36 return $self->_decrypt_auth_internal($enc);
174             } elsif ( $type eq '2' ) {
175 15         48 my $ascii = decode_base64($enc); # It's okay if this ignores bad base64,
176             # since we'll fail decryption.
177 15         28 return $self->_decrypt_auth_internal($ascii);
178             } elsif ( $type eq '3' ) {
179 6         16 $enc =~ tr|-_|+/|;
180 6         20 my $ascii = decode_base64($enc); # It's okay if this ignores bad base64,
181             # since we'll fail decryption.
182 6         14 return $self->_decrypt_auth_internal($ascii);
183             } elsif ( $type eq '4' ) {
184 2         3 $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         7 return $self->_decrypt_auth_internal( $ascii, { text => 1 } );
188             } else {
189 4         23 die("Unsupported encoding type");
190             }
191             }
192              
193 40     40   40 sub _decrypt_auth_internal ( $self, $ct, $opts = {} ) {
  40         39  
  40         42  
  40         76  
  40         76  
194 40 50       63 if ( length($ct) < 32 ) { die("Message too short to be valid") }
  0         0  
195              
196 40         97 for my $opt ( sort keys %$opts ) {
197 2 50       6 if ( $opt eq 'text' ) { next; }
  2         3  
198              
199 0         0 die("Unknown option to decrypt: $opt");
200             }
201              
202 40         59 my $nonce = substr( $ct, 0, 16 );
203 40         71 my $tag = substr( $ct, 16, 16 );
204 40         52 my $enc = substr( $ct, 32 );
205              
206 40         1040 my $frozen = ccm_decrypt_verify( 'AES', $self->raw_key(), $nonce, '', $enc, $tag );
207 40 100       95 if ( !defined($frozen) ) { die("Could not decrypt message") }
  12         87  
208              
209 28 100 66     112 if ( ( !exists( $opts->{text} ) ) && ( !$opts->{text} ) ) {
210             # Perl 5 data structure
211 26         70 my $plaintext = thaw($frozen);
212 26         478 return $$plaintext;
213             } else {
214             # Plain text
215 2         8 return $frozen;
216             }
217             }
218              
219              
220 5     5 1 717 sub generate_key ($self) {
  4         10  
  4         4  
221 4         17 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.220390
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