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