<?php

/**
 * EC Private Key
 *
 * @author    Jim Wigginton <terrafrost@php.net>
 * @copyright 2019-2026 Jim Wigginton
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
 * @link      https://phpseclib.com/
 */

declare(strict_types=1);

namespace phpseclib4\Crypt\EC;

use phpseclib4\Common\Functions\Strings;
use phpseclib4\Crypt\{Common, EC, Hash};
use phpseclib4\Crypt\EC\BaseCurves\{Montgomery as MontgomeryCurve, TwistedEdwards as TwistedEdwardsCurve};
use phpseclib4\Crypt\EC\Curves\{Curve25519, Ed25519};
use phpseclib4\Crypt\EC\Formats\Keys\PKCS1;
use phpseclib4\Crypt\EC\Formats\Signature\ASN1 as ASN1Signature;
use phpseclib4\Exception\{BadConfigurationException, BadMethodCallException, UnexpectedValueException};
use phpseclib4\File\Common\Signable;
use phpseclib4\File\CSR;
use phpseclib4\Math\BigInteger;

/**
 * EC Private Key
 *
 * @author  Jim Wigginton <terrafrost@php.net>
 */
final class PrivateKey extends EC implements Common\PrivateKey
{
    use Common\Traits\PasswordProtected;

    /**
     * Private Key dA
     *
     * sign() converts this to a BigInteger so one might wonder why this is a FiniteFieldInteger instead of
     * a BigInteger. That's because a FiniteFieldInteger, when converted to a byte string, is null padded by
     * a certain amount whereas a BigInteger isn't.
     */
    protected BigInteger $dA;

    protected ?string $secret = null;

    /**
     * Multiplies an encoded point by the private key
     *
     * Used by ECDH
     */
    public function multiply(string $coordinates): string
    {
        if (self::$forcedEngine === 'OpenSSL') {
            throw new BadConfigurationException('Engine OpenSSL is not supported for the multiplication operation');
        }

        if (self::$forcedEngine === 'libsodium' && !$this->curve instanceof Curve25519) {
            throw new BadConfigurationException('Engine libsodium is only supported for Curve25519');
        }

        if ($this->curve instanceof Curve25519 && self::$forcedEngine !== 'PHP') {
            if (self::$forcedEngine === 'libsodium' && !function_exists('sodium_crypto_scalarmult')) {
                throw new BadConfigurationException('Engine libsodium is forced but unsupported for Curve25519');
            }
            if (function_exists('sodium_crypto_scalarmult')) {
                $dA = str_pad($this->dA->toBytes(), 32, "\0", STR_PAD_LEFT);
                return sodium_crypto_scalarmult($dA, $coordinates);
            }
        }

        if ($this->curve instanceof MontgomeryCurve) {
            $point = [$this->curve->convertInteger(new BigInteger(strrev($coordinates), 256))];
            $point = $this->curve->multiplyPoint($point, $this->dA);
            return strrev($point[0]->toBytes(true));
        }

        if (!$this->curve instanceof TwistedEdwardsCurve) {
            $coordinates = "\0$coordinates";
        }

        $point = PKCS1::extractPoint($coordinates, $this->curve);
        $point = $this->curve->multiplyPoint($point, $this->dA);
        if ($this->curve instanceof TwistedEdwardsCurve) {
            return $this->curve->encodePoint($point);
        }
        if (empty($point)) {
            throw new UnexpectedValueException('The infinity point is invalid');
        }
        return "\4" . $point[0]->toBytes(true) . $point[1]->toBytes(true);
    }

    /**
     * Create a signature
     *
     * @see self::verify()
     */
    public function sign(string|Signable $source): string|array
    {
        if ($this->curve instanceof MontgomeryCurve) {
            throw new BadMethodCallException('Montgomery Curves cannot be used to create signatures');
        }

        if ($source instanceof Signable) {
            $public = $this->getPublicKey();
            if ($source instanceof CSR && !$source->hasPublicKey()) {
                $source->setPublicKey($public);
            }
            $source->identifySignatureAlgorithm($this);
            $message = $source->getSignableSection();
        } else {
            $message = $source;
        }

        $dA = $this->dA;
        $order = $this->curve->getOrder();

        $shortFormat = $this->shortFormat;

        if (self::$forcedEngine === 'libsodium' && !$this->curve instanceof Ed25519) {
            throw new BadConfigurationException('Engine libsodium is only supported for Ed25519');
        }

        // at this point either self::$forcedEngine is NOT libsodium or the curve is Ed25519

        if ($this->curve instanceof Ed25519 && self::$forcedEngine !== 'PHP' && self::$forcedEngine !== 'OpenSSL') {
            if (self::$forcedEngine === 'libsodium') {
                if (!function_exists('sodium_crypto_sign_detached')) {
                    throw new BadConfigurationException('Engine libsodium is forced but unsupported for Ed25519 / Ed448');
                }
                if (isset($this->context)) {
                    throw new BadConfigurationException('Engine libsodium is forced but unsupported for Ed25519ctx (context)');
                }
            }
            if (function_exists('sodium_crypto_sign_detached') && !isset($this->context)) {
                $result = sodium_crypto_sign_detached($message, $this->withPassword()->toString('libsodium'));
                $signature = $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result) : $result;
                if ($source instanceof Signable) {
                    $source->setSignature($signature);
                }
                return $signature;
            }
        }

