package be.iminds.ilabt.jfed.util;

import ch.ethz.ssh2.crypto.cipher.DESede;
import com.google.common.base.Charsets;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.slf4j.LoggerFactory;
import sun.security.x509.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import java.io.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import java.security.interfaces.*;
import java.security.spec.*;
import java.util.*;

/**
 * KeyUtil contains various methods to convert java keys and certificates to and from PEM format
 */
public class KeyUtil {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(KeyUtil.class);
    private static final byte DER_ANS_SEQUENCE = 0x30;
    private static final byte DER_ANS_INTEGER = 0x02;
    protected static final String BEGIN_RSA_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----";
    protected static final String END_RSA_PRIVATE_KEY = "-----END RSA PRIVATE KEY-----";
    protected static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
    protected static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
    protected static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
    protected static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
    protected static final String SSH_RSA = "ssh-rsa";
    protected static final String SSH_DSS = "ssh-dss";

    private KeyUtil() {
    }

    public static byte[] keyStoreToPKCS12Bytes(@Nonnull KeyStore keystore, @Nullable String keyStorePass, @Nullable String pkcs12Pass)
            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException {
        KeyStore kspkcs12 = KeyStore.getInstance("pkcs12");

        char[] inphrase = keyStorePass == null ? null : keyStorePass.toCharArray();
        char[] outphrase = pkcs12Pass == null ? null : pkcs12Pass.toCharArray();

        kspkcs12.load(null, inphrase);

        Enumeration eAliases = keystore.aliases();
        int n = 0;
        while (eAliases.hasMoreElements()) {
            String strAlias = (String) eAliases.nextElement();
            System.out.println("Alias " + n++ + ": " + strAlias);

            if (keystore.isKeyEntry(strAlias)) {
                System.out.println("Adding key for alias " + strAlias);
                Key key = keystore.getKey(strAlias, inphrase);

                Certificate[] chain = keystore.getCertificateChain(strAlias);

                kspkcs12.setKeyEntry(strAlias, key, outphrase, chain);
            }
        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        kspkcs12.store(out, outphrase);
        return out.toByteArray();
    }

    public static KeyStore pkcs12BytesToKeyStore(@Nonnull byte[] pkcs12bytes, @Nullable String keyStorePass, @Nullable String pkcs12Pass)
            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException {
        KeyStore kspkcs12 = KeyStore.getInstance("pkcs12");
        KeyStore ksjks = KeyStore.getInstance("jks");

        char[] inphrase = pkcs12Pass == null ? null : pkcs12Pass.toCharArray();
        char[] outphrase = keyStorePass == null ? null : keyStorePass.toCharArray();

        kspkcs12.load(new ByteArrayInputStream(pkcs12bytes), inphrase);
        ksjks.load(null, outphrase); //create new

        Enumeration eAliases = kspkcs12.aliases();
        int n = 0;
        while (eAliases.hasMoreElements()) {
            String strAlias = (String) eAliases.nextElement();
            System.out.println("Alias " + n++ + ": " + strAlias);

            if (kspkcs12.isKeyEntry(strAlias)) {
                System.out.println("Adding key for alias " + strAlias);
                Key key = kspkcs12.getKey(strAlias, inphrase);

                Certificate[] chain = kspkcs12.getCertificateChain(strAlias);

                ksjks.setKeyEntry(strAlias, key, outphrase, chain);
            }
        }

        return ksjks;
    }

    /**
     * This reads an X509Certificate from a string containing the certificate in PEM format.
     * PEM format uses markers -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- to mark the certificate data.
     * the certificate data bytes are encoded in Base64. java.security.cert.CertificateFactory can read the decoded bytes
     * into a certificate.
     *
     * @param pem is a String containing a PEM certificate.
     *            It may contain the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- markers, or it may contain only the data.
     *            If it contains the markers, everything outside these markers is ignored, including any other type of markers.
     *            If there are multiple CERTIFICATE markers, only the certificate between the first is read.
     * @return the certificate as a java.security.cert.X509Certificate object
     */
    public static X509Certificate pemToX509Certificate(@Nonnull String pem) /*throws PEMDecodingError*/ {
        String pemCert = pem.trim().replaceAll("\r\n", "\n");
        String start = "-----BEGIN CERTIFICATE-----";
        int startPos = pemCert.indexOf(start);
        if (startPos != -1) {
            pemCert = pemCert.substring(startPos + start.length());
            int endPos = pemCert.indexOf(END_CERTIFICATE);
            if (endPos < 0) return null; //throw new PEMDecodingError("Did not find end: '"+end+"' in: "+pem);
            pemCert = pemCert.substring(0, endPos);
        }
        byte[] decodedContent = Base64.decodeBase64(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(pemCert));
        InputStream inStream = new ByteArrayInputStream(decodedContent);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            X509Certificate certificate = (X509Certificate) cf.generateCertificate(inStream);
            try {
                inStream.close();
            } catch (IOException ignored) {
            }
            return certificate;
        } catch (CertificateException e) {
            LOG.warn("Note: failed to parse certificate: " + e.getMessage(), e);
            return null;
        }
    }

    /**
     * Convert a String containing PEM certificates to a list of X509 certificates.
     * Non Certificate PEM blocks (keys etc) are skipped
     * Invallid certificate PEM blocks are skipped.
     *
     * @return a list of found PEM certificates. if no certificates are in the PEM block null is returned. (so an empty list is never returned.)
     */
    public static List<X509Certificate> pemToX509CertificateChain(@Nonnull String origPem) /*throws PEMDecodingError*/ {
        String pem = origPem.replaceAll("\r\n", "\n");
        List<X509Certificate> res = new ArrayList<>();

        String start = "-----BEGIN CERTIFICATE-----";
        String end = END_CERTIFICATE;

        String pemCert;

        while (!pem.isEmpty()) {
            pemCert = pem.trim();
            int startPos = pemCert.indexOf(start);
            if (startPos != -1) {
                pemCert = pemCert.substring(startPos + start.length());
                int endPos = pemCert.indexOf(end);
                if (endPos < 0) {
                    break;
                }
                pem = pemCert.substring(endPos + end.length());
                pemCert = pemCert.substring(0, endPos);
            } else {
                break;
            }
            byte[] decodedContent =
                    Base64.decodeBase64(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(pemCert));
            InputStream inStream = new ByteArrayInputStream(decodedContent);
            try {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                X509Certificate certificate = (X509Certificate) cf.generateCertificate(inStream);
                try {
                    inStream.close();
                } catch (IOException ignored) {
                }
                res.add(certificate);
            } catch (CertificateException e) {
                //ignore invalid certificates
                LOG.warn("Note: failed to parse certificate (in cert chain). Will ignore this certificate.", e);
            }
        }
        if (res.isEmpty())
            return null;
        return res;
    }

    public static String x509certificateChainToPem(@Nonnull Collection<X509Certificate> certs) {
        String pem = "";
        for (X509Certificate c : certs)
            pem += x509certificateToPem(c);
        return pem;
    }

