1<?php
2namespace RobRichards\XMLSecLibs;
3
4use DOMDocument;
5use DOMElement;
6use DOMNode;
7use DOMXPath;
8use Exception;
9use RobRichards\XMLSecLibs\Utils\XPath as XPath;
10
11/**
12 * xmlseclibs.php
13 *
14 * Copyright (c) 2007-2019, Robert Richards <rrichards@cdatazone.org>.
15 * All rights reserved.
16 *
17 * Redistribution and use in source and binary forms, with or without
18 * modification, are permitted provided that the following conditions
19 * are met:
20 *
21 *   * Redistributions of source code must retain the above copyright
22 *     notice, this list of conditions and the following disclaimer.
23 *
24 *   * Redistributions in binary form must reproduce the above copyright
25 *     notice, this list of conditions and the following disclaimer in
26 *     the documentation and/or other materials provided with the
27 *     distribution.
28 *
29 *   * Neither the name of Robert Richards nor the names of his
30 *     contributors may be used to endorse or promote products derived
31 *     from this software without specific prior written permission.
32 *
33 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
34 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
36 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
37 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
38 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
39 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
40 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
41 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
42 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
43 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
44 * POSSIBILITY OF SUCH DAMAGE.
45 *
46 * @author    Robert Richards <rrichards@cdatazone.org>
47 * @copyright 2007-2019 Robert Richards <rrichards@cdatazone.org>
48 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
49 */
50
51class XMLSecEnc
52{
53    const template = "<xenc:EncryptedData xmlns:xenc='http://www.w3.org/2001/04/xmlenc#'>
54   <xenc:CipherData>
55      <xenc:CipherValue></xenc:CipherValue>
56   </xenc:CipherData>
57</xenc:EncryptedData>";
58
59    const Element = 'http://www.w3.org/2001/04/xmlenc#Element';
60    const Content = 'http://www.w3.org/2001/04/xmlenc#Content';
61    const URI = 3;
62    const XMLENCNS = 'http://www.w3.org/2001/04/xmlenc#';
63
64    /** @var null|DOMDocument */
65    private $encdoc = null;
66
67    /** @var null|DOMNode  */
68    private $rawNode = null;
69
70    /** @var null|string */
71    public $type = null;
72
73    /** @var null|DOMElement */
74    public $encKey = null;
75
76    /** @var array */
77    private $references = array();
78
79    public function __construct()
80    {
81        $this->_resetTemplate();
82    }
83
84    private function _resetTemplate()
85    {
86        $this->encdoc = new DOMDocument();
87        $this->encdoc->loadXML(self::template);
88    }
89
90    /**
91     * @param string $name
92     * @param DOMNode $node
93     * @param string $type
94     * @throws Exception
95     */
96    public function addReference($name, $node, $type)
97    {
98        if (! $node instanceOf DOMNode) {
99            throw new Exception('$node is not of type DOMNode');
100        }
101        $curencdoc = $this->encdoc;
102        $this->_resetTemplate();
103        $encdoc = $this->encdoc;
104        $this->encdoc = $curencdoc;
105        $refuri = XMLSecurityDSig::generateGUID();
106        $element = $encdoc->documentElement;
107        $element->setAttribute("Id", $refuri);
108        $this->references[$name] = array("node" => $node, "type" => $type, "encnode" => $encdoc, "refuri" => $refuri);
109    }
110
111    /**
112     * @param DOMNode $node
113     */
114    public function setNode($node)
115    {
116        $this->rawNode = $node;
117    }
118
119    /**
120     * Encrypt the selected node with the given key.
121     *
122     * @param XMLSecurityKey $objKey  The encryption key and algorithm.
123     * @param bool           $replace Whether the encrypted node should be replaced in the original tree. Default is true.
124     * @throws Exception
125     *
126     * @return DOMElement  The <xenc:EncryptedData>-element.
127     */
128    public function encryptNode($objKey, $replace = true)
129    {
130        $data = '';
131        if (empty($this->rawNode)) {
132            throw new Exception('Node to encrypt has not been set');
133        }
134        if (! $objKey instanceof XMLSecurityKey) {
135            throw new Exception('Invalid Key');
136        }
137        $doc = $this->rawNode->ownerDocument;
138        $xPath = new DOMXPath($this->encdoc);
139        $objList = $xPath->query('/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue');
140        $cipherValue = $objList->item(0);
141        if ($cipherValue == null) {
142            throw new Exception('Error locating CipherValue element within template');
143        }
144        switch ($this->type) {
145            case (self::Element):
146                $data = $doc->saveXML($this->rawNode);
147                $this->encdoc->documentElement->setAttribute('Type', self::Element);
148                break;
149            case (self::Content):
150                $children = $this->rawNode->childNodes;
151                foreach ($children AS $child) {
152                    $data .= $doc->saveXML($child);
153                }
154                $this->encdoc->documentElement->setAttribute('Type', self::Content);
155                break;
156            default:
157                throw new Exception('Type is currently not supported');
158        }
159
160        $encMethod = $this->encdoc->documentElement->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptionMethod'));
161        $encMethod->setAttribute('Algorithm', $objKey->getAlgorithm());
162        $cipherValue->parentNode->parentNode->insertBefore($encMethod, $cipherValue->parentNode->parentNode->firstChild);
163
164        $strEncrypt = base64_encode($objKey->encryptData($data));
165        $value = $this->encdoc->createTextNode($strEncrypt);
166        $cipherValue->appendChild($value);
167
168        if ($replace) {
169            switch ($this->type) {
170                case (self::Element):
171                    if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) {
172                        return $this->encdoc;
173                    }
174                    $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true);
175                    $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode);
176                    return $importEnc;
177                case (self::Content):
178                    $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true);
179                    while ($this->rawNode->firstChild) {
180                        $this->rawNode->removeChild($this->rawNode->firstChild);
181                    }
182                    $this->rawNode->appendChild($importEnc);
183                    return $importEnc;
184            }
185        } else {
186            return $this->encdoc->documentElement;
187        }
188    }
189
190    /**
191     * @param XMLSecurityKey $objKey
192     * @throws Exception
193     */
194    public function encryptReferences($objKey)
195    {
196        $curRawNode = $this->rawNode;
197        $curType = $this->type;
198        foreach ($this->references AS $name => $reference) {
199            $this->encdoc = $reference["encnode"];
200            $this->rawNode = $reference["node"];
201            $this->type = $reference["type"];
202            try {
203                $encNode = $this->encryptNode($objKey);
204                $this->references[$name]["encnode"] = $encNode;
205            } catch (Exception $e) {
206                $this->rawNode = $curRawNode;
207                $this->type = $curType;
208                throw $e;
209            }
210        }
211        $this->rawNode = $curRawNode;
212        $this->type = $curType;
213    }
214
215    /**
216     * Retrieve the CipherValue text from this encrypted node.
217     *
218     * @throws Exception
219     * @return string|null  The Ciphervalue text, or null if no CipherValue is found.
220     */
221    public function getCipherValue()
222    {
223        if (empty($this->rawNode)) {
224            throw new Exception('Node to decrypt has not been set');
225        }
226
227        $doc = $this->rawNode->ownerDocument;
228        $xPath = new DOMXPath($doc);
229        $xPath->registerNamespace('xmlencr', self::XMLENCNS);
230        /* Only handles embedded content right now and not a reference */
231        $query = "./xmlencr:CipherData/xmlencr:CipherValue";
232        $nodeset = $xPath->query($query, $this->rawNode);
233        $node = $nodeset->item(0);
234
235        if (!$node) {
236                return null;
237        }
238
239        return base64_decode($node->nodeValue);
240    }
241
242    /**
243     * Decrypt this encrypted node.
244     *
245     * The behaviour of this function depends on the value of $replace.
246     * If $replace is false, we will return the decrypted data as a string.
247     * If $replace is true, we will insert the decrypted element(s) into the
248     * document, and return the decrypted element(s).
249     *
250     * @param XMLSecurityKey $objKey  The decryption key that should be used when decrypting the node.
251     * @param boolean        $replace Whether we should replace the encrypted node in the XML document with the decrypted data. The default is true.
252     *
253     * @return string|DOMElement  The decrypted data.
254     */
255    public function decryptNode($objKey, $replace=true)
256    {
257        if (! $objKey instanceof XMLSecurityKey) {
258            throw new Exception('Invalid Key');
259        }
260
261        $encryptedData = $this->getCipherValue();
262        if ($encryptedData) {
263            $decrypted = $objKey->decryptData($encryptedData);
264            if ($replace) {
265                switch ($this->type) {
266                    case (self::Element):
267                        $newdoc = new DOMDocument();
268                        $newdoc->loadXML($decrypted);
269                        if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) {
270                            return $newdoc;
271                        }
272                        $importEnc = $this->rawNode->ownerDocument->importNode($newdoc->documentElement, true);
273                        $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode);
274                        return $importEnc;
275                    case (self::Content):
276                        if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) {
277                            $doc = $this->rawNode;
278                        } else {
279                            $doc = $this->rawNode->ownerDocument;
280                        }
281                        $newFrag = $doc->createDocumentFragment();
282                        $newFrag->appendXML($decrypted);
283                        $parent = $this->rawNode->parentNode;
284                        $parent->replaceChild($newFrag, $this->rawNode);
285                        return $parent;
286                    default:
287                        return $decrypted;
288                }
289            } else {
290                return $decrypted;
291            }
292        } else {
293            throw new Exception("Cannot locate encrypted data");
294        }
295    }
296
297    /**
298     * Encrypt the XMLSecurityKey
299     *
300     * @param XMLSecurityKey $srcKey
301     * @param XMLSecurityKey $rawKey
302     * @param bool $append
303     * @throws Exception
304     */
305    public function encryptKey($srcKey, $rawKey, $append=true)
306    {
307        if ((! $srcKey instanceof XMLSecurityKey) || (! $rawKey instanceof XMLSecurityKey)) {
308            throw new Exception('Invalid Key');
309        }
310        $strEncKey = base64_encode($srcKey->encryptData($rawKey->key));
311        $root = $this->encdoc->documentElement;
312        $encKey = $this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptedKey');
313        if ($append) {
314            $keyInfo = $root->insertBefore($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'), $root->firstChild);
315            $keyInfo->appendChild($encKey);
316        } else {
317            $this->encKey = $encKey;
318        }
319        $encMethod = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptionMethod'));
320        $encMethod->setAttribute('Algorithm', $srcKey->getAlgorith());
321        if (! empty($srcKey->name)) {
322            $keyInfo = $encKey->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'));
323            $keyInfo->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyName', $srcKey->name));
324        }
325        $cipherData = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:CipherData'));
326        $cipherData->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:CipherValue', $strEncKey));
327        if (is_array($this->references) && count($this->references) > 0) {
328            $refList = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:ReferenceList'));
329            foreach ($this->references AS $name => $reference) {
330                $refuri = $reference["refuri"];
331                $dataRef = $refList->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:DataReference'));
332                $dataRef->setAttribute("URI", '#' . $refuri);
333            }
334        }
335        return;
336    }
337
338    /**
339     * @param XMLSecurityKey $encKey
340     * @return DOMElement|string
341     * @throws Exception
342     */
343    public function decryptKey($encKey)
344    {
345        if (! $encKey->isEncrypted) {
346            throw new Exception("Key is not Encrypted");
347        }
348        if (empty($encKey->key)) {
349            throw new Exception("Key is missing data to perform the decryption");
350        }
351        return $this->decryptNode($encKey, false);
352    }
353
354    /**
355     * @param DOMDocument $element
356     * @return DOMNode|null
357     */
358    public function locateEncryptedData($element)
359    {
360        if ($element instanceof DOMDocument) {
361            $doc = $element;
362        } else {
363            $doc = $element->ownerDocument;
364        }
365        if ($doc) {
366            $xpath = new DOMXPath($doc);
367            $query = "//*[local-name()='EncryptedData' and namespace-uri()='".self::XMLENCNS."']";
368            $nodeset = $xpath->query($query);
369            return $nodeset->item(0);
370        }
371        return null;
372    }
373
374    /**
375     * Returns the key from the DOM
376     * @param null|DOMNode $node
377     * @return null|XMLSecurityKey
378     */
379    public function locateKey($node=null)
380    {
381        if (empty($node)) {
382            $node = $this->rawNode;
383        }
384        if (! $node instanceof DOMNode) {
385            return null;
386        }
387        if ($doc = $node->ownerDocument) {
388            $xpath = new DOMXPath($doc);
389            $xpath->registerNamespace('xmlsecenc', self::XMLENCNS);
390            $query = ".//xmlsecenc:EncryptionMethod";
391            $nodeset = $xpath->query($query, $node);
392            if ($encmeth = $nodeset->item(0)) {
393                   $attrAlgorithm = $encmeth->getAttribute("Algorithm");
394                try {
395                    $objKey = new XMLSecurityKey($attrAlgorithm, array('type' => 'private'));
396                } catch (Exception $e) {
397                    return null;
398                }
399                return $objKey;
400            }
401        }
402        return null;
403    }
404
405    /**
406     * @param null|XMLSecurityKey $objBaseKey
407     * @param null|DOMNode $node
408     * @return null|XMLSecurityKey
409     * @throws Exception
410     */
411    public static function staticLocateKeyInfo($objBaseKey=null, $node=null)
412    {
413        if (empty($node) || (! $node instanceof DOMNode)) {
414            return null;
415        }
416        $doc = $node->ownerDocument;
417        if (!$doc) {
418            return null;
419        }
420
421        $xpath = new DOMXPath($doc);
422        $xpath->registerNamespace('xmlsecenc', self::XMLENCNS);
423        $xpath->registerNamespace('xmlsecdsig', XMLSecurityDSig::XMLDSIGNS);
424        $query = "./xmlsecdsig:KeyInfo";
425        $nodeset = $xpath->query($query, $node);
426        $encmeth = $nodeset->item(0);
427        if (!$encmeth) {
428            /* No KeyInfo in EncryptedData / EncryptedKey. */
429            return $objBaseKey;
430        }
431
432        foreach ($encmeth->childNodes AS $child) {
433            switch ($child->localName) {
434                case 'KeyName':
435                    if (! empty($objBaseKey)) {
436                        $objBaseKey->name = $child->nodeValue;
437                    }
438                    break;
439                case 'KeyValue':
440                    foreach ($child->childNodes AS $keyval) {
441                        switch ($keyval->localName) {
442                            case 'DSAKeyValue':
443                                throw new Exception("DSAKeyValue currently not supported");
444                            case 'RSAKeyValue':
445                                $modulus = null;
446                                $exponent = null;
447                                if ($modulusNode = $keyval->getElementsByTagName('Modulus')->item(0)) {
448                                    $modulus = base64_decode($modulusNode->nodeValue);
449                                }
450                                if ($exponentNode = $keyval->getElementsByTagName('Exponent')->item(0)) {
451                                    $exponent = base64_decode($exponentNode->nodeValue);
452                                }
453                                if (empty($modulus) || empty($exponent)) {
454                                    throw new Exception("Missing Modulus or Exponent");
455                                }
456                                $publicKey = XMLSecurityKey::convertRSA($modulus, $exponent);
457                                $objBaseKey->loadKey($publicKey);
458                                break;
459                        }
460                    }
461                    break;
462                case 'RetrievalMethod':
463                    $type = $child->getAttribute('Type');
464                    if ($type !== 'http://www.w3.org/2001/04/xmlenc#EncryptedKey') {
465                        /* Unsupported key type. */
466                        break;
467                    }
468                    $uri = $child->getAttribute('URI');
469                    if ($uri[0] !== '#') {
470                        /* URI not a reference - unsupported. */
471                        break;
472                    }
473                    $id = substr($uri, 1);
474
475                    $query = '//xmlsecenc:EncryptedKey[@Id="'.XPath::filterAttrValue($id, XPath::DOUBLE_QUOTE).'"]';
476                    $keyElement = $xpath->query($query)->item(0);
477                    if (!$keyElement) {
478                        throw new Exception("Unable to locate EncryptedKey with @Id='$id'.");
479                    }
480
481                    return XMLSecurityKey::fromEncryptedKeyElement($keyElement);
482                case 'EncryptedKey':
483                    return XMLSecurityKey::fromEncryptedKeyElement($child);
484                case 'X509Data':
485                    if ($x509certNodes = $child->getElementsByTagName('X509Certificate')) {
486                        if ($x509certNodes->length > 0) {
487                            $x509cert = $x509certNodes->item(0)->textContent;
488                            $x509cert = str_replace(array("\r", "\n", " "), "", $x509cert);
489                            $x509cert = "-----BEGIN CERTIFICATE-----\n".chunk_split($x509cert, 64, "\n")."-----END CERTIFICATE-----\n";
490                            $objBaseKey->loadKey($x509cert, false, true);
491                        }
492                    }
493                    break;
494            }
495        }
496        return $objBaseKey;
497    }
498
499    /**
500     * @param null|XMLSecurityKey $objBaseKey
501     * @param null|DOMNode $node
502     * @return null|XMLSecurityKey
503     */
504    public function locateKeyInfo($objBaseKey=null, $node=null)
505    {
506        if (empty($node)) {
507            $node = $this->rawNode;
508        }
509        return self::staticLocateKeyInfo($objBaseKey, $node);
510    }
511}
512