]> git.sesse.net Git - www-csrf/blobdiff - lib/WWW/CSRF.pm
Update MANIFEST with t/02_check.t.
[www-csrf] / lib / WWW / CSRF.pm
index 202348bf671693472f8db5a0af1d4cd9e47fcea1..095073382ec5d9486c50075f0f3933241a3e268d 100644 (file)
@@ -1,26 +1,35 @@
+package WWW::CSRF;
 
 use strict;
 use warnings;
 use Bytes::Random::Secure;
 use Digest::HMAC_SHA1;
+use constant {
+       CSRF_OK => 0,
+       CSRF_MALFORMED_TOKEN => 1,
+       CSRF_INVALID_SIGNATURE => 2,
+       CSRF_EXPIRED => 3,
+};
 
-package WWW::CSRF;
 require Exporter;
 our @ISA = qw(Exporter);
-our @EXPORT_OK = qw(generate_csrf_token check_csrf_token);
+our @EXPORT_OK = qw(generate_csrf_token check_csrf_token CSRF_OK CSRF_MALFORMED_TOKEN CSRF_INVALID_SIGNATURE CSRF_EXPIRED);
 our $VERSION = '1.00';
 
 sub generate_csrf_token {
-       my ($id, $secret, $random, $time) = @_;
+       my ($id, $secret, $options) = @_;
 
-       $time //= time;
+       my $time = $options->{'Time'} // time;
+       my $random = $options->{'Random'};
 
        my $digest = Digest::HMAC_SHA1::hmac_sha1($time . "/" . $id, $secret);
        my @digest_bytes = _to_byte_array($digest);
 
        # Mask the token to avoid the BREACH attack.
-       if (!defined($random) || length($random) != length($digest)) {
+       if (!defined($random)) {
                $random = Bytes::Random::Secure::random_bytes(scalar @digest_bytes);
+       } elsif (length($random) != length($digest)) {
+               die "Given randomness is of the wrong length (should be " . length($digest) . " bytes)";
        }
        my @random_bytes = _to_byte_array($random);
        
@@ -35,17 +44,18 @@ sub generate_csrf_token {
 }
 
 sub check_csrf_token {
-       my ($id, $secret, $csrf_token, $max_age) = @_;
+       my ($id, $secret, $csrf_token, $options) = @_;
 
        if ($csrf_token !~ /^([0-9a-f]+),([0-9a-f]+),([0-9]+)$/) {
-               # Malformed token.
-               return 0;
+               return CSRF_MALFORMED_TOKEN;
        }
 
+       my $ref_time = $options->{'Time'} // time;
+
        my ($masked_token, $mask, $time) = ($1, $2, $3);
-       if (defined($max_age) && time - $time > $max_age) {
-               # Timed out.
-               return 0;
+       my $max_age = $options->{'MaxAge'};
+       if (defined($max_age) && $ref_time - $time > $max_age) {
+               return CSRF_EXPIRED;
        }
 
        my @masked_bytes = _to_byte_array(pack('H*', $masked_token));
@@ -56,7 +66,7 @@ sub check_csrf_token {
 
        if ($#masked_bytes != $#mask_bytes || $#masked_bytes != $#correct_bytes) {
                # Malformed token (wrong number of characters).
-               return 0;
+               return CSRF_MALFORMED_TOKEN;
        }
 
        # Compare in a way that should make timing attacks hard.
@@ -64,7 +74,11 @@ sub check_csrf_token {
        for my $i (0..$#masked_bytes) {
                $mismatches += $masked_bytes[$i] ^ $mask_bytes[$i] ^ $correct_bytes[$i];
        }
-       return ($mismatches == 0);
+       if ($mismatches == 0) {
+               return CSRF_OK;
+       } else {
+               return CSRF_INVALID_SIGNATURE;
+       }
 }
 
 # Converts each byte in the given string to its numeric value,