Java tutorial
/* Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved 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 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache 2 License for the specific language governing permissions and limitations under the License. */ package com.msopentech.thali.utilities.universal; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v1CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.math.BigInteger; import java.security.*; import java.security.cert.Certificate; import java.security.cert.*; import java.security.interfaces.RSAPublicKey; import java.util.Date; /** * Created by yarong on 11/12/13. */ public class ThaliCryptoUtilities { public final static String ThaliKeyAlias = "thaliKeyAlias"; public final static String PrivateKeyHolderFormat = "PKCS12"; public final static char[] DefaultPassPhrase = "Encrypting key files on a device with a password that is also stored on the device is security theater" .toCharArray(); public final static String KeyTypeIdentifier = "RSA"; public final static int KeySizeInBits = 2048; public final static String SignerAlgorithm = "SHA256withRSA"; // TODO: Need to validate if that's a good choice public final static long ExpirationPeriodForCertsInDays = 365; public final static String X500Name = "CN=Thali"; private static final String KeystoreFileName = "com.msopentech.thali.name.keystore"; private static Logger logger = LoggerFactory.getLogger(ThaliCryptoUtilities.class); /** * This method will try to find an existing Thali keystore. If the keystore is there * then it will be validated. If valid, it will be returned. Otherwise if the keystore * is invalid (which is really, really, bad) then it will be deleted. In either case * however a new keystore will be created. * @param filesDir * @return */ public static KeyStore getThaliKeyStoreByAnyMeansNecessary(File filesDir) { KeyStore clientKeyStore = ThaliCryptoUtilities.validateThaliKeyStore(filesDir); // Unrecoverable error with the keystore (or it doesn't exist) so lets nuke and start over if (clientKeyStore == null) { File keyFile = ThaliCryptoUtilities.getThaliKeyStoreFileObject(filesDir); if (keyFile.exists() && keyFile.delete() == false) { throw new RuntimeException("Could not successfully delete key store file as part of recovery - " + keyFile.getAbsolutePath()); } clientKeyStore = ThaliCryptoUtilities.createNewThaliKeyInKeyStore(filesDir); } return clientKeyStore; } /** * Retrieves the Thali related keystore from the specified directory. * @param filesDir * @return */ public static File getThaliKeyStoreFileObject(File filesDir) { return new File(filesDir, KeystoreFileName).getAbsoluteFile(); } /** * Returns null if there are any problems with the keystore, it's cert or the keys it contains. Generally if this * returns null then the only thing to do is to delete the keystore (if it even exists) and start over. Note however * that right now we treat cert expiration as a failure condition. In the long run that doesn't make sense but * this is not final code and it's actually good to freak out with any keys generated with this generation of code. * They shouldn't last long enough to expire. * @param filesDir * @return */ public static KeyStore validateThaliKeyStore(File filesDir) { assert filesDir != null && filesDir.exists(); File keyStoreFile = getThaliKeyStoreFileObject(filesDir); if (keyStoreFile.exists() == false) { return null; } FileInputStream keyStoreFileStream = null; try { KeyStore keyStore = KeyStore.getInstance(PrivateKeyHolderFormat); keyStoreFileStream = new FileInputStream(keyStoreFile); keyStore.load(keyStoreFileStream, DefaultPassPhrase); KeyStore.Entry thaliKeystoreEntry = getThaliListenerKeyStoreEntry(keyStore); if (thaliKeystoreEntry == null) { logger.debug("Could not find key in keystore under alias " + ThaliKeyAlias); return null; } if ((thaliKeystoreEntry instanceof KeyStore.PrivateKeyEntry) == false) { logger.debug("Entry is not a PrivateKeyEntry"); return null; } KeyStore.PrivateKeyEntry privateThaliKeystoreEntry = (KeyStore.PrivateKeyEntry) thaliKeystoreEntry; Certificate[] certificates = privateThaliKeystoreEntry.getCertificateChain(); if (certificates == null) { logger.debug("No certs in cert chain."); return null; } if (certificates.length != 1) { logger.debug( "More than one cert in chain, someday we will support that but not right now. Actual length was " + certificates.length); return null; } if ((certificates[0] instanceof X509Certificate) == false) { logger.debug("Cert is not a X509Cert!"); return null; } X509Certificate x509Certificate = (X509Certificate) certificates[0]; x509Certificate.checkValidity(); // We don't check the cert name because we just don't care, it doesn't matter for Thali if ((certificates[0].getPublicKey() instanceof RSAPublicKey) == false) { logger.debug("Public key is not a RSA Public Key!"); return null; } RSAPublicKey rsaPublicKey = (RSAPublicKey) certificates[0].getPublicKey(); if (rsaPublicKey.getModulus().bitLength() < KeySizeInBits) { logger.debug("Public key size is less than required minimum, required size is " + KeySizeInBits + ", actual size is " + rsaPublicKey.getModulus().bitLength()); return null; } return keyStore; } catch (FileNotFoundException e) { logger.debug("Could not get a stream from a keyStoreFile we had previously validated existed.", e); return null; } catch (KeyStoreException e) { logger.debug("Could not create a keystore of type " + PrivateKeyHolderFormat, e); return null; } catch (CertificateExpiredException e) { logger.debug("Failure on checkValidity", e); return null; } catch (CertificateNotYetValidException e) { logger.debug("Failure on checkValidity", e); return null; } catch (CertificateException e) { logger.debug("Failure on keyStore.load", e); return null; } catch (NoSuchAlgorithmException e) { logger.debug("Failure on keyStore.load", e); return null; } catch (IOException e) { logger.debug("Failure on keyStore.load", e); return null; } catch (UnrecoverableEntryException e) { logger.debug("Failure on keyStore.getEntry", e); return null; } finally { if (keyStoreFileStream != null) { try { keyStoreFileStream.close(); } catch (IOException e) { logger.debug("Attempt to close keyStoreFileStream failed", e); } } } } public static KeyStore.PrivateKeyEntry getThaliListenerKeyStoreEntry(KeyStore keyStore) throws NoSuchAlgorithmException, UnrecoverableEntryException, KeyStoreException { return getThaliListenerKeyStoreEntry(keyStore, DefaultPassPhrase); } public static KeyStore.PrivateKeyEntry getThaliListenerKeyStoreEntry(KeyStore keyStore, char[] passPhrase) throws UnrecoverableEntryException, NoSuchAlgorithmException, KeyStoreException { return (KeyStore.PrivateKeyEntry) keyStore.getEntry(ThaliKeyAlias, new KeyStore.PasswordProtection(passPhrase)); } /** * This presumes a keystore created by our own utilities and yes we eventually need to come up with a better * wrapper for all of this. * @param keyStore * @return * @throws UnrecoverableKeyException * @throws NoSuchAlgorithmException * @throws KeyStoreException */ public static PublicKey getAppKeyFromKeyStore(KeyStore keyStore) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { return keyStore.getCertificate(ThaliKeyAlias).getPublicKey(); } /** * Creates a new keystore file with a validate Thali public/private key pair and returns the KeyStore object. * @param filesDir * @return */ public static KeyStore createNewThaliKeyInKeyStore(File filesDir) { File keyStoreFile = getThaliKeyStoreFileObject(filesDir); if (keyStoreFile.exists()) { throw new RuntimeException("A keystore already exists!"); } KeyStore keyStore = ThaliCryptoUtilities.CreatePKCS12KeyStoreWithPublicPrivateKeyPair( GenerateThaliAcceptablePublicPrivateKeyPair(), ThaliKeyAlias, DefaultPassPhrase); FileOutputStream fileOutputStream = null; try { // Yes this can swallow exceptions (if you got an exception inside this try and then the finally has an exception, but given what I'm doing here I don't care. try { fileOutputStream = new FileOutputStream(keyStoreFile); keyStore.store(fileOutputStream, DefaultPassPhrase); } catch (Exception e) { logger.error("oops", e); throw e; } finally { if (fileOutputStream != null) { fileOutputStream.close(); } } } catch (Exception e) { logger.error("Ooops", e); throw new RuntimeException(e.getMessage(), e); } return keyStore; } /** * Generates a public/private key pair that meets Thali's security requirements * @return */ public static KeyPair GenerateThaliAcceptablePublicPrivateKeyPair() { KeyPairGenerator keyPairGenerator = null; try { keyPairGenerator = KeyPairGenerator.getInstance(KeyTypeIdentifier); // TODO: http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html talks about security // failures in Android caused by improperly initialized RNGs. It would appear that this issue doesn't // apply to the latest version of Android. But obviously this is something that has to be further investigated // to make sure we are doing this correctly. keyPairGenerator.initialize(KeySizeInBits, new SecureRandom()); return keyPairGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e.getMessage(), e); } } /** * Creates a PKCS12 keystore and puts into it the submitted public/private key pair under the submitted * Key Alias using the submitted passphrase to 'secure' the file. * * Right now we only generate large RSA keys because I'm paranoid that the curves used in * Elliptic Curve crypto may have been designed by folks for whom security was not the paramount * concern. Once this issue is put to rest I would expect to switch to Elliptic Curve because * it is considered (with appropriate curves) to be more secure and is certainly faster. * @param keyPair * @param keyAlias * @param passphrase * @return */ public static KeyStore CreatePKCS12KeyStoreWithPublicPrivateKeyPair(KeyPair keyPair, String keyAlias, char[] passphrase) { try { byte[] publicKeyAsByteArray = keyPair.getPublic().getEncoded(); // Generate a cert for the public key Date startDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); Date endDate = new Date( System.currentTimeMillis() + (ExpirationPeriodForCertsInDays * 24L * 60L * 60L * 1000L)); // Thali security is based on keys NOT on cert values. That is we are not trying to bind a name (like a DNS // address) to a key. The key IS the identity. But the X509 standard requires names so we stick something // in. X500Name x500Name = new X500Name(X500Name); SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo( ASN1Sequence.getInstance(publicKeyAsByteArray)); // Note that by not specify .setProvider("BC") we are using the default provider, this is because bouncy castle as // previously mentioned is installed on Android but is a challenge for the applet so I'll just use the default for now. ContentSigner contentSigner = new JcaContentSignerBuilder(SignerAlgorithm).build(keyPair.getPrivate()); X509v1CertificateBuilder x509v1CertificateBuilder = new X509v1CertificateBuilder(x500Name, BigInteger.ONE, startDate, endDate, x500Name, subjectPublicKeyInfo); X509CertificateHolder x509CertificateHolder = x509v1CertificateBuilder.build(contentSigner); JcaX509CertificateConverter jcaX509CertificateConverter = new JcaX509CertificateConverter(); X509Certificate x509Certificate = jcaX509CertificateConverter.getCertificate(x509CertificateHolder); // Store the private key and the cert in the keystore KeyStore.PrivateKeyEntry privateKeyEntry = new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[] { x509Certificate }); KeyStore keyStore = KeyStore.getInstance(PrivateKeyHolderFormat); // Keystore has to be initialized before being used keyStore.load(null, null); keyStore.setEntry(keyAlias, privateKeyEntry, new KeyStore.PasswordProtection(passphrase)); return keyStore; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } }