#!/usr/bin/perl

use strict;
use Test::More 'no_plan';
use Data::Dumper;
use Net::OpenID::Server;
use Crypt::DH;
use Digest::SHA1 qw(sha1 sha1_hex);

for (my $num=1; $num <= 2000; $num += 20) {
    my $bi = Math::BigInt->new("$num");
    my $bytes = Net::OpenID::Server::_bi2bytes($bi);
    my $bi2 = Net::OpenID::Server::_bytes2bi($bytes);
    is($bi,$bi2);
}


my ($query_string, %get, %post, $ctype, $content);
my $parse = sub {
    %get = map { durl($_) } split(/[&=]/, $query_string);
};
my %res;

my $nos = Net::OpenID::Server->new(
                                   get_args => \%get,
                                   post_args => \%post,
                                   server_secret => "o3kjn3nf9832hf32nfo32nfdo32nro32n29332",
                                   setup_url => "http://server.com/setup.app",
                                   );
ok($nos);
my ($secret, $ahandle);

assoc_clear();
login_success();

assoc_dh();
login_success();

login_im_fail();
login_setup_fail();
login_setup_fail2();

login_bogus_handle();

sub assoc_clear {
    %get = ();
    # regular associate
    %post = (
             "openid.mode" => "associate",
             "openid.assoc_type" => "HMAC-SHA1",
             );
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "text/plain");
    %res = parse_reply($content);
    ok($res{assoc_handle});
    $ahandle = $res{'assoc_handle'};
    ok($ahandle !~ /\bSTLS\./);
    is($res{assoc_type}, "HMAC-SHA1");
    ok(good_date($res{expiry}));
    ok(good_date($res{issued}));
    ok($res{mac_key});
    $secret = $res{'mac_key'};
}

# DH associate
sub assoc_dh {
    my $dh = Crypt::DH->new;
    $dh->p("155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443");
    $dh->g("2");
    $dh->generate_keys;
    %get = ();
    %post = (
             "openid.mode" => "associate",
             "openid.assoc_type" => "HMAC-SHA1",
             "openid.session_type" => "DH-SHA1",
             "openid.dh_consumer_public" => _bi2arg($dh->pub_key),
             );

    ($ctype, $content) = $nos->handle_page;
    is($ctype, "text/plain");
    %res = parse_reply($content);
    ok($res{assoc_handle});
    ok($res{dh_server_public});
    is($res{assoc_type}, "HMAC-SHA1");
    is($res{session_type}, "DH-SHA1");
    ok(good_date($res{expiry}));
    ok(good_date($res{issued}));
    ok($res{enc_mac_key});
    ok(! $res{mac_key});

    my $server_pub = _arg2bi($res{'dh_server_public'});
    my $dh_sec = $dh->compute_secret($server_pub);
    $ahandle = $res{'assoc_handle'};
    ok($ahandle !~ /\bSTLS\./);
    is(length(_d64($res{'enc_mac_key'})), 20);
    is(length(sha1(_bi2bytes($dh_sec))),  20);
    $secret = _d64($res{'enc_mac_key'}) ^ sha1(_bi2bytes($dh_sec));
    is(length($secret), 20);
}

# try to login, with success
sub login_success {
    $nos->is_identity(sub { 1; });
    $nos->is_trusted(sub { 1; });
    $nos->get_user(sub { "brad"; });
    %post = ();
    %get = (
            "openid.mode" => "checkid_immediate",
            "openid.identity" => "http://bradfitz.com/",
            "openid.return_to" => "http://trust.root/return/",
            "openid.trust_root" => "http://trust.root/",
            "openid.assoc_handle" => $ahandle,
            );
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "redirect");
    ok($content =~ s!^http://trust.root/return/\?!!);
    my %rarg = map { durl($_) } split(/[\&\=]/, $content);
    my $token = "";
    foreach my $p (split(/,/, $rarg{'openid.signed'})) {
        $token .= "$p:" . $rarg{"openid.$p"} . "\n";
    }
    my $good_sig = _b64(hmac_sha1($token, $secret));
    ok($rarg{'openid.sig'}, $good_sig);

    # and verify that check_authentication never lets this succeed
    %get = ();
    %post = (
             "openid.mode" => "check_authentication",
             );
    foreach my $p ("assoc_handle", "sig", "signed", "invalidate_handle",
                   split(/,/, $rarg{"openid.signed"}))
    {
        $post{"openid.$p"} ||= $rarg{"openid.$p"};
    }
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "text/plain");
    %rarg = parse_reply($content);
    ok($rarg{"error"} =~ /bad_handle/);
}

