@@ -21,6 +21,17 @@ use Symbol qw(delete_package);
2121use Time::HiRes ();
2222use Unicode::Normalize ();
2323
24+ # Encryption support requires CryptX 0.080+
25+ use constant CRYPTX => $ENV {MOJO_NO_CRYPTX } ? 0 : !!(eval {
26+ require CryptX;
27+ require Crypt::AuthEnc::ChaCha20Poly1305;
28+ require Crypt::KeyDerivation;
29+ require Crypt::Misc;
30+ require Crypt::PRNG;
31+ CryptX-> VERSION(' 0.080' );
32+ 1;
33+ });
34+
2435# Check for monotonic clock support
2536use constant MONOTONIC => !!eval { Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) };
2637
@@ -64,15 +75,15 @@ my $UNQUOTED_VALUE_RE = qr/\G=\s*([^;, ]*)/;
6475# HTML entities
6576my $ENTITY_RE = qr / &(?:\# ((?:[0-9]{1,7}|x[0-9a-fA-F]{1,6}));|(\w +[;=]?))/ ;
6677
67- # Encoding and pattern cache
68- my (%ENCODING , %PATTERN );
78+ # Encoding, encryption and pattern caches
79+ my (%ENCODING , %ENCRYPTION , % PATTERN );
6980
7081our @EXPORT_OK = (
71- qw( b64_decode b64_encode camelize class_to_file class_to_path decamelize decode deprecated dumper encode ) ,
72- qw( extract_usage getopt gunzip gzip header_params hmac_sha1_sum html_attr_unescape html_unescape humanize_bytes ) ,
73- qw( md5_bytes md5_sum monkey_patch network_contains punycode_decode punycode_encode quote scope_guard secure_compare ) ,
74- qw( sha1_bytes sha1_sum slugify split_cookie_header split_header steady_time tablify term_escape trim unindent ) ,
75- qw( unquote url_escape url_unescape xml_escape xor_encode)
82+ qw( b64_decode b64_encode camelize class_to_file class_to_path decamelize decode decrypt_cookie deprecated dumper) ,
83+ qw( encode encrypt_cookie extract_usage generate_secret getopt gunzip gzip header_params hmac_sha1_sum) ,
84+ qw( html_attr_unescape html_unescape humanize_bytes md5_bytes md5_sum monkey_patch network_contains punycode_decode) ,
85+ qw( punycode_encode quote scope_guard secure_compare sha1_bytes sha1_sum slugify split_cookie_header split_header) ,
86+ qw( steady_time tablify term_escape trim unindent unquote url_escape url_unescape xml_escape xor_encode)
7687);
7788
7889# Aliases
@@ -115,6 +126,18 @@ sub decamelize {
115126 } split /::/, $str ;
116127}
117128
129+ sub decrypt_cookie {
130+ my ($value , $key , $salt ) = @_ ;
131+ croak ' CryptX 0.080+ required for encrypted cookie support' unless CRYPTX;
132+
133+ return undef unless $value =~ / ^([^-]+)-([^-]+)-([^-]+)$ / ;
134+ my ($ct , $iv , $tag ) = ($1 , $2 , $3 );
135+ ($ct , $iv , $tag ) = (Crypt::Misc::decode_b64($ct ), Crypt::Misc::decode_b64($iv ), Crypt::Misc::decode_b64($tag ));
136+
137+ my $dk = $ENCRYPTION {$key }{$salt } ||= Crypt::KeyDerivation::pbkdf2($key , $salt );
138+ return Crypt::AuthEnc::ChaCha20Poly1305::chacha20poly1305_decrypt_verify($dk , $iv , ' ' , $ct , $tag );
139+ }
140+
118141sub decode {
119142 my ($encoding , $bytes ) = @_ ;
120143 return undef unless eval { $bytes = _encoding($encoding )-> decode(" $bytes " , 1); 1 };
@@ -130,6 +153,17 @@ sub dumper { Data::Dumper->new([@_])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)
130153
131154sub encode { _encoding($_ [0])-> encode(" $_ [1]" , 0) }
132155
156+ sub encrypt_cookie {
157+ my ($value , $key , $salt ) = @_ ;
158+ croak ' CryptX 0.080+ required for encrypted cookie support' unless CRYPTX;
159+
160+ my $dk = $ENCRYPTION {$key }{$salt } ||= Crypt::KeyDerivation::pbkdf2($key , $salt );
161+ my $iv = Crypt::PRNG::random_bytes(12);
162+ my ($ct , $tag ) = Crypt::AuthEnc::ChaCha20Poly1305::chacha20poly1305_encrypt_authenticate($dk , $iv , ' ' , $value );
163+
164+ return join ' -' , Crypt::Misc::encode_b64($ct ), Crypt::Misc::encode_b64($iv ), Crypt::Misc::encode_b64($tag );
165+ }
166+
133167sub extract_usage {
134168 my $file = @_ ? " $_ [0]" : (caller )[1];
135169
@@ -141,6 +175,12 @@ sub extract_usage {
141175 return unindent($output );
142176}
143177
178+ sub generate_secret {
179+ return Crypt::Misc::encode_b64u(Crypt::PRNG::random_bytes(128)) if CRYPTX;
180+ srand ;
181+ return sha1_sum($$ . steady_time() . rand );
182+ }
183+
144184sub getopt {
145185 my ($array , $opts ) = map { ref $_ [0] eq ' ARRAY' ? shift : $_ } \@ARGV , [];
146186
@@ -634,6 +674,13 @@ Convert C<CamelCase> string to C<snake_case> and replace C<::> with C<->.
634674
635675Decode bytes to characters with L<Encode> , or return C<undef > if decoding failed.
636676
677+ =head2 decrypt_cookie
678+
679+ my $value = decrypt_cookie $encrypted, 'passw0rd', 'salt';
680+
681+ Decrypt cookie value encrypted with L</encrypt_cookie> , returns the decrypted value or C<undef > . Note that this
682+ function is B<EXPERIMENTAL > and might change without warning!
683+
637684=head2 deprecated
638685
639686 deprecated 'foo is DEPRECATED in favor of bar';
@@ -653,6 +700,12 @@ Dump a Perl data structure with L<Data::Dumper>.
653700
654701Encode characters to bytes with L<Encode> .
655702
703+ =head2 encrypt_cookie
704+
705+ my $encrypted = encrypt_cookie $value, 'passw0rd', 'salt';
706+
707+ Encrypt cookie value. Note that this function is B<EXPERIMENTAL > and might change without warning!
708+
656709=head2 extract_usage
657710
658711 my $usage = extract_usage;
@@ -670,6 +723,13 @@ function was called from.
670723
671724 =cut
672725
726+ =head2 generate_secret
727+
728+ my $secret = generate_secret;
729+
730+ Generate a random secret with a cryptographically secure random number generator if available, and a less secure
731+ fallback if not. Note that this function is B<EXPERIMENTAL > and might change without warning!
732+
673733=head2 getopt
674734
675735 getopt
0 commit comments