Java tutorial
/* * Copyright (c) 2013 Game Salutes. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * Contributors: * Game Salutes - Repackaging and modifications of original work under University of Chicago and Apache License 2.0 shown below * * Repackaging from edu.uchicago.nsit.iteco.utils to com.gamesalutes.utils * * Copyright 2008 - 2011 University of Chicago * * 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.gamesalutes.utils; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.KeyFactory; import java.security.KeyStore; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.apache.commons.codec.binary.*; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * Utility methods for encryption and SSL. * * @author Justin Montgomery * @version $Id: EncryptUtils.java 2611 2011-02-07 21:03:43Z jmontgomery $ * */ public final class EncryptUtils { public static final List<String> STRONG_CIPHER_SUITES; private static final String CONF_FILE = "conf.properties"; private static final String CONF_CIPHER_PREFIX = "EncryptUtils.ciphers"; private static final String HTTPS_CIPHER_PROP = "https.cipherSuites"; /** * The type of entry to store * @author Justin Montgomery * @version $Id: EncryptUtils.java 2611 2011-02-07 21:03:43Z jmontgomery $ */ public enum StoreType { /** * Type is a private key. * */ PRIVATE_KEY, /** * Type is a certificate. * */ CERTIFICATE; } /** * Specifies the protocol of the transport layer security. * * @author Justin Montgomery * @version $Id: EncryptUtils.java 2611 2011-02-07 21:03:43Z jmontgomery $ */ public enum TransportSecurityProtocol { /** * TLSv1 protocol. */ TLS, /** * SSLv3 protocol. * */ SSL; } private static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----"; private static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----"; private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; private static final int CHUNK_LEN = 64; // TODO: use "\r\n" ? openssl uses "\n" private static final String KEY_LINE_TERM = "\n"; /** * Specifies the <code>PKCS8</code> private key type. * Encoded key must be in DER format. * */ public static final String PKCS8_TYPE = "pkcs8"; /** * Specifies the <code>PKCS12</code> key store type. * */ public static final String PKCS12_TYPE = "pkcs12"; /** * Specifies the <code>JKS</code> key store type. * */ public static final String JKS_TYPE = "jks"; /** * Specifies that the certificate type is "X509". * */ public static final String CERT_TYPE_X509 = "X509"; private static final String KEY_MANAGEMENT_ALG_SUN_X509 = "SunX509"; private EncryptUtils() { } //load the strong cipher suites static { // set bouncy castle provider // try // { // // some Java system classes may rely on platform provider being first // //Security.insertProviderAt( // // new org.bouncycastle.jce.provider.BouncyCastleProvider(), // // 1); // Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // } // catch(Exception e) // { // Logger.getLogger(EncryptUtils.class).warn( // "Bouncy Castle Provider not available",e); // } SortedMap<String, String> propMap = new TreeMap<String, String>(); //load the "conf.properties" resource Properties prop = new Properties(); try { //load the ciphers prop.load(EncryptUtils.class.getResourceAsStream(CONF_FILE)); for (Map.Entry<?, ?> E : prop.entrySet()) { String key = (String) E.getKey(); String value = (String) E.getValue(); if (key.startsWith(CONF_CIPHER_PREFIX)) propMap.put(key, value); } STRONG_CIPHER_SUITES = Collections.unmodifiableList(new ArrayList<String>(propMap.values())); } catch (IOException e) { throw new AssertionError(EncryptUtils.class.getName() + "could not load ciphers from package resource \"" + CONF_FILE + "\""); } } //end static /** * Returns <code>List</code> of strings in {@link #STRONG_CIPHER_SUITES} that * are supported by the default ssl socket factory. * * @return <code>List</code> of supported strong cipher suites */ public static List<String> getSupportedStrongCipherSuites() { Set<String> suites = new HashSet<String>( Arrays.asList(HttpsURLConnection.getDefaultSSLSocketFactory().getSupportedCipherSuites())); List<String> enabledSuites = new ArrayList<String>(); //get strong suites that are supported by the SSL factory for (String s : STRONG_CIPHER_SUITES) { if (suites.contains(s)) enabledSuites.add(s); } return enabledSuites; //convert list into comma separated string for use in System.setProperty } /** * Returns a comma-separated string containing the strong cipher suites that * are supported by the default ssl socket factory. * * @return comma-separated string containing the supported strong cipher suites or * <code>null</code> if none are supported */ public static String getSupportedStrongCipherSuitesAsStr() { //get the supported strong suites Collection<String> suites = getSupportedStrongCipherSuites(); if (suites.isEmpty()) return null; //get the deliminated string version return CollectionUtils.convertCollectionIntoDelStr(suites, ","); } /** * Sets the System property for the enabled cipher suites of <code>HttpsURLConnection</code> * instances to the enabled strong suites. * */ public static boolean setHttpsEnabledStrongSuites() { //get the deliminated string version for using System.setProperty String suiteStr = getSupportedStrongCipherSuitesAsStr(); if (suiteStr == null) return false; try { System.setProperty(HTTPS_CIPHER_PROP, suiteStr); return true; } catch (Exception e) { return false; } } /** * Reads a DER-encoded certificate using binary or Base64 encoding from the given file. * * @param file * @param certType * @return the read certificate * @throws CertificateException * @throws IOException */ public static java.security.cert.Certificate readCertificate(File file, String certType) throws CertificateException, IOException { return readCertificate(new BufferedInputStream(FileUtils.newFileInputStream(file)), certType); } /** * Reads a DER-encoded certificate that uses binary or Base64 encoding from the given input stream. * * @param in the <code>InputStream</code> * @param certType * @return the read certificate * @throws CertificateException * @throws IOException */ public static java.security.cert.Certificate readCertificate(InputStream in, String certType) throws CertificateException, IOException { // get the raw certificate bytes so that can trim and format cert properly byte[] data = null; try { data = ByteUtils.readBytes(in); // in automatically closed during call to readBytes even in case of exceptions in = null; } catch (Exception e) { in = null; throw new ChainedIOException("Error getting stream bytes", e); } CertificateFactory cf = CertificateFactory.getInstance(certType); try { byte[] pemData = formatBase64ToPem(getRawBase64Key(data), BEGIN_CERTIFICATE, END_CERTIFICATE); in = new ByteArrayInputStream(pemData); return cf.generateCertificate(in); } catch (Exception e) { MiscUtils.closeStream(in); // maybe it was in binary DER and couldn't be read when it was assumed to be in Base64 in = new ByteArrayInputStream(data); return cf.generateCertificate(in); } finally { MiscUtils.closeStream(in); } } /** * Convenience method for reading a private key from the supported store types : * jks,PKCS12,PKCS8. This method attempts to guess the format and algorithm to read the key. * * @param in the key <code>InputStream</code> * @param alias the alias for the key or <code>null</code> if this does not apply * @param passwd the password for the key or <code>null</code> if no password or does not apply * */ public static PrivateKey readPrivateKey(InputStream in, String alias, char[] passwd) throws Exception { // try PKCS8 if (alias == null && passwd == null) { try { return readPKCS8(in, "RSA"); } catch (Exception e) { } try { return readPKCS8(in, "DSA"); } catch (Exception e) { } } //try PKCS12 try { return readPKCS12PrivateKey(in, alias, passwd); } catch (Exception e) { } try { return readJKSPrivateKey(in, alias, passwd); } catch (Exception e) { throw new IOException("Unable to read key stream"); } } /** * Reads a private key from a "jks" key store. * * @param in the key <code>InputStream</code> * @param alias the alias for the key * @param pass the password to retrieve the key * @return the read <code>PrivateKey</code> * @throws Exception if error occurs retrieving the key */ public static PrivateKey readJKSPrivateKey(InputStream in, String alias, char[] pass) throws Exception { return readKeyStoreKey(in, "jks", alias, pass); } /** * Reads a private key from a "pkcs12" key store. * * @param in the key <code>InputStream</code> * @param alias the alias for the key * @param pass the password to retrieve the key * @return the read <code>PrivateKey</code> * @throws Exception if error occurs retrieving the key */ public static PrivateKey readPKCS12PrivateKey(InputStream in, String alias, char[] pass) throws Exception { return readKeyStoreKey(in, "pkcs12", alias, pass); } private static PrivateKey readKeyStoreKey(InputStream in, String storeType, String alias, char[] pass) throws Exception { try { KeyStore ks = KeyStore.getInstance(storeType); //load the key store //TODO: specify other than "null" if want key store integrity check //need key store passwd ks.load(in, null); return (PrivateKey) ks.getKey(alias, pass); } finally { MiscUtils.closeStream(in); } } /** * Reads a PKCS8 formatted private key from file. * * @param in the <code>InputStream</code> containing the key * @param alg the key algorithm * @return the read key * @throws Exception if error occurs reading the key */ public static PrivateKey readPKCS8(InputStream in, String alg) throws Exception { try { if (alg == null) throw new NullPointerException("alg"); //alg = alg.toUpperCase(); //if(!alg.equals("DSA") || !alg.equals("RSA")) // throw new IllegalArgumentException("Illegal key alg=" + alg); byte[] encodedKey = ByteUtils.readBytes(in); KeyFactory kf = KeyFactory.getInstance(alg); try { return kf.generatePrivate(new PKCS8EncodedKeySpec(encodedKey)); } catch (Exception e) { // maybe key was in PEM so convert to binary encodedKey = EncryptUtils.fromPemtoBinary(encodedKey); return kf.generatePrivate(new PKCS8EncodedKeySpec(encodedKey)); } } finally { MiscUtils.closeStream(in); } } /** * Computes an MD5 digest from the contents of the specified <code>InputStream</code>. * * @param in the <code>InputStream</code> * @return the digest bytes * @throws Exception if error occurs reading the stream or computing the digest */ public static byte[] computeMD5Digest(InputStream in) throws Exception { byte[] data = ByteUtils.readBytes(in); return computeMD5Digest(data); } /** * Computes an MD5 digest from the contents of the specified byte array. * * @param data the data bytes * @return the digest bytes * @throws Exception if error occurs reading the stream or computing the digest */ public static byte[] computeMD5Digest(byte[] data) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data); return md.digest(); } /** * Converts PEM-encoded key data to raw binary format. It is expected that * the first and last lines have <code>---BEGIN [xxx]---</code> and * <code>---END [xxx]---</code>, respectively. * * <code>data</code> array is not modified as a result of calling this * operation. * * @param data the PEM-encoded key data * @return the raw binary encoded key data */ public static byte[] fromPemtoBinary(byte[] data) { // first read in the raw pem data and trim the first and last lines if (data == null) throw new NullPointerException("data"); data = getRawBase64Key(data); // now base64 decode the resultant raw Base64 data return Base64.decodeBase64(data); } /** * Returns a raw Base64 key with no line breaks and no <code>---BEGIN [xxx]---</code> or * <code>---END [xxx]---</code> lines that appear in the PEM encoding. * <code>data</code> array is not modified as a result of calling this * operation. * * @param data the input base 64 data using a PEM-encoding * @return the stripped base 64 data */ private static byte[] getRawBase64Key(byte[] data) { // first read in the raw pem data and trim the first and last lines if (data == null) throw new NullPointerException("data"); String str = null; try { str = new String(data, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError("UTF-8 encoding not supported"); } String[] lineSplits = str.split(MiscUtils.LINE_BREAK_REGEX); // replace the first line: "has a -" character in it int firstReplaceIndex = -1; for (int i = 0; i < lineSplits.length; ++i) { if (lineSplits[i].indexOf('-') != -1) { str = str.replace(lineSplits[i], ""); firstReplaceIndex = i; break; } } // replace the last line: if we couldn't find the "first line" then we've searched the // whole string, so don't search again! if (firstReplaceIndex != -1) { for (int i = lineSplits.length - 1; i > firstReplaceIndex; --i) { if (lineSplits[i].indexOf('-') != -1) { str = str.replace(lineSplits[i], ""); break; } } } // remove all the rest of the spaces and line breaks str = str.replaceAll("\\s", ""); try { return str.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError("UTF-8 encoding not supported"); } } /** * Converts the raw binary encoded bytes of the specified * {@link StoreType} key data to <code>PEM</code> encoded bytes. * <code>data</code> array is not modified as a result of calling this * operation. * * @param data the raw binary key data * @param type the <code>StoreType</code> of the data * @return the <code>PEM</code> key data */ public static byte[] fromBinaryToPem(byte[] data, StoreType type) { if (data == null) throw new NullPointerException("data"); if (type == null) throw new NullPointerException("type"); switch (type) { case PRIVATE_KEY: return fromBinaryToPem(data, BEGIN_PRIVATE_KEY, END_PRIVATE_KEY); case CERTIFICATE: return fromBinaryToPem(data, BEGIN_CERTIFICATE, END_CERTIFICATE); default: throw new IllegalArgumentException("type=" + type); } } /** * Creates an <code>SSLContext</code> that accepts all server certificates. * * @param protocol the {@link TransportSecurityProtocol} to use for the context * @return the created <code>SSLContext</code> * @throws Exception if error occurs during the process of creating the context */ public static SSLContext createSSLContext(TransportSecurityProtocol protocol) throws Exception { return createSSLContext(protocol, (java.security.cert.X509Certificate[]) null); } /** * Creates an <code>SSLContext</code> that uses the specified trusted certificates. * * @param protocol the {@link TransportSecurityProtocol} to use for the context * @param trustedCerts certificates to import into the <code>SSLContext</code> or <code>null</code> * to accept all issuers * @return the created <code>SSLContext</code> * @throws Exception if error occurs during the process of creating the context */ public static SSLContext createSSLContext(TransportSecurityProtocol protocol, java.security.cert.X509Certificate... trustedCerts) throws Exception { return createSSLContext(protocol, null, trustedCerts); } /** * Creates an <code>SSLContext</code> that uses the specified trusted certificates. * * @param protocol the {@link TransportSecurityProtocol} to use for the context * @param trustedCerts certificates to import into the <code>SSLContext</code> or <code>null</code> * to accept all issuers * @param privateKey the client key to authenticate the client with the server * @return the created <code>SSLContext</code> * @throws Exception if error occurs during the process of creating the context */ public static SSLContext createSSLContext(TransportSecurityProtocol protocol, PrivateKey privateKey, java.security.cert.X509Certificate... trustedCerts) throws Exception { if (trustedCerts != null && trustedCerts.length == 0) throw new IllegalArgumentException("trustedCerts is empty"); X509TrustManager defaultManager = null; KeyManager[] keyManagers = null; KeyStore keyStore = null; if (privateKey != null || trustedCerts != null) { // create a new key store instance that will install the certificates // and/or the private keys keyStore = KeyStore.getInstance(JKS_TYPE); keyStore.load(null, null); } // import the certs if (trustedCerts != null) { // set up the key manager for the certificates javax.net.ssl.TrustManagerFactory trustFact = javax.net.ssl.TrustManagerFactory .getInstance(KEY_MANAGEMENT_ALG_SUN_X509); // install the certificates in the key store and give them a unique alias int imported = 0; for (java.security.cert.X509Certificate cert : trustedCerts) { if (cert != null) keyStore.setCertificateEntry("cert" + ++imported, cert); } if (imported == 0) throw new IllegalArgumentException("no non-null certs in trustedCerts"); // add the certs to the trust factory trustFact.init(keyStore); // get a default trust manager TrustManager[] tms = trustFact.getTrustManagers(); if (tms != null && tms.length >= 1) defaultManager = (X509TrustManager) tms[0]; } // import the private key if (privateKey != null) { keyStore.setKeyEntry("client", privateKey, null, null); KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(privateKey.getAlgorithm()); kmfactory.init(keyStore, null); keyManagers = kmfactory.getKeyManagers(); } //create the SSL context based on these parameters SSLContext sslContext = SSLContext.getInstance(protocol.toString()); // use a CertX509TrustManager since default one will still fail validation for // self-signed certs sslContext.init(keyManagers, new TrustManager[] { trustedCerts != null ? new CertX509TrustManager(defaultManager, trustedCerts) : new CertX509TrustManager() }, null); return sslContext; } private static byte[] fromBinaryToPem(byte[] data, String begin, String end) { // first base64 encode the data with no chunking data = Base64.encodeBase64(data, false); return formatBase64ToPem(data, begin, end); } private static byte[] formatBase64ToPem(byte[] data, String begin, String end) { // manually chunk the output StringBuilder out = new StringBuilder((int) ((data.length + begin.length() + end.length()) * 1.2)); // write header out.append(begin).append("\n"); int lineCount = 0; for (int i = 0; i < data.length; ++i) { out.append((char) data[i]); if (++lineCount == CHUNK_LEN) { out.append(KEY_LINE_TERM); lineCount = 0; } } // write out a line terminator for last non-full line if (lineCount > 0) out.append(KEY_LINE_TERM); //write footer out.append(end).append("\n"); try { return out.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError("UTF-8 encoding not supported"); } } }