# try to login, with success
sub login_bogus_handle {
    $nos->is_identity(sub { 1; });
    $nos->is_trusted(sub { 1; });
    $nos->get_user(sub { "brad"; });
    %post = ();
    %get = (
            "openid.mode" => "checkid_immediate",
            "openid.identity" => "http://bradfitz.com/",
            "openid.return_to" => "http://trust.root/return/",
            "openid.trust_root" => "http://trust.root/",
            "openid.assoc_handle" => "GIBBERISH",
            );
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "redirect");
    ok($content =~ s!^http://trust.root/return/\?!!);
    my %rarg = map { durl($_) } split(/[\&\=]/, $content);
    is($rarg{'openid.invalidate_handle'}, "GIBBERISH");
    ok($rarg{'openid.assoc_handle'} =~ /\bSTLS\./);

    # try to verify it with check_authentication
    %get = ();
    %post = (
             "openid.mode" => "check_authentication",
             );
    foreach my $p ("assoc_handle", "sig", "signed", "invalidate_handle",
                   split(/,/, $rarg{"openid.signed"}))
    {
        $post{"openid.$p"} ||= $rarg{"openid.$p"};
    }
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "text/plain");
    %rarg = parse_reply($content);
    ok($rarg{"lifetime"} > 0);
    is($rarg{"invalidate_handle"}, "GIBBERISH");
}

# try to login, but fail (immediately)
sub login_im_fail {
    $nos->is_identity(sub { 0; });
    $nos->is_trusted(sub { 1; });
    $nos->get_user(sub { "brad"; });
    %post = ();
    %get = (
            "openid.mode" => "checkid_immediate",
            "openid.identity" => "http://bradfitz.com/",
            "openid.return_to" => "http://trust.root/return/",
            "openid.trust_root" => "http://trust.root/",
            "openid.assoc_handle" => $ahandle,
            );
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "redirect");
    ok($content =~ s!^http://trust.root/return/\?!!);
    my %rarg = map { durl($_) } split(/[\&\=]/, $content);

    is($rarg{'openid.mode'}, "id_res");
    ok($rarg{'openid.user_setup_url'} =~ m!setup\.app.+bradfitz!);
}

# try to login, but fail (w/ setup)
sub login_setup_fail {
    $nos->is_identity(sub { 0; });
    $nos->is_trusted(sub { 1; });
    $nos->get_user(sub { "brad"; });
    %post = ();
    %get = (
            "openid.mode" => "checkid_setup",
            "openid.identity" => "http://bradfitz.com/",
            "openid.return_to" => "http://trust.root/return/",
            "openid.trust_root" => "http://trust.root/",
            "openid.assoc_handle" => $ahandle,
            );
    ($ctype, $content) = $nos->handle_page;
    is($ctype, "setup");
    ok(ref $content eq "HASH");
}

# try to login, but fail (w/ setup redirect)
sub login_setup_fail2 {
    $nos->is_identity(sub { 0; });
    $nos->is_trusted(sub { 1; });
    $nos->get_user(sub { "brad"; });
    %post = ();
    %get = (
            "openid.mode" => "checkid_setup",
            "openid.identity" => "http://bradfitz.com/",
            "openid.return_to" => "http://trust.root/return/",
            "openid.trust_root" => "http://trust.root/",
            "openid.assoc_handle" => $ahandle,
            );
    ($ctype, $content) = $nos->handle_page(redirect_for_setup => 1);
    is($ctype, "redirect");
    ok($content =~ m!^http://.+setup\.app\?!);
}



sub good_date {
    return $_[0] =~ /^(\d{4,4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/;
}

sub parse_reply {
    my $reply = shift;
    my %ret;
    foreach (split /\n/, $reply) {
        next unless /^(\S+?):(.+)/;
        $ret{$1} = $2;
    }
    return %ret;
}

sub durl
{
    my ($a) = @_;
    $a =~ tr/+/ /;
    $a =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    return $a;
}

sub _bi2bytes {
    my $bigint = shift;
    die "Can't deal with negative numbers" if $bigint->is_negative;

    my $bits = $bigint->as_bin;
    die unless $bits =~ s/^0b//;

    # prepend zeros to round to byte boundary, or to unset high bit
    my $prepend = (8 - length($bits) % 8) || ($bits =~ /^1/ ? 8 : 0);
    $bits = ("0" x $prepend) . $bits if $prepend;

    return pack("B*", $bits);
}

sub _bi2arg {
    my $b64 = MIME::Base64::encode_base64(_bi2bytes($_[0]));
    $b64 =~ s/\s+//g;
    return $b64;
}

sub _b64 {
    my $val = MIME::Base64::encode_base64($_[0]);
    $val =~ s/\s+//g;
    return $val;
}

sub _d64 {
    return MIME::Base64::decode_base64($_[0]);
}

sub _bytes2bi {
    return Math::BigInt->new("0b" . unpack("B*", $_[0]));
}

sub _arg2bi {
    return undef unless defined $_[0] and $_[0] ne "";
    # don't acccept base-64 encoded numbers over 700 bytes.  which means
    # those over 4200 bits.
    return Math::BigInt->new("0") if length($_[0]) > 700;
    return _bytes2bi(MIME::Base64::decode_base64($_[0]));
}

# From Digest::HMAC
sub hmac_sha1_hex {
    unpack("H*", &hmac_sha1);
}
sub hmac_sha1 {
    hmac($_[0], $_[1], \&sha1, 64);
}
sub hmac {
    my($data, $key, $hash_func, $block_size) = @_;
    $block_size ||= 64;
    $key = &$hash_func($key) if length($key) > $block_size;

    my $k_ipad = $key ^ (chr(0x36) x $block_size);
    my $k_opad = $key ^ (chr(0x5c) x $block_size);

    &$hash_func($k_opad, &$hash_func($k_ipad, $data));
}
