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