<?php

namespace Drupal\miniorange_saml_idp;

/**
 * Class representing a security key for XML encryption and signing.
 */
class XMLSecurityKey {
  const TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc';
  const AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc';
  const AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc';
  const AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
  const RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5';
  const RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p';
  const DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1';
  const RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
  const RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
  const RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384';
  const RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512';
  const HMAC_SHA1 = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1';

  /**
   * Cryptographic parameters used for key management.
   *
   * @var array
   */
  private $cryptParams = [];

  /**
   * The type of the cryptographic key.
   *
   * @var int|string
   */
  public $type = 0;

  /**
   * The cryptographic key data.
   *
   * @var mixed|null
   */
  public $key = NULL;

  /**
   * The passphrase.
   *
   * @var string
   */
  public $passphrase = "";

  /**
   * The initialization vector (IV) used in encryption.
   *
   * @var string|null
   */
  public $iv = NULL;

  /**
   * The name associated with the cryptographic key.
   *
   * @var string|null
   */
  public $name = NULL;

  /**
   * The key chain, if any, that is associated with the key.
   *
   * @var mixed|null
   */
  public $keyChain = NULL;

  /**
   * Indicates if the key is encrypted.
   *
   * @var bool
   */
  public $isEncrypted = FALSE;

  /**
   * The encrypted context related to the key.
   *
   * @var XMLSecEnc|null
   */
  public $encryptedCtx = NULL;

  /**
   * A globally unique identifier (GUID) for the key.
   *
   * @var mixed|null
   */
  public $guid = NULL;

  /**
   * This variable contains the certificate as a string.
   *
   * If this key represents an X509-certificate.
   * If this key doesn't represent a certificate, this will be null.
   *
   * @var string|null
   */
  private $x509Certificate = NULL;

  /**
   * This variable contains the certificate thumbprint.
   *
   * If we have loaded an X509-certificate.
   *
   * @var string|null
   */
  private $X509Thumbprint = NULL;

  /**
   * Constructor for the XMLSecurityKey class.
   *
   * @param string $type
   *   The type of encryption or signing algorithm (e.g., AES, RSA).
   * @param null|array $params
   *   Optional additional parameters.
   *
   * @throws \Exception
   *   Throws an exception if the "type" is invalid
   *   or if required parameters are missing.
   */
  public function __construct($type, $params = NULL) {
    switch ($type) {
      case (self::TRIPLEDES_CBC):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['cipher'] = 'des-ede3-cbc';
        $this->cryptParams['type'] = 'symmetric';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc';
        $this->cryptParams['keysize'] = 24;
        $this->cryptParams['blocksize'] = 8;
        break;

      case (self::AES128_CBC):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['cipher'] = 'aes-128-cbc';
        $this->cryptParams['type'] = 'symmetric';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc';
        $this->cryptParams['keysize'] = 16;
        $this->cryptParams['blocksize'] = 16;
        break;

      case (self::AES192_CBC):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['cipher'] = 'aes-192-cbc';
        $this->cryptParams['type'] = 'symmetric';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc';
        $this->cryptParams['keysize'] = 24;
        $this->cryptParams['blocksize'] = 16;
        break;

      case (self::AES256_CBC):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['cipher'] = 'aes-256-cbc';
        $this->cryptParams['type'] = 'symmetric';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
        $this->cryptParams['keysize'] = 32;
        $this->cryptParams['blocksize'] = 16;
        break;

      case (self::RSA_1_5):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING;
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5';
        if (is_array($params) && !empty($params['type'])) {
          if ($params['type'] == 'public' || $params['type'] == 'private') {
            $this->cryptParams['type'] = $params['type'];
            break;
          }
        }
        throw new \Exception('Certificate "type" (private/public) must be passed via parameters');

      case (self::RSA_OAEP_MGF1P):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['padding'] = OPENSSL_PKCS1_OAEP_PADDING;
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p';
        $this->cryptParams['hash'] = NULL;
        if (is_array($params) && !empty($params['type'])) {
          if ($params['type'] == 'public' || $params['type'] == 'private') {
            $this->cryptParams['type'] = $params['type'];
            break;
          }
        }
        throw new \Exception('Certificate "type" (private/public) must be passed via parameters');

      case (self::RSA_SHA1):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
        $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING;
        if (is_array($params) && !empty($params['type'])) {
          if ($params['type'] == 'public' || $params['type'] == 'private') {
            $this->cryptParams['type'] = $params['type'];
            break;
          }
        }
        throw new \Exception('Certificate "type" (private/public) must be passed via parameters');

      case (self::RSA_SHA256):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
        $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING;
        $this->cryptParams['digest'] = 'SHA256';
        if (is_array($params) && !empty($params['type'])) {
          if ($params['type'] == 'public' || $params['type'] == 'private') {
            $this->cryptParams['type'] = $params['type'];
            break;
          }
        }
        throw new \Exception('Certificate "type" (private/public) must be passed via parameters');

      case (self::RSA_SHA384):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384';
        $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING;
        $this->cryptParams['digest'] = 'SHA384';
        if (is_array($params) && !empty($params['type'])) {
          if ($params['type'] == 'public' || $params['type'] == 'private') {
            $this->cryptParams['type'] = $params['type'];
            break;
          }
        }
        throw new \Exception('Certificate "type" (private/public) must be passed via parameters');

      case (self::RSA_SHA512):
        $this->cryptParams['library'] = 'openssl';
        $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512';
        $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING;
        $this->cryptParams['digest'] = 'SHA512';
        if (is_array($params) && !empty($params['type'])) {
          if ($params['type'] == 'public' || $params['type'] == 'private') {
            $this->cryptParams['type'] = $params['type'];
            break;
          }
        }
        throw new \Exception('Certificate "type" (private/public) must be passed via parameters');

      case (self::HMAC_SHA1):
        $this->cryptParams['library'] = $type;
        $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1';
        break;

      default:
        throw new \Exception('Invalid Key Type');
    }
    $this->type = $type;
  }

  /**
   * Retrieve the key size for the symmetric encryption algorithm..
   *
   * If the key size is unknown, or this isn't a symmetric encryption algorithm,
   * null is returned.
   *
   * @return int|null
   *   The number of bytes in the key.
   */
  public function getSymmetricKeySize() {
    if (!isset($this->cryptParams['keysize'])) {
      return NULL;
    }
    return $this->cryptParams['keysize'];
  }

  /**
   * Generates a session key using the openssl-extension.
   *
   * In case of using DES3-CBC the key is checked for a proper parity bits set.
   *
   * @return string
   *   Returns generated session key.
   *
   * @throws \Exception
   */
  public function generateSessionKey() {
    if (!isset($this->cryptParams['keysize'])) {
      throw new \Exception('Unknown key size for type "' . $this->type . '".');
    }
    $keysize = $this->cryptParams['keysize'];

    $key = openssl_random_pseudo_bytes($keysize);

    if ($this->type === self::TRIPLEDES_CBC) {
      /* Make sure that the generated key has the proper parity bits set.
       * Mcrypt doesn't care about the parity bits, but others may care.
       */
      for ($i = 0; $i < strlen($key); $i++) {
        $byte = ord($key[$i]) & 0xfe;
        $parity = 1;
        for ($j = 1; $j < 8; $j++) {
          $parity ^= ($byte >> $j) & 1;
        }
        $byte |= $parity;
        $key[$i] = chr($byte);
      }
    }

    $this->key = $key;
    return $key;
  }

