package PVE::Ticket;

use strict;
use warnings;

use Crypt::OpenSSL::Random;
use Crypt::OpenSSL::RSA;
use MIME::Base64;
use Digest::SHA;
use Time::HiRes qw(gettimeofday);
use URI::Escape;

use PVE::Exception qw(raise);

Crypt::OpenSSL::RSA->import_random_seed();

use constant HTTP_UNAUTHORIZED => 401;

sub assemble_csrf_prevention_token {
    my ($secret, $username) = @_;

    my $timestamp = sprintf("%08X", time());

    my $digest = Digest::SHA::hmac_sha256_base64("$timestamp:$username", $secret);

    return "$timestamp:$digest";
}

sub verify_csrf_prevention_token {
    my ($secret, $username, $token, $min_age, $max_age, $noerr) = @_;

    if ($token =~ m/^([A-Z0-9]{8}):(\S+)$/) {
	my $sig = $2;
	my $timestamp = $1;
	my $ttime = hex($timestamp);

	my $digest;
	if (length($sig) == 27) {
	    # detected sha1 csrf token from older proxy, fallback. FIXME: remove with 7.0
	    $digest = Digest::SHA::sha1_base64("$timestamp:$username", $secret);
	} else {
	    $digest = Digest::SHA::hmac_sha256_base64("$timestamp:$username", $secret);
	}

	my $age = time() - $ttime;
	return 1 if ($digest eq $sig) && ($age > $min_age) &&
	    ($age < $max_age);
    }

    raise("Permission denied - invalid csrf token\n", code => HTTP_UNAUTHORIZED)
	if !$noerr;

    return undef;
}

# Note: data may not contain white spaces (verify fails in that case)
sub assemble_rsa_ticket {
    my ($rsa_priv, $prefix, $data, $secret_data) = @_;

    my $timestamp = sprintf("%08X", time());

    my $plain = "$prefix:";

    if (defined($data)) {
	$data = uri_escape($data, ':');
	$plain .= "$data:";
    }

    $plain .= $timestamp;

    my $full = defined($secret_data) ? "$plain:$secret_data" : $plain;

    my $ticket = $plain . "::" . encode_base64($rsa_priv->sign($full), '');

    return $ticket;
}

sub verify_rsa_ticket {
    my ($rsa_pub, $prefix, $ticket, $secret_data, $min_age, $max_age, $noerr) = @_;

    if ($ticket && $ticket =~ m/^(\Q$prefix\E:\S+)::([^:\s]+)$/) {
	my $plain = $1;
	my $sig = $2;

	my $full = defined($secret_data) ? "$plain:$secret_data" : $plain;

	if ($rsa_pub->verify($full, decode_base64($sig))) {
	    if ($plain =~ m/^\Q$prefix\E:(?:(\S+):)?([A-Z0-9]{8})$/) {
		my $data = $1; # Note: not all tickets contains data
		my $timestamp = $2;
		my $ttime = hex($timestamp);

		my $age = time() - $ttime;

		if (defined($data)) {
		    $data = uri_unescape($data);
		}

		if (($age > $min_age) && ($age < $max_age)) {
		    if (defined($data)) {
			return wantarray ? ($data, $age) : $data;
		    } else {
			return wantarray ? (1, $age) : 1;
		    }
		}
	    }
	}
    }

    raise("permission denied - invalid $prefix ticket\n", code => HTTP_UNAUTHORIZED)
	if !$noerr;

    return undef;
}

sub assemble_spice_ticket {
    my ($secret, $username, $vmid, $node) = @_;

    my ($seconds, $microseconds) = gettimeofday;

    my $timestamp = sprintf("%08x", $seconds);

    my $randomstr = "PVESPICE:$timestamp:$username:$vmid:$node:$secret:" .
	':' . sprintf("%08x", $microseconds) .
	':' . sprintf("%08x", $$) .
	':' . rand(1);

    # this should be used as one-time password
    # max length is 60 chars (spice limit)
    # we pass this to qemu set_pasword and limit lifetime there
    # keep this secret
    my $ticket = Digest::SHA::sha1_hex($randomstr);

    # Note: spice proxy connects with HTTP, so $proxyticket is exposed to public
    # we use a signature/timestamp to make sure nobody can fake such a ticket
    # an attacker can use this $proxyticket, but he will fail because $ticket is
    # private.
    # The proxy needs to be able to extract/verify the ticket
    # Note: data needs to be lower case only, because virt-viewer needs that
    # Note: RSA signature are too long (>=256 charaters) and make problems with remote-viewer

    my $plain = "pvespiceproxy:${timestamp}:${vmid}:" . lc($node);

    # produces 40 characters
    my $sig = unpack("H*", Digest::SHA::sha1($plain, $secret));

    #my $sig =  unpack("H*", $rsa_priv->sign($plain)); # this produce too long strings (512)

    my $proxyticket = "${plain}::${sig}";

    return ($ticket, $proxyticket);
}

sub verify_spice_connect_url {
    my ($secret, $connect_str) = @_;

    # Note: we pass the spice ticket as 'host', so the
    # spice viewer connects with "$ticket:$port"

    return undef if !$connect_str;

    if ($connect_str =~m/^pvespiceproxy:([a-z0-9]{8}):(\d+):(\S+)::([a-z0-9]{40}):(\d+)$/) {
	my ($timestamp, $vmid, $node, $hexsig, $port) = ($1, $2, $3, $4, $5, $6);
	my $ttime = hex($timestamp);
	my $age = time() - $ttime;

	# use very limited lifetime - is this enough?
	return undef if !(($age > -20) && ($age < 40));

	my $plain = "pvespiceproxy:$timestamp:$vmid:$node";
	my $sig = unpack("H*", Digest::SHA::sha1($plain, $secret));

	if ($sig eq $hexsig) {
	    return ($vmid, $node, $port);
	}
    }

    return undef;
}

1;