        // at this point self::$forcedEngine CAN'T be libsodium so we won't check for it henceforth

        if ($this->curve instanceof TwistedEdwardsCurve) {
            if (self::$forcedEngine !== 'PHP') {
                $keyTypeConstant = $this->curve instanceof Ed25519 ? 'OPENSSL_KEYTYPE_ED25519' : 'OPENSSL_KEYTYPE_ED448';
                if (self::$forcedEngine === 'OpenSSL') {
                    if (!defined($keyTypeConstant)) {
                        throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for Ed25519 / Ed448');
                    }
                    // OpenSSL supports Ed25519/Ed448 but not Ed25519ctx (context), so skip if context is set
                    if (isset($this->context)) {
                        throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for Ed25519 / Ed448 curves with context\'s');
                    }
                }
                if (defined($keyTypeConstant) && !isset($this->context)) {
                    $result = '';
                    // algorithm 0 is used because EdDSA has a built-in hash
                    openssl_sign($message, $result, $this->withPassword()->toString('PKCS8'), 0);
                    if ($result) {
                        $signature = $shortFormat == 'SSH2'
                            ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result)
                            : $result;
                        if ($source instanceof Signable) {
                            $source->setSignature($signature);
                        }
                        return $signature;
                    }
                    if (self::$forcedEngine === 'OpenSSL') {
                        throw new BadConfigurationException('Engine OpenSSL is forced but was unable to create signature because of ' . openssl_error_string());
                    }
                }
            }

            // contexts (Ed25519ctx) are supported but prehashing (Ed25519ph) is not.
            // quoting https://tools.ietf.org/html/rfc8032#section-8.5 ,
            // "The Ed25519ph and Ed448ph variants ... SHOULD NOT be used"
            $A = $this->curve->encodePoint($this->QA);
            $curve = $this->curve;
            $hash = new Hash($curve::HASH);

            $secret = substr($hash->hash($this->secret), $curve::SIZE);

            if ($curve instanceof Ed25519) {
                $dom = !isset($this->context) ? '' :
                    'SigEd25519 no Ed25519 collisions' . "\0" . chr(strlen($this->context)) . $this->context;
            } else {
                $context = $this->context ?? '';
                $dom = 'SigEd448' . "\0" . chr(strlen($context)) . $context;
            }
            // SHA-512(dom2(F, C) || prefix || PH(M))
            $r = $hash->hash($dom . $secret . $message);
            $r = strrev($r);
            $r = new BigInteger($r, 256);
            [, $r] = $r->divide($order);
            $R = $curve->multiplyPoint($curve->getBasePoint(), $r);
            $R = $curve->encodePoint($R);
            $k = $hash->hash($dom . $R . $A . $message);
            $k = strrev($k);
            $k = new BigInteger($k, 256);
            [, $k] = $k->divide($order);
            $S = $k->multiply($dA)->add($r);
            [, $S] = $S->divide($order);
            $S = str_pad(strrev($S->toBytes()), $curve::SIZE, "\0");
            $signature = $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $R . $S) : $R . $S;
            if ($source instanceof Signable) {
                $source->setSignature($signature);
            }
            return $signature;
        }

        if (self::$forcedEngine === 'OpenSSL' && !function_exists('openssl_get_md_methods')) {
            throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for ECDSA');
        }

        // at this point $forcedEngine is either PHP or null. either that OR openssl_get_md_methods() exists

        if (self::$forcedEngine !== 'PHP') {
            if (in_array($this->hash->getHash(), openssl_get_md_methods())) {
                $signature = '';
                // altho PHP's OpenSSL bindings only supported EC key creation in PHP 7.1 they've long
                // supported signing / verification
                // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve;
                // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even
                // has curve-specific optimizations
                $result = openssl_sign($message, $signature, $this->withPassword()->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash());

                if ($result) {
                    if ($shortFormat == 'ASN1') {
                        if ($source instanceof Signable) {
                            $source->setSignature($signature);
                        }
                        return $signature;
                    }

                    ['r' => $r, 's' => $s] = ASN1Signature::load($signature);
                    $signature = $this->formatSignature($r, $s);

                    if ($source instanceof Signable) {
                        $source->setSignature($signature);
                    }

                    return $signature;
                } elseif (self::$forcedEngine === 'OpenSSL') {
                    throw new BadConfigurationException('Engine OpenSSL is forced but was unable to create signature because of ' . openssl_error_string());
                }
            } elseif (self::$forcedEngine === 'OpenSSL') {
                throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for ECDSA / ' . $this->hash->getHash());
            }
        }

        $e = $this->hash->hash($message);
        $e = new BigInteger($e, 256);

        $Ln = $this->hash->getLength() - $order->getLength();
        $z = $Ln > 0 ? $e->bitwise_rightShift($Ln) : $e;

        while (true) {
            $k = BigInteger::randomRange(self::$one, $order->subtract(self::$one));
            // multiplyPoint() always returns [$x, $y]; only $x is needed here
            [$x, ] = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k);
            $x = $x->toBigInteger();
            [, $r] = $x->divide($order);
            if ($r->equals(self::$zero)) {
                continue;
            }
            $kinv = $k->modInverse($order);
            $temp = $z->add($dA->multiply($r));
            $temp = $kinv->multiply($temp);
            [, $s] = $temp->divide($order);
            if (!$s->equals(self::$zero)) {
                break;
            }
        }

        // the following is an RFC6979 compliant implementation of deterministic ECDSA
        // it's unused because it's mainly intended for use when a good CSPRNG isn't
        // available. if phpseclib's CSPRNG isn't good then even key generation is
        // suspect
        /*
        // if this were actually being used it'd probably be better if this lived in load() and createKey()
        $q = $this->curve->getOrder();
        $x = $this->dA->toBigInteger();

        $h1 = $this->hash->hash($message);
        $k = $this->computek($h1);
        [$x, $y] = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k);
        $x = $x->toBigInteger();
        [, $r] = $x->divide($q);
        $kinv = $k->modInverse($q);
        $h1 = $this->bits2int($h1);
        $temp = $h1->add($dA->multiply($r));
        $temp = $kinv->multiply($temp);
        [, $s] = $temp->divide($q);
        */

        $signature = $this->formatSignature($r, $s);

        if ($source instanceof Signable) {
            $source->setSignature($signature);
        }

        return $signature;
    }

    /**
     * Returns the private key
     *
     * @param array $options optional
     */
    public function toString(string $type, array $options = []): string
    {
        $type = self::validatePlugin('Keys', $type, 'savePrivateKey');

        return $type::savePrivateKey($this->dA, $this->curve, $this->QA, $this->secret, $this->password, $options);
    }

    /**
     * Returns the public key
     *
     * @see self::getPrivateKey()
     */
    public function getPublicKey(): PublicKey
    {
        $format = 'PKCS8';
        if ($this->curve instanceof MontgomeryCurve) {
            $format = 'MontgomeryPublic';
        }

        $type = self::validatePlugin('Keys', $format, 'savePublicKey');

        $key = $type::savePublicKey($this->curve, $this->QA);
        $key = EC::loadFormat($format, $key);
        if ($this->curve instanceof MontgomeryCurve) {
            return $key;
        }
        $key = $key
            ->withHash($this->hash->getHash())
            ->withSignatureFormat($this->shortFormat);
        if ($this->curve instanceof TwistedEdwardsCurve) {
            $key = $key->withContext($this->context);
        }
        return $key;
    }

    /**
     * Returns a signature in the appropriate format
     */
    private function formatSignature(BigInteger $r, BigInteger $s): string|array
    {
        $format = $this->sigFormat;

        $temp = new \ReflectionMethod($format, 'save');
        $paramCount = $temp->getNumberOfRequiredParameters();

        return match ($paramCount) {
            2 => $format::save($r, $s),
            3 => $format::save($r, $s, $this->getCurve()),
            4 => $format::save($r, $s, $this->getCurve(), $this->getLength()),
            default => throw new UnexpectedValueException("$format::save() has $paramCount parameters - the only valid parameter counts are 2, 3 or 4")
        };
    }
}