  /**
   * Get the raw thumbprint of a certificate.
   *
   * @param string $cert
   *   The PEM encoded certificate as a string.
   *
   * @return null|string
   *   Returns the SHA1 hash of the certificate data or
   *   null if the certificate is invalid.
   */
  public static function getRawThumbprint($cert) {

    $arCert = explode("\n", $cert);
    $data = '';
    $inData = FALSE;

    foreach ($arCert as $curData) {
      if (!$inData) {
        if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) {
          $inData = TRUE;
        }
      }
      else {
        if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) {
          break;
        }
        $data .= trim($curData);
      }
    }

    if (!empty($data)) {
      return strtolower(sha1(base64_decode($data)));
    }

    return NULL;
  }

  /**
   * Loads the given key, or - with isFile set true - the key from the keyfile.
   *
   * @param string $key
   *   The key or the path to the key file.
   * @param bool $isFile
   *   If true, loads the key from the file. Defaults to false.
   * @param bool $isCert
   *   If true, treats the key as a certificate
   *   and extracts public key and thumbprint.
   *   Defaults to false.
   *
   * @throws \Exception
   *   If unable to extract the key or
   *   if there is an issue with the key size or type.
   */
  public function loadKey($key, $isFile = FALSE, $isCert = FALSE) {
    if ($isFile) {
      $this->key = file_get_contents($key);
    }
    else {
      $this->key = $key;
    }
    if ($isCert) {
      $this->key = openssl_x509_read($this->key);
      openssl_x509_export($this->key, $str_cert);
      $this->x509Certificate = $str_cert;
      $this->key = $str_cert;
    }
    else {
      $this->x509Certificate = NULL;
    }
    if ($this->cryptParams['library'] == 'openssl') {
      switch ($this->cryptParams['type']) {
        case 'public':
          if ($isCert) {
            /* Load the thumbprint if this is an X509 certificate. */
            $this->X509Thumbprint = self::getRawThumbprint($this->key);
          }
          $this->key = openssl_get_publickey($this->key);
          if (!$this->key) {
            throw new \Exception('Unable to extract public key');
          }
          break;

        case 'private':
          $this->key = openssl_get_privatekey($this->key, $this->passphrase);
          break;

        case'symmetric':
          if (strlen($this->key) < $this->cryptParams['keysize']) {
            throw new \Exception('Key must contain at least 25 characters for this cipher');
          }
          break;

        default:
          throw new \Exception('Unknown type');
      }
    }
  }

  /**
   * ISO 10126 Padding.
   *
   * @param string $data
   *   The data to be padded.
   * @param int $blockSize
   *   The block size for padding.
   *
   * @throws \Exception
   *   If the block size exceeds 256.
   *
   * @return string
   *   The padded data.
   */
  private function padISO10126($data, $blockSize) {
    if ($blockSize > 256) {
      throw new \Exception('Block size higher than 256 not allowed');
    }
    $padChr = $blockSize - (strlen($data) % $blockSize);
    $pattern = chr($padChr);
    return $data . str_repeat($pattern, $padChr);
  }

  /**
   * Remove ISO 10126 Padding.
   *
   * @param string $data
   *   The padded data to be unpadded.
   *
   * @return string
   *   The unpadded data.
   */
  private function unpadISO10126($data) {
    $padChr = substr($data, -1);
    $padLen = ord($padChr);
    return substr($data, 0, -$padLen);
  }

  /**
   * Encrypts the given data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be encrypted.
   *
   * @return string
   *   The encrypted data.
   */
  private function encryptSymmetric($data) {
    $this->iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cryptParams['cipher']));
    $data = $this->padISO10126($data, $this->cryptParams['blocksize']);
    $encrypted = openssl_encrypt($data, $this->cryptParams['cipher'], $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->iv);
    if (FALSE === $encrypted) {
      throw new \Exception('Failure encrypting Data (openssl symmetric) - ' . openssl_error_string());
    }
    return $this->iv . $encrypted;
  }

  /**
   * Decrypts the given data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be decrypted.
   *
   * @return string
   *   The decrypted data.
   */
  private function decryptSymmetric($data) {
    $iv_length = openssl_cipher_iv_length($this->cryptParams['cipher']);
    $this->iv = substr($data, 0, $iv_length);
    $data = substr($data, $iv_length);
    $decrypted = openssl_decrypt($data, $this->cryptParams['cipher'], $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->iv);
    if (FALSE === $decrypted) {
      throw new \Exception('Failure decrypting Data (openssl symmetric) - ' . openssl_error_string());
    }
    return $this->unpadISO10126($decrypted);
  }

  /**
   * Encrypts the given public data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be encrypted.
   *
   * @return string
   *   The encrypted data.
   *
   * @throws \Exception
   *   If encryption fails.
   */
  private function encryptPublic($data) {
    if (!openssl_public_encrypt($data, $encrypted, $this->key, $this->cryptParams['padding'])) {
      throw new \Exception('Failure encrypting Data (openssl public) - ' . openssl_error_string());
    }
    return $encrypted;
  }

  /**
   * Decrypts the given public data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be decrypted.
   *
   * @return string
   *   The decrypted data.
   *
   * @throws \Exception.
   *   If decryption fails.
   */
  private function decryptPublic($data) {
    if (!openssl_public_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) {
      throw new \Exception('Failure decrypting Data (openssl public) - ' . openssl_error_string);
    }
    return $decrypted;
  }

  /**
   * Encrypts the given private data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be encrypted.
   *
   * @return string
   *   The encrypted data.
   *
   * @throws \Exception.
   *   If encryption fails.
   */
  private function encryptPrivate($data) {
    if (!openssl_private_encrypt($data, $encrypted, $this->key, $this->cryptParams['padding'])) {
      throw new \Exception('Failure encrypting Data (openssl private) - ' . openssl_error_string());
    }
    return $encrypted;
  }

  /**
   * Decrypts the given private data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be decrypted.
   *
   * @return string
   *   The decrypted data.
   *
   * @throws \Exception.
   *   If decryption is fails.
   */
  private function decryptPrivate($data) {
    if (!openssl_private_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) {
      throw new \Exception('Failure decrypting Data (openssl private) - ' . openssl_error_string);
    }
    return $decrypted;
  }

  /**
   * Signs the given data (string) using the openssl-extension.
   *
   * @param string $data
   *   The data to be signed.
   *
   * @return string
   *   The generated signature.
   *
   * @throws \Exception
   *   If signing fails.
   */
  private function signOpenSSL($data) {
    $algo = OPENSSL_ALGO_SHA1;
    if (!empty($this->cryptParams['digest'])) {
      $algo = $this->cryptParams['digest'];
    }
    if (!openssl_sign($data, $signature, $this->key, $algo)) {
      throw new \Exception('Failure Signing Data: ' . openssl_error_string() . ' - ' . $algo);
    }
    return $signature;
  }

  /**
   * Verifies the given data against the provided signature.
   *
   * Using the openssl extension.
   *
   * @param string $data
   *   The data to be verified.
   * @param string $signature
   *   The signature to verify against.
   *
   * @return int
   *   The verification result (1 if valid, 0 if invalid, -1 on error).
   */
  private function verifyOpenSSL($data, $signature) {
    $algo = OPENSSL_ALGO_SHA1;
    if (!empty($this->cryptParams['digest'])) {
      $algo = $this->cryptParams['digest'];
    }
    return openssl_verify($data, $signature, $this->key, $algo);
  }

  /**
   * Encrypts the data using the assigned encryption library and algorithm.
   *
   * @param string $data
   *   The data to be encrypted.
   *
   * @return mixed
   *   The encrypted data, depending on the algorithm used.
   */
  public function encryptData($data) {
    if ($this->cryptParams['library'] === 'openssl') {
      switch ($this->cryptParams['type']) {
        case 'symmetric':
          return $this->encryptSymmetric($data);

        case 'public':
          return $this->encryptPublic($data);

        case 'private':
          return $this->encryptPrivate($data);
      }
    }
  }

  /**
   * Decrypts the given data (string) using the regarding php-extension.
   *
   * Depending on the library assigned to algorithm in the contructor.
   *
   * @param string $data
   *   The data to be decrypted.
   *
   * @return mixed|string
   *   The decrypted data, depending on the algorithm used.
   */
  public function decryptData($data) {
    if ($this->cryptParams['library'] === 'openssl') {
      switch ($this->cryptParams['type']) {
        case 'symmetric':
          return $this->decryptSymmetric($data);

        case 'public':
          return $this->decryptPublic($data);

        case 'private':
          return $this->decryptPrivate($data);
      }
    }
  }

  /**
   * Signs the data using the extension assigned to the type in the constructor.
   *
   * @param string $data
   *   The data to be signed.
   *
   * @return mixed|string
   *   The generated signature, depending on the algorithm used.
   */
  public function signData($data) {
    switch ($this->cryptParams['library']) {
      case 'openssl':
        return $this->signOpenSSL($data);

      case (self::HMAC_SHA1):
        return hash_hmac("sha1", $data, $this->key, TRUE);
    }
  }

  /**
   * Verifies the data (string) against the given signature.
   *
   * Using the extension assigned to the type in the constructor.
   *
   * @param string $data
   *   The data to be verified.
   * @param string $signature
   *   The signature to verify against.
   *
   * @return bool|int
   *   The verification result.
   */
  public function verifySignature($data, $signature) {
    switch ($this->cryptParams['library']) {
      case 'openssl':
        return $this->verifyOpenSSL($data, $signature);

      case (self::HMAC_SHA1):
        $expectedSignature = hash_hmac("sha1", $data, $this->key, TRUE);
        return strcmp($signature, $expectedSignature) == 0;
    }
  }

  /**
   * Returns the encryption algorithm method.
   *
   * @return mixed
   *   The encryption method defined in the cryptParams.
   */
  public function getAlgorithm() {
    return $this->cryptParams['method'];
  }

  /**
   * Creates an ASN segment based on the given type and string.
   *
   * @param int $type
   *   The type of the ASN segment.
   * @param string $string
   *   The string to be encoded into the ASN segment.
   *
   * @return null|string
   *   The ASN segment or null if invalid.
   */
  public static function makeAsnSegment($type, $string) {
    switch ($type) {
      case 0x02:
        if (ord($string) > 0x7f) {
          $string = chr(0) . $string;
        }
        break;

      case 0x03:
        $string = chr(0) . $string;
        break;
    }

    $length = strlen($string);

    if ($length < 128) {
      $output = sprintf("%c%c%s", $type, $length, $string);
    }
    elseif ($length < 0x0100) {
      $output = sprintf("%c%c%c%s", $type, 0x81, $length, $string);
    }
    elseif ($length < 0x010000) {
      $output = sprintf("%c%c%c%c%s", $type, 0x82, $length / 0x0100, $length % 0x0100, $string);
    }
    else {
      $output = NULL;
    }
    return $output;
  }

  /**
   * Hint: Modulus and Exponent must already be base64 decoded.
   *
   * @param string $modulus
   *   The RSA modulus (base64 decoded).
   * @param string $exponent
   *   The RSA exponent (base64 decoded).
   *
   * @return string
   *   The PEM-encoded RSA public key.
   */
  public static function convertRSA($modulus, $exponent) {
    /* make an ASN publicKeyInfo */
    $exponentEncoding = self::makeAsnSegment(0x02, $exponent);
    $modulusEncoding = self::makeAsnSegment(0x02, $modulus);
    $sequenceEncoding = self::makeAsnSegment(0x30, $modulusEncoding . $exponentEncoding);
    $bitstringEncoding = self::makeAsnSegment(0x03, $sequenceEncoding);
    $rsaAlgorithmIdentifier = pack("H*", "300D06092A864886F70D0101010500");
    $publicKeyInfo = self::makeAsnSegment(0x30, $rsaAlgorithmIdentifier . $bitstringEncoding);

    /* encode the publicKeyInfo in base64 and add PEM brackets */
    $publicKeyInfoBase64 = base64_encode($publicKeyInfo);
    $encoding = "-----BEGIN PUBLIC KEY-----\n";
    $offset = 0;
    while ($segment = substr($publicKeyInfoBase64, $offset, 64)) {
      $encoding = $encoding . $segment . "\n";
      $offset += 64;
    }
    return $encoding . "-----END PUBLIC KEY-----\n";
  }

  /*public function serializeKey($parent)
  {

  }*/

  /**
   * Retrieve the X509 certificate this key represents.
   *
   * Will return the X509 certificate in PEM-format if this key represents
   * an X509 certificate.
   *
   * @return string
   *   The X509 certificate or null if this key doesn't
   *   represent an X509-certificate.
   */
  public function getX509Certificate() {
    return $this->x509Certificate;
  }

  /**
   * Get the thumbprint of this X509 certificate.
   *
   * Returns:
   *  The thumbprint as a lowercase 40-character hexadecimal number, or null
   *  if this isn't a X509 certificate.
   *
   * @return string Lowercase 40-character hexadecimal number of thumbprint
   */
  /*public function getX509Thumbprint()
  {
  return $this->X509Thumbprint;
  }*/

  /**
   * Create key from an EncryptedKey-element.
   *
   * @param \DOMElement $element
   *   The EncryptedKey-element.
   *
   * @throws \Exception.
   *
   * @return XMLSecurityKey
   *   The new key.
   */
  public static function fromEncryptedKeyElement(\DOMElement $element) {

    $objenc = new XMLSecEnc();
    $objenc->setNode($element);
    if (!$objKey = $objenc->locateKey()) {
      throw new \Exception("Unable to locate algorithm for this Encrypted Key");
    }
    $objKey->isEncrypted = TRUE;
    $objKey->encryptedCtx = $objenc;
    XMLSecEnc::staticLocateKeyInfo($objKey, $element);
    return $objKey;
  }

}

