Subversion Repositories oidplus

Rev

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

  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;
  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.