<?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2020 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Jose\Component\Core\Util; use function array_key_exists; use Base64Url\Base64Url; use function count; use FG\ASN1\Universal\BitString; use FG\ASN1\Universal\Integer; use FG\ASN1\Universal\NullObject; use FG\ASN1\Universal\ObjectIdentifier; use FG\ASN1\Universal\OctetString; use FG\ASN1\Universal\Sequence; use InvalidArgumentException; use function is_array; use Jose\Component\Core\JWK; use RuntimeException; /** * @internal */ class RSAKey { /** * @var Sequence */ private $sequence; /** * @var bool */ private $private; /** * @var array */ private $values; /** * @var BigInteger */ private $modulus; /** * @var int */ private $modulus_length; /** * @var BigInteger */ private $public_exponent; /** * @var null|BigInteger */ private $private_exponent; /** * @var BigInteger[] */ private $primes = []; /** * @var BigInteger[] */ private $exponents = []; /** * @var null|BigInteger */ private $coefficient; private function __construct(JWK $data) { $this->values = $data->all(); $this->populateBigIntegers(); $this->private = array_key_exists('d', $this->values); } /** * @return RSAKey */ public static function createFromJWK(JWK $jwk): self { return new self($jwk); } public function getModulus(): BigInteger { return $this->modulus; } public function getModulusLength(): int { return $this->modulus_length; } public function getExponent(): BigInteger { $d = $this->getPrivateExponent(); if (null !== $d) { return $d; } return $this->getPublicExponent(); } public function getPublicExponent(): BigInteger { return $this->public_exponent; } public function getPrivateExponent(): ?BigInteger { return $this->private_exponent; } /** * @return BigInteger[] */ public function getPrimes(): array { return $this->primes; } /** * @return BigInteger[] */ public function getExponents(): array { return $this->exponents; } public function getCoefficient(): ?BigInteger { return $this->coefficient; } public function isPublic(): bool { return !array_key_exists('d', $this->values); } /** * @param RSAKey $private * * @return RSAKey */ public static function toPublic(self $private): self { $data = $private->toArray(); $keys = ['p', 'd', 'q', 'dp', 'dq', 'qi']; foreach ($keys as $key) { if (array_key_exists($key, $data)) { unset($data[$key]); } } return new self(new JWK($data)); } public function toArray(): array { return $this->values; } public function toPEM(): string { if (null === $this->sequence) { $this->sequence = new Sequence(); if (array_key_exists('d', $this->values)) { $this->initPrivateKey(); } else { $this->initPublicKey(); } } $result = '-----BEGIN '.($this->private ? 'RSA PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL; $result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL); $result .= '-----END '.($this->private ? 'RSA PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL; return $result; } /** * Exponentiate with or without Chinese Remainder Theorem. * Operation with primes 'p' and 'q' is appox. 2x faster. * * @param RSAKey $key * * @throws RuntimeException if the exponentiation cannot be achieved */ public static function exponentiate(self $key, BigInteger $c): BigInteger { if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare($key->getModulus()) > 0) { throw new RuntimeException(); } if ($key->isPublic() || null === $key->getCoefficient() || 0 === count($key->getPrimes()) || 0 === count($key->getExponents())) { return $c->modPow($key->getExponent(), $key->getModulus()); } $p = $key->getPrimes()[0]; $q = $key->getPrimes()[1]; $dP = $key->getExponents()[0]; $dQ = $key->getExponents()[1]; $qInv = $key->getCoefficient(); $m1 = $c->modPow($dP, $p); $m2 = $c->modPow($dQ, $q); $h = $qInv->multiply($m1->subtract($m2)->add($p))->mod($p); return $m2->add($h->multiply($q)); } private function populateBigIntegers(): void { $this->modulus = $this->convertBase64StringToBigInteger($this->values['n']); $this->modulus_length = mb_strlen($this->getModulus()->toBytes(), '8bit'); $this->public_exponent = $this->convertBase64StringToBigInteger($this->values['e']); if (!$this->isPublic()) { $this->private_exponent = $this->convertBase64StringToBigInteger($this->values['d']); if (array_key_exists('p', $this->values) && array_key_exists('q', $this->values)) { $this->primes = [ $this->convertBase64StringToBigInteger($this->values['p']), $this->convertBase64StringToBigInteger($this->values['q']), ]; if (array_key_exists('dp', $this->values) && array_key_exists('dq', $this->values) && array_key_exists('qi', $this->values)) { $this->exponents = [ $this->convertBase64StringToBigInteger($this->values['dp']), $this->convertBase64StringToBigInteger($this->values['dq']), ]; $this->coefficient = $this->convertBase64StringToBigInteger($this->values['qi']); } } } } private function convertBase64StringToBigInteger(string $value): BigInteger { return BigInteger::createFromBinaryString(Base64Url::decode($value)); } private function initPublicKey(): void { $oid_sequence = new Sequence(); $oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1')); $oid_sequence->addChild(new NullObject()); $this->sequence->addChild($oid_sequence); $n = new Integer($this->fromBase64ToInteger($this->values['n'])); $e = new Integer($this->fromBase64ToInteger($this->values['e'])); $key_sequence = new Sequence(); $key_sequence->addChild($n); $key_sequence->addChild($e); $key_bit_string = new BitString(bin2hex($key_sequence->getBinary())); $this->sequence->addChild($key_bit_string); } private function initPrivateKey(): void { $this->sequence->addChild(new Integer(0)); $oid_sequence = new Sequence(); $oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1')); $oid_sequence->addChild(new NullObject()); $this->sequence->addChild($oid_sequence); $v = new Integer(0); $n = new Integer($this->fromBase64ToInteger($this->values['n'])); $e = new Integer($this->fromBase64ToInteger($this->values['e'])); $d = new Integer($this->fromBase64ToInteger($this->values['d'])); $p = new Integer($this->fromBase64ToInteger($this->values['p'])); $q = new Integer($this->fromBase64ToInteger($this->values['q'])); $dp = array_key_exists('dp', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dp'])) : new Integer(0); $dq = array_key_exists('dq', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dq'])) : new Integer(0); $qi = array_key_exists('qi', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['qi'])) : new Integer(0); $key_sequence = new Sequence(); $key_sequence->addChild($v); $key_sequence->addChild($n); $key_sequence->addChild($e); $key_sequence->addChild($d); $key_sequence->addChild($p); $key_sequence->addChild($q); $key_sequence->addChild($dp); $key_sequence->addChild($dq); $key_sequence->addChild($qi); $key_octet_string = new OctetString(bin2hex($key_sequence->getBinary())); $this->sequence->addChild($key_octet_string); } /** * @param string $value * * @return string */ private function fromBase64ToInteger($value) { $unpacked = unpack('H*', Base64Url::decode($value)); if (!is_array($unpacked) || 0 === count($unpacked)) { throw new InvalidArgumentException('Unable to get the private key'); } return \Brick\Math\BigInteger::fromBase(current($unpacked), 16)->toBase(10); } }