/*******************************************************************************
* Copyright (c) 2008 Cambridge Semantics Incorporated.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Created by: Jordi Albornoz Mulligan ( <a href="mailto:jordi@cambridgesemantics.com">jordi@cambridgesemantics.com </a>)
*
* Contributors:
* Cambridge Semantics Incorporated - initial API and implementation
*******************************************************************************/
package org.openanzo.security.keystore;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Dictionary;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.openanzo.exceptions.AnzoException;
import org.openanzo.exceptions.ExceptionConstants;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A component used for encrypting and decrypting data based on a secret key held by the component. Encryption is done with a symmetric cypher with a secret key
* saved to a keystore.
*
* Be careful to only encrypt and decrypt using corresponding methods. See {@link ISecretKeystore} for more information.
*
* @author Jordi Albornoz Mulligan <jordi@cambridgesemantics.com>
* @author Joe Betz <jpbetz@cambridgesemantics.com>
*/
public class SecretKeyStore implements ISecretKeystore {
private static final Logger log = LoggerFactory.getLogger(SecretKeyStore.class);
/** Prefix for loading a resource URL */
public static final String resourcePrefix = "resource:";
private static final String KEY_NAME = "service-container-key";
private static final String KEY_STORE_ENCODING = "JCEKS";
private static final String STRING_ENCODING = "UTF-8";
private File dataDirectory = null;
private String algorithm;
private SecretKey skey;
private BundleContext bundleContext = null;
@SuppressWarnings("unchecked")
private Dictionary configurationProperties;
/**
* Create a secret key store
*
* @param configurationProperties
* configuration properties for secret key store
* @param dataDirectory
* directory for the keystore
*/
public SecretKeyStore(Dictionary<? extends Object, ? extends Object> configurationProperties, File dataDirectory) {
this.configurationProperties = configurationProperties;
this.dataDirectory = dataDirectory;
}
public void start() throws AnzoException {
String filelocation = KeyStoreDictionary.getKeyFileLocation(configurationProperties);
String keyPassword = KeyStoreDictionary.getKeyPassword(configurationProperties);
String algorithm = KeyStoreDictionary.getAlgorithm(configurationProperties);
String keystoreType = KeyStoreDictionary.getKeystoreType(configurationProperties);
if (StringUtils.isEmpty(algorithm)) {
algorithm = "AES";
}
if (StringUtils.isEmpty(keystoreType)) {
keystoreType = KEY_STORE_ENCODING;
}
if (StringUtils.isEmpty(keyPassword)) {
log.info("Using default key password since no '{}' specified in configuration.", KeyStoreDictionary.KEY_PASSWORD);
keyPassword = "secret";
}
if (StringUtils.isEmpty(filelocation)) {
throw new AnzoException(ExceptionConstants.OSGI.MISSING_COMPONENT_PARAMETER, KeyStoreDictionary.KEY_FILE_LOCATION);
}
log.info("Using keyLocation ('{}') and algorithm ('{}').", filelocation, algorithm);
this.algorithm = algorithm; // The loadKey method may need to use this.algorithm so we set it here.
InputStream inputStream = null;
try {
File keyStoreFile = null;
if (filelocation.startsWith(resourcePrefix) && bundleContext != null) { // handle resource:/ file locations
String resourceLocation = filelocation.substring(resourcePrefix.length());
URL resourceUrl = bundleContext.getBundle().getResource(resourceLocation);
if (resourceUrl == null) {
throw new AnzoException(ExceptionConstants.IO.READ_ERROR, resourceLocation);
}
inputStream = resourceUrl.openStream();
} else {
keyStoreFile = (filelocation.startsWith(".") && dataDirectory != null) ? new File(dataDirectory, filelocation) : new File(filelocation);
if (keyStoreFile.exists()) {
inputStream = new FileInputStream(keyStoreFile);
} else {
log.warn("Could not find keystore at '{}'. Creating a new keystore at that location.", keyStoreFile.getAbsolutePath());
}
}
SecretKey key = loadKey(inputStream, keyPassword, keyStoreFile, keystoreType);
initialize(key, algorithm);
} catch (IOException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error("error closing keystore input stream", e);
}
}
}
}
public void stop() throws AnzoException {
}
/**
* Initialize the encoder based on the given configuration. This method is typically only used by unit tests.
*
* @param key
* @param algorithm
* @throws AnzoException
*/
public void initialize(SecretKey key, String algorithm) throws AnzoException {
this.algorithm = algorithm;
this.skey = key;
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#encryptAndBase64EncodeString(java.lang.String)
*/
public String encryptAndBase64EncodeString(String plaintext) throws AnzoException {
byte[] plainbytes;
try {
plainbytes = plaintext.getBytes(STRING_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return encryptAndBase64EncodeBytes(plainbytes);
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#encryptAndBase64EncodeBytes(byte[])
*/
public String encryptAndBase64EncodeBytes(byte plaintext[]) throws AnzoException {
byte[] cypherbytes = encryptBytes(plaintext);
byte[] encodedCypherbytes = Base64.encodeBase64(cypherbytes);
String encodedEncryptedStr;
try {
encodedEncryptedStr = new String(encodedCypherbytes, STRING_ENCODING); // base64 is ASCII so we use an ASCII compatible encoding to create the String.
} catch (UnsupportedEncodingException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return encodedEncryptedStr;
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#encryptString(java.lang.String)
*/
public byte[] encryptString(String plaintext) throws AnzoException {
byte[] plainbytes;
try {
plainbytes = plaintext.getBytes(STRING_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return encryptBytes(plainbytes);
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#encryptBytes(byte[])
*/
public byte[] encryptBytes(byte plaintext[]) throws AnzoException {
byte[] cyphertext;
try {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, skey);
cyphertext = cipher.doFinal(plaintext);
} catch (GeneralSecurityException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return cyphertext;
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#decryptAndBase64DecodeString(java.lang.String)
*/
public String decryptAndBase64DecodeString(String base64encodedCyphertext) throws AnzoException {
byte[] cypherbytes;
try {
byte[] base64encodedCypherbytes = base64encodedCyphertext.getBytes(STRING_ENCODING);
cypherbytes = Base64.decodeBase64(base64encodedCypherbytes);
} catch (UnsupportedEncodingException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return decryptString(cypherbytes);
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#decryptAndBase64DecodeBytes(java.lang.String)
*/
public byte[] decryptAndBase64DecodeBytes(String base64encodedCyphertext) throws AnzoException {
byte[] plaintext;
try {
byte[] base64encodedCypherbytes = base64encodedCyphertext.getBytes(STRING_ENCODING);
byte[] cypherbytes = Base64.decodeBase64(base64encodedCypherbytes);
plaintext = decryptBytes(cypherbytes);
} catch (UnsupportedEncodingException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return plaintext;
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#decryptString(byte[])
*/
public String decryptString(byte cyphertext[]) throws AnzoException {
String plaintext;
try {
byte[] plainbytes = decryptBytes(cyphertext);
plaintext = new String(plainbytes, STRING_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return plaintext;
}
/* (non-Javadoc)
* @see org.openanzo.server.security.SecretKeyEncoder#decryptBytes(byte[])
*/
public byte[] decryptBytes(byte cyphertext[]) throws AnzoException {
byte[] plaintext;
try {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, skey);
plaintext = cipher.doFinal(cyphertext);
} catch (GeneralSecurityException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
return plaintext;
}
/**
* Loads the secret key to use for encryption and decryption. It will read the key from the keystore if it exists. Otherwise it will create a new randomly
* generated key and save it in a keystore at the given file. It will use the algorithm defined in the <code>algorithm</code> member.
*
* @param keyStoreStream
* stream from which to read the keystore which holds the secret key. If null, a new keystore is created.
* @param password
* password used to protect the and integrity-check the secret key.
* @param keyStoreDestination
* File path to which to save the keystore in case it is newly created or a new key was added. If null, then nothing is written out.
* @return the loaded or newly generated secret key.
* @throws AnzoException
*/
private SecretKey loadKey(InputStream keyStoreStream, String password, File keyStoreDestination, String keystoreType) throws AnzoException {
try {
KeyStore keyStore = KeyStore.getInstance(keystoreType);
keyStore.load(keyStoreStream, password.toCharArray());
Key key = null;
if (keyStore.containsAlias(KEY_NAME)) {
key = keyStore.getKey(KEY_NAME, password.toCharArray());
} else {
log.warn("Could not find key '{}' within keystore. Generating a new key.", KEY_NAME);
KeyGenerator kgen = KeyGenerator.getInstance(algorithm);
key = kgen.generateKey();
keyStore.setKeyEntry(KEY_NAME, key, password.toCharArray(), new Certificate[0]);
if (keyStoreDestination != null) {
log.warn("Storing new key in the keystore.");
OutputStream outputStream = null;
try {
outputStream = FileUtils.openOutputStream(keyStoreDestination);
keyStore.store(outputStream, password.toCharArray());
} finally {
if (outputStream != null) {
outputStream.close();
}
}
}
}
if (!(key instanceof SecretKey))
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, "key must be of type SecretKey: " + key);
return (SecretKey) key;
} catch (GeneralSecurityException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
} catch (IOException e) {
throw new AnzoException(ExceptionConstants.OSGI.INTERNAL_COMPONENT_ERROR, e);
}
}
}