<?php

namespace Buckhill\U2F;

use Buckhill\U2F\Authenticate\Response;
use Buckhill\U2F\Exceptions\ReplayException;
use Buckhill\U2F\Exceptions\InvalidChallengeException;
use Buckhill\U2F\Exceptions\InvalidKeyHandleException;
use Buckhill\U2F\Exceptions\InvalidSignatureException;

class U2FKey
{
    protected $app_id;
    protected $key_handle;
    protected $version;
    protected $challenge;
    protected $public_key;
    protected $counter = 0;
    protected $error;

    public function __construct($app_id, $key_handle, $version, $challenge = null)
    {
        $this->app_id = $app_id;
        $this->key_handle = $key_handle;
        $this->version = $version;
        $this->challenge = $challenge;
    }


    /**
     * @return array
     */
    public function toArray()
    {
        return [
            'appId' => $this->app_id,
            'keyHandle' => $this->key_handle,
            'version' => $this->version,
            'challenge' => $this->challenge
        ];
    }


    /**
     * @param array|null $additional
     *
     * @return string
     */
    public function toJSON(array $additional = null)
    {
        $json = !empty($additional) ?
            array_merge($this->toArray(), $additional)
            :
            $this->toArray()
        ;

        $result = json_encode($json, JSON_UNESCAPED_SLASHES);

        if(json_last_error())
        {
            $this->error = json_last_error_msg();
        }

        return $result;
    }


    /**
     * @return mixed
     */
    public function getLastError()
    {
        return $this->error;
    }


    /**
     * @return mixed
     */
    public function getAppId()
    {
        return $this->app_id;
    }


    /**
     * @return mixed
     */
    public function getKeyHandle()
    {
        return $this->key_handle;
    }


    /**
     * @return mixed
     */
    public function getVersion()
    {
        return $this->version;
    }


    /**
     * @return null
     */
    public function getChallenge()
    {
        return $this->challenge;
    }


    /**
     * @param array $input
     *
     * @return static
     */
    public static function fromArray(array $input)
    {
        static::validate($input);

        $key = new static($input['appId'], $input['keyHandle'], $input['version'], isset($input['challenge']) ? $input['challenge'] : null);

        if(isset($input['publicKey']))
        {
            $key->setPublicKey($input['publicKey']);
        }

        if(isset($input['counter']))
        {
            $key->setCounter($input['counter']);
        }

        return $key;
    }


    /**
     * Parameter must be base64 encoded public key. That's what should be stored in the KSM
     *
     * @param $public_key
     *
     * @return $this
     */
    public function setPublicKey($public_key)
    {
        $key = U2FHelper::pub2pem(U2FHelper::decode($public_key));

        if(empty($key))
        {
            throw new \InvalidArgumentException("Invalid public key specified.");
        }

        $this->public_key = $key;

        return $this;
    }


    /**
     * @return mixed
     */
    public function getPublicKey()
    {
        return $this->public_key;
    }


    /**
     * @param int $counter
     *
     * @return $this
     */
    public function setCounter($counter = 0)
    {
        if(!is_int($counter))
        {
            throw new \InvalidArgumentException(sprintf("Counter must be an integer, got: %s", gettype($counter)));
        }

        if($counter < 0)
        {
            throw new \InvalidArgumentException(sprintf("Counter can't be less than 0. Got: %d", $counter));
        }

        $this->counter = $counter;

        return $this;
    }


    /**
     * @return int
     */
    public function getCounter()
    {
        return $this->counter;
    }

    /**
     * @param string $challenge
     *
     * @return $this
     */
    public function setChallenge($challenge = '')
    {
        $this->challenge = $challenge;

        return $this;
    }


    /**
     * @return U2FKey
     */
    public function createChallenge()
    {
        $challenge = substr(base64_encode(random_bytes(32)), 0, 32);

        return $this->setChallenge($challenge);
    }


    /**
     * @param Response $response
     *
     * @return U2FKey
     * @throws InvalidChallengeException
     * @throws InvalidKeyHandleException
     * @throws InvalidSignatureException
     * @throws ReplayException
     */
    public function verifyAgainst(Response $response)
    {
        if(!$this->compareKeyHandle($response))
        {
            throw new InvalidKeyHandleException(sprintf("Invalid key handle. Expected: %s. Received: %s", $this->getKeyHandle(), $response->getKeyHandle()));
        }

        if(!$this->compareChallenge($response))
        {
            throw new InvalidChallengeException(sprintf("Invalid challenge. Expected %s. Received: %s", $this->getChallenge(), $response->getChallenge()));
        }

        if(!$this->verifySignature($response))
        {
            throw new InvalidSignatureException(sprintf("Invalid U2F signature."));
        }

        if(!$this->checkCounter($response))
        {
            throw new ReplayException(sprintf("Replay detected. Counter value in permanent storage: %s. Counter value provided in the U2F response: %s", $this->getCounter(), $response->getCounter()));
        }

        return $this->setCounter($response->getCounter());
    }


    /**
     * @param Response $response
     *
     * @return bool
     */
    protected function compareKeyHandle(Response $response)
    {
        return 0 === strcmp($this->getKeyHandle(), $response->getKeyHandle());
    }


    /**
     * @param Response $response
     *
     * @return bool
     */
    protected function compareChallenge(Response $response)
    {
        return 0 === strcmp($this->getChallenge(), $response->getChallenge());
    }


    /**
     * @param Response $response
     *
     * @return bool
     */
    protected function verifySignature(Response $response)
    {
        // Signature in the HTTP response
        $http_response_signature = U2FHelper::decode($response->getSignatureData());

        $data_to_verify = hash('sha256', $this->getAppId(), true);
        $data_to_verify .= substr($http_response_signature, 0, 5);
        $data_to_verify .= hash('sha256', $response->getSignatureJSON(), true);

        $signature = substr($http_response_signature, 5);

        $result = openssl_verify($data_to_verify, $signature, $this->getPublicKey(), 'sha256') === 1;

        if($result)
        {
            $response->setCounterFromSignature($http_response_signature);
        }

        return $result;
    }


    /**
     * @param Response $response
     *
     * @return bool
     */
    protected function checkCounter(Response $response)
    {
        return $this->getCounter() < $response->getCounter();
    }


    /**
     * @param array $input
     */
    protected static function validate(array $input)
    {
        $required = ['appId', 'keyHandle', 'version'];

        foreach($required as $key)
        {
            if(!isset($input[$key]))
            {
                throw new \InvalidArgumentException(sprintf("Required key (%s) is not present in the argument.", $key));
            }
        }
    }
}