/**
 * Class for handling XML Digital Signature (XMLDSig) operations.
 *
 * Provides methods for generating and verifying XML signatures
 * based on various algorithms, including SHA1, SHA256, SHA512,
 * and others.
 */
class XMLSecurityDSig {
  const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#';
  const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1';
  const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256';
  const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384';
  const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512';
  const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160';

  const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
  const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments';
  const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#';
  const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments';

  const template = '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
  <ds:SignedInfo>
    <ds:SignatureMethod />
  </ds:SignedInfo>
</ds:Signature>';

  const BASE_TEMPLATE = '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  <SignedInfo>
    <SignatureMethod />
  </SignedInfo>
</Signature>';

  /**
   * The signature node.
   *
   * @var \DOMElement|null
   */
  public $sigNode = NULL;

  /**
   * An array of ID keys used in the signature process.
   *
   * @var array
   */
  public $idKeys = [];

  /**
   * An array of ID namespaces associated with the signature.
   *
   * @var array
   */
  public $idNS = [];

  /**
   * The signed information in the signature.
   *
   * @var string|null
   */
  private $signedInfo = NULL;

  /**
   * The XPath context used for querying XML nodes.
   *
   * @var DomXPath|null
   */
  private $xPathCtx = NULL;

  /**
   * The canonicalization method for the XML signature.
   *
   * @var string|null
   */
  private $canonicalMethod = NULL;

  /**
   * The prefix used for the XML signature namespace.
   *
   * @var string
   */
  private $prefix = '';

  /**
   * The search prefix used in signature queries.
   *
   * @var string
   */
  private $searchpfx = 'secdsig';

  /**
   * This variable contains an associative array of validated nodes.
   *
   * @var array|null
   */
  private $validatedNodes = NULL;

  /**
   * Initializes the object with an optional XML namespace prefix.
   *
   * @param string $prefix
   *   The namespace prefix (default is 'ds').
   */
  public function __construct($prefix = 'ds') {
    $template = self::BASE_TEMPLATE;
    if (!empty($prefix)) {
      $this->prefix = $prefix . ':';
      $search = ["<S", "</S", "xmlns="];
      $replace = ["<$prefix:S", "</$prefix:S", "xmlns:$prefix="];
      $template = str_replace($search, $replace, $template);
    }
    $sigdoc = new \DOMDocument();
    $sigdoc->loadXML($template);
    $this->sigNode = $sigdoc->documentElement;
  }

  /**
   * Reset the XPathObj to null.
   */
  private function resetXPathObj() {
    $this->xPathCtx = NULL;
  }

  /**
   * Returns the XPathObj or null if xPathCtx is set and sigNode is empty.
   *
   * @return \DOMXPath|null
   *   The XPath object or null.
   */
  private function getXPathObj() {
    if (empty($this->xPathCtx) && !empty($this->sigNode)) {
      $xpath = new \DOMXPath($this->sigNode->ownerDocument);
      $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
      $this->xPathCtx = $xpath;
    }
    return $this->xPathCtx;
  }

  /**
   * Generate guid.
   *
   * @param string $prefix
   *   Prefix to use for guid. defaults to pfx.
   *
   * @return string
   *   The generated guid
   */
  public static function generateGUID($prefix = 'pfx') {
    $uuid = md5(uniqid(mt_rand(), TRUE));
    $guid = $prefix . substr($uuid, 0, 8) . "-" .
                substr($uuid, 8, 4) . "-" .
                substr($uuid, 12, 4) . "-" .
                substr($uuid, 16, 4) . "-" .
                substr($uuid, 20, 12);
    return $guid;
  }

