<?php declare(strict_types=1);

/**
 * This file is part of the NetDNS2 package.
 *
 * (c) Mike Pultz <mike@mikepultz.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 */

namespace NetDNS2;

/**
 * SSL Private Key container class
 *
 */
final class PrivateKey
{
    /**
     * the filename that was loaded; stored for reference
     */
    public string $filename;

    /**
     * the keytag for the signature
     */
    public int $keytag;

    /**
     * the sign name for the signature
     */
    public \NetDNS2\Data\Domain $signname;

    /**
     * the algorithm used for the signature
     */
    public \NetDNS2\ENUM\DNSSEC\Algorithm $algorithm;

    /**
     * the key format of the signature
     */
    public int $key_format;

    /**
     * the openssl private key id
     */
    public \OpenSSLAsymmetricKey $instance;

    /**
     * RSA: modulus
     */
    private string $m_modulus;

    /**
     * RSA: public exponent
     */
    private string $m_public_exponent;

    /**
     * RSA: rivate exponent
     */
    private string $m_private_exponent;

    /**
     * RSA: prime1
     */
    private string $m_prime1;

    /**
     * RSA: prime2
     */
    private string $m_prime2;

    /**
     * RSA: exponent 1
     */
    private string $m_exponent1;

    /**
     * RSA: exponent 2
     */
    private string $m_exponent2;

    /**
     * RSA: coefficient
     */
    private string $m_coefficient;

    /**
     * DSA: prime
     */
    public string $prime;

    /**
     * DSA: subprime
     */
    public string $subprime;

    /**
     * DSA: base
     */
    public string $base;

    /**
     * DSA: private value
     */
    public string $private_value;

    /**
     * DSA: public value
     */
    public string $public_value;

    /**
     * Constructor - base constructor the private key container class
     *
     * @param string $_file path to a private-key file to parse and load
     *
     * @throws \NetDNS2\Exception
     *
     */
    public function __construct(?string $_file = null)
    {
        if (is_null($_file) == false)
        {
            $this->parseFile($_file);
        }
    }

