@@ -21,6 +21,17 @@ use Symbol qw(delete_package);
21
21
use Time::HiRes ();
22
22
use Unicode::Normalize ();
23
23
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
+
24
35
# Check for monotonic clock support
25
36
use constant MONOTONIC => !!eval { Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) };
26
37
@@ -64,15 +75,15 @@ my $UNQUOTED_VALUE_RE = qr/\G=\s*([^;, ]*)/;
64
75
# HTML entities
65
76
my $ENTITY_RE = qr / &(?:\# ((?:[0-9]{1,7}|x[0-9a-fA-F]{1,6}));|(\w +[;=]?))/ ;
66
77
67
- # Encoding and pattern cache
68
- my (%ENCODING , %PATTERN );
78
+ # Encoding, encryption and pattern caches
79
+ my (%ENCODING , %ENCRYPTION , % PATTERN );
69
80
70
81
our @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)
76
87
);
77
88
78
89
# Aliases
@@ -115,6 +126,18 @@ sub decamelize {
115
126
} split /::/, $str ;
116
127
}
117
128
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
+
118
141
sub decode {
119
142
my ($encoding , $bytes ) = @_ ;
120
143
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)
130
153
131
154
sub encode { _encoding($_ [0])-> encode(" $_ [1]" , 0) }
132
155
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
+
133
167
sub extract_usage {
134
168
my $file = @_ ? " $_ [0]" : (caller )[1];
135
169
@@ -141,6 +175,12 @@ sub extract_usage {
141
175
return unindent($output );
142
176
}
143
177
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
+
144
184
sub getopt {
145
185
my ($array , $opts ) = map { ref $_ [0] eq ' ARRAY' ? shift : $_ } \@ARGV , [];
146
186
@@ -634,6 +674,13 @@ Convert C<CamelCase> string to C<snake_case> and replace C<::> with C<->.
634
674
635
675
Decode bytes to characters with L<Encode> , or return C<undef > if decoding failed.
636
676
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
+
637
684
=head2 deprecated
638
685
639
686
deprecated 'foo is DEPRECATED in favor of bar';
@@ -653,6 +700,12 @@ Dump a Perl data structure with L<Data::Dumper>.
653
700
654
701
Encode characters to bytes with L<Encode> .
655
702
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
+
656
709
=head2 extract_usage
657
710
658
711
my $usage = extract_usage;
@@ -670,6 +723,13 @@ function was called from.
670
723
671
724
=cut
672
725
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
+
673
733
=head2 getopt
674
734
675
735
getopt
0 commit comments