Subversion Repositories oidplus

Rev

Rev 868 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
868 daniel-mar 1
<?php
2
namespace RobRichards\XMLSecLibs;
3
 
4
use DOMDocument;
5
use DOMElement;
6
use DOMNode;
7
use DOMXPath;
8
use Exception;
9
use RobRichards\XMLSecLibs\Utils\XPath as XPath;
10
 
11
/**
12
 * xmlseclibs.php
13
 *
14
 * Copyright (c) 2007-2020, 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-2020 Robert Richards <rrichards@cdatazone.org>
48
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
49
 */
50
 
51
class 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;
1050 daniel-mar 205
            } catch (\Exception $e) {
868 daniel-mar 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'));
1050 daniel-mar 396
                } catch (\Exception $e) {
868 daniel-mar 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
}