    /**
     * parses a private key file generated by dnssec-keygen
     *
     * @param string $_file path to a private-key file to parse and load
     *
     * @throws \NetDNS2\Exception
     *
     */
    public function parseFile(string $_file): bool
    {
        //
        // check for OpenSSL
        //
        if (extension_loaded('openssl') === false)
        {
            throw new \NetDNS2\Exception('the openssl extension is required to parse private key.', \NetDNS2\ENUM\Error::INT_INVALID_EXTENSION);
        }

        //
        // check to make sure the file exists
        //
        $file = realpath($_file);

        if ( ($file == false) || (is_readable($file) == false) )
        {
            throw new \NetDNS2\Exception(sprintf('invalid private key file: %s', $_file), \NetDNS2\ENUM\Error::INT_INVALID_PRIVATE_KEY);
        }

        //
        // get the base filename, and parse it for the local value
        //
        $keyname = basename($file);

        if (strlen($keyname) == 0)
        {
            throw new \NetDNS2\Exception(sprintf('failed to get basename() for: %s', $file), \NetDNS2\ENUM\Error::INT_PARSE_ERROR);
        }

        //
        // parse the keyname
        //
        if (preg_match("/K(.*)\.\+(\d{3})\+(\d*)\.private/", $keyname, $matches) == 1)
        {
            $this->signname  = new \NetDNS2\Data\Domain(\NetDNS2\Data::DATA_TYPE_RFC2535, $matches[1]);
            $this->algorithm = \NetDNS2\ENUM\DNSSEC\Algorithm::set(intval($matches[2]));
            $this->keytag    = intval($matches[3]);

        } else
        {
            throw new \NetDNS2\Exception(sprintf('file %s does not look like a private key file!', $keyname), \NetDNS2\ENUM\Error::INT_INVALID_PRIVATE_KEY);
        }

        //
        // read all the data from the
        //
        $data = file($file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
        if ( ($data === false) || (count($data) == 0) )
        {
            throw new \NetDNS2\Exception(sprintf('file %s is empty.', $keyname), \NetDNS2\ENUM\Error::INT_INVALID_PRIVATE_KEY);
        }

        foreach((array)$data as $line)
        {
            list($key, $value) = explode(' ', $line, 3);

            $key   = strtolower(trim(rtrim($key, ':')));
            $value = trim($value);

            switch($key)
            {
                case 'private-key-format':
                {
                    $this->key_format = intval($value);
                }
                break;
                case 'algorithm':
                {
                    if ($this->algorithm->value != $value)
                    {
                        throw new \NetDNS2\Exception(sprintf('algorithm mis-match! filename is %s, contents say %s', $this->algorithm->value, $value), \NetDNS2\ENUM\Error::INT_INVALID_ALGORITHM);
                    }
                }
                break;

                //
                // RSA
                //
                case 'modulus':
                {
                    $this->m_modulus = $value;
                }
                break;
                case 'publicexponent':
                {
                    $this->m_public_exponent = $value;
                }
                break;
                case 'privateexponent':
                {
                    $this->m_private_exponent = $value;
                }
                break;
                case 'prime1':
                {
                    $this->m_prime1 = $value;
                }
                break;
                case 'prime2':
                {
                    $this->m_prime2 = $value;
                }
                break;
                case 'exponent1':
                {
                    $this->m_exponent1 = $value;
                }
                break;
                case 'exponent2':
                {
                    $this->m_exponent2 = $value;
                }
                break;
                case 'coefficient':
                {
                    $this->m_coefficient = $value;
                }
                break;

                //
                // DSA - this won't work in PHP until the OpenSSL extension is better
                //
                case 'prime(p)':
                {
                    $this->prime = $value;
                }
                break;
                case 'subprime(q)':
                {
                    $this->subprime = $value;
                }
                break;
                case 'base(g)':
                {
                    $this->base = $value;
                }
                break;
                case 'private_value(x)':
                {
                    $this->private_value = $value;
                }
                break;
                case 'public_value(y)':
                {
                    $this->public_value = $value;
                }
                break;

                default:
                {
                    //TODO: throw new \NetDNS2\Exception(sprintf('unknown private key data: %s: %s', $key, $value), \NetDNS2\ENUM\Error::INT_INVALID_PRIVATE_KEY);
                }
            }
        }

        //
        // generate the private key
        //
        $args = [];

        switch($this->algorithm)
        {
            //
            // RSA
            //
            case \NetDNS2\ENUM\DNSSEC\Algorithm::RSAMD5:
            case \NetDNS2\ENUM\DNSSEC\Algorithm::RSASHA1:
            case \NetDNS2\ENUM\DNSSEC\Algorithm::RSASHA256:
            case \NetDNS2\ENUM\DNSSEC\Algorithm::RSASHA512:
            {
                $args = [

                    'rsa' => [

                        'n'     => base64_decode($this->m_modulus),
                        'e'     => base64_decode($this->m_public_exponent),
                        'd'     => base64_decode($this->m_private_exponent),
                        'p'     => base64_decode($this->m_prime1),
                        'q'     => base64_decode($this->m_prime2),
                        'dmp1'  => base64_decode($this->m_exponent1),
                        'dmq1'  => base64_decode($this->m_exponent2),
                        'iqmp'  => base64_decode($this->m_coefficient)
                    ]
                ];
            }
            break;

            //
            // DSA
            //
            case \NetDNS2\ENUM\DNSSEC\Algorithm::DSA:
            {
                $args = [

                    'dsa' => [

                        'p'         => base64_decode($this->prime),
                        'q'         => base64_decode($this->subprime),
                        'g'         => base64_decode($this->base),
                        'priv_key'  => base64_decode($this->private_value),
                        'pub_key'   => base64_decode($this->public_value)
                    ]
                ];
            }
            break;
            default:
            {
                throw new \NetDNS2\Exception('we only currently support RSAMD5 and RSASHA1 encryption.', \NetDNS2\ENUM\Error::INT_INVALID_PRIVATE_KEY);
            }
        }

        //
        // generate and store the key
        //
        $instance = openssl_pkey_new($args);
        if ($instance === false)
        {
            throw new \NetDNS2\Exception(sprintf('openssl exception: %s', strval(openssl_error_string())), \NetDNS2\ENUM\Error::INT_FAILED_OPENSSL);
        }

        $this->instance = $instance;

        //
        // store the filename incase we need it for something
        //
        $this->filename = $file;

        return true;
    }
}