  /**
   * Creates a new XML element node with the specified name and optional value.
   *
   * @param string $name
   *   The name of the new node.
   * @param null|string $value
   *   The value of the node (optional).
   *
   * @return \DOMElement
   *   The created XML element node.
   */
  public function createNewSignNode($name, $value = NULL) {
    $doc = $this->sigNode->ownerDocument;
    if (!is_null($value)) {
      $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix . $name, $value);
    }
    else {
      $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix . $name);
    }
    return $node;
  }

  /**
   * Sets the canonicalization method for the signature.
   *
   * @param string $method
   *   The canonicalization method URL.
   *
   * @throws \Exception
   *   If the provided method is invalid.
   */
  public function setCanonicalMethod($method) {
    switch ($method) {
      case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
      case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
      case 'http://www.w3.org/2001/10/xml-exc-c14n#':
      case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
        $this->canonicalMethod = $method;
        break;

      default:
        throw new \Exception('Invalid Canonical Method');
    }
    if ($xpath = $this->getXPathObj()) {
      $query = './' . $this->searchpfx . ':SignedInfo';
      $nodeset = $xpath->query($query, $this->sigNode);
      if ($sinfo = $nodeset->item(0)) {
        $query = './' . $this->searchpfx . 'CanonicalizationMethod';
        $nodeset = $xpath->query($query, $sinfo);
        if (!($canonNode = $nodeset->item(0))) {
          $canonNode = $this->createNewSignNode('CanonicalizationMethod');
          $sinfo->insertBefore($canonNode, $sinfo->firstChild);
        }
        $canonNode->setAttribute('Algorithm', $this->canonicalMethod);
      }
    }
  }

  /**
   * Canonicalizes the given XML node.
   *
   * @param DOMNode $node
   *   The XML node to be canonicalized.
   * @param string $canonicalmethod
   *   The canonicalization method to be used.
   * @param null|array $arXPath
   *   XPath expressions for specific node selection (optional).
   * @param null|array $prefixList
   *   A list of namespace prefixes (optional).
   *
   * @return string
   *   The canonicalized XML data as a string.
   */
  private function canonicalizeData($node, $canonicalmethod, $arXPath = NULL, $prefixList = NULL) {
    $exclusive = FALSE;
    $withComments = FALSE;
    switch ($canonicalmethod) {
      case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
        $exclusive = FALSE;
        $withComments = FALSE;
        break;

      case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
        $withComments = TRUE;
        break;

      case 'http://www.w3.org/2001/10/xml-exc-c14n#':
        $exclusive = TRUE;
        break;

      case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
        $exclusive = TRUE;
        $withComments = TRUE;
        break;
    }

    if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== NULL) && $node->isSameNode($node->ownerDocument->documentElement)) {
      /* Check for any PI or comments as they would have been excluded */
      $element = $node;
      while ($refnode = $element->previousSibling) {
        if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) {
          break;
        }
        $element = $refnode;
      }
      if ($refnode == NULL) {
        $node = $node->ownerDocument;
      }
    }

    return $node->C14N($exclusive, $withComments, $arXPath, $prefixList);
  }

  /**
   * Canonicalizes the SignedInfo element and returns the result.
   *
   * @return null|string
   *   The canonicalized SignedInfo data, or null if not found.
   */
  public function canonicalizeSignedInfo() {

    $doc = $this->sigNode->ownerDocument;
    $canonicalmethod = NULL;
    if ($doc) {
      $xpath = $this->getXPathObj();
      $query = "./secdsig:SignedInfo";
      $nodeset = $xpath->query($query, $this->sigNode);
      if ($signInfoNode = $nodeset->item(0)) {
        $query = "./secdsig:CanonicalizationMethod";
        $nodeset = $xpath->query($query, $signInfoNode);
        if ($canonNode = $nodeset->item(0)) {
          $canonicalmethod = $canonNode->getAttribute('Algorithm');
        }
        $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod);
        return $this->signedInfo;
      }
    }
    return NULL;
  }

  /**
   * Calculates the digest of the given data using the specified algorithm.
   *
   * @param string $digestAlgorithm
   *   The algorithm to use for digest calculation (e.g., SHA1, SHA256).
   * @param string $data
   *   The data to be digested.
   * @param bool $encode
   *   Whether to base64 encode the resulting digest (default is true).
   *
   * @return string
   *   The calculated digest, optionally base64 encoded.
   *
   * @throws \Exception
   *   If the provided digest algorithm is unsupported.
   */
  public function calculateDigest($digestAlgorithm, $data, $encode = TRUE) {
    switch ($digestAlgorithm) {
      case self::SHA1:
        $alg = 'sha1';
        break;

      case self::SHA256:
        $alg = 'sha256';
        break;

      case self::SHA384:
        $alg = 'sha384';
        break;

      case self::SHA512:
        $alg = 'sha512';
        break;

      case self::RIPEMD160:
        $alg = 'ripemd160';
        break;

      default:
        throw new \Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>");
    }

    $digest = hash($alg, $data, TRUE);
    if ($encode) {
      $digest = base64_encode($digest);
    }
    return $digest;

  }

  /**
   * Validates the digest of the reference node against the given data.
   *
   * @param DOMNode $refNode
   *   The reference node containing the digest information.
   * @param string $data
   *   The data to validate against the stored digest.
   *
   * @return bool
   *   True if the digest matches, false otherwise.
   */
  public function validateDigest($refNode, $data) {
    $xpath = new \DOMXPath($refNode->ownerDocument);
    $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
    $query = 'string(./secdsig:DigestMethod/@Algorithm)';
    $digestAlgorithm = $xpath->evaluate($query, $refNode);

    $digValue = $this->calculateDigest($digestAlgorithm, $data, FALSE);
    $query = 'string(./secdsig:DigestValue)';
    $digestValue = $xpath->evaluate($query, $refNode);
    return ($digValue == base64_decode($digestValue));
  }

  /**
   * Processes the transforms for a given reference node.
   *
   * @param DOMNode $refNode
   *   The reference node containing the transform information.
   * @param DOMNode $objData
   *   The data to apply the transforms to.
   * @param bool $includeCommentNodes
   *   Include comment nodes during canonicalization (default is true).
   *
   * @return string
   *   The transformed data as a string.
   */
  public function processTransforms($refNode, $objData, $includeCommentNodes = TRUE) {
    $data = $objData;
    $xpath = new \DOMXPath($refNode->ownerDocument);
    $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
    $query = './secdsig:Transforms/secdsig:Transform';
    $nodelist = $xpath->query($query, $refNode);
    $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
    $arXPath = NULL;
    $prefixList = NULL;
    foreach ($nodelist as $transform) {
      $algorithm = $transform->getAttribute("Algorithm");
      switch ($algorithm) {
        case 'http://www.w3.org/2001/10/xml-exc-c14n#':
        case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':

          if (!$includeCommentNodes) {
            /* We remove comment nodes by forcing it to use a canonicalization
             * without comments.
             */
            $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#';
          }
          else {
            $canonicalMethod = $algorithm;
          }

          $node = $transform->firstChild;
          while ($node) {
            if ($node->localName == 'InclusiveNamespaces') {
              if ($pfx = $node->getAttribute('PrefixList')) {
                $arpfx = [];
                $pfxlist = explode(" ", $pfx);
                foreach ($pfxlist as $pfx) {
                  $val = trim($pfx);
                  if (!empty($val)) {
                    $arpfx[] = $val;
                  }
                }
                if (count($arpfx) > 0) {
                  $prefixList = $arpfx;
                }
              }
              break;
            }
            $node = $node->nextSibling;
          }

          break;

        case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
        case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
          if (!$includeCommentNodes) {
            /* We remove comment nodes by forcing it to use a canonicalization
             * without comments.
             */
            $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
          }
          else {
            $canonicalMethod = $algorithm;
          }

          break;

        case 'http://www.w3.org/TR/1999/REC-xpath-19991116':
          $node = $transform->firstChild;
          while ($node) {
            if ($node->localName == 'XPath') {
              $arXPath = [];
              $arXPath['query'] = '(.//. | .//@* | .//namespace::*)[' . $node->nodeValue . ']';
              $arXpath['namespaces'] = [];
              $nslist = $xpath->query('./namespace::*', $node);
              foreach ($nslist as $nsnode) {
                if ($nsnode->localName != "xml") {
                  $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;
                }
              }
              break;
            }
            $node = $node->nextSibling;
          }
          break;
      }
    }
    if ($data instanceof \DOMElement) {

      $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList);
    }

    return $data;
  }

  /**
   * Processes a reference node.
   *
   * @param DOMNode $refNode
   *   The reference node to process.
   *
   * @return bool
   *   True if the reference node is valid, false otherwise.
   */
  public function processRefNode($refNode) {
    $dataObject = NULL;

    /*
     * Depending on the URI, we may not want to include comments in the result
     * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
     */
    $includeCommentNodes = TRUE;
    if ($uri = $refNode->getAttribute("URI")) {
      $arUrl = parse_url($uri);
      if (empty($arUrl['path'])) {
        if ($identifier = $arUrl['fragment']) {

          /* This reference identifies a node with the given id by using
           * a URI on the form "#identifier". This should not include comments.
           */
          $includeCommentNodes = FALSE;

          $xPath = new \DOMXPath($refNode->ownerDocument);
          if ($this->idNS && is_array($this->idNS)) {
            foreach ($this->idNS as $nspf => $ns) {
              $xPath->registerNamespace($nspf, $ns);
            }
          }
          $iDlist = '@Id="' . $identifier . '"';
          if (is_array($this->idKeys)) {
            foreach ($this->idKeys as $idKey) {
              $iDlist .= " or @$idKey='$identifier'";
            }
          }
          $query = '//*[' . $iDlist . ']';
          $dataObject = $xPath->query($query)->item(0);
        }
        else {
          $dataObject = $refNode->ownerDocument;
        }
      }
      else {
        $dataObject = file_get_contents($arUrl);
      }
    }
    else {
      /* This reference identifies the root node with an empty URI. This should
       * not include comments.
       */
      $includeCommentNodes = FALSE;

      $dataObject = $refNode->ownerDocument;
    }

    $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes);

    if (!$this->validateDigest($refNode, $data)) {
      return FALSE;
    }

    if ($dataObject instanceof \DOMElement) {
      /* Add this node to the list of validated nodes. */
      if (!empty($identifier)) {
        $this->validatedNodes[$identifier] = $dataObject;
      }
      else {
        $this->validatedNodes[] = $dataObject;
      }
    }

    return TRUE;
  }

  /**
   * Retrieves the identifier from the URI of the reference node, if available.
   *
   * @param DOMNode $refNode
   *   The reference node containing the URI.
   *
   * @return null|string
   *   The identifier from the URI fragment, or null if not found.
   */
  public function getRefNodeID($refNode) {
    if ($uri = $refNode->getAttribute("URI")) {
      $arUrl = parse_url($uri);
      if (empty($arUrl['path'])) {
        if ($identifier = $arUrl['fragment']) {
          return $identifier;
        }
      }
    }
    return NULL;
  }

  /**
   * Validates the reference nodes.
   *
   * @return bool
   *   True if all references are valid, false otherwise.
   *
   * @throws \Exception
   *   If reference nodes are not found or validation fails.
   */
  public function validateReference() {
    $docElem = $this->sigNode->ownerDocument->documentElement;
    if (!$docElem->isSameNode($this->sigNode)) {
      if ($this->sigNode->parentNode != NULL) {
        $this->sigNode->parentNode->removeChild($this->sigNode);
      }
    }
    $xpath = $this->getXPathObj();
    $query = "./secdsig:SignedInfo/secdsig:Reference";
    $nodeset = $xpath->query($query, $this->sigNode);
    if ($nodeset->length == 0) {
      throw new \Exception("Reference nodes not found");
    }

    /* Initialize/reset the list of validated nodes. */
    $this->validatedNodes = [];

    foreach ($nodeset as $refNode) {
      if (!$this->processRefNode($refNode)) {
        /* Clear the list of validated nodes. */
        $this->validatedNodes = NULL;
        throw new \Exception("Reference validation failed");
      }
    }
    return TRUE;
  }

  /**
   * Adds a reference node.
   *
   * @param DOMNode $sinfoNode
   *   The SignedInfo node.
   * @param \DOMDocument $node
   *   The data node to reference.
   * @param string $algorithm
   *   The digest algorithm.
   * @param null|array $arTransforms
   *   Optional transformations.
   * @param null|array $options
   *   Optional options for reference processing.
   */
  private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms = NULL, $options = NULL) {
    $prefix = NULL;
    $prefix_ns = NULL;
    $id_name = 'Id';
    $overwrite_id = TRUE;
    $force_uri = FALSE;

    if (is_array($options)) {
      $prefix = empty($options['prefix']) ? NULL : $options['prefix'];
      $prefix_ns = empty($options['prefix_ns']) ? NULL : $options['prefix_ns'];
      $id_name = empty($options['id_name']) ? 'Id' : $options['id_name'];
      $overwrite_id = !isset($options['overwrite']) ? TRUE : (bool) $options['overwrite'];
      $force_uri = !isset($options['force_uri']) ? FALSE : (bool) $options['force_uri'];
    }

    $attname = $id_name;
    if (!empty($prefix)) {
      $attname = $prefix . ':' . $attname;
    }

    $refNode = $this->createNewSignNode('Reference');
    $sinfoNode->appendChild($refNode);

    if (!$node instanceof \DOMDocument) {
      $uri = NULL;
      if (!$overwrite_id) {
        $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name);
      }
      if (empty($uri)) {
        $uri = self::generateGUID();
        $node->setAttributeNS($prefix_ns, $attname, $uri);
      }
      $refNode->setAttribute("URI", '#' . $uri);
    }
    elseif ($force_uri) {
      $refNode->setAttribute("URI", '');
    }

    $transNodes = $this->createNewSignNode('Transforms');
    $refNode->appendChild($transNodes);

    if (is_array($arTransforms)) {
      foreach ($arTransforms as $transform) {
        $transNode = $this->createNewSignNode('Transform');
        $transNodes->appendChild($transNode);
        if (is_array($transform) &&
              (!empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) &&
              (!empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) {
          $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116');
          $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']);
          $transNode->appendChild($XPathNode);
          if (!empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) {
            foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] as $prefix => $namespace) {
              $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace);
            }
          }
        }
        else {
          $transNode->setAttribute('Algorithm', $transform);
        }
      }
    }
    elseif (!empty($this->canonicalMethod)) {
      $transNode = $this->createNewSignNode('Transform');
      $transNodes->appendChild($transNode);
      $transNode->setAttribute('Algorithm', $this->canonicalMethod);
    }

    $canonicalData = $this->processTransforms($refNode, $node);
    $digValue = $this->calculateDigest($algorithm, $canonicalData);

    $digestMethod = $this->createNewSignNode('DigestMethod');
    $refNode->appendChild($digestMethod);
    $digestMethod->setAttribute('Algorithm', $algorithm);

    $digestValue = $this->createNewSignNode('DigestValue', $digValue);
    $refNode->appendChild($digestValue);
  }

  /**
   * Adds reference node.
   *
   * @param \DOMDocument $node
   *   The data node to be referenced.
   * @param string $algorithm
   *   The digest algorithm.
   * @param null|array $arTransforms
   *   Optional transformations to apply.
   * @param null|array $options
   *   Optional options for reference processing.
   */
  public function addReference($node, $algorithm, $arTransforms = NULL, $options = NULL) {
    if ($xpath = $this->getXPathObj()) {
      $query = "./secdsig:SignedInfo";
      $nodeset = $xpath->query($query, $this->sigNode);
      if ($sInfo = $nodeset->item(0)) {
        $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);
      }
    }
  }

  /**
   * Adds a list of reference nodes.
   *
   * @param array $arNodes
   *   The list of data nodes to be referenced.
   * @param string $algorithm
   *   The digest algorithm.
   * @param null|array $arTransforms
   *   Optional transformations to apply.
   * @param null|array $options
   *   Optional options for reference processing.
   */
  public function addReferenceList($arNodes, $algorithm, $arTransforms = NULL, $options = NULL) {
    if ($xpath = $this->getXPathObj()) {
      $query = "./secdsig:SignedInfo";
      $nodeset = $xpath->query($query, $this->sigNode);
      if ($sInfo = $nodeset->item(0)) {
        foreach ($arNodes as $node) {
          $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);
        }
      }
    }
  }

  /**
   * Locates and returns the public key associated with the signature.
   *
   * @param null|DOMNode $node
   *   The node to locate the key from.
   *
   * @return null|XMLSecurityKey
   *   The public key object, or null if not found.
   */
  public function locateKey($node = NULL) {
    if (empty($node)) {
      $node = $this->sigNode;
    }
    if (!$node instanceof DOMNode) {
      return NULL;
    }
    if ($doc = $node->ownerDocument) {
      $xpath = new \DOMXPath($doc);
      $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
      $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)";
      $algorithm = $xpath->evaluate($query, $node);
      if ($algorithm) {
        try {
          $objKey = new XMLSecurityKey($algorithm, ['type' => 'public']);
        }
        catch (\Exception $e) {
          return NULL;
        }
        return $objKey;
      }
    }
    return NULL;
  }

  /**
   * Verifies the signature using the provided public key.
   *
   * @param XMLSecurityKey $objKey
   *   The public key used to verify the signature.
   *
   * @return bool|int
   *   Returns true if verification succeeds,
   *   false or an integer error code if it fails.
   *
   * @throws \Exception
   *   If the signature value is not found.
   */
  public function verify($objKey) {
    $doc = $this->sigNode->ownerDocument;
    $xpath = new \DOMXPath($doc);
    $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
    $query = "string(./secdsig:SignatureValue)";
    $sigValue = $xpath->evaluate($query, $this->sigNode);
    if (empty($sigValue)) {
      throw new \Exception("Unable to locate SignatureValue");
    }
    return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue));
  }

  /**
   * Signs the given data using the provided private key.
   *
   * @param XMLSecurityKey $objKey
   *   The private key used to sign the data.
   * @param string $data
   *   The data to sign.
   *
   * @return mixed|string
   *   The signed data.
   */
  public function signData($objKey, $data) {
    return $objKey->signData($data);
  }

  /**
   * Signs data using the provided key.
   *
   * @param XMLSecurityKey $objKey
   *   The key to sign the data.
   * @param null|DOMNode $appendToNode
   *   The node to append the signature to (optional).
   */
  public function sign($objKey, $appendToNode = NULL) {
    // If we have a parent node append it now so C14N properly works.
    if ($appendToNode != NULL) {
      $this->resetXPathObj();
      $this->appendSignature($appendToNode);
      $this->sigNode = $appendToNode->lastChild;
    }
    if ($xpath = $this->getXPathObj()) {
      $query = "./secdsig:SignedInfo";
      $nodeset = $xpath->query($query, $this->sigNode);
      if ($sInfo = $nodeset->item(0)) {
        $query = "./secdsig:SignatureMethod";
        $nodeset = $xpath->query($query, $sInfo);
        $sMethod = $nodeset->item(0);
        $sMethod->setAttribute('Algorithm', $objKey->type);
        $data = $this->canonicalizeData($sInfo, $this->canonicalMethod);
        $sigValue = base64_encode($this->signData($objKey, $data));
        $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue);
        if ($infoSibling = $sInfo->nextSibling) {
          $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling);
        }
        else {
          $this->sigNode->appendChild($sigValueNode);
        }
      }
    }
  }

  /**
   * This function inserts the signature element.
   *
   * The signature element will be appended to the element,
   * unless $beforeNode is specified. If $beforeNode is specified,
   * the signature element will be inserted as the last element
   * before $beforeNode.
   *
   * @param DOMNode $node
   *   The node the signature element should be inserted into.
   * @param DOMNode $beforeNode
   *   The node the signature element should be located before.
   *
   * @return DOMNode
   *   The signature element node
   */
  public function insertSignature($node, $beforeNode = NULL) {

    $document = $node->ownerDocument;
    $signatureElement = $document->importNode($this->sigNode, TRUE);

    if ($beforeNode == NULL) {
      return $node->insertBefore($signatureElement);
    }
    else {
      return $node->insertBefore($signatureElement, $beforeNode);
    }
  }

  /**
   * Appends a signature to the parent node.
   *
   * @param DOMNode $parentNode
   *   The parent node to append to.
   * @param bool $insertBefore
   *   Whether to insert before the first child.
   *
   * @return DOMNode
   *   The appended signature node.
   */
  public function appendSignature($parentNode, $insertBefore = FALSE) {
    $beforeNode = $insertBefore ? $parentNode->firstChild : NULL;
    return $this->insertSignature($parentNode, $beforeNode);
  }

  /**
   * Extracts X.509 certificates from a string.
   *
   * @param string $certs
   *   The certificate string.
   * @param bool $isPEMFormat
   *   Whether the certificates are in PEM format.
   *
   * @return array
   *   The extracted certificates.
   */
  public static function staticGet509XCerts($certs, $isPEMFormat = TRUE) {
    if ($isPEMFormat) {
      $data = '';
      $certlist = [];
      $arCert = explode("\n", $certs);
      $inData = FALSE;
      foreach ($arCert as $curData) {
        if (!$inData) {
          if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) {
            $inData = TRUE;
          }
        }
        else {
          if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) {
            $inData = FALSE;
            $certlist[] = $data;
            $data = '';
            continue;
          }
          $data .= trim($curData);
        }
      }
      return $certlist;
    }
    else {
      return [$certs];
    }
  }

  /**
   * Adds an X.509 certificate.
   *
   * @param \DOMElement $parentRef
   *   The parent reference element.
   * @param string $cert
   *   The X.509 certificate (PEM or URL).
   * @param bool $isPEMFormat
   *   Whether the certificate is in PEM format (default: true).
   * @param bool $isURL
   *   Whether the certificate is a URL (default: false).
   * @param null|DOMXPath $xpath
   *   Optional XPath object.
   * @param null|array $options
   *   Optional settings like 'issuerSerial' and 'subjectName'.
   *
   * @throws \Exception
   *   If the parent reference is invalid.
   */
  public static function staticAdd509Cert($parentRef, $cert, $isPEMFormat = TRUE, $isURL = FALSE, $xpath = NULL, $options = NULL) {
    if ($isURL) {
      $cert = file_get_contents($cert);
    }
    if (!$parentRef instanceof \DOMElement) {
      throw new \Exception('Invalid parent Node parameter');
    }
    $baseDoc = $parentRef->ownerDocument;

    if (empty($xpath)) {
      $xpath = new \DOMXPath($parentRef->ownerDocument);
      $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
    }

    $query = "./secdsig:KeyInfo";
    $nodeset = $xpath->query($query, $parentRef);
    $keyInfo = $nodeset->item(0);
    $dsig_pfx = '';
    if (!$keyInfo) {
      $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);
      if (!empty($pfx)) {
        $dsig_pfx = $pfx . ":";
      }
      $inserted = FALSE;
      $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'KeyInfo');

      $query = "./secdsig:Object";
      $nodeset = $xpath->query($query, $parentRef);
      if ($sObject = $nodeset->item(0)) {
        $sObject->parentNode->insertBefore($keyInfo, $sObject);
        $inserted = TRUE;
      }

      if (!$inserted) {
        $parentRef->appendChild($keyInfo);
      }
    }
    else {
      $pfx = $keyInfo->lookupPrefix(self::XMLDSIGNS);
      if (!empty($pfx)) {
        $dsig_pfx = $pfx . ":";
      }
    }

    // Add all certs if there are more than one.
    $certs = self::staticGet509XCerts($cert, $isPEMFormat);

    // Attach X509 data node.
    $x509DataNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'X509Data');
    $keyInfo->appendChild($x509DataNode);

    $issuerSerial = FALSE;
    $subjectName = FALSE;
    if (is_array($options)) {
      if (!empty($options['issuerSerial'])) {
        $issuerSerial = TRUE;
      }
      if (!empty($options['subjectName'])) {
        $subjectName = TRUE;
      }
    }

    // Attach all certificate nodes and any additional data.
    foreach ($certs as $X509Cert) {
      if ($issuerSerial || $subjectName) {
        if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n" . chunk_split($X509Cert, 64, "\n") . "-----END CERTIFICATE-----\n")) {
          if ($subjectName && !empty($certData['subject'])) {
            if (is_array($certData['subject'])) {
              $parts = [];
              foreach ($certData['subject'] as $key => $value) {
                if (is_array($value)) {
                  foreach ($value as $valueElement) {
                    array_unshift($parts, "$key=$valueElement");
                  }
                }
                else {
                  array_unshift($parts, "$key=$value");
                }
              }
              $subjectNameValue = implode(',', $parts);
            }
            else {
              $subjectNameValue = $certData['issuer'];
            }
            $x509SubjectNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'X509SubjectName', $subjectNameValue);
            $x509DataNode->appendChild($x509SubjectNode);
          }
          if ($issuerSerial && !empty($certData['issuer']) && !empty($certData['serialNumber'])) {
            if (is_array($certData['issuer'])) {
              $parts = [];
              foreach ($certData['issuer'] as $key => $value) {
                array_unshift($parts, "$key=$value");
              }
              $issuerName = implode(',', $parts);
            }
            else {
              $issuerName = $certData['issuer'];
            }

            $x509IssuerNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'X509IssuerSerial');
            $x509DataNode->appendChild($x509IssuerNode);

            $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'X509IssuerName', $issuerName);
            $x509IssuerNode->appendChild($x509Node);
            $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'X509SerialNumber', $certData['serialNumber']);
            $x509IssuerNode->appendChild($x509Node);
          }
        }

      }
      $x509CertNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx . 'X509Certificate', $X509Cert);
      $x509DataNode->appendChild($x509CertNode);
    }
  }

  /**
   * Adds an X.509 certificate to the signature node.
   *
   * @param string $cert
   *   The X.509 certificate.
   * @param bool $isPEMFormat
   *   Whether the certificate is in PEM format (default: true).
   * @param bool $isURL
   *   Whether the certificate is a URL (default: false).
   * @param null|array $options
   *   Optional settings like 'issuerSerial' and 'subjectName'.
   */
  public function add509Cert($cert, $isPEMFormat = TRUE, $isURL = FALSE, $options = NULL) {
    if ($xpath = $this->getXPathObj()) {
      self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options);
    }
  }

  /**
   * This function retrieves an associative array of the validated nodes.
   *
   * The array will contain the id of the referenced node as the key and
   * the node itself as the value.
   *
   * @return array
   *   Associative array of validated nodes
   */
  public function getValidatedNodes() {
    return $this->validatedNodes;
  }

}

