package Crypt::OdinAuth;

use 5.006;
use strict;
use warnings;

=head1 NAME

Crypt::OdinAuth - Cryptographic calculations for the OdinAuth SSO system

=head1 VERSION

Version 0.2

=cut

our $VERSION = '0.2';

use Digest;
use Digest::HMAC;
use Digest::SHA256;
use MIME::Base64 qw(encode_base64url decode_base64url);

use constant OLD_COOKIE => 24*60*60; # cookie older than 24h is discarded

=head1 SYNOPSIS

This module exports functions for calculating and verifying signed
cookies for OdinAuth SSO Apache handler.

    use Crypt::OdinAuth;

    Crypt::OdinAuth::hmac_for('secret', 'login_name', 'role1,role2,role3', 1337357387, 'netcat')
    #=> '349b7135f43bd4c0111564960e7d9d583dde0c5c'

    Crypt::OdinAuth::cookie_for('secret', 'login_name', 'role1,role2,role3', 'netcat')
    #=> 'login_name-role1,role2,role3-1337357638-7ec415a6816c8e9dab7b788e1262769ef80af7d8'

=head1 SUBROUTINES

=head2 hmac_for(secret, user, roles, timestamp, user_agent)

=cut
sub hmac_for ($$$$$) {
    my ( $secret, $user, $roles, $ts, $ua ) = @_;
    my $hmac = Digest::HMAC->new($secret, Digest->new("SHA-256"));

    if ($ua =~ /AppleWebKit/) {
        $ua = "StupidAppleWebkitHacksGRRR";
    }
    $ua =~ s/ FirePHP\/\d+\.\d+//;

    $hmac->add(join(',',
                    encode_base64url($user),
                    encode_base64url($roles),
                    $ts,
                    encode_base64url($ua)));
    return $hmac->hexdigest;
}

=head2 cookie_for(secret, user, roles, user_agent)

=cut

sub cookie_for {
    my ( $secret, $user, $roles, $ua, $ts ) = @_;
    $ts = time() unless defined($ts);

    my $hmac = hmac_for($secret, $user, $roles, $ts, $ua);
    return join(',',
                encode_base64url($user),
                encode_base64url($roles),
                $ts,
                $hmac);
}

=head2 check_cookie(secret, cookie, user_agent)

=cut

sub check_cookie ($$$) {
    my ( $secret, $cookie, $ua ) = @_;

    $cookie =~ /^\s*([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+),(\d+),([0-9a-f]+)\s*$/
      or die "Wrong cookie format\n";

    my $user = decode_base64url($1);
    my $roles = decode_base64url($2);
    my $ts = $3;
    my $hmac = $4;

    # Use double hmac to prevent timing attacks
    # http://codahale.com/a-lesson-in-timing-attacks/
    # http://www.isecpartners.com/blog/2011/2/18/double-hmac-verification.html
    my $hmac_received   = Digest::HMAC->new($secret, Digest->new("SHA-256"));
    my $hmac_calculated = Digest::HMAC->new($secret, Digest->new("SHA-256"));

    $hmac_received->add($hmac);
    $hmac_calculated->add(hmac_for($secret, $user, $roles, $ts, $ua));

    die "Invalid signature\n"
      if ( $hmac_received->digest ne $hmac_calculated->digest );

    die "Cookie is old\n"
        if ( $ts < time() - OLD_COOKIE );

    die "Cookie is in future\n"
        if ( $ts > time() + 5*60 );

    return $user, $roles;
}

=head1 AUTHOR

Maciej Pasternacki, C<< <maciej at pasternacki.net> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-apache2-authen-odinauth at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Apache2-Authen-OdinAuth>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.




=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Crypt::OdinAuth


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Apache2-Authen-OdinAuth>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Apache2-Authen-OdinAuth>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Apache2-Authen-OdinAuth>

=item * Search CPAN

L<http://search.cpan.org/dist/Apache2-Authen-OdinAuth/>

=back


=head1 ACKNOWLEDGEMENTS


=head1 LICENSE AND COPYRIGHT

Copyright 2012 Maciej Pasternacki.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.


=cut

1; # End of Crypt::OdinAuth
