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\Filter\Encrypt;
11
12use Traversable;
13use Zend\Filter\Compress;
14use Zend\Filter\Decompress;
15use Zend\Filter\Exception;
16use Zend\Stdlib\ArrayUtils;
17
18/**
19 * Encryption adapter for openssl
20 */
21class Openssl implements EncryptionAlgorithmInterface
22{
23    /**
24     * Definitions for encryption
25     * array(
26     *     'public'   => public keys
27     *     'private'  => private keys
28     *     'envelope' => resulting envelope keys
29     * )
30     */
31    protected $keys = array(
32        'public'   => array(),
33        'private'  => array(),
34        'envelope' => array(),
35    );
36
37    /**
38     * Internal passphrase
39     *
40     * @var string
41     */
42    protected $passphrase;
43
44    /**
45     * Internal compression
46     *
47     * @var array
48     */
49    protected $compression;
50
51    /**
52     * Internal create package
53     *
54     * @var bool
55     */
56    protected $package = false;
57
58    /**
59     * Class constructor
60     * Available options
61     *   'public'      => public key
62     *   'private'     => private key
63     *   'envelope'    => envelope key
64     *   'passphrase'  => passphrase
65     *   'compression' => compress value with this compression adapter
66     *   'package'     => pack envelope keys into encrypted string, simplifies decryption
67     *
68     * @param string|array|Traversable $options Options for this adapter
69     * @throws Exception\ExtensionNotLoadedException
70     */
71    public function __construct($options = array())
72    {
73        if (!extension_loaded('openssl')) {
74            throw new Exception\ExtensionNotLoadedException('This filter needs the openssl extension');
75        }
76
77        if ($options instanceof Traversable) {
78            $options = ArrayUtils::iteratorToArray($options);
79        }
80
81        if (!is_array($options)) {
82            $options = array('public' => $options);
83        }
84
85        if (array_key_exists('passphrase', $options)) {
86            $this->setPassphrase($options['passphrase']);
87            unset($options['passphrase']);
88        }
89
90        if (array_key_exists('compression', $options)) {
91            $this->setCompression($options['compression']);
92            unset($options['compress']);
93        }
94
95        if (array_key_exists('package', $options)) {
96            $this->setPackage($options['package']);
97            unset($options['package']);
98        }
99
100        $this->_setKeys($options);
101    }
102
103    /**
104     * Sets the encryption keys
105     *
106     * @param  string|array $keys Key with type association
107     * @return self
108     * @throws Exception\InvalidArgumentException
109     */
110    protected function _setKeys($keys)
111    {
112        if (!is_array($keys)) {
113            throw new Exception\InvalidArgumentException('Invalid options argument provided to filter');
114        }
115
116        foreach ($keys as $type => $key) {
117            if (is_file($key) and is_readable($key)) {
118                $file = fopen($key, 'r');
119                $cert = fread($file, 8192);
120                fclose($file);
121            } else {
122                $cert = $key;
123                $key  = count($this->keys[$type]);
124            }
125
126            switch ($type) {
127                case 'public':
128                    $test = openssl_pkey_get_public($cert);
129                    if ($test === false) {
130                        throw new Exception\InvalidArgumentException("Public key '{$cert}' not valid");
131                    }
132
133                    openssl_free_key($test);
134                    $this->keys['public'][$key] = $cert;
135                    break;
136                case 'private':
137                    $test = openssl_pkey_get_private($cert, $this->passphrase);
138                    if ($test === false) {
139                        throw new Exception\InvalidArgumentException("Private key '{$cert}' not valid");
140                    }
141
142                    openssl_free_key($test);
143                    $this->keys['private'][$key] = $cert;
144                    break;
145                case 'envelope':
146                    $this->keys['envelope'][$key] = $cert;
147                    break;
148                default:
149                    break;
150            }
151        }
152
153        return $this;
154    }
155
156    /**
157     * Returns all public keys
158     *
159     * @return array
160     */
161    public function getPublicKey()
162    {
163        $key = $this->keys['public'];
164        return $key;
165    }
166
167    /**
168     * Sets public keys
169     *
170     * @param  string|array $key Public keys
171     * @return self
172     */
173    public function setPublicKey($key)
174    {
175        if (is_array($key)) {
176            foreach ($key as $type => $option) {
177                if ($type !== 'public') {
178                    $key['public'] = $option;
179                    unset($key[$type]);
180                }
181            }
182        } else {
183            $key = array('public' => $key);
184        }
185
186        return $this->_setKeys($key);
187    }
188
189    /**
190     * Returns all private keys
191     *
192     * @return array
193     */
194    public function getPrivateKey()
195    {
196        $key = $this->keys['private'];
197        return $key;
198    }
199
200    /**
201     * Sets private keys
202     *
203     * @param  string $key Private key
204     * @param  string $passphrase
205     * @return self
206     */
207    public function setPrivateKey($key, $passphrase = null)
208    {
209        if (is_array($key)) {
210            foreach ($key as $type => $option) {
211                if ($type !== 'private') {
212                    $key['private'] = $option;
213                    unset($key[$type]);
214                }
215            }
216        } else {
217            $key = array('private' => $key);
218        }
219
220        if ($passphrase !== null) {
221            $this->setPassphrase($passphrase);
222        }
223
224        return $this->_setKeys($key);
225    }
226
227    /**
228     * Returns all envelope keys
229     *
230     * @return array
231     */
232    public function getEnvelopeKey()
233    {
234        $key = $this->keys['envelope'];
235        return $key;
236    }
237
238    /**
239     * Sets envelope keys
240     *
241     * @param  string|array $key Envelope keys
242     * @return self
243     */
244    public function setEnvelopeKey($key)
245    {
246        if (is_array($key)) {
247            foreach ($key as $type => $option) {
248                if ($type !== 'envelope') {
249                    $key['envelope'] = $option;
250                    unset($key[$type]);
251                }
252            }
253        } else {
254            $key = array('envelope' => $key);
255        }
256
257        return $this->_setKeys($key);
258    }
259
260    /**
261     * Returns the passphrase
262     *
263     * @return string
264     */
265    public function getPassphrase()
266    {
267        return $this->passphrase;
268    }
269
270    /**
271     * Sets a new passphrase
272     *
273     * @param string $passphrase
274     * @return self
275     */
276    public function setPassphrase($passphrase)
277    {
278        $this->passphrase = $passphrase;
279        return $this;
280    }
281
282    /**
283     * Returns the compression
284     *
285     * @return array
286     */
287    public function getCompression()
288    {
289        return $this->compression;
290    }
291
292    /**
293     * Sets an internal compression for values to encrypt
294     *
295     * @param string|array $compression
296     * @return self
297     */
298    public function setCompression($compression)
299    {
300        if (is_string($this->compression)) {
301            $compression = array('adapter' => $compression);
302        }
303
304        $this->compression = $compression;
305        return $this;
306    }
307
308    /**
309     * Returns if header should be packaged
310     *
311     * @return bool
312     */
313    public function getPackage()
314    {
315        return $this->package;
316    }
317
318    /**
319     * Sets if the envelope keys should be included in the encrypted value
320     *
321     * @param  bool $package
322     * @return self
323     */
324    public function setPackage($package)
325    {
326        $this->package = (bool) $package;
327        return $this;
328    }
329
330    /**
331     * Encrypts $value with the defined settings
332     * Note that you also need the "encrypted" keys to be able to decrypt
333     *
334     * @param  string $value Content to encrypt
335     * @return string The encrypted content
336     * @throws Exception\RuntimeException
337     */
338    public function encrypt($value)
339    {
340        $encrypted     = array();
341        $encryptedkeys = array();
342
343        if (count($this->keys['public']) == 0) {
344            throw new Exception\RuntimeException('Openssl can not encrypt without public keys');
345        }
346
347        $keys         = array();
348        $fingerprints = array();
349        $count        = -1;
350        foreach ($this->keys['public'] as $key => $cert) {
351            $keys[$key] = openssl_pkey_get_public($cert);
352            if ($this->package) {
353                $details = openssl_pkey_get_details($keys[$key]);
354                if ($details === false) {
355                    $details = array('key' => 'ZendFramework');
356                }
357
358                ++$count;
359                $fingerprints[$count] = md5($details['key']);
360            }
361        }
362
363        // compress prior to encryption
364        if (!empty($this->compression)) {
365            $compress = new Compress($this->compression);
366            $value    = $compress($value);
367        }
368
369        $crypt  = openssl_seal($value, $encrypted, $encryptedkeys, $keys);
370        foreach ($keys as $key) {
371            openssl_free_key($key);
372        }
373
374        if ($crypt === false) {
375            throw new Exception\RuntimeException('Openssl was not able to encrypt your content with the given options');
376        }
377
378        $this->keys['envelope'] = $encryptedkeys;
379
380        // Pack data and envelope keys into single string
381        if ($this->package) {
382            $header = pack('n', count($this->keys['envelope']));
383            foreach ($this->keys['envelope'] as $key => $envKey) {
384                $header .= pack('H32n', $fingerprints[$key], strlen($envKey)) . $envKey;
385            }
386
387            $encrypted = $header . $encrypted;
388        }
389
390        return $encrypted;
391    }
392
393    /**
394     * Defined by Zend\Filter\FilterInterface
395     *
396     * Decrypts $value with the defined settings
397     *
398     * @param  string $value Content to decrypt
399     * @return string The decrypted content
400     * @throws Exception\RuntimeException
401     */
402    public function decrypt($value)
403    {
404        $decrypted = "";
405        $envelope  = current($this->getEnvelopeKey());
406
407        if (count($this->keys['private']) !== 1) {
408            throw new Exception\RuntimeException('Please give a private key for decryption with Openssl');
409        }
410
411        if (!$this->package && empty($envelope)) {
412            throw new Exception\RuntimeException('Please give an envelope key for decryption with Openssl');
413        }
414
415        foreach ($this->keys['private'] as $cert) {
416            $keys = openssl_pkey_get_private($cert, $this->getPassphrase());
417        }
418
419        if ($this->package) {
420            $details = openssl_pkey_get_details($keys);
421            if ($details !== false) {
422                $fingerprint = md5($details['key']);
423            } else {
424                $fingerprint = md5("ZendFramework");
425            }
426
427            $count = unpack('ncount', $value);
428            $count = $count['count'];
429            $length  = 2;
430            for ($i = $count; $i > 0; --$i) {
431                $header = unpack('H32print/nsize', substr($value, $length, 18));
432                $length  += 18;
433                if ($header['print'] == $fingerprint) {
434                    $envelope = substr($value, $length, $header['size']);
435                }
436
437                $length += $header['size'];
438            }
439
440            // remainder of string is the value to decrypt
441            $value = substr($value, $length);
442        }
443
444        $crypt  = openssl_open($value, $decrypted, $envelope, $keys);
445        openssl_free_key($keys);
446
447        if ($crypt === false) {
448            throw new Exception\RuntimeException('Openssl was not able to decrypt you content with the given options');
449        }
450
451        // decompress after decryption
452        if (!empty($this->compression)) {
453            $decompress = new Decompress($this->compression);
454            $decrypted  = $decompress($decrypted);
455        }
456
457        return $decrypted;
458    }
459
460    /**
461     * Returns the adapter name
462     *
463     * @return string
464     */
465    public function toString()
466    {
467        return 'Openssl';
468    }
469}
470