/**
 * Class representing XML encryption functionality.
 */
class XMLSecEnc {
  const template = "<xenc:EncryptedData xmlns:xenc='http://www.w3.org/2001/04/xmlenc#'>
   <xenc:CipherData>
      <xenc:CipherValue></xenc:CipherValue>
   </xenc:CipherData>
</xenc:EncryptedData>";

  const Element = 'http://www.w3.org/2001/04/xmlenc#Element';
  const Content = 'http://www.w3.org/2001/04/xmlenc#Content';
  const URI = 3;
  const XMLENCNS = 'http://www.w3.org/2001/04/xmlenc#';

  /**
   * The DOM document holding encrypted data.
   *
   * @var null|DOMDocument
   */
  private $encdoc = NULL;

  /**
   * The raw node to be encrypted.
   *
   * @var null|DOMNode
   */
  private $rawNode = NULL;

  /**
   * The type of encryption.
   *
   * @var null|string
   */
  public $type = NULL;

  /**
   * The encryption key element.
   *
   * @var null|DOMElement
   */
  public $encKey = NULL;

  /**
   * List of references for encryption.
   *
   * @var array
   */
  private $references = [];

  /**
   * Constructor for the XMLSecEnc class.
   */
  public function __construct() {
    $this->_resetTemplate();
  }

