001/*
002 * Copyright 2017 Emmanuel Bourg
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package net.jsign;
018
019import java.io.File;
020import java.io.FileReader;
021import java.io.IOException;
022import java.lang.reflect.Field;
023import java.security.KeyException;
024import java.security.PrivateKey;
025import java.util.HashMap;
026import java.util.function.Function;
027
028import org.bouncycastle.asn1.ASN1ObjectIdentifier;
029import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
030import org.bouncycastle.jce.provider.BouncyCastleProvider;
031import org.bouncycastle.openssl.PEMDecryptorProvider;
032import org.bouncycastle.openssl.PEMEncryptedKeyPair;
033import org.bouncycastle.openssl.PEMKeyPair;
034import org.bouncycastle.openssl.PEMParser;
035import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
036import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
037import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
038import org.bouncycastle.operator.InputDecryptorProvider;
039import org.bouncycastle.operator.OperatorCreationException;
040import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
041import org.bouncycastle.pkcs.PKCSException;
042import sun.misc.Unsafe;
043
044/**
045 * Helper class for loading private keys (PVK or PEM, encrypted or not).
046 * 
047 * @author Emmanuel Bourg
048 * @since 2.0
049 */
050public class PrivateKeyUtils {
051
052    private PrivateKeyUtils() {
053    }
054
055    /**
056     * Load the private key from the specified file. Supported formats are PVK and PEM,
057     * encrypted or not. The type of the file is inferred by trying the supported formats
058     * in sequence until one parses successfully.
059     *
060     * @param file     the file to load the key from
061     * @param password the password protecting the key
062     * @return the private key loaded
063     * @throws KeyException if the key cannot be loaded
064     */
065    public static PrivateKey load(File file, String password) throws KeyException {
066        Exception pemParseException;
067        try {
068            return readPrivateKeyPEM(file, password != null ? password.toCharArray() : null);
069        } catch (Exception e) {
070            pemParseException = e;
071        }
072
073        Exception pvkParseException;
074        try {
075            return PVK.parse(file, password);
076        } catch (Exception e) {
077            pvkParseException = e;
078        }
079
080        KeyException keyException = new KeyException("Failed to load the private key from " + file + " (valid PEM or PVK file expected)");
081        keyException.addSuppressed(pemParseException);
082        keyException.addSuppressed(pvkParseException);
083        throw keyException;
084    }
085
086    /**
087     * Disables the signature verification of the jar containing the BouncyCastle provider.
088     */
089    private static void disableJceSecurity() {
090        try {
091            Class<?> jceSecurityClass = Class.forName("javax.crypto.JceSecurity");
092            Field field = jceSecurityClass.getDeclaredField("verificationResults");
093            field.setAccessible(true);
094
095            Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
096            unsafeField.setAccessible(true);
097            Unsafe unsafe = (Unsafe) unsafeField.get(null);
098
099            unsafe.putObject(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field), new HashMap<Object, Boolean>() {
100                @Override
101                public Boolean get(Object key) {
102                    // This is not the provider you are looking for, you don't need to see its identification, move along
103                    return Boolean.TRUE;
104                }
105
106                @Override
107                public Boolean computeIfAbsent(Object key, Function<? super Object, ? extends Boolean> mappingFunction) {
108                    return super.computeIfAbsent(key, object -> Boolean.TRUE);
109                }
110            });
111        } catch (Exception e) {
112            e.printStackTrace();
113        }
114    }
115
116    private static PrivateKey readPrivateKeyPEM(File file, char[] password) throws IOException, OperatorCreationException, PKCSException {
117        try (FileReader reader = new FileReader(file)) {
118            PEMParser parser = new PEMParser(reader);
119            Object object = parser.readObject();
120            if (object instanceof ASN1ObjectIdentifier) {
121                // ignore the EC key parameters
122                object = parser.readObject();
123            }
124            
125            if (object == null) {
126                throw new IllegalArgumentException("No key found in " + file);
127            }
128
129            if (BouncyCastleProvider.class.getName().startsWith("net.jsign")) {
130                // disable JceSecurity to allow the use of the repackaged BouncyCastle provider
131                disableJceSecurity();
132            }
133            BouncyCastleProvider provider = new BouncyCastleProvider();
134            JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(provider);
135
136            if (object instanceof PEMEncryptedKeyPair) {
137                // PKCS1 encrypted key
138                PEMDecryptorProvider decryptionProvider = new JcePEMDecryptorProviderBuilder().setProvider(provider).build(password);
139                PEMKeyPair keypair = ((PEMEncryptedKeyPair) object).decryptKeyPair(decryptionProvider);
140                return converter.getPrivateKey(keypair.getPrivateKeyInfo());
141
142            } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
143                // PKCS8 encrypted key
144                InputDecryptorProvider decryptionProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider(provider).build(password);
145                PrivateKeyInfo info = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(decryptionProvider);
146                return converter.getPrivateKey(info);
147                
148            } else if (object instanceof PEMKeyPair) {
149                // PKCS1 unencrypted key
150                return converter.getKeyPair((PEMKeyPair) object).getPrivate();
151                
152            } else if (object instanceof PrivateKeyInfo) {
153                // PKCS8 unencrypted key
154                return converter.getPrivateKey((PrivateKeyInfo) object);
155                
156            } else {
157                throw new UnsupportedOperationException("Unsupported PEM object: " + object.getClass().getSimpleName());
158            }
159        }
160    }
161}