KeyManagerUtils.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.net.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;

import javax.net.ssl.KeyManager;
import javax.net.ssl.X509ExtendedKeyManager;

import org.apache.commons.net.io.Util;

/**
 * General KeyManager utilities
 * <p>
 * How to use with a client certificate:
 *
 * <pre>
 * KeyManager km = KeyManagerUtils.createClientKeyManager("JKS",
 *     "/path/to/privatekeystore.jks","storepassword",
 *     "privatekeyalias", "keypassword");
 * FTPSClient cl = new FTPSClient();
 * cl.setKeyManager(km);
 * cl.connect(...);
 * </pre>
 *
 * If using the default store type and the key password is the same as the store password, these parameters can be omitted. <br>
 * If the desired key is the first or only key in the keystore, the keyAlias parameter can be omitted, in which case the code becomes:
 *
 * <pre>
 * KeyManager km = KeyManagerUtils.createClientKeyManager(
 *     "/path/to/privatekeystore.jks","storepassword");
 * FTPSClient cl = new FTPSClient();
 * cl.setKeyManager(km);
 * cl.connect(...);
 * </pre>
 *
 * @since 3.0
 */
public final class KeyManagerUtils {

    private static final class ClientKeyStore {

        private final X509Certificate[] certChain;
        private final PrivateKey key;
        private final String keyAlias;

        ClientKeyStore(final KeyStore ks, final String keyAlias, final String keyPass) throws GeneralSecurityException {
            this.keyAlias = keyAlias;
            this.key = (PrivateKey) ks.getKey(this.keyAlias, keyPass.toCharArray());
            final Certificate[] certs = ks.getCertificateChain(this.keyAlias);
            final X509Certificate[] x509certs = new X509Certificate[certs.length];
            Arrays.setAll(x509certs, i -> (X509Certificate) certs[i]);
            this.certChain = x509certs;
        }

        String getAlias() {
            return this.keyAlias;
        }

        X509Certificate[] getCertificateChain() {
            return this.certChain;
        }

        PrivateKey getPrivateKey() {
            return this.key;
        }
    }

    private static final class X509KeyManager extends X509ExtendedKeyManager {

        private final ClientKeyStore keyStore;

        X509KeyManager(final ClientKeyStore keyStore) {
            this.keyStore = keyStore;
        }

        // Call sequence: 1
        @Override
        public String chooseClientAlias(final String[] keyType, final Principal[] issuers, final Socket socket) {
            return keyStore.getAlias();
        }

        @Override
        public String chooseServerAlias(final String keyType, final Principal[] issuers, final Socket socket) {
            return null;
        }

        // Call sequence: 2
        @Override
        public X509Certificate[] getCertificateChain(final String alias) {
            return keyStore.getCertificateChain();
        }

        @Override
        public String[] getClientAliases(final String keyType, final Principal[] issuers) {
            return new String[] { keyStore.getAlias() };
        }

        // Call sequence: 3
        @Override
        public PrivateKey getPrivateKey(final String alias) {
            return keyStore.getPrivateKey();
        }

        @Override
        public String[] getServerAliases(final String keyType, final Principal[] issuers) {
            return null;
        }

    }

    private static final String DEFAULT_STORE_TYPE = KeyStore.getDefaultType();

    /**
     * Create a client key manager which returns a particular key. Does not handle server keys. Uses the default store type and assumes the key password is the
     * same as the store password. The key alias is found by searching the keystore for the first private key entry
     *
     * @param storePath the path to the keyStore
     * @param storePass the keyStore password
     * @return the customised KeyManager
     * @throws IOException              if there is a problem creating the keystore
     * @throws GeneralSecurityException if there is a problem creating the keystore
     */
    public static KeyManager createClientKeyManager(final File storePath, final String storePass) throws IOException, GeneralSecurityException {
        return createClientKeyManager(DEFAULT_STORE_TYPE, storePath, storePass, null, storePass);
    }

    /**
     * Create a client key manager which returns a particular key. Does not handle server keys. Uses the default store type and assumes the key password is the
     * same as the store password
     *
     * @param storePath the path to the keyStore
     * @param storePass the keyStore password
     * @param keyAlias  the alias of the key to use, may be {@code null} in which case the first key entry alias is used
     * @return the customised KeyManager
     * @throws IOException              if there is a problem creating the keystore
     * @throws GeneralSecurityException if there is a problem creating the keystore
     */
    public static KeyManager createClientKeyManager(final File storePath, final String storePass, final String keyAlias)
            throws IOException, GeneralSecurityException {
        return createClientKeyManager(DEFAULT_STORE_TYPE, storePath, storePass, keyAlias, storePass);
    }

    /**
     * Create a client key manager which returns a particular key. Does not handle server keys.
     *
     * @param ks       the keystore to use
     * @param keyAlias the alias of the key to use, may be {@code null} in which case the first key entry alias is used
     * @param keyPass  the password of the key to use
     * @return the customised KeyManager
     * @throws GeneralSecurityException if there is a problem creating the keystore
     */
    public static KeyManager createClientKeyManager(final KeyStore ks, final String keyAlias, final String keyPass) throws GeneralSecurityException {
        final ClientKeyStore cks = new ClientKeyStore(ks, keyAlias != null ? keyAlias : findAlias(ks), keyPass);
        return new X509KeyManager(cks);
    }

    /**
     * Create a client key manager which returns a particular key. Does not handle server keys.
     *
     * @param storeType the type of the keyStore, e.g. "JKS"
     * @param storePath the path to the keyStore
     * @param storePass the keyStore password
     * @param keyAlias  the alias of the key to use, may be {@code null} in which case the first key entry alias is used
     * @param keyPass   the password of the key to use
     * @return the customised KeyManager
     * @throws GeneralSecurityException if there is a problem creating the keystore
     * @throws IOException              if there is a problem creating the keystore
     */
    public static KeyManager createClientKeyManager(final String storeType, final File storePath, final String storePass, final String keyAlias,
            final String keyPass) throws IOException, GeneralSecurityException {
        final KeyStore ks = loadStore(storeType, storePath, storePass);
        return createClientKeyManager(ks, keyAlias, keyPass);
    }

    private static String findAlias(final KeyStore ks) throws KeyStoreException {
        final Enumeration<String> e = ks.aliases();
        while (e.hasMoreElements()) {
            final String entry = e.nextElement();
            if (ks.isKeyEntry(entry)) {
                return entry;
            }
        }
        throw new KeyStoreException("Cannot find a private key entry");
    }

    private static KeyStore loadStore(final String storeType, final File storePath, final String storePass)
            throws KeyStoreException, IOException, GeneralSecurityException {
        final KeyStore ks = KeyStore.getInstance(storeType);
        FileInputStream stream = null;
        try {
            stream = new FileInputStream(storePath);
            ks.load(stream, storePass.toCharArray());
        } finally {
            Util.closeQuietly(stream);
        }
        return ks;
    }

    private KeyManagerUtils() {
        // Not instantiable
    }

}