  /**
   * Resets the encryption template by loading the XML structure.
   */
  private function _resetTemplate() {
    $this->encdoc = new \DOMDocument();
    $this->encdoc->loadXML(self::template);
  }

  /**
   * Adds a reference to the encryption document.
   *
   * @param string $name
   *   The reference name.
   * @param DOMNode $node
   *   The DOMNode to be referenced.
   * @param string $type
   *   The type of reference (e.g., Element or Content).
   *
   * @throws \Exception
   *   If the provided node is not a DOMNode.
   */
  public function addReference($name, $node, $type) {
    if (!$node instanceof DOMNode) {
      throw new \Exception('$node is not of type DOMNode');
    }
    $curencdoc = $this->encdoc;
    $this->_resetTemplate();
    $encdoc = $this->encdoc;
    $this->encdoc = $curencdoc;
    $refuri = XMLSecurityDSig::generateGUID();
    $element = $encdoc->documentElement;
    $element->setAttribute("Id", $refuri);
    $this->references[$name] = ["node" => $node, "type" => $type, "encnode" => $encdoc, "refuri" => $refuri];
  }

  /**
   * Sets the raw node to be encrypted.
   *
   * @param DOMNode $node
   *   The node to be set for encryption.
   */
  public function setNode($node) {
    $this->rawNode = $node;
  }

