<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Crypt\PublicKey; use Zend\Crypt\Exception; use Zend\Math; /** * PHP implementation of the Diffie-Hellman public key encryption algorithm. * Allows two unassociated parties to establish a joint shared secret key * to be used in encrypting subsequent communications. */ class DiffieHellman { const DEFAULT_KEY_SIZE = 2048; /** * Key formats */ const FORMAT_BINARY = 'binary'; const FORMAT_NUMBER = 'number'; const FORMAT_BTWOC = 'btwoc'; /** * Static flag to select whether to use PHP5.3's openssl extension * if available. * * @var bool */ public static $useOpenssl = true; /** * Default large prime number; required by the algorithm. * * @var string */ private $prime = null; /** * The default generator number. This number must be greater than 0 but * less than the prime number set. * * @var string */ private $generator = null; /** * A private number set by the local user. It's optional and will * be generated if not set. * * @var string */ private $privateKey = null; /** * BigInteger support object courtesy of Zend\Math * * @var \Zend\Math\BigInteger\Adapter\AdapterInterface */ private $math = null; /** * The public key generated by this instance after calling generateKeys(). * * @var string */ private $publicKey = null; /** * The shared secret key resulting from a completed Diffie Hellman * exchange * * @var string */ private $secretKey = null; /** * @var resource */ protected $opensslKeyResource = null; /** * Constructor; if set construct the object using the parameter array to * set values for Prime, Generator and Private. * If a Private Key is not set, one will be generated at random. * * @param string $prime * @param string $generator * @param string $privateKey * @param string $privateKeyFormat */ public function __construct($prime, $generator, $privateKey = null, $privateKeyFormat = self::FORMAT_NUMBER) { $this->setPrime($prime); $this->setGenerator($generator); if ($privateKey !== null) { $this->setPrivateKey($privateKey, $privateKeyFormat); } // set up BigInteger adapter $this->math = Math\BigInteger\BigInteger::factory(); } /** * Set whether to use openssl extension * * @static * @param bool $flag */ public static function useOpensslExtension($flag = true) { static::$useOpenssl = (bool) $flag; } /** * Generate own public key. If a private number has not already been set, * one will be generated at this stage. * * @return DiffieHellman * @throws \Zend\Crypt\Exception\RuntimeException */ public function generateKeys() { if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) { $details = array( 'p' => $this->convert($this->getPrime(), self::FORMAT_NUMBER, self::FORMAT_BINARY), 'g' => $this->convert($this->getGenerator(), self::FORMAT_NUMBER, self::FORMAT_BINARY) ); if ($this->hasPrivateKey()) { $details['priv_key'] = $this->convert( $this->privateKey, self::FORMAT_NUMBER, self::FORMAT_BINARY ); $opensslKeyResource = openssl_pkey_new(array('dh' => $details)); } else { $opensslKeyResource = openssl_pkey_new(array( 'dh' => $details, 'private_key_bits' => self::DEFAULT_KEY_SIZE, 'private_key_type' => OPENSSL_KEYTYPE_DH )); } if (false === $opensslKeyResource) { throw new Exception\RuntimeException( 'Can not generate new key; openssl ' . openssl_error_string() ); } $data = openssl_pkey_get_details($opensslKeyResource); $this->setPrivateKey($data['dh']['priv_key'], self::FORMAT_BINARY); $this->setPublicKey($data['dh']['pub_key'], self::FORMAT_BINARY); $this->opensslKeyResource = $opensslKeyResource; } else { // Private key is lazy generated in the absence of ext/openssl $publicKey = $this->math->powmod($this->getGenerator(), $this->getPrivateKey(), $this->getPrime()); $this->setPublicKey($publicKey); } return $this; } /** * Setter for the value of the public number * * @param string $number * @param string $format * @return DiffieHellman * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function setPublicKey($number, $format = self::FORMAT_NUMBER) { $number = $this->convert($number, $format, self::FORMAT_NUMBER); if (!preg_match('/^\d+$/', $number)) { throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number'); } $this->publicKey = (string) $number; return $this; } /** * Returns own public key for communication to the second party to this transaction * * @param string $format * @return string * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function getPublicKey($format = self::FORMAT_NUMBER) { if ($this->publicKey === null) { throw new Exception\InvalidArgumentException( 'A public key has not yet been generated using a prior call to generateKeys()' ); } return $this->convert($this->publicKey, self::FORMAT_NUMBER, $format); } /** * Compute the shared secret key based on the public key received from the * the second party to this transaction. This should agree to the secret * key the second party computes on our own public key. * Once in agreement, the key is known to only to both parties. * By default, the function expects the public key to be in binary form * which is the typical format when being transmitted. * * If you need the binary form of the shared secret key, call * getSharedSecretKey() with the optional parameter for Binary output. * * @param string $publicKey * @param string $publicKeyFormat * @param string $secretKeyFormat * @return string * @throws \Zend\Crypt\Exception\InvalidArgumentException * @throws \Zend\Crypt\Exception\RuntimeException */ public function computeSecretKey($publicKey, $publicKeyFormat = self::FORMAT_NUMBER, $secretKeyFormat = self::FORMAT_NUMBER) { if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) { $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_BINARY); $secretKey = openssl_dh_compute_key($publicKey, $this->opensslKeyResource); if (false === $secretKey) { throw new Exception\RuntimeException( 'Can not compute key; openssl ' . openssl_error_string() ); } $this->secretKey = $this->convert($secretKey, self::FORMAT_BINARY, self::FORMAT_NUMBER); } else { $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_NUMBER); if (!preg_match('/^\d+$/', $publicKey)) { throw new Exception\InvalidArgumentException( 'Invalid parameter; not a positive natural number' ); } $this->secretKey = $this->math->powmod($publicKey, $this->getPrivateKey(), $this->getPrime()); } return $this->getSharedSecretKey($secretKeyFormat); } /** * Return the computed shared secret key from the DiffieHellman transaction * * @param string $format * @return string * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function getSharedSecretKey($format = self::FORMAT_NUMBER) { if (!isset($this->secretKey)) { throw new Exception\InvalidArgumentException( 'A secret key has not yet been computed; call computeSecretKey() first' ); } return $this->convert($this->secretKey, self::FORMAT_NUMBER, $format); } /** * Setter for the value of the prime number * * @param string $number * @return DiffieHellman * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function setPrime($number) { if (!preg_match('/^\d+$/', $number) || $number < 11) { throw new Exception\InvalidArgumentException( 'Invalid parameter; not a positive natural number or too small: ' . 'should be a large natural number prime' ); } $this->prime = (string) $number; return $this; } /** * Getter for the value of the prime number * * @param string $format * @return string * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function getPrime($format = self::FORMAT_NUMBER) { if (!isset($this->prime)) { throw new Exception\InvalidArgumentException('No prime number has been set'); } return $this->convert($this->prime, self::FORMAT_NUMBER, $format); } /** * Setter for the value of the generator number * * @param string $number * @return DiffieHellman * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function setGenerator($number) { if (!preg_match('/^\d+$/', $number) || $number < 2) { throw new Exception\InvalidArgumentException( 'Invalid parameter; not a positive natural number greater than 1' ); } $this->generator = (string) $number; return $this; } /** * Getter for the value of the generator number * * @param string $format * @return string * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function getGenerator($format = self::FORMAT_NUMBER) { if (!isset($this->generator)) { throw new Exception\InvalidArgumentException('No generator number has been set'); } return $this->convert($this->generator, self::FORMAT_NUMBER, $format); } /** * Setter for the value of the private number * * @param string $number * @param string $format * @return DiffieHellman * @throws \Zend\Crypt\Exception\InvalidArgumentException */ public function setPrivateKey($number, $format = self::FORMAT_NUMBER) { $number = $this->convert($number, $format, self::FORMAT_NUMBER); if (!preg_match('/^\d+$/', $number)) { throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number'); } $this->privateKey = (string) $number; return $this; } /** * Getter for the value of the private number * * @param string $format * @return string */ public function getPrivateKey($format = self::FORMAT_NUMBER) { if (!$this->hasPrivateKey()) { $this->setPrivateKey($this->generatePrivateKey(), self::FORMAT_BINARY); } return $this->convert($this->privateKey, self::FORMAT_NUMBER, $format); } /** * Check whether a private key currently exists. * * @return bool */ public function hasPrivateKey() { return isset($this->privateKey); } /** * Convert number between formats * * @param $number * @param string $inputFormat * @param string $outputFormat * @return string */ protected function convert($number, $inputFormat = self::FORMAT_NUMBER, $outputFormat = self::FORMAT_BINARY) { if ($inputFormat == $outputFormat) { return $number; } // convert to number switch ($inputFormat) { case self::FORMAT_BINARY: case self::FORMAT_BTWOC: $number = $this->math->binToInt($number); break; case self::FORMAT_NUMBER: default: // do nothing break; } // convert to output format switch ($outputFormat) { case self::FORMAT_BINARY: return $this->math->intToBin($number); break; case self::FORMAT_BTWOC: return $this->math->intToBin($number, true); break; case self::FORMAT_NUMBER: default: return $number; break; } } /** * In the event a private number/key has not been set by the user, * or generated by ext/openssl, a best attempt will be made to * generate a random key. Having a random number generator installed * on linux/bsd is highly recommended! The alternative is not recommended * for production unless without any other option. * * @return string */ protected function generatePrivateKey() { return Math\Rand::getBytes(strlen($this->getPrime()), true); } }