hu.akarnokd.utils.crypto.KeystoreManager.java Source code

Java tutorial

Introduction

Here is the source code for hu.akarnokd.utils.crypto.KeystoreManager.java

Source

/*
 * Copyright 2012-2014 David Karnok
 *
 * Licensed 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 hu.akarnokd.utils.crypto;

import hu.akarnokd.utils.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;

/**
 * Manges keystore specific operations.
 * Allows to create, save and load a keystore. Can generate
 * new private key and certificate. Creates certificate signing
 * requests and allows the import of the reply from the CA.
 * Uses the Bouncy Castle JCE provider and 1024 bit RSA keys
 * Example:<p>
 *       KeystoreManager km = new KeystoreManager();<br>
 *      km.create();<br>
 *      String dn = "CN=David Karnok, OU=EMI, O=MTA SZTAKI";<br>
 *
 *      km.generateRSACert("test", dn, dn, "localhost", "abc".toCharArray());<br>
 *      
 *      System.out.println(km.getCertificate("test"));<br>
 *      System.out.println(km.getPrivateKey("test", "abc".toCharArray()));<br>
 *      System.out.println(km.createRSASigningRequest("test", "abc".toCharArray()));<br>
 * <p>
 * Original idea: http://www.koders.com/java/fid165B893DAEA5F8D4AD44CCF9B8FBE75B01CE6575.aspx
 * @author akarnokd, 2007.12.06.
 */
@SuppressWarnings("deprecation")
public class KeystoreManager {
    /**
     * The Logger object.
     */
    private static final Logger LOG = Logger.getLogger(KeystoreManager.class.getName());
    /**
     * The managed keystore object.
     */
    private KeyStore keystore;
    /**
     * The Bouncy castle cryptography provider.
     */
    public static final Provider BC_PROVIDER = new BouncyCastleProvider();
    /**
     * Register bouncy castle provider.
     */
    static {
        Security.addProvider(BC_PROVIDER);
    }

    /**
     * Constructor. Creates an empty keystore in memory.
     */
    public KeystoreManager() {
        createEmpty();
    }

    /**
     * Create a new JKS keystore.
     * Throws KeystoreFault if the operation fails
     */
    public void create() {
        createEmpty();
    }