  /**
   * Encrypt the selected node with the given key.
   *
   * @param XMLSecurityKey $objKey
   *   The encryption key and algorithm.
   * @param bool $replace
   *   Encrypted node should be replaced in the original tree. Default is true.
   *
   * @throws \Exception.
   *
   * @return \DOMElement
   *   The <xenc:EncryptedData>-element.
   */
  public function encryptNode($objKey, $replace = TRUE) {
    $data = '';
    if (empty($this->rawNode)) {
      throw new \Exception('Node to encrypt has not been set');
    }
    if (!$objKey instanceof XMLSecurityKey) {
      throw new \Exception('Invalid Key');
    }
    $doc = $this->rawNode->ownerDocument;
    $xPath = new \DOMXPath($this->encdoc);
    $objList = $xPath->query('/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue');
    $cipherValue = $objList->item(0);
    if ($cipherValue == NULL) {
      throw new \Exception('Error locating CipherValue element within template');
    }
    switch ($this->type) {
      case (self::Element):
        $data = $doc->saveXML($this->rawNode);
        $this->encdoc->documentElement->setAttribute('Type', self::Element);
        break;

      case (self::Content):
        $children = $this->rawNode->childNodes;
        foreach ($children as $child) {
          $data .= $doc->saveXML($child);
        }
        $this->encdoc->documentElement->setAttribute('Type', self::Content);
        break;

      default:
        throw new \Exception('Type is currently not supported');
    }

    $encMethod = $this->encdoc->documentElement->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptionMethod'));
    $encMethod->setAttribute('Algorithm', $objKey->getAlgorithm());
    $cipherValue->parentNode->parentNode->insertBefore($encMethod, $cipherValue->parentNode->parentNode->firstChild);

    $strEncrypt = base64_encode($objKey->encryptData($data));
    $value = $this->encdoc->createTextNode($strEncrypt);
    $cipherValue->appendChild($value);

    if ($replace) {
      switch ($this->type) {
        case (self::Element):
          if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) {
            return $this->encdoc;
          }
          $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, TRUE);
          $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode);
          return $importEnc;

