1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Crypt\PublicKey; 11 12use Zend\Crypt\Exception; 13use Zend\Math; 14 15/** 16 * PHP implementation of the Diffie-Hellman public key encryption algorithm. 17 * Allows two unassociated parties to establish a joint shared secret key 18 * to be used in encrypting subsequent communications. 19 */ 20class DiffieHellman 21{ 22 const DEFAULT_KEY_SIZE = 2048; 23 24 /** 25 * Key formats 26 */ 27 const FORMAT_BINARY = 'binary'; 28 const FORMAT_NUMBER = 'number'; 29 const FORMAT_BTWOC = 'btwoc'; 30 31 /** 32 * Static flag to select whether to use PHP5.3's openssl extension 33 * if available. 34 * 35 * @var bool 36 */ 37 public static $useOpenssl = true; 38 39 /** 40 * Default large prime number; required by the algorithm. 41 * 42 * @var string 43 */ 44 private $prime = null; 45 46 /** 47 * The default generator number. This number must be greater than 0 but 48 * less than the prime number set. 49 * 50 * @var string 51 */ 52 private $generator = null; 53 54 /** 55 * A private number set by the local user. It's optional and will 56 * be generated if not set. 57 * 58 * @var string 59 */ 60 private $privateKey = null; 61 62 /** 63 * BigInteger support object courtesy of Zend\Math 64 * 65 * @var \Zend\Math\BigInteger\Adapter\AdapterInterface 66 */ 67 private $math = null; 68 69 /** 70 * The public key generated by this instance after calling generateKeys(). 71 * 72 * @var string 73 */ 74 private $publicKey = null; 75 76 /** 77 * The shared secret key resulting from a completed Diffie Hellman 78 * exchange 79 * 80 * @var string 81 */ 82 private $secretKey = null; 83 84 /** 85 * @var resource 86 */ 87 protected $opensslKeyResource = null; 88 89 /** 90 * Constructor; if set construct the object using the parameter array to 91 * set values for Prime, Generator and Private. 92 * If a Private Key is not set, one will be generated at random. 93 * 94 * @param string $prime 95 * @param string $generator 96 * @param string $privateKey 97 * @param string $privateKeyFormat 98 */ 99 public function __construct($prime, $generator, $privateKey = null, $privateKeyFormat = self::FORMAT_NUMBER) 100 { 101 $this->setPrime($prime); 102 $this->setGenerator($generator); 103 if ($privateKey !== null) { 104 $this->setPrivateKey($privateKey, $privateKeyFormat); 105 } 106 107 // set up BigInteger adapter 108 $this->math = Math\BigInteger\BigInteger::factory(); 109 } 110 111 /** 112 * Set whether to use openssl extension 113 * 114 * @static 115 * @param bool $flag 116 */ 117 public static function useOpensslExtension($flag = true) 118 { 119 static::$useOpenssl = (bool) $flag; 120 } 121 122 /** 123 * Generate own public key. If a private number has not already been set, 124 * one will be generated at this stage. 125 * 126 * @return DiffieHellman 127 * @throws \Zend\Crypt\Exception\RuntimeException 128 */ 129 public function generateKeys() 130 { 131 if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) { 132 $details = array( 133 'p' => $this->convert($this->getPrime(), self::FORMAT_NUMBER, self::FORMAT_BINARY), 134 'g' => $this->convert($this->getGenerator(), self::FORMAT_NUMBER, self::FORMAT_BINARY) 135 ); 136 if ($this->hasPrivateKey()) { 137 $details['priv_key'] = $this->convert( 138 $this->privateKey, 139 self::FORMAT_NUMBER, 140 self::FORMAT_BINARY 141 ); 142 $opensslKeyResource = openssl_pkey_new(array('dh' => $details)); 143 } else { 144 $opensslKeyResource = openssl_pkey_new(array( 145 'dh' => $details, 146 'private_key_bits' => self::DEFAULT_KEY_SIZE, 147 'private_key_type' => OPENSSL_KEYTYPE_DH 148 )); 149 } 150 151 if (false === $opensslKeyResource) { 152 throw new Exception\RuntimeException( 153 'Can not generate new key; openssl ' . openssl_error_string() 154 ); 155 } 156 157 $data = openssl_pkey_get_details($opensslKeyResource); 158 159 $this->setPrivateKey($data['dh']['priv_key'], self::FORMAT_BINARY); 160 $this->setPublicKey($data['dh']['pub_key'], self::FORMAT_BINARY); 161 162 $this->opensslKeyResource = $opensslKeyResource; 163 } else { 164 // Private key is lazy generated in the absence of ext/openssl 165 $publicKey = $this->math->powmod($this->getGenerator(), $this->getPrivateKey(), $this->getPrime()); 166 $this->setPublicKey($publicKey); 167 } 168 169 return $this; 170 } 171 172 /** 173 * Setter for the value of the public number 174 * 175 * @param string $number 176 * @param string $format 177 * @return DiffieHellman 178 * @throws \Zend\Crypt\Exception\InvalidArgumentException 179 */ 180 public function setPublicKey($number, $format = self::FORMAT_NUMBER) 181 { 182 $number = $this->convert($number, $format, self::FORMAT_NUMBER); 183 if (!preg_match('/^\d+$/', $number)) { 184 throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number'); 185 } 186 $this->publicKey = (string) $number; 187 188 return $this; 189 } 190 191 /** 192 * Returns own public key for communication to the second party to this transaction 193 * 194 * @param string $format 195 * @return string 196 * @throws \Zend\Crypt\Exception\InvalidArgumentException 197 */ 198 public function getPublicKey($format = self::FORMAT_NUMBER) 199 { 200 if ($this->publicKey === null) { 201 throw new Exception\InvalidArgumentException( 202 'A public key has not yet been generated using a prior call to generateKeys()' 203 ); 204 } 205 206 return $this->convert($this->publicKey, self::FORMAT_NUMBER, $format); 207 } 208 209 /** 210 * Compute the shared secret key based on the public key received from the 211 * the second party to this transaction. This should agree to the secret 212 * key the second party computes on our own public key. 213 * Once in agreement, the key is known to only to both parties. 214 * By default, the function expects the public key to be in binary form 215 * which is the typical format when being transmitted. 216 * 217 * If you need the binary form of the shared secret key, call 218 * getSharedSecretKey() with the optional parameter for Binary output. 219 * 220 * @param string $publicKey 221 * @param string $publicKeyFormat 222 * @param string $secretKeyFormat 223 * @return string 224 * @throws \Zend\Crypt\Exception\InvalidArgumentException 225 * @throws \Zend\Crypt\Exception\RuntimeException 226 */ 227 public function computeSecretKey( 228 $publicKey, 229 $publicKeyFormat = self::FORMAT_NUMBER, 230 $secretKeyFormat = self::FORMAT_NUMBER 231 ) { 232 if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) { 233 $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_BINARY); 234 $secretKey = openssl_dh_compute_key($publicKey, $this->opensslKeyResource); 235 if (false === $secretKey) { 236 throw new Exception\RuntimeException( 237 'Can not compute key; openssl ' . openssl_error_string() 238 ); 239 } 240 $this->secretKey = $this->convert($secretKey, self::FORMAT_BINARY, self::FORMAT_NUMBER); 241 } else { 242 $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_NUMBER); 243 if (!preg_match('/^\d+$/', $publicKey)) { 244 throw new Exception\InvalidArgumentException( 245 'Invalid parameter; not a positive natural number' 246 ); 247 } 248 $this->secretKey = $this->math->powmod($publicKey, $this->getPrivateKey(), $this->getPrime()); 249 } 250 251 return $this->getSharedSecretKey($secretKeyFormat); 252 } 253 254 /** 255 * Return the computed shared secret key from the DiffieHellman transaction 256 * 257 * @param string $format 258 * @return string 259 * @throws \Zend\Crypt\Exception\InvalidArgumentException 260 */ 261 public function getSharedSecretKey($format = self::FORMAT_NUMBER) 262 { 263 if (!isset($this->secretKey)) { 264 throw new Exception\InvalidArgumentException( 265 'A secret key has not yet been computed; call computeSecretKey() first' 266 ); 267 } 268 269 return $this->convert($this->secretKey, self::FORMAT_NUMBER, $format); 270 } 271 272 /** 273 * Setter for the value of the prime number 274 * 275 * @param string $number 276 * @return DiffieHellman 277 * @throws \Zend\Crypt\Exception\InvalidArgumentException 278 */ 279 public function setPrime($number) 280 { 281 if (!preg_match('/^\d+$/', $number) || $number < 11) { 282 throw new Exception\InvalidArgumentException( 283 'Invalid parameter; not a positive natural number or too small: ' . 284 'should be a large natural number prime' 285 ); 286 } 287 $this->prime = (string) $number; 288 289 return $this; 290 } 291 292 /** 293 * Getter for the value of the prime number 294 * 295 * @param string $format 296 * @return string 297 * @throws \Zend\Crypt\Exception\InvalidArgumentException 298 */ 299 public function getPrime($format = self::FORMAT_NUMBER) 300 { 301 if (!isset($this->prime)) { 302 throw new Exception\InvalidArgumentException('No prime number has been set'); 303 } 304 305 return $this->convert($this->prime, self::FORMAT_NUMBER, $format); 306 } 307 308 /** 309 * Setter for the value of the generator number 310 * 311 * @param string $number 312 * @return DiffieHellman 313 * @throws \Zend\Crypt\Exception\InvalidArgumentException 314 */ 315 public function setGenerator($number) 316 { 317 if (!preg_match('/^\d+$/', $number) || $number < 2) { 318 throw new Exception\InvalidArgumentException( 319 'Invalid parameter; not a positive natural number greater than 1' 320 ); 321 } 322 $this->generator = (string) $number; 323 324 return $this; 325 } 326 327 /** 328 * Getter for the value of the generator number 329 * 330 * @param string $format 331 * @return string 332 * @throws \Zend\Crypt\Exception\InvalidArgumentException 333 */ 334 public function getGenerator($format = self::FORMAT_NUMBER) 335 { 336 if (!isset($this->generator)) { 337 throw new Exception\InvalidArgumentException('No generator number has been set'); 338 } 339 340 return $this->convert($this->generator, self::FORMAT_NUMBER, $format); 341 } 342 343 /** 344 * Setter for the value of the private number 345 * 346 * @param string $number 347 * @param string $format 348 * @return DiffieHellman 349 * @throws \Zend\Crypt\Exception\InvalidArgumentException 350 */ 351 public function setPrivateKey($number, $format = self::FORMAT_NUMBER) 352 { 353 $number = $this->convert($number, $format, self::FORMAT_NUMBER); 354 if (!preg_match('/^\d+$/', $number)) { 355 throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number'); 356 } 357 $this->privateKey = (string) $number; 358 359 return $this; 360 } 361 362 /** 363 * Getter for the value of the private number 364 * 365 * @param string $format 366 * @return string 367 */ 368 public function getPrivateKey($format = self::FORMAT_NUMBER) 369 { 370 if (!$this->hasPrivateKey()) { 371 $this->setPrivateKey($this->generatePrivateKey(), self::FORMAT_BINARY); 372 } 373 374 return $this->convert($this->privateKey, self::FORMAT_NUMBER, $format); 375 } 376 377 /** 378 * Check whether a private key currently exists. 379 * 380 * @return bool 381 */ 382 public function hasPrivateKey() 383 { 384 return isset($this->privateKey); 385 } 386 387 /** 388 * Convert number between formats 389 * 390 * @param string $number 391 * @param string $inputFormat 392 * @param string $outputFormat 393 * @return string 394 */ 395 protected function convert($number, $inputFormat = self::FORMAT_NUMBER, $outputFormat = self::FORMAT_BINARY) 396 { 397 if ($inputFormat == $outputFormat) { 398 return $number; 399 } 400 401 // convert to number 402 switch ($inputFormat) { 403 case self::FORMAT_BINARY: 404 case self::FORMAT_BTWOC: 405 $number = $this->math->binToInt($number); 406 break; 407 case self::FORMAT_NUMBER: 408 default: 409 // do nothing 410 break; 411 } 412 413 // convert to output format 414 switch ($outputFormat) { 415 case self::FORMAT_BINARY: 416 return $this->math->intToBin($number); 417 case self::FORMAT_BTWOC: 418 return $this->math->intToBin($number, true); 419 case self::FORMAT_NUMBER: 420 default: 421 return $number; 422 } 423 } 424 425 /** 426 * In the event a private number/key has not been set by the user, 427 * or generated by ext/openssl, a best attempt will be made to 428 * generate a random key. Having a random number generator installed 429 * on linux/bsd is highly recommended! The alternative is not recommended 430 * for production unless without any other option. 431 * 432 * @return string 433 */ 434 protected function generatePrivateKey() 435 { 436 return Math\Rand::getBytes(strlen($this->getPrime()), true); 437 } 438} 439