]> git.sesse.net Git - www-csrf/blob - lib/WWW/CSRF.pm
202348bf671693472f8db5a0af1d4cd9e47fcea1
[www-csrf] / lib / WWW / CSRF.pm
1
2 use strict;
3 use warnings;
4 use Bytes::Random::Secure;
5 use Digest::HMAC_SHA1;
6
7 package WWW::CSRF;
8 require Exporter;
9 our @ISA = qw(Exporter);
10 our @EXPORT_OK = qw(generate_csrf_token check_csrf_token);
11 our $VERSION = '1.00';
12
13 sub generate_csrf_token {
14         my ($id, $secret, $random, $time) = @_;
15
16         $time //= time;
17
18         my $digest = Digest::HMAC_SHA1::hmac_sha1($time . "/" . $id, $secret);
19         my @digest_bytes = _to_byte_array($digest);
20
21         # Mask the token to avoid the BREACH attack.
22         if (!defined($random) || length($random) != length($digest)) {
23                 $random = Bytes::Random::Secure::random_bytes(scalar @digest_bytes);
24         }
25         my @random_bytes = _to_byte_array($random);
26         
27         my $masked_token = "";
28         my $mask = "";
29         for my $i (0..$#digest_bytes) {
30                 $masked_token .= sprintf "%02x", ($digest_bytes[$i] ^ $random_bytes[$i]);
31                 $mask .= sprintf "%02x", $random_bytes[$i];
32         }
33
34         return sprintf("%s,%s,%d", $masked_token, $mask, $time);
35 }
36
37 sub check_csrf_token {
38         my ($id, $secret, $csrf_token, $max_age) = @_;
39
40         if ($csrf_token !~ /^([0-9a-f]+),([0-9a-f]+),([0-9]+)$/) {
41                 # Malformed token.
42                 return 0;
43         }
44
45         my ($masked_token, $mask, $time) = ($1, $2, $3);
46         if (defined($max_age) && time - $time > $max_age) {
47                 # Timed out.
48                 return 0;
49         }
50
51         my @masked_bytes = _to_byte_array(pack('H*', $masked_token));
52         my @mask_bytes = _to_byte_array(pack('H*', $mask));
53
54         my $correct_token = Digest::HMAC_SHA1::hmac_sha1($time . '/' . $id, $secret);
55         my @correct_bytes = _to_byte_array($correct_token);
56
57         if ($#masked_bytes != $#mask_bytes || $#masked_bytes != $#correct_bytes) {
58                 # Malformed token (wrong number of characters).
59                 return 0;
60         }
61
62         # Compare in a way that should make timing attacks hard.
63         my $mismatches = 0;
64         for my $i (0..$#masked_bytes) {
65                 $mismatches += $masked_bytes[$i] ^ $mask_bytes[$i] ^ $correct_bytes[$i];
66         }
67         return ($mismatches == 0);
68 }
69
70 # Converts each byte in the given string to its numeric value,
71 # e.g., "ABCabc" becomes (65, 66, 67, 97, 98, 99).
72 sub _to_byte_array {
73         return unpack("C*", $_[0]);
74 }
75
76 1;