/*
* Copyright (c) 2016 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.security;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStore.Entry;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.TrustedCertificateEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.validation.constraints.NotNull;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.obiba.crypt.CacheablePasswordCallback;
import org.obiba.crypt.CachingCallbackHandler;
import org.obiba.crypt.KeyPairNotFoundException;
import org.obiba.crypt.KeyProviderException;
import org.obiba.crypt.KeyProviderSecurityException;
import org.obiba.crypt.ObibaCryptRuntimeException;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
public class KeyStoreManager {
public static final String PASSWORD_FOR = "Password for";
public enum KeyType {
KEY_PAIR, CERTIFICATE
}
private final String name;
private final KeyStore store;
private CallbackHandler callbackHandler;
public KeyStoreManager(String name, KeyStore store) {
this.name = name;
this.store = store;
}
public Set<String> listAliases() {
try {
return ImmutableSet.copyOf(Iterators.forEnumeration(store.aliases()));
} catch(KeyStoreException e) {
throw new RuntimeException(e);
}
}
public Entry getEntry(String alias) {
try {
if(store.isKeyEntry(alias)) {
CacheablePasswordCallback passwordCallback = createPasswordCallback("Password for '" + alias + "': ");
return store.getEntry(alias, new PasswordProtection(getKeyPassword(passwordCallback)));
}
if(store.isCertificateEntry(alias)) {
return store.getEntry(alias, null);
}
throw new UnsupportedOperationException("Unsupported key type for alias " + alias);
} catch(KeyStoreException | IOException | UnsupportedCallbackException | UnrecoverableEntryException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private CacheablePasswordCallback createPasswordCallback(String prompt) {
return CacheablePasswordCallback.Builder.newCallback().key(name).prompt(prompt).build();
}
public Set<String> listKeyPairs() {
return ImmutableSet.copyOf(Iterables.filter(listAliases(), new Predicate<String>() {
@Override
public boolean apply(String input) {
try {
return store.isKeyEntry(input) && store.entryInstanceOf(input, PrivateKeyEntry.class);
} catch(KeyStoreException e) {
throw new RuntimeException(e);
}
}
}));
}
public Set<String> listCertificates() {
return ImmutableSet.copyOf(Iterables.filter(listAliases(), new Predicate<String>() {
@Override
public boolean apply(String input) {
try {
return store.isCertificateEntry(input);
} catch(KeyStoreException e) {
throw new RuntimeException(e);
}
}
}));
}
public boolean hasKeyPair(String alias) {
return listKeyPairs().contains(alias);
}
public KeyPair getKeyPair(String alias) {
try {
return findKeyPairForPrivateKey(alias);
} catch(KeyPairNotFoundException ex) {
throw ex;
} catch(UnrecoverableKeyException ex) {
if(callbackHandler instanceof CachingCallbackHandler) {
((CachingCallbackHandler) callbackHandler).clearPasswordCache(name);
}
throw new KeyProviderSecurityException("Wrong key password");
} catch(Exception ex) {
throw new RuntimeException(ex);
}
}
public KeyPair getKeyPair(PublicKey publicKey) {
try {
return findKeyPairForPublicKey(publicKey, store.aliases());
} catch(KeyStoreException ex) {
throw new RuntimeException(ex);
}
}
public X509Certificate importCertificate(String alias, InputStream pem) {
X509Certificate cert = getCertificate(pem);
try {
store.setCertificateEntry(alias, cert);
} catch(KeyStoreException e) {
throw new ObibaCryptRuntimeException(e);
}
return cert;
}
public Map<String, Certificate> getCertificates() {
Map<String, Certificate> map = Maps.newHashMap();
for(String alias : listAliases()) {
Entry keyEntry = getEntry(alias);
if(keyEntry instanceof TrustedCertificateEntry) {
map.put(alias, ((TrustedCertificateEntry) keyEntry).getTrustedCertificate());
}
}
return map;
}
public void setCallbackHandler(CallbackHandler callbackHandler) {
this.callbackHandler = callbackHandler;
}
public String getName() {
return name;
}
public KeyStore getKeyStore() {
return store;
}
private char[] getKeyPassword(CacheablePasswordCallback passwordCallback)
throws UnsupportedCallbackException, IOException {
callbackHandler.handle(new CacheablePasswordCallback[] { passwordCallback });
return passwordCallback.getPassword();
}
private KeyPair findKeyPairForPrivateKey(String alias)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, UnsupportedCallbackException,
IOException {
Key key = store.getKey(alias, getKeyPassword(createPasswordCallback("Password for '" + alias + "': ")));
if(key == null) {
throw new KeyPairNotFoundException("KeyPair not found for specified alias (" + alias + ")");
}
if(key instanceof PrivateKey) {
// Get certificate of public key
Certificate cert = store.getCertificate(alias);
// Get public key
PublicKey publicKey = cert.getPublicKey();
// Return a key pair
return new KeyPair(publicKey, (PrivateKey) key);
}
throw new KeyPairNotFoundException("KeyPair not found for specified alias (" + alias + ")");
}
private KeyPair findKeyPairForPublicKey(Key publicKey, Enumeration<String> aliases) {
KeyPair keyPair = null;
while(aliases.hasMoreElements()) {
String alias = aliases.nextElement();
KeyPair currentKeyPair = getKeyPair(alias);
if(Arrays.equals(currentKeyPair.getPublic().getEncoded(), publicKey.getEncoded())) {
keyPair = currentKeyPair;
break;
}
}
if(keyPair == null) {
throw new KeyPairNotFoundException("KeyPair not found for specified public key");
}
return keyPair;
}
public static X509Certificate makeCertificate(PrivateKey issuerPrivateKey, PublicKey subjectPublicKey,
String certificateInfo, String signatureAlgorithm)
throws SignatureException, InvalidKeyException, CertificateEncodingException, NoSuchAlgorithmException {
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
X509Name issuerDN = new X509Name(certificateInfo);
X509Name subjectDN = new X509Name(certificateInfo);
int daysTillExpiry = 30 * 365;
Calendar expiry = Calendar.getInstance();
expiry.add(Calendar.DAY_OF_YEAR, daysTillExpiry);
certificateGenerator.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
certificateGenerator.setIssuerDN(issuerDN);
certificateGenerator.setSubjectDN(subjectDN);
certificateGenerator.setPublicKey(subjectPublicKey);
certificateGenerator.setNotBefore(new Date());
certificateGenerator.setNotAfter(expiry.getTime());
certificateGenerator.setSignatureAlgorithm(signatureAlgorithm);
return certificateGenerator.generate(issuerPrivateKey);
}
public void createOrUpdateKey(String alias, String algorithm, int size, String certificateInfo) {
try {
KeyPair keyPair = generateKeyPair(algorithm, size);
X509Certificate cert = makeCertificate(algorithm, certificateInfo, keyPair);
CacheablePasswordCallback passwordCallback = createPasswordCallback(getPasswordFor(name));
store.setKeyEntry(alias, keyPair.getPrivate(), getKeyPassword(passwordCallback), new X509Certificate[] { cert });
} catch(GeneralSecurityException e) {
throw new ObibaCryptRuntimeException(e);
} catch(IOException | UnsupportedCallbackException e) {
throw new RuntimeException(e);
}
}
/**
* Deletes the key associated with the provided alias.
*
* @param alias key to delete
*/
public void deleteKey(String alias) {
try {
store.deleteEntry(alias);
} catch(KeyStoreException e) {
throw new KeyProviderException(e);
}
}
/**
* Returns true if the provided alias exists.
*
* @param alias check if this alias exists in the KeyStore.
* @return true if the alias exists
*/
public boolean aliasExists(String alias) {
try {
return store.containsAlias(alias);
} catch(KeyStoreException e) {
throw new KeyProviderException(e);
}
}
public KeyType getKeyType(String alias) {
if(listKeyPairs().contains(alias)) {
return KeyType.KEY_PAIR;
}
if(listCertificates().contains(alias)) {
return KeyType.CERTIFICATE;
}
throw new IllegalArgumentException("unknown alias '" + alias + "'or key type");
}
public static void loadBouncyCastle() {
if(Security.getProvider("BC") == null) Security.addProvider(new BouncyCastleProvider());
}
/**
* Import a private key and it's associated certificate into the keystore at the given alias.
*
* @param alias name of the key
* @param privateKey private key in the PEM format
* @param certificate certificate in the PEM format
*/
public void importKey(String alias, InputStream privateKey, InputStream certificate) {
storeKeyEntry(alias, getPrivateKey(privateKey), getCertificate(certificate));
}
private void storeKeyEntry(String alias, Key key, X509Certificate cert) {
CacheablePasswordCallback passwordCallback = createPasswordCallback(getPasswordFor(alias));
try {
store.setKeyEntry(alias, key, getKeyPassword(passwordCallback), new X509Certificate[] { cert });
} catch(KeyStoreException | IOException | UnsupportedCallbackException e) {
throw new RuntimeException(e);
}
}
/**
* Import a private key into the keystore and generate an associated certificate at the given alias.
*
* @param alias name of the key
* @param privateKey private key in the PEM format
* @param certificateInfo Certificate attributes as a String (e.g. CN=Administrator, OU=Bioinformatics, O=GQ,
* L=Montreal, ST=Quebec, C=CA)
*/
public void importKey(String alias, InputStream privateKey, String certificateInfo) {
makeAndStoreKeyEntry(alias, getKeyPair(privateKey), certificateInfo);
}
private void makeAndStoreKeyEntry(String alias, KeyPair keyPair, String certificateInfo) {
X509Certificate cert;
try {
cert = makeCertificate(keyPair.getPrivate(), keyPair.getPublic(), certificateInfo,
chooseSignatureAlgorithm(keyPair.getPrivate().getAlgorithm()));
CacheablePasswordCallback passwordCallback = createPasswordCallback(getPasswordFor(alias));
store.setKeyEntry(alias, keyPair.getPrivate(), getKeyPassword(passwordCallback), new X509Certificate[] { cert });
} catch(GeneralSecurityException | IOException | UnsupportedCallbackException e) {
throw new RuntimeException(e);
}
}
private X509Certificate makeCertificate(String algorithm, String certificateInfo, KeyPair keyPair)
throws SignatureException, InvalidKeyException, CertificateEncodingException, NoSuchAlgorithmException {
return makeCertificate(keyPair.getPrivate(), keyPair.getPublic(), certificateInfo,
chooseSignatureAlgorithm(algorithm));
}
private KeyPair generateKeyPair(String algorithm, int size) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
keyPairGenerator.initialize(size);
return keyPairGenerator.generateKeyPair();
}
private String chooseSignatureAlgorithm(String keyAlgorithm) {
// TODO add more algorithms here.
return "DSA".equals(keyAlgorithm) ? "SHA1withDSA" : "SHA1WithRSA";
}
protected KeyPair getKeyPair(InputStream privateKey) {
try(PEMReader pemReader = getPEMReader(privateKey)) {
Object object = getPemObject(pemReader);
if(object instanceof KeyPair) {
return (KeyPair) object;
}
throw new RuntimeException("Unexpected type [" + object + "]. Expected KeyPair.");
} catch(IOException e) {
throw new RuntimeException(e);
}
}
protected Key getPrivateKey(InputStream privateKey) {
try(PEMReader pemReader = getPEMReader(privateKey)) {
return toPrivateKey(getPemObject(pemReader));
} catch(IOException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("ChainOfInstanceofChecks")
private Key toPrivateKey(Object pemObject) {
if(pemObject instanceof KeyPair) {
return ((KeyPair) pemObject).getPrivate();
}
if(pemObject instanceof Key) {
return (Key) pemObject;
}
throw new RuntimeException("Unexpected type [" + pemObject + "]. Expected KeyPair or Key.");
}
protected X509Certificate getCertificate(InputStream certificate) {
try(PEMReader pemReader = getPEMReader(certificate)) {
Object object = getPemObject(pemReader);
if(object instanceof X509Certificate) {
return (X509Certificate) object;
}
throw new RuntimeException("Unexpected type [" + object + "]. Expected X509Certificate.");
} catch(IOException e) {
throw new RuntimeException(e);
}
}
@NotNull
private PEMReader getPEMReader(InputStream certificate) {
return new PEMReader(new InputStreamReader(certificate), new PasswordFinder() {
@Override
public char[] getPassword() {
return System.console().readPassword("%s: ", "Password for imported certificate");
}
});
}
@NotNull
private Object getPemObject(PEMReader pemReader) throws IOException {
Object object = pemReader.readObject();
if(object == null) throw new RuntimeException("No PEM information.");
return object;
}
/**
* Returns "Password for 'name': ".
*/
private String getPasswordFor(String target) {
return PASSWORD_FOR + " '" + target + "': ";
}
@SuppressWarnings({ "StaticMethodOnlyUsedInOneClass", "ParameterHidesMemberVariable" })
public static class Builder {
protected String name;
protected CallbackHandler callbackHandler;
public static Builder newStore() {
return new Builder();
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder passwordPrompt(CallbackHandler callbackHandler) {
this.callbackHandler = callbackHandler;
return this;
}
private char[] getKeyPassword(CacheablePasswordCallback passwordCallback)
throws UnsupportedCallbackException, IOException {
callbackHandler.handle(new CacheablePasswordCallback[] { passwordCallback });
return passwordCallback.getPassword();
}
public KeyStoreManager build() {
if (name == null || name.isEmpty()) throw new IllegalArgumentException("name must not be null or empty");
if (callbackHandler == null) throw new IllegalArgumentException("callbackHandler must not be null");
loadBouncyCastle();
CacheablePasswordCallback passwordCallback = CacheablePasswordCallback.Builder.newCallback().key(name)
.prompt("Enter '" + name + "' keystore password: ")
.confirmation("Re-enter '" + name + "' keystore password: ").build();
KeyStore keyStore = createEmptyKeyStore(passwordCallback);
return createKeyStoreManager(keyStore);
}
protected KeyStore createEmptyKeyStore(CacheablePasswordCallback passwordCallback) {
KeyStore keyStore = null;
try {
keyStore = KeyStore.getInstance("JCEKS");
keyStore.load(null, getKeyPassword(passwordCallback));
} catch(KeyStoreException e) {
clearPasswordCache(callbackHandler, name);
throw new KeyProviderSecurityException("Wrong keystore password or keystore was tampered with");
} catch(GeneralSecurityException | UnsupportedCallbackException e) {
throw new RuntimeException(e);
} catch(IOException ex) {
clearPasswordCache(callbackHandler, name);
translateAndRethrowKeyStoreIOException(ex);
}
return keyStore;
}
private static void clearPasswordCache(CallbackHandler callbackHandler, String alias) {
if(callbackHandler instanceof CachingCallbackHandler) {
((CachingCallbackHandler) callbackHandler).clearPasswordCache(alias);
}
}
private static void translateAndRethrowKeyStoreIOException(IOException ex) {
if(ex.getCause() != null && ex.getCause() instanceof UnrecoverableKeyException) {
throw new KeyProviderSecurityException("Wrong keystore password");
}
throw new RuntimeException(ex);
}
protected KeyStoreManager createKeyStoreManager(KeyStore keyStore) {
KeyStoreManager keyStoreManager = new KeyStoreManager(name, keyStore);
keyStoreManager.setCallbackHandler(callbackHandler);
return keyStoreManager;
}
}
}