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