/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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 com.motorolamobility.studio.android.certmanager.core;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStore.Builder;
import java.security.KeyStore.Entry;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.ProtectionParameter;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Map;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStrictStyle;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcContentSignerBuilder;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.eclipse.osgi.util.NLS;
import com.motorola.studio.android.common.log.StudioLogger;
import com.motorola.studio.android.common.utilities.FileUtil;
import com.motorolamobility.studio.android.certmanager.exception.InvalidPasswordException;
import com.motorolamobility.studio.android.certmanager.exception.KeyStoreManagerException;
import com.motorolamobility.studio.android.certmanager.i18n.CertificateManagerNLS;
import com.motorolamobility.studio.android.certmanager.ui.model.CertificateDetailsInfo;
public class KeyStoreUtils
{
private static final String ERROR_DELETING_ALIAS =
CertificateManagerNLS.KeyStoreUtils_ErrorDeletingAlias;
/**
* Creates a new empty KeyStore, from the default type, located at keyStoreFile with the password, password
* @param keyStoreFile The file pointing o where the new KeyStore will be located
* @param password the password for the new KeyStore
* @return the {@link KeyStore} representing the new KeyStore
* @throws InvalidPasswordException
* @throws KeyStoreException if KeyStore can't be created
*/
public static KeyStore createKeystore(File keyStoreFile, char[] password)
throws KeyStoreManagerException, InvalidPasswordException
{
return createKeystore(keyStoreFile, KeyStore.getDefaultType(), password);
}
/**
* Creates a new empty KeyStore, located at keyStoreFile with the password, password
* @param keyStoreFile The file pointing o where the new KeyStore will be located
* @param keyStoreType The type of the new KeyStore
* @param password the password for the new KeyStore
* @return the {@link KeyStore} representing the new KeyStore
* @throws InvalidPasswordException
* @throws KeyStoreException if KeyStore can't be created
*/
public static KeyStore createKeystore(File keyStoreFile, String keyStoreType, char[] password)
throws KeyStoreManagerException, InvalidPasswordException
{
KeyStore keyStore = null;
if ((keyStoreFile != null) && !keyStoreFile.exists())
{
keyStore = loadKeystore(keyStoreFile, password, keyStoreType);
try
{
writeKeyStore(keyStore, password, keyStoreFile);
}
catch (Exception e)
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_WriteKeyStore, keyStoreFile), e);
}
}
else
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_FileAlreadyExists, keyStoreFile));
}
return keyStore;
}
public static void writeKeyStore(KeyStore keyStore, char[] password, File keyStoreFile)
throws FileNotFoundException, KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException, KeyStoreManagerException, InvalidPasswordException
{
writeKeyStore(keyStore, null, password, keyStoreFile);
}
private static void writeKeyStore(KeyStore keyStore, char[] oldPassword, char[] newPassword,
File keyStoreFile) throws FileNotFoundException, KeyStoreException, IOException,
NoSuchAlgorithmException, CertificateException, KeyStoreManagerException,
InvalidPasswordException
{
FileOutputStream fos = null;
try
{
if (oldPassword != null)
{
if (loadKeystore(keyStoreFile, oldPassword, keyStore.getType()) != null)
{
fos = new FileOutputStream(keyStoreFile);
keyStore.store(fos, newPassword);
}
}
else
{
fos = new FileOutputStream(keyStoreFile);
keyStore.store(fos, newPassword);
}
}
finally
{
if (fos != null)
{
try
{
fos.close();
}
catch (IOException e)
{
StudioLogger.error("Could not close steam while writing keystore file. "
+ e.getMessage());
}
}
}
}
/**
* Loads a KeyStore from a given file from the default type, usually JKS.
* If keyStoreFile path don't exist then a new empty KeyStore will be created on the given location.
* <b>Note:</b> Calling this method is the same as calling loadKeystore(keyStoreFile, password, KeyStore.getDefaultType())
* @param keyStoreFile The keyStore location.
* @param password The KeyStore password
* @return the {@link KeyStore} representing the file.
* @throws KeyStoreManagerException
* @throws InvalidPasswordException
*/
public static KeyStore loadKeystore(File keyStoreFile, char[] password)
throws KeyStoreManagerException, InvalidPasswordException
{
return loadKeystore(keyStoreFile, password, KeyStore.getDefaultType());
}
/**
* Loads a KeyStore from a given file.
* If keyStoreFile path don't exist then a new empty KeyStore will be created on memory.
* If you want o create a new KeyStore file, calling createStore is recommended.
* @param keyStoreFile The keyStore location.
* @param password The KeyStore password
* @param storeType The Type of the keystore o be loaded.
* @return the {@link KeyStore} representing the file.
* @throws KeyStoreManagerException
* @throws InvalidPasswordException
*/
public static KeyStore loadKeystore(File keyStoreFile, char[] password, String storeType)
throws KeyStoreManagerException, InvalidPasswordException
{
KeyStore keyStore = null;
FileInputStream fis = null;
try
{
keyStore = KeyStore.getInstance(storeType);
if ((keyStoreFile != null) && keyStoreFile.exists() && (keyStoreFile.length() > 0))
{
fis = new FileInputStream(keyStoreFile);
}
//fis = null means a new keyStore will be created
keyStore.load(fis, password);
}
catch (IOException e)
{
if (e.getMessage().contains("password was incorrect")
|| (e.getCause() instanceof UnrecoverableKeyException))
{
throw new InvalidPasswordException(e.getMessage());
}
else
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_LoadKeyStore, keyStoreFile), e);
}
}
catch (Exception e)
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_LoadKeyStore, keyStoreFile), e);
}
finally
{
if (fis != null)
{
try
{
fis.close();
}
catch (IOException e)
{
StudioLogger.error("Could not close steam while loading keystore. "
+ e.getMessage());
}
}
}
return keyStore;
}
/**
* Simply deletes the KeyStore File
* @param keyStoreFile teh KeyStore file to be deleted.
* @throws KeyStoreException If any error occur.
*/
public static void deleteKeystore(File keyStoreFile) throws KeyStoreManagerException
{
try
{
FileUtil.deleteFile(keyStoreFile);
}
catch (IOException e)
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_DeleteKeyStore, keyStoreFile), e);
}
}
/**
* Write the keyStore in to the given file, protecting it with password.
* Warn: Since there's actually no way to change the password this method will overwrite the existing file with the keyStore contents,
* without further warning.
* @param keyStore the {@link KeyStore} to be written.
* @param keyStoreFile The KeyStore location
* @param oldPassword
* @param sourcePassword the new Password
* @throws KeyStoreException If file could no be write.
*/
public static void changeKeystorePasswd(KeyStore keyStore, File keyStoreFile,
char[] oldPassword, char[] newPassword) throws KeyStoreManagerException
{
try
{
keyStore = loadKeystore(keyStoreFile, oldPassword, keyStore.getType());
writeKeyStore(keyStore, oldPassword, newPassword, keyStoreFile);
}
catch (Exception e)
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_WriteKeyStore, keyStoreFile), e);
}
}
/**
* Adds a new enty to a given keyStore.
* @param keyStore The Keystore that will receive the entry
* @param keyStorePassword The KeyStore password
* @param keyStoreFile The KeyStore file path
* @param alias The new entry alias
* @param entry The Entry to be added
* @param entryPassword The password to protect the entry
* @throws KeyStoreManagerException if any error occurs.
*/
public static void addEntry(KeyStore keyStore, char[] keyStorePassword, File keyStoreFile,
String alias, Entry entry, char[] entryPassword) throws KeyStoreManagerException
{
try
{
PasswordProtection passwordProtection = new KeyStore.PasswordProtection(entryPassword);
keyStore = loadKeystore(keyStoreFile, keyStorePassword, keyStore.getType());
if (!keyStore.containsAlias(alias))
{
keyStore.setEntry(alias, entry, passwordProtection);
writeKeyStore(keyStore, keyStorePassword, keyStoreFile);
}
else
{
throw new KeyStoreManagerException(NLS.bind("Alias \"{0}\" already exists.", alias));
}
}
catch (KeyStoreManagerException e)
{
throw e;
}
catch (Exception e)
{
throw new KeyStoreManagerException(NLS.bind(
CertificateManagerNLS.KeyStoreUtils_Error_AddEntryToKeyStore, alias), e);
}
}
/**
* Adds a new enty to a given keyStore.
* @param keyStore The Keystore that will receive the entry
* @param keyStorePassword The KeyStore password
* @param keyStoreFile The KeyStore file path
* @param alias The new entry alias
* @param entry The Entry to be added
* @param entryPassword The password to protect the entry
* @throws KeyStoreManagerException if any error occurs.
*/
public static void changeEntryPassword(KeyStore keyStore, char[] keyStorePassword,
File keyStoreFile, String alias, Entry entry, char[] entryPassword)
throws KeyStoreManagerException
{
try
{
PasswordProtection passwordProtection = new KeyStore.PasswordProtection(entryPassword);
keyStore.setEntry(alias, entry, passwordProtection);
writeKeyStore(keyStore, keyStorePassword, keyStoreFile);
}
catch (Exception e)
{
throw new KeyStoreManagerException(NLS.bind(
"Error attempting to change password for {0}", alias), e);
}
}
/**
* Create a new X509 certificate for a given KeyPair
* @param keyPair the {@link KeyPair} used to create the certificate,
* RSAPublicKey and RSAPrivateKey are mandatory on keyPair, IllegalArgumentExeption will be thrown otherwise.
* @param issuerName The issuer name to be used on the certificate
* @param ownerName The owner name to be used on the certificate
* @param expireDate The expire date
* @return The {@link X509Certificate}
* @throws IOException
* @throws OperatorCreationException
* @throws CertificateException
*/
public static X509Certificate createX509Certificate(KeyPair keyPair,
CertificateDetailsInfo certDetails) throws IOException, OperatorCreationException,
CertificateException
{
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
if (!(publicKey instanceof RSAPublicKey) || !(privateKey instanceof RSAPrivateKey))
{
throw new IllegalArgumentException(
CertificateManagerNLS.KeyStoreUtils_RSA_Keys_Expected);
}
RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
//Transform the PublicKey into the BouncyCastle expected format
ASN1InputStream asn1InputStream = null;
X509Certificate x509Certificate = null;
try
{
asn1InputStream =
new ASN1InputStream(new ByteArrayInputStream(rsaPublicKey.getEncoded()));
SubjectPublicKeyInfo pubKey =
new SubjectPublicKeyInfo((ASN1Sequence) asn1InputStream.readObject());
X500NameBuilder nameBuilder = new X500NameBuilder(new BCStrictStyle());
addField(BCStyle.C, certDetails.getCountry(), nameBuilder);
addField(BCStyle.ST, certDetails.getState(), nameBuilder);
addField(BCStyle.L, certDetails.getLocality(), nameBuilder);
addField(BCStyle.O, certDetails.getOrganization(), nameBuilder);
addField(BCStyle.OU, certDetails.getOrganizationUnit(), nameBuilder);
addField(BCStyle.CN, certDetails.getCommonName(), nameBuilder);
X500Name subjectName = nameBuilder.build();
X500Name issuerName = subjectName;
X509v3CertificateBuilder certBuilder =
new X509v3CertificateBuilder(issuerName, BigInteger.valueOf(new SecureRandom()
.nextInt()), GregorianCalendar.getInstance().getTime(),
certDetails.getExpirationDate(), subjectName, pubKey);
AlgorithmIdentifier sigAlgId =
new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA"); //$NON-NLS-1$
AlgorithmIdentifier digAlgId =
new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
BcContentSignerBuilder sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId);
//Create RSAKeyParameters, the private key format expected by Bouncy Castle
RSAKeyParameters keyParams =
new RSAKeyParameters(true, rsaPrivateKey.getPrivateExponent(),
rsaPrivateKey.getModulus());
ContentSigner contentSigner = sigGen.build(keyParams);
X509CertificateHolder certificateHolder = certBuilder.build(contentSigner);
//Convert the X509Certificate from BouncyCastle format to the java.security format
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter();
x509Certificate = certConverter.getCertificate(certificateHolder);
}
finally
{
if (asn1InputStream != null)
{
try
{
asn1InputStream.close();
}
catch (IOException e)
{
StudioLogger.error("Could not close stream while creating X509 certificate. "
+ e.getMessage());
}
}
}
return x509Certificate;
}
private static void addField(ASN1ObjectIdentifier objectId, String value,
X500NameBuilder nameBuilder)
{
if (value.length() > 0)
{
nameBuilder.addRDN(objectId, value);
}
}
/**
* Creates a new RSA KeyPair
* @return the new {@link KeyPair}
*/
public static KeyPair genKeyPair() throws NoSuchAlgorithmException
{
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); //$NON-NLS-1$
keyPairGen.initialize(2048); //As recommended by Android guys, key is created with 2048 bits.
KeyPair keyPair = keyPairGen.genKeyPair();
return keyPair;
}
/**
* Create a new private key entry inside the key pair
* @param keyPair
* @param x509Certificate
* @return
*/
public static PrivateKeyEntry createPrivateKeyEntry(KeyPair keyPair,
X509Certificate x509Certificate)
{
Certificate[] certChain = new Certificate[]
{
x509Certificate
};
PrivateKeyEntry privateKeyEntry =
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certChain);
return privateKeyEntry;
}
public static void deleteEntry(KeyStore keyStore, char[] password, File keyStoreFile,
String alias) throws KeyStoreManagerException
{
try
{
keyStore = loadKeystore(keyStoreFile, password, keyStore.getType());
keyStore.deleteEntry(alias);
writeKeyStore(keyStore, password, keyStoreFile);
}
catch (Exception e)
{
StudioLogger.error(KeyStoreUtils.class, ERROR_DELETING_ALIAS + alias, e);
throw new KeyStoreManagerException(ERROR_DELETING_ALIAS + alias, e);
}
}
/**
* Change a keyStore type.
* @param keyStoreFile The KeyStoreFile
* @param password The KeyStore Password
* @param originalType the original Type
* @param destinationType the new KeyStore Type
* @throws KeyStoreManagerException If any error occurs, the operation will be canceled and reverted automatically.
* @throws InvalidPasswordException
*/
public static void changeKeyStoreType(File keyStoreFile, char[] password, String originalType,
String destinationType, Map<String, String> aliases) throws KeyStoreManagerException,
InvalidPasswordException
{
boolean rollBack = false;
String timeStamp = Long.toString(Calendar.getInstance().getTimeInMillis());
File oldKsFile = new File(keyStoreFile.getAbsolutePath() + "_" + timeStamp);
oldKsFile.delete();
boolean renamed = false;
renamed = keyStoreFile.renameTo(oldKsFile);
if (renamed)
{
try
{
Builder oldKsBuilder =
KeyStore.Builder.newInstance(originalType, null, oldKsFile,
new PasswordProtection(password));
KeyStore oldKeyStore = oldKsBuilder.getKeyStore();
KeyStore newKeyStore = createKeystore(keyStoreFile, destinationType, password);
for (String alias : aliases.keySet())
{
ProtectionParameter protectionParameter =
new PasswordProtection(aliases.get(alias).toCharArray());
Entry entry = oldKeyStore.getEntry(alias, protectionParameter);
newKeyStore.setEntry(alias, entry, protectionParameter);
}
writeKeyStore(newKeyStore, password, keyStoreFile);
}
catch (InvalidPasswordException e)
{
rollBack = true;
StudioLogger
.error(KeyStoreUtils.class,
"Invalid password while trying to create a new keystore, changing a keyStore type.",
e);
}
catch (Exception e)
{
if (e.getMessage().contains("password was incorrect")
|| e.getCause().getMessage().contains("password was incorrect"))
{
keyStoreFile.delete();
oldKsFile.renameTo(keyStoreFile);
throw new InvalidPasswordException(e.getMessage());
}
else
{
StudioLogger.error(KeyStoreUtils.class,
"Exception occurred while attempting to change a keyStore type.", e);
rollBack = true;
}
}
if (rollBack)
{
keyStoreFile.delete();
oldKsFile.renameTo(keyStoreFile);
throw new KeyStoreManagerException(NLS.bind(
"Could not convert the KeyStore {0} to type {1}", keyStoreFile,
destinationType));
}
}
else
{
throw new KeyStoreManagerException(
NLS.bind(
"Could not convert the KeyStore {0} to type {1}, could not backup the current keyStore file, maybe it's in use by another program.",
keyStoreFile, destinationType));
}
oldKsFile.delete();
}
/**
* Import a set of entries from sourcekeystore into the targetkeystore.
* If alias already exists on the target keystore then the alias is concatenated with the
* source keystore file name.
* @param targetKeyStore
* @param targetFile
* @param targetType
* @param targetPasswd
* @param sourceKeyStore
* @param sourceKeyStoreFile
* @param sourcePasswd
* @param aliases a map<String, String> containing alias as key and its password as value. this method assume that the password is correct
* @throws InvalidPasswordException
* @throws KeyStoreManagerException
*/
public static void importKeys(KeyStore targetKeyStore, File targetFile, String targetType,
char[] targetPasswd, KeyStore sourceKeyStore, File sourceKeyStoreFile,
char[] sourcePasswd, Map<String, String> aliases) throws InvalidPasswordException,
KeyStoreManagerException
{
if (!isValidKeyStorePasswd(targetFile, targetType, targetPasswd))
{
throw new InvalidPasswordException(
CertificateManagerNLS.PasswordChanged_InvalidKeystorePassword);
}
try
{
for (String alias : aliases.keySet())
{
if (sourceKeyStore.containsAlias(alias))
{
ProtectionParameter protectionParameter =
new PasswordProtection(aliases.get(alias).toCharArray());
Entry entry = sourceKeyStore.getEntry(alias, protectionParameter);
if (targetKeyStore.containsAlias(alias))
{
alias += "_" + sourceKeyStoreFile.getName();
}
int i = 1;
while (targetKeyStore.containsAlias(alias))
{
alias += "_" + i;
i++;
}
targetKeyStore.setEntry(alias, entry, protectionParameter);
}
else
{
StudioLogger
.error(KeyStoreUtils.class,
NLS.bind(
"Alias {0} could not be imported because it doesn't exists on originKeyStore",
alias));
}
}
writeKeyStore(targetKeyStore, targetPasswd, targetFile);
}
catch (Exception e)
{
throw new KeyStoreManagerException("Could not import the selected aliases into "
+ targetFile.getName(), e);
}
}
/**
* Verifies if the password if valid
* @param keyStoreFile
* @param keyStoreType
* @param passwd
* @return true if password is valid, false otherwise.
* @throws KeyStoreManagerException
*/
public static boolean isValidKeyStorePasswd(File keyStoreFile, String keyStoreType,
char[] passwd) throws KeyStoreManagerException
{
KeyStore keystore = null;
try
{
keystore = loadKeystore(keyStoreFile, passwd, keyStoreType);
}
catch (InvalidPasswordException e)
{
//Do nothing, password is invalid
}
return keystore != null;
}
}