content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.apache.cassandra.security;
  
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.security.AlgorithmParameters;
  import java.security.GeneralSecurityException;
  import java.security.Key;
  import java.security.KeyFactory;
  import java.security.PrivateKey;
  import java.security.cert.Certificate;
  import java.security.cert.CertificateFactory;
  import java.security.cert.X509Certificate;
  import java.security.spec.PKCS8EncodedKeySpec;
  import java.util.ArrayList;
  import java.util.Base64;
  import java.util.List;
  import java.util.Set;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import javax.crypto.BadPaddingException;
  import javax.crypto.Cipher;
  import javax.crypto.EncryptedPrivateKeyInfo;
  import javax.crypto.SecretKeyFactory;
  import javax.crypto.spec.PBEKeySpec;
  
  import com.google.common.collect.ImmutableSet;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static java.util.regex.Pattern.CASE_INSENSITIVE;
  
  /**
   * This is a helper class to read private keys and X509 certifificates encoded based on <a href="https://datatracker.ietf.org/doc/html/rfc1421">PEM (RFC 1421)</a>
   * format. It can read Password Based Encrypted (PBE henceforth) private keys as well as non-encrypted private keys
   * along with the X509 certificates/cert-chain based on the textual encoding defined in the <a href="https://datatracker.ietf.org/doc/html/rfc7468">RFC 7468</a>
   * <p>
   * The input private key must be in PKCS#8 format.
   * <p>
   * It returns PKCS#8 formatted private key and X509 certificates.
   */
  public final class PEMReader
  {
      /**
       * The private key can be with any of these algorithms in order for this read to successfully parse it.
       * Currently, supported algorithms are,
       * <pre>
       *     RSA, DSA or EC
       * </pre>
       * The first one to be evaluated is RSA, being the most common for private keys.
       */
      public static final Set<String> SUPPORTED_PRIVATE_KEY_ALGORITHMS = ImmutableSet.of("RSA", "DSA", "EC");
      private static final Logger logger = LoggerFactory.getLogger(PEMReader.class);
      private static final Pattern CERT_PATTERN = Pattern.compile("-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+([a-z0-9+/=\\r\\n]+)-+END\\s+.*CERTIFICATE[^-]*-+", CASE_INSENSITIVE);
      private static final Pattern KEY_PATTERN = Pattern.compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+([a-z0-9+/=\\r\\n]+)-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", CASE_INSENSITIVE);
  
      /**
       * Extracts private key from the PEM content for the private key, assuming its not PBE.
       *
       * @param unencryptedPEMKey private key stored as PEM content
       * @return {@link PrivateKey} upon successful reading of the private key
       * @throws IOException              in case PEM reading fails
       * @throws GeneralSecurityException in case any issue encountered while reading the private key
       */
      public static PrivateKey extractPrivateKey(String unencryptedPEMKey) throws IOException, GeneralSecurityException
      {
          return extractPrivateKey(unencryptedPEMKey, null);
      }
  
      /**
       * Extracts private key from the Password Based Encrypted PEM content for the private key.
       *
       * @param pemKey      PBE private key stored as PEM content
       * @param keyPassword password to be used for the private key decryption
       * @return {@link PrivateKey} upon successful reading of the private key
       * @throws IOException              in case PEM reading fails
       * @throws GeneralSecurityException in case any issue encountered while reading the private key
       */
      public static PrivateKey extractPrivateKey(String pemKey, String keyPassword) throws IOException,
                                                                                           GeneralSecurityException
      {
          PKCS8EncodedKeySpec keySpec;
          String base64EncodedKey = extractBase64EncodedKey(pemKey);
          byte[] derKeyBytes = decodeBase64(base64EncodedKey);
  
          if (!StringUtils.isEmpty(keyPassword))
          {
              logger.debug("Encrypted key's length: {}, key's password length: {}",
                           derKeyBytes.length, keyPassword.length());
  
              EncryptedPrivateKeyInfo epki = new EncryptedPrivateKeyInfo(derKeyBytes);
              logger.debug("Encrypted private key info's algorithm name: {}", epki.getAlgName());
  
              AlgorithmParameters params = epki.getAlgParameters();
              PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword.toCharArray());
              Key encryptionKey = SecretKeyFactory.getInstance(epki.getAlgName()).generateSecret(pbeKeySpec);
              pbeKeySpec.clearPassword();
              logger.debug("Key algorithm: {}, key format: {}", encryptionKey.getAlgorithm(), encryptionKey.getFormat());
  
              Cipher cipher = Cipher.getInstance(epki.getAlgName());
              cipher.init(Cipher.DECRYPT_MODE, encryptionKey, params);
              byte[] rawKeyBytes;
              try
              {
                  rawKeyBytes = cipher.doFinal(epki.getEncryptedData());
              }
              catch (BadPaddingException e)
              {
                  throw new GeneralSecurityException("Failed to decrypt the private key data. Either the password " +
                                                     "provided for the key is wrong or the private key data is " +
                                                     "corrupted. msg=" + e.getMessage(), e);
              }
              logger.debug("Decrypted private key's length: {}", rawKeyBytes.length);
  
              keySpec = new PKCS8EncodedKeySpec(rawKeyBytes);
          }
          else
          {
              logger.debug("Key length: {}", derKeyBytes.length);
              keySpec = new PKCS8EncodedKeySpec(derKeyBytes);
          }
  
          PrivateKey privateKey = null;
  
          /*
           * Ideally we can inspect the OID (Object Identifier) from the private key with ASN.1 parser and identify the
           * actual algorithm of the private key. For doing that, we have to use some special library like BouncyCastle.
           * However in the absence of that, below brute-force approach can work- that is to try out all the supported
           * private key algorithms given that there are only three major algorithms to verify against.
           */
          for (String privateKeyAlgorithm : SUPPORTED_PRIVATE_KEY_ALGORITHMS)
          {
              try
              {
                  privateKey = KeyFactory.getInstance(privateKeyAlgorithm).generatePrivate(keySpec);
                  logger.info("Parsing for the private key finished with {} algorithm.", privateKeyAlgorithm);
                  return privateKey;
              }
              catch (Exception e)
              {
                  logger.debug("Failed to parse the private key with {} algorithm. Will try the other supported " +
                               "algorithms.", privateKeyAlgorithm);
              }
          }
          throw new GeneralSecurityException("The given private key could not be parsed with any of the supported " +
                                             "algorithms. Please see PEMReader#SUPPORTED_PRIVATE_KEY_ALGORITHMS.");
      }
  
      /**
       * Extracts the certificates/cert-chain from the PEM content.
       *
       * @param pemCerts certificates/cert-chain stored as PEM content
       * @return X509 certiificate list
       * @throws GeneralSecurityException in case any issue encountered while reading the certificates
       */
      public static Certificate[] extractCertificates(String pemCerts) throws GeneralSecurityException
      {
          List<Certificate> certificateList = new ArrayList<>();
          List<String> base64EncodedCerts = extractBase64EncodedCerts(pemCerts);
          for (String base64EncodedCertificate : base64EncodedCerts)
          {
              certificateList.add(generateCertificate(base64EncodedCertificate));
          }
          Certificate[] certificates = certificateList.toArray(new Certificate[0]);
          return certificates;
      }
  
      /**
       * Generates the X509 certificate object given the base64 encoded PEM content.
       *
       * @param base64Certificate base64 encoded PEM content for the certificate
       * @return X509 certificate
       * @throws GeneralSecurityException in case any issue encountered while reading the certificate
       */
      private static Certificate generateCertificate(String base64Certificate) throws GeneralSecurityException
      {
          byte[] decodedCertificateBytes = decodeBase64(base64Certificate);
          CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
          X509Certificate certificate =
          (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(decodedCertificateBytes));
          logCertificateDetails(certificate);
          return certificate;
      }
  
      /**
       * Logs X509 certificate details for the debugging purpose with {@code INFO} level log.
       * Namely, it prints - Subject DN, Issuer DN, Certificate serial number and the certificate expiry date which
       * could be very valuable for debugging any certificate related issues.
       *
       * @param certificate certificate to log
       */
      private static void logCertificateDetails(X509Certificate certificate)
      {
          assert certificate != null;
          logger.info("*********** Certificate Details *****************");
          logger.info("Subject DN: {}", certificate.getSubjectDN());
          logger.info("Issuer DN: {}", certificate.getIssuerDN());
          logger.info("Serial Number: {}", certificate.getSerialNumber());
          logger.info("Expiry: {}", certificate.getNotAfter());
      }
  
      /**
       * Parses the PEM formatted private key based on the standard pattern specified by the <a href="https://datatracker.ietf.org/doc/html/rfc7468#section-11">RFC 7468</a>.
       *
       * @param pemKey private key stored as PEM content
       * @return base64 string contained within the defined encapsulation boundaries by the above RFC
       * @throws GeneralSecurityException in case any issue encountered while parsing the key
       */
      private static String extractBase64EncodedKey(String pemKey) throws GeneralSecurityException
      {
          Matcher matcher = KEY_PATTERN.matcher(pemKey);
          if (matcher.find())
          {
              return matcher.group(1).replaceAll("\\s", "");
          }
          else
          {
              throw new GeneralSecurityException("Invalid private key format");
          }
      }
  
      /**
       * Parses the PEM formatted certificate/public-key based on the standard pattern specified by the
       * <a href="https://datatracker.ietf.org/doc/html/rfc7468#section-13">RFC 7468</a>.
       *
       * @param pemCerts certificate/public-key stored as PEM content
       * @return list of base64 encoded certificates within the defined encapsulation boundaries by the above RFC
       * @throws GeneralSecurityException in case any issue encountered parsing the certificate
       */
      private static List<String> extractBase64EncodedCerts(String pemCerts) throws GeneralSecurityException
      {
          List<String> certificateList = new ArrayList<>();
          Matcher matcher = CERT_PATTERN.matcher(pemCerts);
          if (!matcher.find())
          {
              throw new GeneralSecurityException("Invalid certificate format");
          }
  
          for (int start = 0; matcher.find(start); start = matcher.end())
          {
              String certificate = matcher.group(1).replaceAll("\\s", "");
              certificateList.add(certificate);
          }
          return certificateList;
      }
  
      /**
       * Decodes given input in Base64 format.
       *
       * @param base64Input input to be decoded
       * @return byte[] containing decoded bytes
       * @throws GeneralSecurityException in case it fails to decode the given base64 input
       */
      private static byte[] decodeBase64(String base64Input) throws GeneralSecurityException
      {
          try
          {
              return Base64.getDecoder().decode(base64Input);
          }
          catch (IllegalArgumentException e)
          {
              throw new GeneralSecurityException("Failed to decode given base64 input. msg=" + e.getMessage(), e);
          }
      }
  }
  