    @Nullable
    public static String x509certificateToPem(@Nullable X509Certificate cert) {
        if (cert == null)
            return null;

        try {
            String s = Base64.encodeBase64String(cert.getEncoded());
            String begin = "-----BEGIN CERTIFICATE-----\n";
            String end = "-----END CERTIFICATE-----\n";
            s = TextUtil.wrap(s + "\n", 64);
            return begin + s + end;
        } catch (CertificateEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * same as x509certificateToPem but without -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
     */
    @Nullable
    public static String x509certificateToCredentialXmlGid(@Nullable X509Certificate cert) {
        if (cert == null)
            return null;

        try {
            String s = Base64.encodeBase64String(cert.getEncoded());
            s = TextUtil.wrap(s + "\n", 64);
            return s;
        } catch (CertificateEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * This code comes from sun.security.tools.KeyTool
     * choose signature algorithm that is compatible with the private key algorithm argument (keyAlgName)
     */
    public static String getCompatibleSigAlgName(@Nonnull String keyAlgName) {
        if ("DSA".equalsIgnoreCase(keyAlgName)) {
            return "SHA1WithDSA";
        } else if ("RSA".equalsIgnoreCase(keyAlgName)) {
            return "SHA256WithRSA";
        } else if ("EC".equalsIgnoreCase(keyAlgName)) {
            return "SHA256withECDSA";
        } else {
            throw new RuntimeException("Cannot Derive Signature Algorithm");
        }
    }

    public static boolean matchingKeys(@Nonnull PublicKey publicKey, @Nonnull PrivateKey privateKey) {
        try {
            Cipher pkCipher = Cipher.getInstance(privateKey.getAlgorithm());
            pkCipher.init(Cipher.ENCRYPT_MODE, publicKey);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            CipherOutputStream os = new CipherOutputStream(baos, pkCipher);
            byte[] testBytes = {0, 1, 2, 3, 4, 127, -126, -100, 100};
            os.write(testBytes);
            os.close();

            byte enc[] = baos.toByteArray();

            ByteArrayInputStream bais = new ByteArrayInputStream(enc);
            pkCipher.init(Cipher.DECRYPT_MODE, privateKey);
            CipherInputStream is = new CipherInputStream(bais, pkCipher);
            byte[] testReadBytes = new byte[testBytes.length];
            int bytesRead = is.read(testReadBytes);
            assert bytesRead == testReadBytes.length;
            is.close();

            for (int i = 0; i < testBytes.length; i++)
                if (testBytes[i] != testReadBytes[i]) {
                    String orig = "";
                    String read = "";
                    for (int j = 0; j < testBytes.length; j++) {
                        orig += testBytes[j] + " ";
                        read += testReadBytes[j] + " ";
                    }
                    String encString = "";
                    for (byte anEnc : enc) encString += anEnc + " ";
                    LOG.debug("Difference wile checking if private and public key match: byte " + i + " differs.\norig=" + orig + "\nenc=" + encString + "\ndec=" + read);
                    return false;
                }
        } catch (Exception e) {
            LOG.debug("Error checking if private and public key match (probably because keys don't match)", e);
            return false;
        }
        return true;
    }

    /*
     * This code comes from sun.security.tools.KeyTool and  sun.security.x509.CertAndKeyGen
     *
     * Note: since it uses a lot of sun.* classes, it is not stable
     * */
    @Deprecated
    public static X509Certificate makeSelfSigned(@Nonnull KeyPair keyPair, @Nonnull String dn, int validityDays) {
        Calendar expiry = Calendar.getInstance();
        expiry.add(Calendar.DAY_OF_YEAR, validityDays);

        try {
            PrivateKey privKey = keyPair.getPrivate();
            PublicKey pubKey = keyPair.getPublic();
            assert privKey != null;
            assert pubKey != null;

            // Determine the signature algorithm
            String sigAlgName = getCompatibleSigAlgName(privKey.getAlgorithm());


            Date firstDate = new Date(System.currentTimeMillis() - (24L * 60 * 60 * 1000));
            Date lastDate = new Date(System.currentTimeMillis() + (validityDays * 24L * 60 * 60 * 1000));
            CertificateValidity interval = new CertificateValidity(firstDate, lastDate);
            X500Name owner = new X500Name("CN=" + dn);


            X509CertInfo info = new X509CertInfo();
            // Add all mandatory attributes
            info.set(X509CertInfo.VERSION,
                    new CertificateVersion(CertificateVersion.V3));
            info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(
                    new java.util.Random().nextInt() & 0x7fffffff));
            AlgorithmId algID = AlgorithmId.getAlgorithmId(sigAlgName);
            info.set(X509CertInfo.ALGORITHM_ID,
                    new CertificateAlgorithmId(algID));
            info.set(X509CertInfo.SUBJECT, owner);
            info.set(X509CertInfo.KEY, new CertificateX509Key(pubKey));
            info.set(X509CertInfo.VALIDITY, interval);
            info.set(X509CertInfo.ISSUER, owner);

            X509CertImpl cert = new X509CertImpl(info);
            cert.sign(privKey, sigAlgName);

            return cert;
        } catch (IOException | SignatureException | InvalidKeyException | NoSuchProviderException | CertificateException | NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * This uses PKCS8EncodedKeySpec (java.security.spec) to read a PKCS#8 encoded private key from a -----BEGIN PRIVATE KEY----- PEM block
     * <p/>
     * Only the first -----BEGIN PRIVATE KEY----- to -----END PRIVATE KEY----- PEM block is used, all other pem blocks are ignored.
     */


    public static boolean isPemPrivateKeyEncrypted(@Nonnull String pem) {
        if (pem.contains("Proc-Type")) return true;
        if (pem.contains("DEK-Info")) return true;
        return false;
    }

    /**
     * This reads a PrivateKey from a string containing the private key in PEM format.
     * PEM format uses markers -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- to mark the private key.
     * the key data bytes are encoded in Base64. The bytes are in PKCS#8 format. java.security.spec.PKCS8EncodedKeySpec
     * can read the decoded bytes into a PrivateKey.
     * <p/>
     * Note: pemToRsaPrivateKey reads data between -----BEGIN/END RSA PRIVATE KEY----- markers. This data is not so different.
     * Between these markers, PKCS#1 is used to encode the RSA key.
     * PKCS#8 has header info, where PKCS#1 does not. This header contains info about the key, and if it specifies
     * that the key is RSA, it also includes the RSA key in PKCS#1.
     *
     * @param pem is a String containing a PEM private key.
     *            It must contain the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- markers.
     *            Everything outside these markers is ignored, including any other type of markers.
     *            If there are multiple PRIVATE KEY markers, only the private key between the first is read.
     * @return the private key as a PrivateKey object
     */
    public static PrivateKey pemToPrivateKey(@Nonnull String pem, final char[] password) throws PEMDecodingException {
        //see http://www.bitpapers.com/2012/04/using-asn1-in-java-some-real-examples.html on how to support encrypted keys them
        //but you are probably looking for "pemToRsaPrivateKey" which parses -----BEGIN RSA PRIVATE KEY----- and DOES support encrypted keys.
        assert !isPemPrivateKeyEncrypted(pem) : "This method does not support encrypted keys";
        assert password == null || password.length == 0 :
                "This method does not support encrypted keys, but you supplied a password";

        String pemKey = pem.trim().replaceAll("\r\n", "\n");
        String start = "-----BEGIN PRIVATE KEY-----";
        String end = "-----END PRIVATE KEY-----";
        int startPos = pemKey.indexOf(start);
        if (startPos != -1) {
            pemKey = pemKey.substring(startPos + start.length());
            int endPos = pemKey.indexOf(end);
            if (endPos < 0) throw new PEMDecodingException("Did not find end: '" + end + "' in: " + pem);
            pemKey = pemKey.substring(0, endPos).trim();
            String fullPemKey = start + "\n" + pemKey + "\n" + end;

            try {
                //this uses PKCS8EncodedKeySpec (java.security.spec) to read a PKCS#8 encoded private key from a -----BEGIN PRIVATE KEY----- pem block
                byte[] pkcs8KeyBytes = Base64.decodeBase64(pemKey.replaceAll("\n", ""));
                PKCS8EncodedKeySpec pkcs8Spec = new PKCS8EncodedKeySpec(pkcs8KeyBytes);
                KeyFactory keyFact = KeyFactory.getInstance("RSA");
                return keyFact.generatePrivate(pkcs8Spec);
            } catch (Exception e) {
                throw new PEMDecodingException("Error reading PEM private key: " + e.getMessage(), e);
            }
        }
        return null;
    }

    /**
     * RSAPrivateCrtKey contains the public key, this method extracts it
     */
    public static RSAPublicKey rsaPrivateKeyToRsaPublicKey(@Nonnull RSAPrivateKey privateKey) {
        if (privateKey instanceof RSAPrivateCrtKey)
            return rsaPrivateCrtKeyToPublicKey((RSAPrivateCrtKey) privateKey);
        else {
            LOG.error("Cannot reconstruct public key from private key. privateKey class=" + privateKey.getClass().getName());
            return null;
        }
    }

    /**
     * RSAPrivateCrtKey contains the public key, this method extracts it
     */
    public static RSAPublicKey rsaPrivateCrtKeyToPublicKey(@Nonnull RSAPrivateCrtKey rsaPrivateKey) {
        RSAPublicKeySpec publicKeySpec =
                new java.security.spec.RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent());
        KeyFactory keyFactory;
        try {
            keyFactory = KeyFactory.getInstance("RSA");
            PublicKey myPublicKey = keyFactory.generatePublic(publicKeySpec);
            return (RSAPublicKey) myPublicKey;
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            LOG.warn("Failed to create public key from private key", e);
            return null;
        }
    }

    /**
     * read a length stored in DER
     */
    private static BigInteger readDerLength(@Nonnull LinkedList<Byte> bytes) {
        byte first = bytes.removeFirst();

        boolean shortform = (first & 0x80) == 0;
        int len = first & 0x7F;
        if (shortform) {
            assert len > 0;
            BigInteger r = BigInteger.valueOf(len);
            assert r.signum() >= 0;
            return r; //len is actual length
        } else {
            //len is number of octests needed to express length
            int resBytesLen = len;
            int resBytesStart = 0;
            if (bytes.getFirst() < 0) {
                //this is in base 256, so cannot be negative!
                resBytesLen += 1;
                resBytesStart = 1;
            }
            byte[] res = new byte[resBytesLen];
            res[0] = 0;
            for (int i = resBytesStart; i < resBytesLen; i++) {
                res[i] = bytes.removeFirst();
            }
            BigInteger r = new BigInteger(res);
            assert r.signum() > 0;
            return r;
        }
    }

    /**
     * read a length stored in DER
     */
    private static BigInteger readDerInt(@Nonnull LinkedList<Byte> bytes) throws PEMDecodingException {
        byte first = bytes.removeFirst();
        if (first != DER_ANS_INTEGER)
            throw new PEMDecodingException("Expected INTEGER in PEM bytes, but got " + first);
        BigInteger len = readDerLength(bytes);
        int l = len.intValue();
        assert l <= bytes.size();

        byte[] intBytes = new byte[l];
        for (int i = 0; i < l; i++)
            intBytes[i] = bytes.removeFirst();

        return new BigInteger(intBytes);
    }

    /*
     * Read a der sequence containing integers. Store these integers.
     * Any non integer is ignored, and the rest of the sequence is ignored (including integers)! (however, bytes is still left in an usable state)
     */
    private static List<BigInteger> readDerBigIntSequence(@Nonnull LinkedList<Byte> bytes) throws PEMDecodingException {
        List<BigInteger> res = new ArrayList<>();

        byte first = bytes.removeFirst();
        if (first != DER_ANS_SEQUENCE)
            throw new PEMDecodingException("Expected SEQUENCE in PEM bytes, but got " + first);
        BigInteger octetLen = readDerLength(bytes);
        int l = octetLen.intValue();
        int targetLen = bytes.size() - l;

        while (bytes.size() > targetLen) {
            byte nextStart = bytes.getFirst();
            if (nextStart != DER_ANS_INTEGER)
                return res;
            BigInteger nextInt = readDerInt(bytes);
            res.add(nextInt);
        }

        if (bytes.size() > targetLen)
            throw new PEMDecodingException("Expected reading to end at targetLen=" + targetLen + " but read more: bytes.size=" + bytes.size());

        //skip last part if encountered any non DER_ANS_INTEGER
        while (bytes.size() < targetLen)
            bytes.removeFirst();

        return res;
    }

    /**
     * This uses only java internals, and no external lib to read a PKCS#1 encoded RSAPrivateKey from a -----BEGIN RSA PRIVATE KEY----- PEM block
     * <p/>
     * Only the first -----BEGIN RSA PRIVATE KEY----- to -----END RSA PRIVATE KEY----- PEM block is used, all other pem blocks are ignored.
     */
    @Nullable
    public static KeyPair pemToRsaKeyPair(@Nullable String pem, final char[] password) throws PEMDecodingException {
        if (pem == null) return null;
        String pemKey = pem.trim().replaceAll("\r\n", "\n");

        if (LOG.isDebugEnabled()) {
            try {
                String dataMd5String = pemKey == null ? "null" : "empty";
                if (pemKey != null && !pemKey.isEmpty()) {
                    MessageDigest md = MessageDigest.getInstance("MD5");
                    byte[] dataMd5 = md.digest(pemKey.getBytes("UTF-8"));
                    dataMd5String = Base64.encodeBase64String(dataMd5);
                }

                String pwMd5String = password == null ? "null" : "empty";
                if (password != null && password.length > 0) {
                    MessageDigest md = MessageDigest.getInstance("MD5");
                    byte[] pwMd5 = md.digest(new String(password).getBytes("UTF-8"));
                    pwMd5String = Base64.encodeBase64String(pwMd5);
                }

                LOG.debug("pemToRsaKeyPair() MD5's:  pem=" + dataMd5String + "  pass=" + pwMd5String);
            } catch (Exception e) {
                LOG.debug("Error generating debug output", e);
            }
        }
        assert pemKey != null;
        int startPos = pemKey.indexOf(BEGIN_RSA_PRIVATE_KEY);
        if (startPos != -1) {
            pemKey = pemKey.substring(startPos + BEGIN_RSA_PRIVATE_KEY.length());
            int endPos = pemKey.indexOf(END_RSA_PRIVATE_KEY);
            if (endPos < 0)
                throw new PEMDecodingException("Did not find end: '" + END_RSA_PRIVATE_KEY + "' in: " + pem);
            pemKey = pemKey.substring(0, endPos).trim();

            //example of encryption header:
            //-----BEGIN RSA PRIVATE KEY-----
            //Proc-Type: 4,ENCRYPTED
            //DEK-Info: DES-EDE3-CBC,B9A4EBF539D5D355
            //
            //<data>

            String extraDebugOutput = "";
            try {
                String passString = null;
                if (password != null)
                    passString = new String(password);

                String pemEncodedData = pemKey;
                byte[] data;
                if (pemKey.trim().startsWith("Proc-Type: 4,ENCRYPTED")) {
                    int dekInfoIndex = pemKey.indexOf("DEK-Info: ");
                    if (dekInfoIndex < 0)
                        throw new PEMDecodingException("Found Proc-Type: 4,ENCRYPTED in RSA PRIVATE KEY, but no DEK-Info");
                    int dekInfoEndIndex = pemKey.indexOf("\n", dekInfoIndex);
                    int emptyLineIndex = pemKey.indexOf("\n\n");
                    LOG.debug("emptyLineIndex=" + emptyLineIndex);
                    extraDebugOutput = pemKey.substring(0, 70); //typical emptyLineIndex = 62
                    if (emptyLineIndex < 0)
                        throw new PEMDecodingException("Did not find empty line in encoded PEM block");
                    extraDebugOutput = pemKey.substring(0, emptyLineIndex);

                    assert dekInfoEndIndex != -1;
                    String dekInfoLine = pemKey.substring(dekInfoIndex, dekInfoEndIndex);
                    LOG.debug("pemToRsaKeyPair() read dekInfoLine=\"" + dekInfoLine + "\"");
                    assert dekInfoLine.startsWith("DEK-Info: ");
                    String dekInfoContent = dekInfoLine.substring("DEK-Info: ".length());
                    String[] dekInfoContentParts = dekInfoContent.split(",");
                    LOG.debug("pemToRsaKeyPair() dekInfoContent=\"" + dekInfoContent + "\" dekInfoContentParts=" + Arrays.toString(dekInfoContentParts));
                    if (dekInfoContentParts.length != 2)
                        throw new PEMDecodingException("Did not find 2 parts of info in DEK-Info: \"" + dekInfoContent + "\"");
                    String algo = dekInfoContentParts[0];
//                    byte[] salt = new BigInteger(dekInfoContentParts[1], 16).toByteArray(); //convert salt hexadecimal number String to byte array
                    byte[] salt =
                            hexToByteArray(dekInfoContentParts[1]); //convert salt hexadecimal number String to byte array
                    LOG.debug("pemToRsaKeyPair() read dekInfoLine algo=\"" + algo + "\" salt=\"" + dekInfoContentParts[1] + "\"");

                    pemEncodedData = pemKey.substring(emptyLineIndex + 1);
                    data = Base64.decodeBase64(pemEncodedData.replaceAll("\n", ""));

                    if(passString != null) {
                        byte[] passBytes =
                                passString.getBytes("US-ASCII"); //TODO check which charset is supported for PEM passwords (ISO-8859-1? UTF-8? (note: ISO-8859-1 and UTF-8 are supported in all java implementations))
                        data = decryptPEM(algo, salt, data, passBytes);
                    }
                } else {
                    data = Base64.decodeBase64(pemEncodedData.replaceAll("\n", ""));
                }

                if (LOG.isDebugEnabled()) {
                    try {
                        String dataMd5String = data == null ? "null" : "empty";
                        if (data != null && data.length > 0) {
                            MessageDigest md = MessageDigest.getInstance("MD5");
                            byte[] dataMd5 = md.digest(data);
                            dataMd5String = Base64.encodeBase64String(dataMd5);
                        }

                        LOG.debug("pemToRsaKeyPair() MD5's:  decr pem=" + dataMd5String);
                    } catch (Exception e) {
                        LOG.debug("Error generating debug output", e);
                    }
                }

                LinkedList<Byte> dataList = new LinkedList<>();
                for (byte aData : data) dataList.add(aData);
                List<BigInteger> ints =
                        readDerBigIntSequence(dataList); //skips any otherPrimeInfos SEQUENCE at the end!

                if (ints.size() < 9)
                    throw new RuntimeException("Not enough Integers in DER sequence: " + ints.size());

                BigInteger version = ints.get(0);
                BigInteger modulus = ints.get(1);         //n
                BigInteger publicExponent = ints.get(2);  //e
                BigInteger privateExponent = ints.get(3); //d
                BigInteger primeP = ints.get(4);          //p
                BigInteger primeQ = ints.get(5);          //q
                BigInteger primeExponentP = ints.get(6);  //dmp1
                BigInteger primeExponentQ = ints.get(7);  //dmq1
                BigInteger crtCoefficient = ints.get(8);  //iqmp

                RSAPrivateCrtKeySpec rSAPrivateCrtKeySpec = new RSAPrivateCrtKeySpec(modulus, publicExponent,
                        privateExponent, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient);
                KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                RSAPrivateCrtKey key = (RSAPrivateCrtKey) keyFactory.generatePrivate(rSAPrivateCrtKeySpec);

                KeySpec pubKeySpec = new RSAPublicKeySpec(modulus, publicExponent);
                RSAPublicKey pubkey = (RSAPublicKey) keyFactory.generatePublic(pubKeySpec);

                return new KeyPair(pubkey, key);
            } catch (Exception e) {
                if (!extraDebugOutput.isEmpty())
                    LOG.debug("Error reading PEM private key (len=" + pemKey.length() + "). " +
                            "encrypted private key head =\n" + extraDebugOutput + "" +
                            " same as json=\n" + XmlRpcPrintUtil.xmlRpcObjectToString(extraDebugOutput), e);
                else
                    LOG.debug("Error reading PEM private key (len=" + pemKey.length() + ")", e);
                throw new PEMDecodingException("Error reading PEM private key: " + e.getMessage(), e);
            }
        }
        LOG.debug("No " + BEGIN_RSA_PRIVATE_KEY + " found in given pemkey (len=" + pemKey.length() + ")");
        return null;
    }


    /**
     * This reads an RsaPrivateKey from a string containing the private key in PEM format.
     * PEM format uses markers -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- to mark the private key.
     * the key data bytes are encoded in Base64. The bytes are in PKCS#1 format. This uses Ganymed SSH2 RSAPrivateKey to
     * read a PKCS#1 encoded RSAPrivateKey. That library supports encoded keys.
     * <p>
     * Note: see pemToPrivateKey for info about the difference between PKCS#1 and PKCS#8
     *
     * @param pem is a String containing a PEM private key.
     *            It must contain the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- markers.
     *            Everything outside these markers is ignored, including any other type of markers.
     *            If there are multiple RSA PRIVATE KEY markers, only the private key between the first is read.
     * @return the RSA private key as a java.security.interfaces.RSAPrivateKey object
     */
    @Nullable
    public static java.security.interfaces.RSAPrivateKey pemToRsaPrivateKey(@Nullable String pem, @Nullable final char[] password) throws PEMDecodingException {
        if (pem == null) return null;
        KeyPair res = pemToRsaKeyPair(pem, password);
        if (res == null) return null;
        return (RSAPrivateKey) res.getPrivate();
    }

    /**
     * This find either a PRIVATE KEY or a RSA PRIVATE KEY in a PEM file.
     * The RSA PRIVATE KEY will be searched first, and returned if it exists.
     * If there is no RSA PRIVATE KEY, a PRIVATE KEY will be searched and returned if it exists.
     * null will be returned if neither can be found.
     *
     * @see KeyUtil#pemToRsaPrivateKey
     * @see KeyUtil#pemToPrivateKey
     */
    @Nullable
    public static PrivateKey pemToAnyPrivateKey(@Nonnull String pem, final char[] password) throws PEMDecodingException {
        PrivateKey pk1 = pemToRsaPrivateKey(pem, password);
        if (pk1 != null) return pk1;
        PrivateKey pk2 = pemToPrivateKey(pem, password);
        if (pk2 != null) return pk2;
        return null;
    }

    /**
     * Parses all the PEM certificates that can be found in the String and returns them.
     * <p/>
     * Certificates are be found between:
     * -----BEGIN CERTIFICATE-----
     * and
     * -----END CERTIFICATE-----
     *
     * @param pemCerts String containing PEM certificates
     * @return a list with all certificates found
     * @see KeyUtil#pemToX509Certificate
     */
    public static List<Certificate> parseAllPEMCertificates(@Nonnull String pemCerts) {
        List<Certificate> res = new ArrayList<>();

        int startIndex = pemCerts.indexOf(BEGIN_CERTIFICATE);
        while (startIndex != -1) {
            int endIndex = pemCerts.indexOf(END_CERTIFICATE, startIndex + 1);
            if (endIndex == -1) break;
            String certString =
                    pemCerts.substring(startIndex + BEGIN_CERTIFICATE.length(), endIndex).trim().replaceAll("\r\n", "\n");
            Certificate cert = pemToX509Certificate(certString);
            if (cert != null)
                res.add(cert);
            startIndex = pemCerts.indexOf(BEGIN_CERTIFICATE, endIndex + END_CERTIFICATE.length());
        }

        return res;
    }

    /**
     * helper for DER encoding. DER Length octets for a certain length
     */
    static public List<Byte> derLength(int len) {
        assert len >= 0;
        //dirty and quick implementation!
        List<Byte> res = new ArrayList<>();
        if (len >= 0 && len <= 127) {
            res.add((byte) len);
            return res;
        }
        BigInteger helper = BigInteger.valueOf(len);
        byte[] helperBytes = helper.toByteArray();
        assert helper.toByteArray()[0] >= 0;

        int octets_needed = helperBytes.length;//helper.bitLength()+2 / 8;
        boolean ignoreLeadingZero = helperBytes[0] == 0;
        if (ignoreLeadingZero) //never mind negative numbers fix
            octets_needed--;

        assert octets_needed < 128 && octets_needed >= 0;
        byte first = (byte) (0x80 | octets_needed);
        res.add(first);

        for (byte b : helperBytes) {
            if (ignoreLeadingZero) {
                ignoreLeadingZero = false;
                continue;
            }
            res.add(b);
        }
        return res;
    }

    /**
     * helper for DER encoding. Encodes a java BigInt as list of DER bytes.
     */
    static private List<Byte> derBigInt(BigInteger bigint) {
        List<Byte> res = new ArrayList<>();

        if (bigint.signum() == 0) {
            res.add((byte) 0);
            return res;
        }

        byte[] bytes = bigint.toByteArray();
        assert bytes.length > 0;
        boolean skipFirstZeroes = true;
        for (byte b : bytes) {
            if (b != 0 && skipFirstZeroes && b < 0 && bigint.signum() > 0) {
                //Note: Bigint probably does this, but we make sure here
                //first is negative but number is positive, so add 00 padding to prevent negative integer
                res.add((byte) 0x00);
            }
            if (b != 0 || !skipFirstZeroes) {
                skipFirstZeroes = false;
                res.add(b);
            }
        }
        return res;
    }

    /**
     * helper for DER encoding. Encodes a list of java BigInt as list of DER bytes.
     */
    static public List<Byte> derSequenceBigInt(@Nonnull List<BigInteger> bigints) {
        List<Byte> integers = new ArrayList<>();
        for (BigInteger bigint : bigints) {
            integers.add((byte) 0x02); //ANS Integer (public exponent)
            List<Byte> e = derBigInt(bigint);
            integers.addAll(derLength(e.size())); //ANS Integer Size
            integers.addAll(e); //Integer itself
        }

        List<Byte> der = new ArrayList<>();
        der.add((byte) 0x30); //ANS Sequence
        der.addAll(derLength(integers.size())); //ANS Sequence size
        der.addAll(integers);

        return der;
    }

    /**
     * encodes a java RSAPrivateKey object using PKCS#1 standard
     *
     * @param crtKey the RSAPrivateKey to encode
     * @return an array of bytes containing the PKCS#1 encoded key
     */
    public static byte[] getPrivateKeyPKCS1(@Nonnull RSAPrivateCrtKey crtKey) {
        /*
                In the case of an RSA private key, PKCS #1 defines a structure called RSAPrivateKey, which looks as follows:

                RSAPrivateKey ::= SEQUENCE {
                                    version Version,
                                    modulus INTEGER,
                                    publicExponent INTEGER,
                                    privateExponent INTEGER,
                                    prime1 INTEGER,
                                    prime2 INTEGER,
                                    exponent1 INTEGER,
                                    exponent2 INTEGER,
                                    coefficient INTEGER,
                                    otherPrimeInfos OtherPrimeInfos OPTIONAL }
                Version ::= INTEGER { two-prime(0), multi(1) }
                (CONSTRAINED BY {-- version must be multi if otherPrimeInfos present --})

                OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo

                OtherPrimeInfo ::= SEQUENCE {
                                    prime INTEGER,
                                    exponent INTEGER,
                                    coefficient INTEGER }
       */

        List<BigInteger> bigints = new ArrayList<>();
        bigints.add(BigInteger.valueOf(0));
        bigints.add(crtKey.getModulus());
        bigints.add(crtKey.getPublicExponent());
        bigints.add(crtKey.getPrivateExponent());
        bigints.add(crtKey.getPrimeP());
        bigints.add(crtKey.getPrimeQ());
        bigints.add(crtKey.getPrimeExponentP());
        bigints.add(crtKey.getPrimeExponentQ());
        bigints.add(crtKey.getCrtCoefficient());


        List<Byte> der = derSequenceBigInt(bigints);

        byte derBytes[] = new byte[der.size()];
        int i = 0;
        for (byte b : der) {
            derBytes[i++] = b;
        }
        return derBytes;
    }

    /**
     * encodes a java RSAPrivateKey object using PKCS#1 standard
     *
     * @param sshPublicKey the RSAPrivateKey to encode
     * @return an array of bytes containing the PKCS#1 encoded key
     */
    public static byte[] getPublicKeyPKCS1(@Nonnull RSAPublicKey sshPublicKey) {
        /*
      An RSA public key should be represented with the ASN.1 type
         RSAPublicKey:

            RSAPublicKey ::= SEQUENCE {
                modulus           INTEGER,  -- n
                publicExponent    INTEGER   -- e
            }

         The fields of type RSAPublicKey have the following meanings:

          * modulus is the RSA modulus n.

          * publicExponent is the RSA public exponent e.
       * */

        List<BigInteger> bigints = new ArrayList<>();
        bigints.add(sshPublicKey.getModulus());
        bigints.add(sshPublicKey.getPublicExponent());
        List<Byte> der = derSequenceBigInt(bigints);

        byte derBytes[] = new byte[der.size()];
        int i = 0;
        for (byte b : der) {
            derBytes[i++] = b;
        }

        return derBytes;
    }

    /**
     * encodes a java RSAPrivateKey object using PKCS#1 standard. Then encodes the bytes using Base64.
     *
     * @param sshPrivateKey the RSAPrivateKey to encode
     * @return an array of bytes containing the PKCS#1 encoded key, encoded using Base64
     */
    public static char[] getPrivateKeyCharsPKCS1Base64(@Nonnull RSAPrivateCrtKey sshPrivateKey) {
        return getPrivateKeyCharsPKCS1Base64(sshPrivateKey, null);
    }

    public static char[] getPrivateKeyCharsPKCS1Base64(@Nonnull RSAPrivateCrtKey sshPrivateKey, @Nullable char[] password) {
        byte[] pkcs1Key = getPrivateKeyPKCS1(sshPrivateKey);
        String s;
        if (password == null)
            s = Base64.encodeBase64String(pkcs1Key);
        else
            try {
                s = encryptPEM(pkcs1Key, new String(password).getBytes("UTF-8"));
            } catch (PEMDecodingException | UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }

        return s.toCharArray();
    }

    /**
     * encodes a java RSAPublicKey object using PKCS#1 standard. Then encodes the bytes using Base64.
     *
     * @param sshPublicKey the RSAPrivateKey to encode
     * @return an array of bytes containing the PKCS#1 encoded key, encoded using Base64
     */
    public static char[] getPublicKeyCharsPKCS1Base64(@Nonnull RSAPublicKey sshPublicKey) {
        String s = Base64.encodeBase64String(getPublicKeyPKCS1(sshPublicKey));
        return s.toCharArray();
    }

    /**
     * encodes a java PrivateKey object using PKCS#8 standard.
     * The java PrivateKey class normally actually supports this conversion. This checks that and does the conversion.
     *
     * @param privateKey the PrivateKey to encode
     * @return an array of bytes containing the PKCS#8 encoded key
     */
    public static byte[] getPrivateKeyCharsPKCS8(@Nonnull PrivateKey privateKey) {
        if (!Objects.equals(privateKey.getFormat(), "PKCS#8"))
            throw new RuntimeException(privateKey.getClass().getName() + " does not support PKCS#8");
        assert Objects.equals(privateKey.getFormat(), "PKCS#8");
        return privateKey.getEncoded();
    }

    /**
     * encodes a java PrivateKey object using PKCS#8 standard. Then encodes the bytes using Base64.
     *
     * @param privateKey the PrivateKey to encode
     * @return an array of bytes containing the PKCS#8 encoded key, encoded using Base64
     */
    public static char[] getPrivateKeyCharsPKCS8Base64(@Nonnull PrivateKey privateKey) {
        return getPrivateKeyCharsPKCS8Base64(privateKey, null);
    }

    public static char[] getPrivateKeyCharsPKCS8Base64(@Nonnull PrivateKey privateKey, @Nullable char[] password) {
        byte[] pkcs8bytes = getPrivateKeyCharsPKCS8(privateKey);
        String s;
        if (password == null)
            s = Base64.encodeBase64String(pkcs8bytes);
        else
            try {
                s = encryptPEM(pkcs8bytes, new String(password).getBytes("UTF-8"));
            } catch (PEMDecodingException | UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        return s.toCharArray();
    }

    /**
     * Convert an PrivateKey into a PEM "PRIVATE KEY". (PKCS#8)
     *
     * @param privateKey PrivateKey to encode into PEM format
     * @return an array of chars containing the private key in PEM format
     */
    public static char[] privateKeyToPem(@Nonnull PrivateKey privateKey) {
        return privateKeyToPem(privateKey, null);
    }

    public static char[] privateKeyToPem(@Nonnull PrivateKey privateKey, @Nullable char[] password) {
        String pem =
                TextUtil.wrap(new String(getPrivateKeyCharsPKCS8Base64(privateKey, password)), 64); //PKCS#8 encoded
        String res =
                BEGIN_PRIVATE_KEY + "\n" + pem + "\n" + END_PRIVATE_KEY + "\n"; //RSA does not need to be mentioned, since "RSA" algo is in PKCS#8 header (which also contains PKCS#1 key)
        return res.toCharArray();
    }

    public static char[] privateKeyToAnyPem(@Nonnull PrivateKey privateKey) {
        return privateKeyToAnyPem(privateKey, null);
    }

    public static char[] privateKeyToAnyPem(@Nonnull PrivateKey privateKey, @Nullable char[] password) {
        if (privateKey instanceof RSAPrivateCrtKey)
            return rsaPrivateKeyToPem((RSAPrivateCrtKey) privateKey, password);
        else
            return privateKeyToPem(privateKey, password);
    }

    /**
     * Convert an RsaPrivateKey into a PEM "RSA PRIVATE KEY". (PKCS#1)
     *
     * @param rsaPrivateKey RsaPrivateKey to encode into PEM format
     * @return an array of chars containing the RSA private key in PEM format
     */
    public static char[] rsaPrivateKeyToPem(@Nonnull RSAPrivateKey rsaPrivateKey) {
        return rsaPrivateKeyToPem(rsaPrivateKey, null);
    }

    public static char[] rsaPrivateKeyToPem(@Nonnull RSAPrivateKey rsaPrivateKey, @Nullable char[] password) {
        if (!(rsaPrivateKey instanceof RSAPrivateCrtKey)) {
            throw new RuntimeException("rsaPrivateKey is not of class RSAPrivateCrtKey but of class=" +
                    rsaPrivateKey.getClass().getName() +
                    " -> does not contain enough data to transform it to the PKCS1 format needed for PEM \"RSA PRIVATE KEY\"");
        }

        RSAPrivateCrtKey rsaPrivateCrtKey = (RSAPrivateCrtKey) rsaPrivateKey;

        String tmp = new String(getPrivateKeyCharsPKCS1Base64(rsaPrivateCrtKey, password));
        String pem = TextUtil.wrap(tmp, 64); //PKCS#1 encoded
        String res =
                BEGIN_RSA_PRIVATE_KEY + "\n" + pem + "\n" + END_RSA_PRIVATE_KEY + "\n"; //this only works if the key is in PKCS#1 format
        return res.toCharArray();
    }

    public static boolean hasAnyPrivateKey(@Nonnull String keyCertContent) {
        return hasPrivateKey(keyCertContent) || hasRsaPrivateKey(keyCertContent);
    }

    public static boolean hasPrivateKey(@Nonnull String pemContent) {
        return pemContent.contains(BEGIN_PRIVATE_KEY);
    }

    public static boolean hasRsaPrivateKey(@Nonnull String pemContent) {
        return pemContent.contains(BEGIN_RSA_PRIVATE_KEY);
    }

    public static boolean hasX509Certificate(@Nonnull String pemContent) {
        return pemContent.contains(BEGIN_CERTIFICATE);
    }

    public static boolean isOpenSshRsaKey(@Nonnull File file) throws IOException {
        return isOpenSshRsaKey(IOUtils.fileToString(file));
    }

    public static boolean isOpenSshRsaKey(@Nullable String content) {
        return content != null && content.startsWith("ssh-rsa ");
    }

    public static boolean hasEncryptedRsaPrivateKey(@Nonnull String pemContent) {
        return pemContent.contains(BEGIN_RSA_PRIVATE_KEY) &&
                (pemContent.contains("Proc-Type:")
                        || pemContent.contains("DEK-Info: ")
                        || pemContent.contains("ENCRYPTED"));
    }

    @Nullable
    public static String publicKeyToOpenSshAuthorizedKeysFormat(@Nullable PublicKey publicKey) {
        if (publicKey == null) return null;
        if (publicKey instanceof RSAPublicKey)
            return rsaPublicKeyToOpenSshAuthorizedKeysFormat((RSAPublicKey) publicKey);
        throw new UnsupportedOperationException("Cannot convert non RSAPublicKey to OpenSshAuthorizedKeysFormat: " + publicKey.getClass().getName());
    }

    public static String rsaPublicKeyToOpenSshAuthorizedKeysFormatData(@Nonnull RSAPublicKey rsaPublicKey) {
          /*
           * See: http://blog.oddbit.com/post/converting-openssh-public-keys
           * For an ssh-rsa key, the PEM-encoded data is a series of (length, data) pairs. The length is encoded as four octets (in big-endian order). The values encoded are:
           *      - algorithm name (one of (ssh-rsa, ssh-dsa)). This duplicates the key type in the first field of the public key.
           *      - RSA exponent
           *      - RSA modulus
           */
        byte[] keytype = StringUtils.getBytesUtf8(SSH_RSA);
        byte[] exp = rsaPublicKey.getPublicExponent().toByteArray();
        assert Objects.equals(new BigInteger(exp), rsaPublicKey.getPublicExponent());
        byte[] mod = rsaPublicKey.getModulus().toByteArray();
        byte[] all = ByteBuffer.allocate(exp.length + mod.length + keytype.length + 12).order(ByteOrder.BIG_ENDIAN).
                putInt(keytype.length).put(keytype).
                putInt(exp.length).put(exp).
                putInt(mod.length).put(mod).
                array();

        return Base64.encodeBase64String(all);
    }

    /**
     * Converts an RsaPublicKey to the format of the OpenSSH "authorized_keys" file.
     * <p/>
     * Note: This format is not a standard (not PEM, PKCS#1, ...)
     * <p/>
     * Example public key in this format (... used to shorten example):
     * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...3DxZtLq2nuf4n name@example.com
     *
     * @see KeyUtil#openSshAuthorizedKeysFormatRsaPublicKey(String) for the reverse
     */
    public static String rsaPublicKeyToOpenSshAuthorizedKeysFormat(@Nonnull RSAPublicKey rsaPublicKey) {
        String encoded_all = rsaPublicKeyToOpenSshAuthorizedKeysFormatData(rsaPublicKey);

        String res = "ssh-rsa " + encoded_all;

        //self check
        assert Objects.equals(openSshAuthorizedKeysFormatRsaPublicKey(res).getModulus(), rsaPublicKey.getModulus());
        assert Objects.equals(openSshAuthorizedKeysFormatRsaPublicKey(res).getPublicExponent(), rsaPublicKey.getPublicExponent());

        return res;
    }

    /**
     * Converts a string containing an RSA public key in the format of the OpenSSH "authorized_keys" file to a
     * RsaPublicKey object
     * <p/>
     * Note: This format is not a standard (not PEM, PKCS#1, ...)
     * <p/>
     * Example public key in this format (... used to shorten example):
     * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...3DxZtLq2nuf4n name@example.com
     *
     * @see KeyUtil#rsaPublicKeyToOpenSshAuthorizedKeysFormat(RSAPublicKey) for the reverse
     */
    public static RSAPublicKey openSshAuthorizedKeysFormatRsaPublicKey(@Nonnull String ssh_rsa) {
        //full format (space seperated fields, check also "man sshd" under "authorized_keys"): options, keytype, base64-encoded key, comment
        //with keytype one of ssh-rsa and ssh-dsa
        String[] parts = ssh_rsa.trim().replaceAll("\r\n", "\n").split(" ");

        String encodedKey = null;
        for (int i = 0; i < parts.length - 1; i++) {
            if (Objects.equals(parts[i], SSH_RSA)) {
                encodedKey = parts[i + 1];
                break;
            }
            if (parts[i].equals("ssh-dsa") || parts[i].equals(SSH_DSS))
                throw new RuntimeException("DSA key found in OpenSSH authorized_keys format. DSA keys are not supported by this method, only RSA is supported. input=\"" + ssh_rsa + "\"");
        }
        if (encodedKey == null) {
            throw new RuntimeException("No key found in OpenSSH authorized_keys format. format syntax: <options> <keytype> <base64-encoded key> <comment>. input=\"" + ssh_rsa + "\"");
        }

        byte[] all_restored = Base64.decodeBase64(encodedKey);
        ByteBuffer bb = ByteBuffer.wrap(all_restored).order(ByteOrder.BIG_ENDIAN);

        int kts = bb.getInt();
        byte[] kt = new byte[kts];
        bb.get(kt);
        String keyType = StringUtils.newStringUtf8(kt);
        assert Objects.equals(keyType, SSH_RSA);

        int es = bb.getInt();
        byte[] exp = new byte[es];
        bb.get(exp);

        int ms = bb.getInt();
        byte[] mod = new byte[ms];
        bb.get(mod);

        BigInteger exponent = new BigInteger(exp);
        BigInteger modulus = new BigInteger(mod);

        RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
        RSAPublicKey res;
        try {
            KeyFactory keyFact = KeyFactory.getInstance("RSA");
            PublicKey key = keyFact.generatePublic(spec);
            res = (RSAPublicKey) key;
        } catch (Exception e) {
            throw new RuntimeException("Error creating RSAPublicKey: " + e.getMessage(), e);
        }
        return res;
    }

    private static byte[] generateKeyFromPasswordSaltWithMD5(@Nonnull byte[] password, @Nonnull byte[] salt, int keyLen)
            throws PEMDecodingException {
        if (salt.length < 8)
            throw new PEMDecodingException("Salt needs to be at least 8 bytes for key generation. It is only " + salt.length + " bytes");

        ch.ethz.ssh2.crypto.digest.MD5 md5 = new ch.ethz.ssh2.crypto.digest.MD5();

        byte[] key = new byte[keyLen];
        byte[] tmp = new byte[md5.getDigestLength()];

        while (true) {
            md5.update(password, 0, password.length);
            md5.update(salt, 0, 8);

            int copy = (keyLen < tmp.length) ? keyLen : tmp.length;

            md5.digest(tmp, 0);

            System.arraycopy(tmp, 0, key, key.length - keyLen, copy);

            keyLen -= copy;

            if (keyLen == 0)
                return key;

            md5.update(tmp, 0, tmp.length);
        }
    }


    /**
     * Proc-Type: 4,ENCRYPTED
     * DEK-Info: DES-EDE3-CBC,B9A4 EBF5 39D5 D355
     */
    private static String encryptPEM(@Nonnull byte[] data, @Nonnull byte[] pw) throws PEMDecodingException {
        byte[] salt = new byte[8];
        Random rand = new Random();
        rand.nextBytes(salt);
        String saltString = "";
        for (byte aSalt : salt) {
            saltString += String.format("%02X", aSalt);
        }
        byte[] crypted = encryptPEMhelper(data, salt, pw);
        return "Proc-Type: 4,ENCRYPTED\n" +
                "DEK-Info: DES-EDE3-CBC," + saltString + "\n\n"
                + Base64.encodeBase64String(crypted);
    }

    private static byte[] encryptPEMhelper(@Nonnull byte[] data, @Nonnull byte[] salt, @Nonnull byte[] pw) throws PEMDecodingException {
        DESede des3 = new DESede();
        des3.init(true/*encrypting*/, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
        ch.ethz.ssh2.crypto.cipher.BlockCipher bc =
                new ch.ethz.ssh2.crypto.cipher.CBCMode(des3, salt, true/*doEncrypt*/);

        byte[] paddedData = addPadding(data, bc.getBlockSize());

        byte[] res = new byte[paddedData.length];

        for (int i = 0; i < paddedData.length / bc.getBlockSize(); i++)
            bc.transformBlock(paddedData, i * bc.getBlockSize(), res, i * bc.getBlockSize());

        return res;
    }


    /**
     * code below is based on  ch.ethz.ssh2.crypto.PEMDecoder
     * the library is included in this project, see it's license
     * (binary and source distribution, with or without modification is allowed, with some restrictions.)
     */
    private static byte[] decryptPEM(@Nonnull String algo, @Nonnull byte[] salt, @Nonnull byte[] data, @Nonnull byte[] pw) throws PEMDecodingException {
        ch.ethz.ssh2.crypto.cipher.BlockCipher bc;

        if (LOG.isDebugEnabled()) {
            try {
                String dataMd5String = data == null ? "null" : "empty";
                if (data != null && data.length > 0) {
                    MessageDigest md = MessageDigest.getInstance("MD5");
                    byte[] dataMd5 = md.digest(data);
                    dataMd5String = Base64.encodeBase64String(dataMd5);
                }
                String pwMd5String = pw == null ? "null" : "empty";
                if (pw != null && pw.length > 0) {
                    MessageDigest md = MessageDigest.getInstance("MD5");
                    byte[] pwMd5 = md.digest(pw);
                    pwMd5String = Base64.encodeBase64String(pwMd5);
                }

                LOG.debug("decryptPEM()" +
                        " algo=" + algo +
                        " salt=" + (salt == null ? "null" : Base64.encodeBase64String(salt)) +
                        " dataMd5=" + dataMd5String +
                        " pwMd5=" + pwMd5String);
            } catch (NoSuchAlgorithmException e) {
                LOG.debug("decryptPEM() Error creating debug message", e);
            }

        }

        switch (algo) {
            case "DES-EDE3-CBC":
                DESede des3 = new DESede();
                des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
                bc = new ch.ethz.ssh2.crypto.cipher.CBCMode(des3, salt, false);
                break;
            case "DES-CBC":
                ch.ethz.ssh2.crypto.cipher.DES des = new ch.ethz.ssh2.crypto.cipher.DES();
                des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8));
                bc = new ch.ethz.ssh2.crypto.cipher.CBCMode(des, salt, false);
                break;
            case "AES-128-CBC": {
                ch.ethz.ssh2.crypto.cipher.AES aes = new ch.ethz.ssh2.crypto.cipher.AES();
                aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 16));
                bc = new ch.ethz.ssh2.crypto.cipher.CBCMode(aes, salt, false);
                break;
            }
            case "AES-192-CBC": {
                ch.ethz.ssh2.crypto.cipher.AES aes = new ch.ethz.ssh2.crypto.cipher.AES();
                aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
                bc = new ch.ethz.ssh2.crypto.cipher.CBCMode(aes, salt, false);
                break;
            }
            case "AES-256-CBC": {
                ch.ethz.ssh2.crypto.cipher.AES aes = new ch.ethz.ssh2.crypto.cipher.AES();
                aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32));
                bc = new ch.ethz.ssh2.crypto.cipher.CBCMode(aes, salt, false);
                break;
            }
            default:
                LOG.error("decryptPEM() Cannot decrypt PEM structure, unknown cipher " + algo);
                throw new PEMDecodingException("Cannot decrypt PEM structure, unknown cipher " + algo);
        }

        if ((data.length % bc.getBlockSize()) != 0) {
            LOG.error("Invalid PEM structure, size of encrypted block is not a multiple of "
                    + bc.getBlockSize());
            throw new PEMDecodingException("Invalid PEM structure, size of encrypted block is not a multiple of "
                    + bc.getBlockSize());
        }

        /* Now decrypt the content */

        byte[] dz = new byte[data.length];

        for (int i = 0; i < data.length / bc.getBlockSize(); i++)
            bc.transformBlock(data, i * bc.getBlockSize(), dz, i * bc.getBlockSize());

        /* Now check and remove RFC 1423/PKCS #7 padding */
        dz = removePadding(dz, bc.getBlockSize());

        return dz;
    }