    /**
     * Create an empty keystore.
     */
    private void createEmpty() {
        try {
            keystore = KeyStore.getInstance("JKS");
            keystore.load(null, null);
        } catch (Exception ex) {
            LOG.log(Level.SEVERE, ex.toString(), ex);
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Load a keystore from the given file.
     * Throws KeystoreFault if something failed.
     * @param fileName the filename
     * @param password the keystore password if supplied
     */
    public void load(String fileName, char[] password) {
        try (InputStream in = new FileInputStream(fileName)) {
            load(in, password);
        } catch (IOException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Load a keystore from the given input stream.
     * Does not close the input stream.
     * Throws KeystoreFault if something failed.
     * @param in the input stream, should not be null
     * @param password the keystore password if supplied
     */
    public void load(InputStream in, char[] password) {
        try {
            keystore = KeyStore.getInstance("JKS");
            keystore.load(in, password);
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Save the current keystore into the given file.
     * @param fileName the filename
     * @param password the password if supplied
     */
    public void save(String fileName, char[] password) {
        try (OutputStream out = new FileOutputStream(fileName)) {
            save(out, password);
        } catch (IOException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Save the current keystore into the given output stream.
     * Does not close the stream
     * @param out the output stream
     * @param password the password if supplied
     */
    public void save(OutputStream out, char[] password) {
        try {
            keystore.store(out, password);
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Generate a public/private keypair using the given algorithm and size.
     * @param algorithm the algorithm (DSA/RSA)
     * @param size the key size, usually 1024
     * @return the generated keypair.
     */
    public KeyPair generateKeyPair(String algorithm, int size) {
        try {
            KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm, BC_PROVIDER);
            generator.initialize(size, new SecureRandom());
            return generator.generateKeyPair();
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Generate a X509 certificate for the given keypair.
     * The distinguished names must be in format: CN=cName, OU=orgUnit, O=org, L=city, S=state, C=countryCode
     * use backslash to escape a comma
     * @param keypair the keypair
     * @param months the validity length in months
     * @param issuerDN the issuer distinguished name: "CN=David Karnok,OU=EMI,O=MTA SZTAKI"
     * @param subjectDN the subject distinguished name: "CN=David Karnok,OU=EMI,O=MTA SZTAKI"
     * @param domain domain of the server to store in the subject alternative name extension
     * @param signAlgorithm the signing algorithm to use
     * @return the generated X509 certificate
     */
    public X509Certificate createX509Certificate(KeyPair keypair, int months, String issuerDN, String subjectDN,
            String domain, String signAlgorithm) {
        try {
            // calendar for date calculations
            GregorianCalendar cal = new GregorianCalendar();

            // extract keypair components
            PublicKey pubKey = keypair.getPublic();
            PrivateKey privKey = keypair.getPrivate();

            // generate a random serial number
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(System.currentTimeMillis());
            byte[] serialNo = new byte[8];
            random.nextBytes(serialNo);
            BigInteger serial = new BigInteger(serialNo).abs();

            // create the certificate generator
            X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
            certGen.reset();

            // set certificate attributes
            certGen.setSerialNumber(serial);
            cal.setTimeInMillis(System.currentTimeMillis());
            certGen.setNotBefore(cal.getTime());
            cal.add(GregorianCalendar.MONTH, months);
            certGen.setNotAfter(cal.getTime());
            certGen.setPublicKey(pubKey);
            certGen.setSignatureAlgorithm(signAlgorithm);
            certGen.setIssuerDN(new X509Name(issuerDN));
            certGen.setSubjectDN(new X509Name(subjectDN));

            certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false,
                    new SubjectKeyIdentifierStructure(pubKey));

            // create subject alternative name
            boolean isCritical = subjectDN == null || "".equals(subjectDN.trim());
            DERSequence othernameSeq = new DERSequence(
                    new ASN1Encodable[] { new DERObjectIdentifier("1.3.6.1.5.5.7.8.5"),
                            new DERTaggedObject(true, 0, new DERUTF8String(domain)) });
            GeneralName othernameGen = new GeneralName(GeneralName.otherName, othernameSeq);
            GeneralNames subjectAlternatives = new GeneralNames(othernameGen);
            certGen.addExtension(X509Extensions.SubjectAlternativeName, isCritical, subjectAlternatives);

            // finally generate the certificate
            X509Certificate cert = certGen.generateX509Certificate(privKey, BC_PROVIDER.getName(),
                    new SecureRandom());
            cert.checkValidity(new Date());
            cert.verify(pubKey);

            return cert;
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        } catch (CertificateException ex) {
            throw new KeystoreFault(ex);
        } catch (SignatureException ex) {
            throw new KeystoreFault(ex);
        } catch (NoSuchProviderException ex) {
            throw new KeystoreFault(ex);
        } catch (InvalidKeyException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Generate an RSA 1024 bit key and certificate and add it to the current keystore
     * under the given alias.
     * The distinguished names must be in format: CN=cName, OU=orgUnit, O=org, L=city, S=state, C=countryCode
     * use backslash to escape a comma
     * @param alias the certificate alias
     * @param issuerDN the issuer distinguished name: "CN=David Karnok,OU=EMI,O=MTA SZTAKI"
     * @param subjectDN the subject distinguished name: "CN=David Karnok,OU=EMI,O=MTA SZTAKI"
     * @param domain domain of the server to store in the subject alternative name extension
     * @param password the key password
     * @return the generated X509Certificate
     */
    public X509Certificate generateRSACert(String alias, String issuerDN, String subjectDN, String domain,
            char[] password) {
        KeyPair keypair = generateKeyPair("RSA", 1024);
        X509Certificate cert = createX509Certificate(keypair, 12, issuerDN, subjectDN, domain, "MD5withRSA");
        try {
            keystore.setKeyEntry(alias, keypair.getPrivate(), password, new X509Certificate[] { cert });
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        }
        return cert;
    }

    /**
     * Create a certificate signing request.
     * The created text can be sent to a Certificate Authority to request
     * a countersigning.
     * @param cert the local X509Certificate object
     * @param privKey the private key of the certificate
     * @return the request string
     */
    public String createRSASigningRequest(X509Certificate cert, PrivateKey privKey) {
        X509Name xname = new X509Name(cert.getSubjectDN().getName());
        try {
            PKCS10CertificationRequest certReq = new PKCS10CertificationRequest("MD5withRSA", xname,
                    cert.getPublicKey(), null, privKey, BC_PROVIDER.getName());

            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            DEROutputStream dout = new DEROutputStream(bout);
            try {
                dout.writeObject(certReq.toASN1Primitive());
            } finally {
                dout.close();
            }

            String s = Base64.encodeBytes(bout.toByteArray());
            StringBuilder result = new StringBuilder(s.length() + 100);
            result.append("-----BEGIN NEW CERTIFICATE REQUEST-----\n");
            // split base64 string into 76 character lines
            int lineLen = 76;
            int len = s.length();
            int idx = 0;
            while (len > 0) {
                if (len > lineLen) {
                    result.append(s.substring(idx, idx + lineLen)).append('\n');
                    len -= lineLen;
                    idx += lineLen;
                } else {
                    result.append(s.substring(idx)).append('\n');
                    break;
                }
            }
            result.append("-----END NEW CERTIFICATE REQUEST-----\n");
            return result.toString();
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Generate an RSA signing request for the given key alias.
     * @param alias the alias
     * @param password the private key password
     * @return the signing request text
     */
    public String createRSASigningRequest(String alias, char[] password) {
        return createRSASigningRequest(getCertificate(alias), getPrivateKey(alias, password));
    }

    /**
     * Installs the signing request response to an existing certificate
     * in the keystore.
     * @param alias the alias of the original certificate
     * @param password the password of the certificate
     * @param caReply the input stream pointing to the CA reply data, must be closed by the caller
     * @param verifyRoot verifiy the root of imported certificate using the keystore?
     */
    public void installReply(String alias, char[] password, InputStream caReply, boolean verifyRoot) {
        try {
            X509Certificate cert = (X509Certificate) keystore.getCertificate(alias);
            if (cert == null) {
                throw new KeystoreFault("Certificate not found: " + alias);
            }
            PrivateKey privKey = (PrivateKey) keystore.getKey(alias, password);

            List<X509Certificate> certs = new ArrayList<>();
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            // collect the certificate elements into a list
            for (Certificate c : cf.generateCertificates(caReply)) {
                certs.add((X509Certificate) c);
            }
            if (certs.isEmpty()) {
                throw new KeystoreFault("Empty CA response");
            }
            List<X509Certificate> newCerts = null;
            if (certs.size() == 1) {
                newCerts = establishCertChain(cert, certs.get(0));
            } else {
                newCerts = verifyReply(alias, cert, certs, verifyRoot);
            }
            // add only new certificates to the keystore
            if (newCerts != null) {
                keystore.setKeyEntry(alias, privKey, password,
                        newCerts.toArray(new X509Certificate[newCerts.size()]));
            }
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Establish a certificate chain list using only one caReply element.
     * The public keys are checked to be equal.
     * @param cert the local certificate
     * @param caReply the reply certificate of the CA
     * @return the list of the certificates
     */
    private List<X509Certificate> establishCertChain(X509Certificate cert, X509Certificate caReply) {
        PublicKey pubKey = cert.getPublicKey();
        if (!pubKey.equals(caReply.getPublicKey())) {
            throw new KeystoreFault("The keys in reply and keystore don't match");
        }
        if (cert.equals(caReply)) {
            throw new KeystoreFault("The local and reply certificates are identical");
        }
        Map<Principal, Set<X509Certificate>> knownCerts = new HashMap<>();
        try {
            if (keystore.size() > 0) {
                knownCerts.putAll(getCertsByIssuer());
            }
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        }
        LinkedList<X509Certificate> result = new LinkedList<>();
        buildChain(caReply, result, knownCerts);
        return result;
    }

    /**
     * Build the chain of certificates.
     * @param cert the CA reply certificate
     * @param chain the resulting chain
     * @param knownCerts the known certificates of each Principal
     */
    private void buildChain(X509Certificate cert, LinkedList<X509Certificate> chain,
            Map<Principal, Set<X509Certificate>> knownCerts) {
        Principal subject = cert.getSubjectDN();
        Principal issuer = cert.getIssuerDN();
        if (subject.equals(issuer)) {
            chain.addFirst(cert);
            return;
        }
        Set<X509Certificate> issuerCerts = knownCerts.get(issuer);
        if (issuerCerts == null || issuerCerts.isEmpty()) {
            throw new KeystoreFault("No certificate found for " + issuer);
        }
        for (X509Certificate issuerCert : issuerCerts) {
            try {
                cert.verify(issuerCert.getPublicKey());
                buildChain(issuerCert, chain, knownCerts);
            } catch (Exception ex) {
                throw new KeystoreFault(ex);
            }
        }
        chain.addFirst(cert);
    }

    /**
     * Groups the X509 certificates - found in keystore - by subject DN and returns it as a map.
     * @return the grouped X509 certificates
     */
    private Map<Principal, Set<X509Certificate>> getCertsByIssuer() {
        try {
            Map<Principal, Set<X509Certificate>> result = new HashMap<>();
            Enumeration<String> aliases = keystore.aliases();
            while (aliases.hasMoreElements()) {
                String alias = aliases.nextElement();
                X509Certificate cert = (X509Certificate) keystore.getCertificate(alias);
                if (cert != null) {
                    Principal principal = cert.getSubjectDN();
                    Set<X509Certificate> list = result.get(principal);
                    if (list == null) {
                        list = new HashSet<>();
                        result.put(principal, list);
                        list.add(cert);
                    } else {
                        if (!list.contains(cert)) {
                            list.add(cert);
                        }
                    }
                }
            }
            return result;
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
      * Validates chain in certification reply, and returns the ordered
      * elements of the chain (with user certificate first, and root
      * certificate last in the array).
      *
      * @param alias the alias name
      * @param localCert the user certificate of the alias
      * @param replyCerts the chain provided in the reply
      * @param verifyRoot verify the root CA?
     * @return the ordered list of certificates
     */
    private List<X509Certificate> verifyReply(String alias, X509Certificate localCert,
            List<X509Certificate> replyCerts, boolean verifyRoot) {
        PublicKey pubKey = localCert.getPublicKey();
        X509Certificate current = null;
        Map<Principal, X509Certificate> chainMap = new HashMap<>();
        for (X509Certificate replyCert : replyCerts) {
            if (pubKey.equals(replyCert.getPublicKey())) {
                current = replyCert;
            }
            chainMap.put(replyCert.getSubjectDN(), replyCert);
        }
        if (current == null) {
            throw new KeystoreFault("Certificate reply does not contain public key for " + alias);
        }
        List<X509Certificate> result = new ArrayList<>();
        // reorder certificates into chain where the following element signs the previous element
        while (result.size() < replyCerts.size()) {
            result.add(current);
            // find the current elements issuer certificate
            X509Certificate next = chainMap.get(current.getIssuerDN());
            if (next == null) {
                throw new KeystoreFault("Broken certificate chain");
            }
            // verify the chain
            try {
                current.verify(next.getPublicKey());
            } catch (Exception ex) {
                throw new KeystoreFault(ex);
            }
            // move to the issuer
            current = next;
        }
        if (verifyRoot) {
            // verify root issuer
            X509Certificate root = result.get(result.size() - 1);

            try {
                // check keystore for it
                String rootAlias = keystore.getCertificateAlias(root);
                if (rootAlias == null) {
                    boolean verified = false;
                    X509Certificate rootCert = null;
                    // locate the true root be trying to verify the current root
                    Enumeration<String> aliases = keystore.aliases();
                    while (aliases.hasMoreElements()) {
                        rootCert = (X509Certificate) keystore.getCertificate(aliases.nextElement());
                        if (rootCert != null) {
                            try {
                                root.verify(rootCert.getPublicKey());
                                verified = true;
                                break;
                            } catch (Exception ex) {
                                // ignore
                            }
                        }
                    }
                    if (!verified) {
                        throw new KeystoreFault("Root CA could not be verified");
                    } else {
                        // check if it is self signed
                        if (!root.getSubjectDN().equals(root.getIssuerDN())) {
                            result.add(rootCert);
                        }
                    }
                }
            } catch (Exception ex) {
                throw new KeystoreFault(ex);
            }

        }
        return result;
    }

    /**
     * Set a certificate for an entry. Does not close the input stream.
     * @param alias the alias
     * @param caReply the certificate input stream
     */
    public void importCertificate(String alias, InputStream caReply) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            // collect the certificate elements into a list
            Certificate cert = cf.generateCertificate(caReply);
            keystore.setCertificateEntry(alias, cert);
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Load a DER certificate from a textual or binary stream.
     * Does not close the stream.
     * @param in the input stream, not null
     * @return the loaded certificate, not null
     */
    public static Certificate loadCertificate(InputStream in) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            // collect the certificate elements into a list
            return cf.generateCertificate(in);
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Retrieve the JDK's most trusted certificates.
     * Original idea: http://jug.org.ua/wiki/display/JavaAlmanac/Listing+the+Most-Trusted+Certificate+Authorities+%28CA%29+in+a+Key+Store
     * @return the nonnul list of the most trusted certificates
     */
    public static List<X509Certificate> getJDKTrustedCerts() {
        List<X509Certificate> result = new ArrayList<>();
        String filename = getJDKCertFile();
        try (FileInputStream fin = new FileInputStream(filename)) {
            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
            ks.load(fin, "changeit".toCharArray());

            PKIXParameters params = new PKIXParameters(ks);
            for (TrustAnchor a : params.getTrustAnchors()) {
                result.add(a.getTrustedCert());
            }
        } catch (IOException ex) {
            throw new KeystoreFault(ex);
        } catch (CertificateException ex) {
            throw new KeystoreFault(ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        } catch (InvalidAlgorithmParameterException ex) {
            throw new KeystoreFault(ex);
        }
        return result;
    }

    /**
     * @return the JDK's own trusted certificate keystore location.
     */
    public static String getJDKCertFile() {
        return System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
    }

    /**
     * Exports a given certificate into an output stream.
     * Throws KeystoreFault if the alias cannot be found
     * or there is a problem with the keystore operations.
     * Does not close the output stream
     * @param alias the alias
     * @param out the output stream
     * @param binary the output should be binary (true), or text (false)?
     * @throws IOException when writing to the output stream fails
     */
    public void exportCertificate(String alias, OutputStream out, boolean binary) throws IOException {
        try {
            Certificate cert = keystore.getCertificate(alias);
            if (cert != null) {
                serializeCertificate(out, binary, cert);
            } else {
                throw new KeystoreFault("Unknown certificate: " + alias);
            }
        } catch (CertificateEncodingException ex) {
            throw new KeystoreFault(ex);
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Export a private key entry into a choosable file format.
     * Might throw KeystoreFault if the alias is not found or not a private key
     * or any KeyStore exception is thrown.
     * @param alias the private key alias.
     * @param password the key password, can be null
     * @param out the output stream to write to, cannot be null
     * @param binary do binary (true) or textual output?
     * @throws IOException if the serialization throws IOException
     */
    public void exportPrivateKey(String alias, char[] password, OutputStream out, boolean binary)
            throws IOException {
        try {
            Key key = keystore.getKey(alias, password);
            if (key instanceof PrivateKey) {
                serializePrivateKey(out, binary, (PrivateKey) key);
            } else {
                throw new KeystoreFault("Unknown key: " + alias);
            }
        } catch (UnrecoverableKeyException ex) {
            throw new KeystoreFault(ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Serialize the given private key into a binary or textual stream format.
     * @param out the output stream, cannot be null
     * @param binary binary export?
     * @param key the private key, cannot be null
     * @throws IOException if an error occurs during serialization.
     */
    public static void serializePrivateKey(OutputStream out, boolean binary, PrivateKey key) throws IOException {
        if (binary) {
            out.write(key.getEncoded());
            out.flush();
        } else {
            Writer wr = new OutputStreamWriter(out, "UTF-8");
            wr.write(BEGIN_PRIVATE_KEY);
            wr.write("\n");
            wr.write(Base64.encodeBytes(key.getEncoded()));
            wr.write("\n");
            wr.write(END_PRIVATE_KEY);
            wr.write("\n");
            wr.flush();
        }
    }

    /**
     * Serializes the given certificate into the given output stream
     * in binary or text format the output stream gets flushed but not closed.
     * @param out the output stream, cannot be null.
     * @param binary binary export?
     * @param cert the certificate
     * @throws IOException when the IO operation fails
     * @throws CertificateEncodingException on certificate problems
     */
    public static void serializeCertificate(OutputStream out, boolean binary, Certificate cert)
            throws IOException, CertificateEncodingException {
        if (binary) {
            out.write(cert.getEncoded());
            out.flush();
        } else {
            Writer wr = new OutputStreamWriter(out, "UTF-8");
            wr.write("-----BEGIN CERTIFICATE-----\n");
            wr.write(Base64.encodeBytes(cert.getEncoded()));
            wr.write("\n-----END CERTIFICATE-----\n");
            wr.flush();
        }
    }

    /**
     * Retrieve a certificate for the given private key or certificate.
     * @param alias the alias
     * @return the found X509 certificate or null if alias not found or the
     * entry does not have any certificates
     */
    public X509Certificate getCertificate(String alias) {
        if (alias == null) {
            return null;
        }
        try {
            return (X509Certificate) keystore.getCertificate(alias);
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Retrieve a private key named by the alias.
     * @param alias the alias
     * @param password the key password
     * @return the private key or null if not found or not a key entry
     */
    public PrivateKey getPrivateKey(String alias, char[] password) {
        try {
            return (PrivateKey) keystore.getKey(alias, password);
        } catch (Exception ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * @return the underlying keystore, can be null if the keystore was not loaded or created
     */
    public KeyStore getKeyStore() {
        return keystore;
    }

    /**
     * Main program. Generates a key with a self signing certificate
     * and shows that it is in the keystore.
     * @param args the arguments - not used
     * @throws Exception on error
     */
    public static void main(String[] args) throws Exception {
        KeystoreManager km = new KeystoreManager();
        km.create();
        DistinguishedName dn = new DistinguishedName("CN=David Karnok\\,, OU=EMI, O=MTA SZTAKI");

        LOG.info(String.valueOf(dn));

        km.generateRSACert("test", dn.toString(), dn.toString(), "localhost", "abc".toCharArray());

        LOG.info(String.valueOf(km.getPrivateKey("test", "abc".toCharArray())));
        LOG.info(String.valueOf(km.createRSASigningRequest("test", "abc".toCharArray())));
        LOG.info(String.valueOf(km.getCertificate("test")));

        km.load("conf/client.jks", "apache".toCharArray());

        LOG.info(String.valueOf(km.getCertificate("client")));

        LOG.info(String.valueOf(KeystoreManager.getJDKTrustedCerts()));
    }

    /**
     * Import a private key from an input stream.
     * Might throw KeystoreFault if there was an error with the keystore
     * @param alias the private key alias, cannot be null
     * @param password the optional key password
     * @param key the input stream of the key data, cannot be null
     * @param cert the input stream of the certificate data, cannot be null
     * @throws IOException if error occurs when reading the input stream
     */
    public void importPrivateKey(String alias, char[] password, InputStream key, InputStream cert)
            throws IOException {
        try {
            KeyFactory kf = KeyFactory.getInstance("RSA");
            PKCS8EncodedKeySpec keyspec = new PKCS8EncodedKeySpec(getEncodedKeyFrom(key));

            PrivateKey privKey = kf.generatePrivate(keyspec);

            CertificateFactory cf = CertificateFactory.getInstance("X509");
            // collect the certificate elements into a list
            Collection<? extends Certificate> certs = cf.generateCertificates(cert);

            keystore.setKeyEntry(alias, privKey, password, certs.toArray(new Certificate[certs.size()]));
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        } catch (InvalidKeySpecException ex) {
            throw new KeystoreFault(ex);
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        } catch (CertificateException ex) {

        }
    }

    /**
     * Extract a PKCS#8 encoded key spec from a potentially binary or textual
     * input file.
     * @param in the input stream, cannot be null
     * @return the encoded key bytes
     * @throws IOException if an IO error occurs or the text file is corrupted
     */
    private byte[] getEncodedKeyFrom(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
        byte[] buffer = new byte[4096];
        int read = 0;
        while ((read = in.read(buffer)) >= 0) {
            if (read > 0) {
                out.write(buffer, 0, read);
            }
        }
        buffer = out.toByteArray();
        String asText = new String(buffer, "ISO-8859-1");
        // check for the textual private key signal
        int idx = asText.indexOf(BEGIN_PRIVATE_KEY);
        if (idx >= 0) {
            int idxEnd = asText.indexOf(END_PRIVATE_KEY);
            if (idxEnd < 0) {
                throw new IOException("Missing " + END_PRIVATE_KEY);
            }
            StringBuilder nowhitespaces = new StringBuilder();
            for (int i = idx + BEGIN_PRIVATE_KEY.length(); i < idxEnd; i++) {
                char c = asText.charAt(i);
                if (!Character.isWhitespace(c)) {
                    nowhitespaces.append(c);
                }
            }
            buffer = Base64.decode(nowhitespaces.toString());
        }
        return buffer;
    }

    /**
     * Set the certificate for the given alias.
     * Might throw KeyStoreFault.
     * @param alias the alias, cannot be null
     * @param cert the certificate, cannot be null
     */
    public void setCertificate(String alias, Certificate cert) {
        try {
            keystore.setCertificateEntry(alias, cert);
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        }
    }

    /**
     * Create a keystore with a single key from the another keystore.
     * @param source the source keystore
     * @param alias the key alias
     * @param password the key password
     * @return the new keystore
     */
    public static KeyStore singleKey(KeyStore source, String alias, char[] password) {
        KeyStore result = null;
        try {
            result = KeyStore.getInstance("JKS");
            result.load(null, null);

            Certificate cert = source.getCertificate(alias);
            PrivateKey key = (PrivateKey) source.getKey(alias, password);

            result.setKeyEntry(alias, key, password, new Certificate[] { cert });
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        } catch (CertificateException ex) {
            throw new KeystoreFault(ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        } catch (IOException ex) {
            throw new KeystoreFault(ex);
        } catch (UnrecoverableKeyException ex) {
            throw new KeystoreFault(ex);
        }
        return result;
    }

    /**
     * Create a managed keystore with the single key from this keystore.
     * @param alias the key alias
     * @param password the key password
     * @return the new keystore
     */
    public KeystoreManager singleKey(String alias, char[] password) {
        KeystoreManager result = new KeystoreManager();
        result.create();

        try {
            Certificate cert = keystore.getCertificate(alias);
            Key key = keystore.getKey(alias, password);

            result.keystore.setKeyEntry(alias, key, password, new Certificate[] { cert });
        } catch (KeyStoreException ex) {
            throw new KeystoreFault(ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new KeystoreFault(ex);
        } catch (UnrecoverableKeyException ex) {
            throw new KeystoreFault(ex);
        }

        return result;
    }

    /** Begin private key constant. */
    private static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
    /** End private key constant. */
    private static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
}