
use strict;
use warnings;
use Bytes::Random::Secure;
use Digest::HMAC_SHA1;

package WWW::CSRF;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(generate_csrf_token check_csrf_token);
our $VERSION = '1.00';

sub generate_csrf_token {
	my ($id, $secret) = @_;

	my $time = time;

	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.
	my $random = Bytes::Random::Secure::random_bytes(scalar @digest_bytes);
	my @random_bytes = _to_byte_array($random);
	
	my $masked_token = "";
	my $mask = "";
	for my $i (0..$#digest_bytes) {
		$masked_token .= sprintf "%02x", ($digest_bytes[$i] ^ $random_bytes[$i]);
		$mask .= sprintf "%02x", $random_bytes[$i];
	}

	return sprintf("%s,%s,%d", $masked_token, $mask, $time);
}

sub check_csrf_token {
	my ($id, $secret, $csrf_token, $max_age) = @_;

	if ($csrf_token !~ /^([0-9a-f]+),([0-9a-f]+),([0-9]+)$/) {
		# Malformed token.
		return 0;
	}

	my ($masked_token, $mask, $time) = ($1, $2, $3);
	if (defined($max_age) && time - $time > $max_age) {
		# Timed out.
		return 0;
	}

	my @masked_bytes = _to_byte_array(pack('H*', $masked_token));
	my @mask_bytes = _to_byte_array(pack('H*', $mask));

	my $correct_token = Digest::HMAC_SHA1::hmac_sha1($time . '/' . $id, $secret);
	my @correct_bytes = _to_byte_array($correct_token);

	if ($#masked_bytes != $#mask_bytes || $#masked_bytes != $#correct_bytes) {
		# Malformed token (wrong number of characters).
		return 0;
	}

	# Compare in a way that should make timing attacks hard.
	my $mismatches = 0;
	for my $i (0..$#masked_bytes) {
		$mismatches += $masked_bytes[$i] ^ $mask_bytes[$i] ^ $correct_bytes[$i];
	}
	return ($mismatches == 0);
}

# Converts each byte in the given string to its numeric value,
# e.g., "ABCabc" becomes (65, 66, 67, 97, 98, 99).
sub _to_byte_array {
	return unpack("C*", $_[0]);
}

1;