    /* from RFC 1423:
    * The input to the DES CBC encryption process shall be padded to a
       multiple of 8 octets, in the following manner.  Let n be the length
       in octets of the input.  Pad the input by appending 8-(n mod 8)
       octets to the end of the message, each having the value 8-(n mod 8),
       the number of octets being added.  In hexadecimal, the possible
       paddings are:  01, 0202, 030303, 04040404, 0505050505, 060606060606,
       07070707070707, and 0808080808080808.  All input is padded with 1 to
       8 octets to produce a multiple of 8 octets in length.  The padding
       can be removed unambiguously after decryption.
       */
    private static byte[] addPadding(@Nonnull byte[] buff, int blockSize) {
        assert blockSize == 8;
        /* Removes RFC 1423/PKCS #7 padding */
        int origLen = buff.length;
        int extraBytes = 8 - buff.length % blockSize;
        int resLen = origLen + extraBytes;

        byte[] res = new byte[resLen];
        System.arraycopy(buff, 0, res, 0, origLen);

        for (int i = origLen; i < resLen; i++) {
            res[i] = (byte) extraBytes;
        }

        return res;
    }

    private static byte[] removePadding(@Nonnull byte[] buff, int blockSize) throws PEMDecodingException {
        /* Removes RFC 1423/PKCS #7 padding */

        int rfc_1423_padding = buff[buff.length - 1] & 0xff;

        if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize))
            throw new PEMDecodingException("Decrypted PEM has wrong padding, did you specify the correct password?");

        for (int i = 2; i <= rfc_1423_padding; i++) {
            if (buff[buff.length - i] != rfc_1423_padding)
                throw new PEMDecodingException("Decrypted PEM has wrong padding, did you specify the correct password?");
        }

        byte[] tmp = new byte[buff.length - rfc_1423_padding];
        System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding);
        return tmp;
    }

    public static int hexToInt(char c) {
        if ((c >= 'a') && (c <= 'f')) {
            return (c - 'a') + 10;
        }

        if ((c >= 'A') && (c <= 'F')) {
            return (c - 'A') + 10;
        }

        if ((c >= '0') && (c <= '9')) {
            return (c - '0');
        }

        throw new IllegalArgumentException("Need hex char");
    }

    public static byte[] hexToByteArray(@Nonnull String hex) {
        //this should do the same, but it doesn't it pads with 0 sometimes: return new BigInteger(hex, 16).toByteArray();

        if ((hex.length() % 2) != 0)
            throw new IllegalArgumentException("Uneven string length in hex encoding \"" + hex + "\" len=" + hex.length());

        byte decoded[] = new byte[hex.length() / 2];

        for (int i = 0; i < decoded.length; i++) {
            int hi = hexToInt(hex.charAt(i * 2));
            int lo = hexToInt(hex.charAt((i * 2) + 1));

            decoded[i] = (byte) (hi * 16 + lo);
        }

        return decoded;
    }

    public static class PEMDecodingException extends Exception {
        public PEMDecodingException() {
        }

        public PEMDecodingException(String message) {
            super(message);
        }

        public PEMDecodingException(String message, Throwable cause) {
            super(message, cause);
        }

        public PEMDecodingException(Throwable cause) {
            super(cause);
        }

    }

    public enum AltNamesSource {SUBJECT_ALT_NAMES, ISSUES_ALT_NAMES}

    @Nonnull
    public static List<GeniUrn> findUrnsInCertAltNames(@Nonnull X509Certificate cert, @Nonnull AltNamesSource altNamesSource, boolean onlyUsers) {
        List<GeniUrn> res = new ArrayList<>();

        Collection<List<?>> altNames = null;
        try {
            switch (altNamesSource) {
                case SUBJECT_ALT_NAMES: {
                    altNames = cert.getSubjectAlternativeNames();
                    break;
                }
                case ISSUES_ALT_NAMES: {
                    altNames = cert.getIssuerAlternativeNames();
                    break;
                }
                default:
                    throw new RuntimeException("Unsupported AltNamesSource: " + altNamesSource);
            }
        } catch (CertificateParsingException e) {
            LOG.error("Error processing certificate alternate names: " + e.getMessage());
        }

        LOG.trace("certificate has alt names: " + (altNames != null && altNames.isEmpty()) + "\n");

        if (altNames != null) {
            String invalidUrnErrors = "";
            boolean hasvalidUrn = false;
            for (List<?> altName : altNames) {
                Integer nameType = (Integer) altName.get(0);
                LOG.trace("certificate has altname of type " + nameType + "\n");
                if (nameType == 6) {
                    //uniformResourceIdentifier
                    String urn = (String) altName.get(1);
                    GeniUrn geniUrn = GeniUrn.parse(urn);
                    if (geniUrn == null)
                        invalidUrnErrors +=
                                "Warning: certificate alternative name URI is not a valid urn: \"" + urn + "\"  (will be ignored)";
                    else {
                        if (onlyUsers && !Objects.equals(geniUrn.getEncodedResourceType(), "user"))
                            invalidUrnErrors +=
                                    "Warning: certificate alternative name URI is not a user urn: \"" + urn + "\"  (will be ignored)";
                        else {
                            hasvalidUrn = true;
                            res.add(geniUrn);
                            LOG.trace("processed altName of URN type. userUrn=" + geniUrn + "\n");
                        }
                    }
                }
            }
            if (!hasvalidUrn && !invalidUrnErrors.isEmpty()) {
                LOG.warn(invalidUrnErrors);
            }
        }

        return res;
    }

    private static class Buffer {
        private final ByteBuffer bb = ByteBuffer.allocate(1024 * 10 * 2);
        private int size = 0;

        public void put(String str) {
            put(str.getBytes(Charsets.UTF_8));
        }

        public void put(BigInteger bigInteger) {
            put(bigInteger.toByteArray());
        }

        public void put(byte[] bytes) {
            bb.putInt(bytes.length);
            size += 4;
            bb.put(bytes);
            size += bytes.length;
        }

        public byte[] toByteArray() {
            byte[] result = new byte[size];
            bb.rewind();
            bb.get(result);
            return result;
        }
    }


    public static byte[] keyToSshAgent(@Nonnull RSAPrivateCrtKey privateKey, @Nonnull String comment) {
        Buffer b = new Buffer();
        b.put(SSH_RSA);
        b.put(privateKey.getModulus());  //n_array or n
        b.put(privateKey.getPublicExponent()); //pub_array or e
        b.put(privateKey.getPrivateExponent()); //prv_array or d
        b.put(privateKey.getCrtCoefficient()); //Carray or u
        b.put(privateKey.getPrimeP()); //p
        b.put(privateKey.getPrimeQ());  //q
        b.put(comment);

        return b.toByteArray();
    }

    public static byte[] keyToSshAgent(@Nonnull DSAPrivateKey privateKey, @Nonnull DSAPublicKey publicKey, @Nonnull String comment) {
        assert Objects.equals(privateKey.getParams().getP(), publicKey.getParams().getP());
        assert Objects.equals(privateKey.getParams().getQ(), publicKey.getParams().getQ());
        assert Objects.equals(privateKey.getParams().getG(), publicKey.getParams().getG());

        Buffer b = new Buffer();
        b.put(SSH_DSS);
        b.put(privateKey.getParams().getP());  //p
        b.put(privateKey.getParams().getQ()); //q
        b.put(privateKey.getParams().getG()); //g
        b.put(publicKey.getY()); //y
        b.put(privateKey.getX()); //x
        b.put(comment);

        return b.toByteArray();
    }

    @Nullable
    public static PublicKey blobToPublicKey(byte[] input) {
        ByteBuffer bb = ByteBuffer.wrap(input).order(ByteOrder.BIG_ENDIAN);

        int kts = bb.getInt();
        byte[] kt = new byte[kts];
        bb.get(kt);
        String keyType = StringUtils.newStringUtf8(kt);

        switch (keyType) {
            case SSH_RSA: {
                int pubExpSize = bb.getInt();
                byte[] pubExp = new byte[pubExpSize];
                bb.get(pubExp);

                int modulusSize = bb.getInt();
                byte[] modulus = new byte[modulusSize];
                bb.get(modulus);

                KeyFactory keyFactory;
                try {
                    keyFactory = KeyFactory.getInstance("RSA");
                    return keyFactory.generatePublic(new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(pubExp)));
                } catch (NoSuchAlgorithmException e) {
                    LOG.warn("Could not load RSA Algorithm", e);
                    return null;
                } catch (InvalidKeySpecException e) {
                    LOG.warn("Failed to create RSA public key from SSH Agent info", e);
                    return null;
                }
            }
            case SSH_DSS: {
                int ps = bb.getInt();
                byte[] p = new byte[ps];
                bb.get(p);

                int qs = bb.getInt();
                byte[] q = new byte[qs];
                bb.get(q);

                int gs = bb.getInt();
                byte[] g = new byte[gs];
                bb.get(g);

                int ys = bb.getInt();
                byte[] y = new byte[ys];
                bb.get(y);

                KeyFactory keyFactory;
                try {
                    keyFactory = KeyFactory.getInstance("DSA");
                    return keyFactory.generatePublic(new DSAPublicKeySpec(new BigInteger(y), new BigInteger(p), new BigInteger(q), new BigInteger(g)));
                } catch (NoSuchAlgorithmException e) {
                    LOG.warn("Failed to load DSA Algorithm", e);
                    return null;
                } catch (InvalidKeySpecException e) {
                    LOG.warn("Failed to create DSA public key from SSH Agent Info", e);
                    return null;
                }
            }
            default:
                LOG.warn("Unknown key type: {}", keyType);
                return null;
        }

    }


}