        case (self::Content):
          $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, TRUE);
          while ($this->rawNode->firstChild) {
            $this->rawNode->removeChild($this->rawNode->firstChild);
          }
          $this->rawNode->appendChild($importEnc);
          return $importEnc;
      }
    }
    else {
      return $this->encdoc->documentElement;
    }
  }

  /**
   * Retrieve the CipherValue text from this encrypted node.
   *
   * @throws \Exception
   *
   * @return string|null
   *   The Ciphervalue text, or null if no CipherValue is found.
   */
  public function getCipherValue() {
    if (empty($this->rawNode)) {
      throw new \Exception('Node to decrypt has not been set');
    }

    $doc = $this->rawNode->ownerDocument;
    $xPath = new \DOMXPath($doc);
    $xPath->registerNamespace('xmlencr', self::XMLENCNS);
    /* Only handles embedded content right now and not a reference */
    $query = "./xmlencr:CipherData/xmlencr:CipherValue";
    $nodeset = $xPath->query($query, $this->rawNode);
    $node = $nodeset->item(0);

    if (!$node) {
      return NULL;
    }

    return base64_decode($node->nodeValue);
  }

  /**
   * Decrypt this encrypted node.
   *
   * The behaviour of this function depends on the value of $replace.
   * If $replace is false, we will return the decrypted data as a string.
   * If $replace is true, we will insert the decrypted element(s) into the
   * document, and return the decrypted element(s).
   *
   * @param XMLSecurityKey $objKey
   *   The decryption key that should be used when decrypting the node.
   * @param bool $replace
   *   Whether we should replace the encrypted node in the XML document
   *   with the decrypted data. The default is true.
   *
   * @return string|DOMElement
   *   The decrypted data.
   */
  public function decryptNode($objKey, $replace = TRUE) {
    if (!$objKey instanceof XMLSecurityKey) {
      throw new \Exception('Invalid Key');
    }

    $encryptedData = $this->getCipherValue();
    if ($encryptedData) {
      $decrypted = $objKey->decryptData($encryptedData);
      if ($replace) {
        switch ($this->type) {
          case (self::Element):
            $newdoc = new \DOMDocument();
            $newdoc->loadXML($decrypted);
            if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) {
              return $newdoc;
            }
            $importEnc = $this->rawNode->ownerDocument->importNode($newdoc->documentElement, TRUE);
            $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode);
            return $importEnc;

          case (self::Content):
            if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) {
              $doc = $this->rawNode;
            }
            else {
              $doc = $this->rawNode->ownerDocument;
            }
            $newFrag = $doc->createDocumentFragment();
            $newFrag->appendXML($decrypted);
            $parent = $this->rawNode->parentNode;
            $parent->replaceChild($newFrag, $this->rawNode);
            return $parent;

          default:
            return $decrypted;
        }
      }
      else {
        return $decrypted;
      }
    }
    else {
      throw new \Exception("Cannot locate encrypted data");
    }
  }

  /**
   * Encrypt the XMLSecurityKey.
   *
   * @param XMLSecurityKey $srcKey
   *   The source key for encryption.
   * @param XMLSecurityKey $rawKey
   *   The raw key to encrypt.
   * @param bool $append
   *   Whether to append the encrypted key.
   *
   * @throws \Exception
   *   If keys are invalid or encryption fails.
   */
  public function encryptKey($srcKey, $rawKey, $append = TRUE) {
    if ((!$srcKey instanceof XMLSecurityKey) || (!$rawKey instanceof XMLSecurityKey)) {
      throw new \Exception('Invalid Key');
    }
    $strEncKey = base64_encode($srcKey->encryptData($rawKey->key));
    $root = $this->encdoc->documentElement;
    $encKey = $this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptedKey');
    if ($append) {
      $keyInfo = $root->insertBefore($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'), $root->firstChild);
      $keyInfo->appendChild($encKey);
    }
    else {
      $this->encKey = $encKey;
    }
    $encMethod = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptionMethod'));
    $encMethod->setAttribute('Algorithm', $srcKey->getAlgorithm());
    if (!empty($srcKey->name)) {
      $keyInfo = $encKey->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'));
      $keyInfo->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyName', $srcKey->name));
    }
    $cipherData = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:CipherData'));
    $cipherData->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:CipherValue', $strEncKey));
    if (is_array($this->references) && count($this->references) > 0) {
      $refList = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:ReferenceList'));
      foreach ($this->references as $name => $reference) {
        $refuri = $reference["refuri"];
        $dataRef = $refList->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:DataReference'));
        $dataRef->setAttribute("URI", '#' . $refuri);
      }
    }
    return;
  }

  /**
   * Decrypts the encrypted key.
   *
   * @param XMLSecurityKey $encKey
   *   The encrypted key to be decrypted.
   *
   * @return \DOMElement|string
   *   The decrypted key.
   *
   * @throws \Exception
   *   If the key is not encrypted or lacks data for decryption.
   */
  public function decryptKey($encKey) {
    if (!$encKey->isEncrypted) {
      throw new \Exception("Key is not Encrypted");
    }
    if (empty($encKey->key)) {
      throw new \Exception("Key is missing data to perform the decryption");
    }
    return $this->decryptNode($encKey, FALSE);
  }

  /**
   * Returns the key from the DOM.
   *
   * @param null|DOMNode $node
   *   The DOM node to locate the key from.
   *
   * @return null|XMLSecurityKey
   *   The located key or null if not found.
   */
  public function locateKey($node = NULL) {
    if (empty($node)) {
      $node = $this->rawNode;
    }
    if (!$node instanceof DOMNode) {
      return NULL;
    }
    if ($doc = $node->ownerDocument) {
      $xpath = new \DOMXPath($doc);
      $xpath->registerNamespace('xmlsecenc', self::XMLENCNS);
      $query = ".//xmlsecenc:EncryptionMethod";
      $nodeset = $xpath->query($query, $node);
      if ($encmeth = $nodeset->item(0)) {
        $attrAlgorithm = $encmeth->getAttribute("Algorithm");
        try {
          $objKey = new XMLSecurityKey($attrAlgorithm, ['type' => 'private']);
        }
        catch (\Exception $e) {
          return NULL;
        }
        return $objKey;
      }
    }
    return NULL;
  }

  /**
   * Finds and returns the key from the provided DOM node's KeyInfo element.
   *
   * @param null|XMLSecurityKey $objBaseKey
   *   The base key to populate.
   * @param null|DOMNode $node
   *   The DOM node to search within.
   *
   * @return null|XMLSecurityKey
   *   The found key or base key.
   *
   * @throws \Exception
   *   If key information is invalid or missing.
   */
  public static function staticLocateKeyInfo($objBaseKey = NULL, $node = NULL) {
    if (empty($node) || (!$node instanceof DOMNode)) {
      return NULL;
    }
    $doc = $node->ownerDocument;
    if (!$doc) {
      return NULL;
    }

    $xpath = new \DOMXPath($doc);
    $xpath->registerNamespace('xmlsecenc', self::XMLENCNS);
    $xpath->registerNamespace('xmlsecdsig', XMLSecurityDSig::XMLDSIGNS);
    $query = "./xmlsecdsig:KeyInfo";
    $nodeset = $xpath->query($query, $node);
    $encmeth = $nodeset->item(0);
    if (!$encmeth) {
      /* No KeyInfo in EncryptedData / EncryptedKey. */
      return $objBaseKey;
    }

    foreach ($encmeth->childNodes as $child) {
      switch ($child->localName) {
        case 'KeyName':
          if (!empty($objBaseKey)) {
            $objBaseKey->name = $child->nodeValue;
          }
          break;

        case 'KeyValue':
          foreach ($child->childNodes as $keyval) {
            switch ($keyval->localName) {
              case 'DSAKeyValue':
                throw new \Exception("DSAKeyValue currently not supported");

              case 'RSAKeyValue':
                $modulus = NULL;
                $exponent = NULL;
                if ($modulusNode = $keyval->getElementsByTagName('Modulus')->item(0)) {
                  $modulus = base64_decode($modulusNode->nodeValue);
                }
                if ($exponentNode = $keyval->getElementsByTagName('Exponent')->item(0)) {
                  $exponent = base64_decode($exponentNode->nodeValue);
                }
                if (empty($modulus) || empty($exponent)) {
                  throw new \Exception("Missing Modulus or Exponent");
                }
                $publicKey = XMLSecurityKey::convertRSA($modulus, $exponent);
                $objBaseKey->loadKey($publicKey);
                break;
            }
          }
          break;

        case 'RetrievalMethod':
          $type = $child->getAttribute('Type');
          if ($type !== 'http://www.w3.org/2001/04/xmlenc#EncryptedKey') {
            /* Unsupported key type. */
            break;
          }
          $uri = $child->getAttribute('URI');
          if ($uri[0] !== '#') {
            /* URI not a reference - unsupported. */
            break;
          }
          $id = substr($uri, 1);

          $query = "//xmlsecenc:EncryptedKey[@Id='$id']";
          $keyElement = $xpath->query($query)->item(0);
          if (!$keyElement) {
            throw new \Exception("Unable to locate EncryptedKey with @Id='$id'.");
          }

          return XMLSecurityKey::fromEncryptedKeyElement($keyElement);

        case 'EncryptedKey':
          return XMLSecurityKey::fromEncryptedKeyElement($child);

        case 'X509Data':
          if ($x509certNodes = $child->getElementsByTagName('X509Certificate')) {
            if ($x509certNodes->length > 0) {
              $x509cert = $x509certNodes->item(0)->textContent;
              $x509cert = str_replace(["\r", "\n", " "], "", $x509cert);
              $x509cert = "-----BEGIN CERTIFICATE-----\n" . chunk_split($x509cert, 64, "\n") . "-----END CERTIFICATE-----\n";
              $objBaseKey->loadKey($x509cert, FALSE, TRUE);
            }
          }
          break;
      }
    }
    return $objBaseKey;
  }

  /**
   * Locates and returns the key from the provided DOM node's KeyInfo element.
   *
   * @param null|XMLSecurityKey $objBaseKey
   *   The base key to populate.
   * @param null|DOMNode $node
   *   The DOM node to search within.
   *
   * @return null|XMLSecurityKey
   *   The found key or base key.
   */
  public function locateKeyInfo($objBaseKey = NULL, $node = NULL) {
    if (empty($node)) {
      $node = $this->rawNode;
    }
    return self::staticLocateKeyInfo($objBaseKey, $node);
  }

}
