1<?php
2
3/*
4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * DomainKey Signer used to apply DomainKeys Signature to a message.
13 *
14 * @author Xavier De Cock <xdecock@gmail.com>
15 */
16class Swift_Signers_DomainKeySigner implements Swift_Signers_HeaderSigner
17{
18    /**
19     * PrivateKey.
20     *
21     * @var string
22     */
23    protected $_privateKey;
24
25    /**
26     * DomainName.
27     *
28     * @var string
29     */
30    protected $_domainName;
31
32    /**
33     * Selector.
34     *
35     * @var string
36     */
37    protected $_selector;
38
39    /**
40     * Hash algorithm used.
41     *
42     * @var string
43     */
44    protected $_hashAlgorithm = 'rsa-sha1';
45
46    /**
47     * Canonisation method.
48     *
49     * @var string
50     */
51    protected $_canon = 'simple';
52
53    /**
54     * Headers not being signed.
55     *
56     * @var array
57     */
58    protected $_ignoredHeaders = array();
59
60    /**
61     * Signer identity.
62     *
63     * @var string
64     */
65    protected $_signerIdentity;
66
67    /**
68     * Must we embed signed headers?
69     *
70     * @var bool
71     */
72    protected $_debugHeaders = false;
73
74    // work variables
75    /**
76     * Headers used to generate hash.
77     *
78     * @var array
79     */
80    private $_signedHeaders = array();
81
82    /**
83     * Stores the signature header.
84     *
85     * @var Swift_Mime_Headers_ParameterizedHeader
86     */
87    protected $_domainKeyHeader;
88
89    /**
90     * Hash Handler.
91     *
92     * @var resource|null
93     */
94    private $_hashHandler;
95
96    private $_hash;
97
98    private $_canonData = '';
99
100    private $_bodyCanonEmptyCounter = 0;
101
102    private $_bodyCanonIgnoreStart = 2;
103
104    private $_bodyCanonSpace = false;
105
106    private $_bodyCanonLastChar = null;
107
108    private $_bodyCanonLine = '';
109
110    private $_bound = array();
111
112    /**
113     * Constructor.
114     *
115     * @param string $privateKey
116     * @param string $domainName
117     * @param string $selector
118     */
119    public function __construct($privateKey, $domainName, $selector)
120    {
121        $this->_privateKey = $privateKey;
122        $this->_domainName = $domainName;
123        $this->_signerIdentity = '@'.$domainName;
124        $this->_selector = $selector;
125    }
126
127    /**
128     * Instanciate DomainKeySigner.
129     *
130     * @param string $privateKey
131     * @param string $domainName
132     * @param string $selector
133     *
134     * @return self
135     */
136    public static function newInstance($privateKey, $domainName, $selector)
137    {
138        return new static($privateKey, $domainName, $selector);
139    }
140
141    /**
142     * Resets internal states.
143     *
144     * @return $this
145     */
146    public function reset()
147    {
148        $this->_hash = null;
149        $this->_hashHandler = null;
150        $this->_bodyCanonIgnoreStart = 2;
151        $this->_bodyCanonEmptyCounter = 0;
152        $this->_bodyCanonLastChar = null;
153        $this->_bodyCanonSpace = false;
154
155        return $this;
156    }
157
158    /**
159     * Writes $bytes to the end of the stream.
160     *
161     * Writing may not happen immediately if the stream chooses to buffer.  If
162     * you want to write these bytes with immediate effect, call {@link commit()}
163     * after calling write().
164     *
165     * This method returns the sequence ID of the write (i.e. 1 for first, 2 for
166     * second, etc etc).
167     *
168     * @param string $bytes
169     *
170     * @throws Swift_IoException
171     *
172     * @return $this
173     */
174    public function write($bytes)
175    {
176        $this->_canonicalizeBody($bytes);
177        foreach ($this->_bound as $is) {
178            $is->write($bytes);
179        }
180
181        return $this;
182    }
183
184    /**
185     * For any bytes that are currently buffered inside the stream, force them
186     * off the buffer.
187     *
188     * @throws Swift_IoException
189     *
190     * @return $this
191     */
192    public function commit()
193    {
194        // Nothing to do
195        return $this;
196    }
197
198    /**
199     * Attach $is to this stream.
200     * The stream acts as an observer, receiving all data that is written.
201     * All {@link write()} and {@link flushBuffers()} operations will be mirrored.
202     *
203     * @param Swift_InputByteStream $is
204     *
205     * @return $this
206     */
207    public function bind(Swift_InputByteStream $is)
208    {
209        // Don't have to mirror anything
210        $this->_bound[] = $is;
211
212        return $this;
213    }
214
215    /**
216     * Remove an already bound stream.
217     * If $is is not bound, no errors will be raised.
218     * If the stream currently has any buffered data it will be written to $is
219     * before unbinding occurs.
220     *
221     * @param Swift_InputByteStream $is
222     *
223     * @return $this
224     */
225    public function unbind(Swift_InputByteStream $is)
226    {
227        // Don't have to mirror anything
228        foreach ($this->_bound as $k => $stream) {
229            if ($stream === $is) {
230                unset($this->_bound[$k]);
231
232                break;
233            }
234        }
235
236        return $this;
237    }
238
239    /**
240     * Flush the contents of the stream (empty it) and set the internal pointer
241     * to the beginning.
242     *
243     * @throws Swift_IoException
244     *
245     * @return $this
246     */
247    public function flushBuffers()
248    {
249        $this->reset();
250
251        return $this;
252    }
253
254    /**
255     * Set hash_algorithm, must be one of rsa-sha256 | rsa-sha1 defaults to rsa-sha256.
256     *
257     * @param string $hash
258     *
259     * @return $this
260     */
261    public function setHashAlgorithm($hash)
262    {
263        $this->_hashAlgorithm = 'rsa-sha1';
264
265        return $this;
266    }
267
268    /**
269     * Set the canonicalization algorithm.
270     *
271     * @param string $canon simple | nofws defaults to simple
272     *
273     * @return $this
274     */
275    public function setCanon($canon)
276    {
277        if ($canon == 'nofws') {
278            $this->_canon = 'nofws';
279        } else {
280            $this->_canon = 'simple';
281        }
282
283        return $this;
284    }
285
286    /**
287     * Set the signer identity.
288     *
289     * @param string $identity
290     *
291     * @return $this
292     */
293    public function setSignerIdentity($identity)
294    {
295        $this->_signerIdentity = $identity;
296
297        return $this;
298    }
299
300    /**
301     * Enable / disable the DebugHeaders.
302     *
303     * @param bool $debug
304     *
305     * @return $this
306     */
307    public function setDebugHeaders($debug)
308    {
309        $this->_debugHeaders = (bool) $debug;
310
311        return $this;
312    }
313
314    /**
315     * Start Body.
316     */
317    public function startBody()
318    {
319    }
320
321    /**
322     * End Body.
323     */
324    public function endBody()
325    {
326        $this->_endOfBody();
327    }
328
329    /**
330     * Returns the list of Headers Tampered by this plugin.
331     *
332     * @return array
333     */
334    public function getAlteredHeaders()
335    {
336        if ($this->_debugHeaders) {
337            return array('DomainKey-Signature', 'X-DebugHash');
338        }
339
340        return array('DomainKey-Signature');
341    }
342
343    /**
344     * Adds an ignored Header.
345     *
346     * @param string $header_name
347     *
348     * @return $this
349     */
350    public function ignoreHeader($header_name)
351    {
352        $this->_ignoredHeaders[strtolower($header_name)] = true;
353
354        return $this;
355    }
356
357    /**
358     * Set the headers to sign.
359     *
360     * @param Swift_Mime_HeaderSet $headers
361     *
362     * @return $this
363     */
364    public function setHeaders(Swift_Mime_HeaderSet $headers)
365    {
366        $this->_startHash();
367        $this->_canonData = '';
368        // Loop through Headers
369        $listHeaders = $headers->listAll();
370        foreach ($listHeaders as $hName) {
371            // Check if we need to ignore Header
372            if (!isset($this->_ignoredHeaders[strtolower($hName)])) {
373                if ($headers->has($hName)) {
374                    $tmp = $headers->getAll($hName);
375                    foreach ($tmp as $header) {
376                        if ($header->getFieldBody() != '') {
377                            $this->_addHeader($header->toString());
378                            $this->_signedHeaders[] = $header->getFieldName();
379                        }
380                    }
381                }
382            }
383        }
384        $this->_endOfHeaders();
385
386        return $this;
387    }
388
389    /**
390     * Add the signature to the given Headers.
391     *
392     * @param Swift_Mime_HeaderSet $headers
393     *
394     * @return $this
395     */
396    public function addSignature(Swift_Mime_HeaderSet $headers)
397    {
398        // Prepare the DomainKey-Signature Header
399        $params = array('a' => $this->_hashAlgorithm, 'b' => chunk_split(base64_encode($this->_getEncryptedHash()), 73, ' '), 'c' => $this->_canon, 'd' => $this->_domainName, 'h' => implode(': ', $this->_signedHeaders), 'q' => 'dns', 's' => $this->_selector);
400        $string = '';
401        foreach ($params as $k => $v) {
402            $string .= $k.'='.$v.'; ';
403        }
404        $string = trim($string);
405        $headers->addTextHeader('DomainKey-Signature', $string);
406
407        return $this;
408    }
409
410    /* Private helpers */
411
412    protected function _addHeader($header)
413    {
414        switch ($this->_canon) {
415            case 'nofws':
416                // Prepare Header and cascade
417                $exploded = explode(':', $header, 2);
418                $name = strtolower(trim($exploded[0]));
419                $value = str_replace("\r\n", '', $exploded[1]);
420                $value = preg_replace("/[ \t][ \t]+/", ' ', $value);
421                $header = $name.':'.trim($value)."\r\n";
422            case 'simple':
423                // Nothing to do
424        }
425        $this->_addToHash($header);
426    }
427
428    protected function _endOfHeaders()
429    {
430        $this->_bodyCanonEmptyCounter = 1;
431    }
432
433    protected function _canonicalizeBody($string)
434    {
435        $len = strlen($string);
436        $canon = '';
437        $nofws = ($this->_canon == 'nofws');
438        for ($i = 0; $i < $len; ++$i) {
439            if ($this->_bodyCanonIgnoreStart > 0) {
440                --$this->_bodyCanonIgnoreStart;
441                continue;
442            }
443            switch ($string[$i]) {
444                case "\r":
445                    $this->_bodyCanonLastChar = "\r";
446                    break;
447                case "\n":
448                    if ($this->_bodyCanonLastChar == "\r") {
449                        if ($nofws) {
450                            $this->_bodyCanonSpace = false;
451                        }
452                        if ($this->_bodyCanonLine == '') {
453                            ++$this->_bodyCanonEmptyCounter;
454                        } else {
455                            $this->_bodyCanonLine = '';
456                            $canon .= "\r\n";
457                        }
458                    } else {
459                        // Wooops Error
460                        throw new Swift_SwiftException('Invalid new line sequence in mail found \n without preceding \r');
461                    }
462                    break;
463                case ' ':
464                case "\t":
465                case "\x09": //HTAB
466                    if ($nofws) {
467                        $this->_bodyCanonSpace = true;
468                        break;
469                    }
470                default:
471                    if ($this->_bodyCanonEmptyCounter > 0) {
472                        $canon .= str_repeat("\r\n", $this->_bodyCanonEmptyCounter);
473                        $this->_bodyCanonEmptyCounter = 0;
474                    }
475                    $this->_bodyCanonLine .= $string[$i];
476                    $canon .= $string[$i];
477            }
478        }
479        $this->_addToHash($canon);
480    }
481
482    protected function _endOfBody()
483    {
484        if (strlen($this->_bodyCanonLine) > 0) {
485            $this->_addToHash("\r\n");
486        }
487        $this->_hash = hash_final($this->_hashHandler, true);
488    }
489
490    private function _addToHash($string)
491    {
492        $this->_canonData .= $string;
493        hash_update($this->_hashHandler, $string);
494    }
495
496    private function _startHash()
497    {
498        // Init
499        switch ($this->_hashAlgorithm) {
500            case 'rsa-sha1':
501                $this->_hashHandler = hash_init('sha1');
502                break;
503        }
504        $this->_bodyCanonLine = '';
505    }
506
507    /**
508     * @throws Swift_SwiftException
509     *
510     * @return string
511     */
512    private function _getEncryptedHash()
513    {
514        $signature = '';
515        $pkeyId = openssl_get_privatekey($this->_privateKey);
516        if (!$pkeyId) {
517            throw new Swift_SwiftException('Unable to load DomainKey Private Key ['.openssl_error_string().']');
518        }
519        if (openssl_sign($this->_canonData, $signature, $pkeyId, OPENSSL_ALGO_SHA1)) {
520            return $signature;
521        }
522        throw new Swift_SwiftException('Unable to sign DomainKey Hash  ['.openssl_error_string().']');
523    }
524}
525