4 use Bytes::Random::Secure;
9 our @ISA = qw(Exporter);
10 our @EXPORT_OK = qw(generate_csrf_token check_csrf_token);
11 our $VERSION = '1.00';
13 sub generate_csrf_token {
14 my ($id, $secret, $options) = @_;
16 my $time = $options->{'Time'} // time;
17 my $random = $options->{'Random'};
19 my $digest = Digest::HMAC_SHA1::hmac_sha1($time . "/" . $id, $secret);
20 my @digest_bytes = _to_byte_array($digest);
22 # Mask the token to avoid the BREACH attack.
23 if (!defined($random)) {
24 $random = Bytes::Random::Secure::random_bytes(scalar @digest_bytes);
25 } elsif (length($random) != length($digest)) {
26 die "Given randomness is of the wrong length (should be " . length($digest) . " bytes)";
28 my @random_bytes = _to_byte_array($random);
30 my $masked_token = "";
32 for my $i (0..$#digest_bytes) {
33 $masked_token .= sprintf "%02x", ($digest_bytes[$i] ^ $random_bytes[$i]);
34 $mask .= sprintf "%02x", $random_bytes[$i];
37 return sprintf("%s,%s,%d", $masked_token, $mask, $time);
40 sub check_csrf_token {
41 my ($id, $secret, $csrf_token, $options) = @_;
43 if ($csrf_token !~ /^([0-9a-f]+),([0-9a-f]+),([0-9]+)$/) {
48 my $ref_time = $options->{'Time'} // time;
50 my ($masked_token, $mask, $time) = ($1, $2, $3);
51 my $max_age = $options->{'MaxAge'};
52 if (defined($max_age) && $ref_time - $time > $max_age) {
57 my @masked_bytes = _to_byte_array(pack('H*', $masked_token));
58 my @mask_bytes = _to_byte_array(pack('H*', $mask));
60 my $correct_token = Digest::HMAC_SHA1::hmac_sha1($time . '/' . $id, $secret);
61 my @correct_bytes = _to_byte_array($correct_token);
63 if ($#masked_bytes != $#mask_bytes || $#masked_bytes != $#correct_bytes) {
64 # Malformed token (wrong number of characters).
68 # Compare in a way that should make timing attacks hard.
70 for my $i (0..$#masked_bytes) {
71 $mismatches += $masked_bytes[$i] ^ $mask_bytes[$i] ^ $correct_bytes[$i];
73 return ($mismatches == 0);
76 # Converts each byte in the given string to its numeric value,
77 # e.g., "ABCabc" becomes (65, 66, 67, 97, 98, 99).
79 return unpack("C*", $_[0]);