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