org.jboss.ejb3.examples.ch05.encryption.EncryptionBean.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.ejb3.examples.ch05.encryption.EncryptionBean.java

Source

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2009, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
  *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ejb3.examples.ch05.encryption;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.concurrent.Future;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;

import org.apache.commons.codec.binary.Base64;

/**
 * Bean implementation class of the EncryptionEJB.  Shows
 * how lifecycle callbacks are implemented (@PostConstruct),
 * and two ways of obtaining externalized environment
 * entries. 
 *
 * @author <a href="mailto:alr@jboss.org">ALR</a>
 */
@Stateless(name = EncryptionBean.EJB_NAME)
@Local(EncryptionLocalBusiness.class)
@Remote(EncryptionRemoteBusiness.class)
public class EncryptionBean implements EncryptionLocalBusiness, EncryptionRemoteBusiness {
    // ---------------------------------------------------------------------------||
    // Class Members -------------------------------------------------------------||
    // ---------------------------------------------------------------------------||

    /**
     * Logger
     */
    private static final Logger log = Logger.getLogger(EncryptionBean.class.getName());

    /**
     * Name we'll assign to this EJB, will be referenced in the corresponding 
     * META-INF/ejb-jar.xml file
     */
    static final String EJB_NAME = "EncryptionEJB";

    /**
     * Name of the environment entry representing the ciphers' passphrase supplied
     * in ejb-jar.xml
     */
    private static final String ENV_ENTRY_NAME_CIPHERS_PASSPHRASE = "ciphersPassphrase";

    /**
     * Name of the environment entry representing the message digest algorithm supplied
     * in ejb-jar.xml
     */
    private static final String ENV_ENTRY_NAME_MESSAGE_DIGEST_ALGORITHM = "messageDigestAlgorithm";

    /**
     * Default Algorithm used by the Digest for one-way hashing
     */
    private static final String DEFAULT_ALGORITHM_MESSAGE_DIGEST = "MD5";

    /**
     * Charset used for encoding/decoding Strings to/from byte representation
     */
    private static final String CHARSET = "UTF-8";

    /**
     * Default Algorithm used by the Cipher Key for symmetric encryption
     */
    private static final String DEFAULT_ALGORITHM_CIPHER = "PBEWithMD5AndDES";

    /**
     * The default passphrase for symmetric encryption/decryption
     */
    private static final String DEFAULT_PASSPHRASE = "LocalTestingPassphrase";

    /**
     * The salt used in symmetric encryption/decryption
     */
    private static final byte[] DEFAULT_SALT_CIPHERS = { (byte) 0xB4, (byte) 0xA2, (byte) 0x43, (byte) 0x89, 0x3E,
            (byte) 0xC5, (byte) 0x78, (byte) 0x53 };

    /**
     * Iteration count used for symmetric encryption/decryption
     */
    private static final int DEFAULT_ITERATION_COUNT_CIPHERS = 20;

    // ---------------------------------------------------------------------------||
    // Instance Members ----------------------------------------------------------||
    // ---------------------------------------------------------------------------||

    /*
     * The following members represent the internal
     * state of the Service.  Note how these are *not* leaked out
     * via the end-user API, and are hence part of "internal state"
     * and not "conversational state".
     */

    /**
     * SessionContext of this EJB; this will be injected by the EJB 
     * Container because it's marked w/ @Resource
     */
    @Resource
    private SessionContext context;

    /**
     * Passphrase to use  for the key in cipher operations; lazily initialized
     * and loaded via SessionContext.lookup
     */
    private String ciphersPassphrase;

    /**
     * Algorithm to use in message digest (hash) operations, injected
     * via @Resource annotation with name property equal to env-entry name
     */
    @Resource(name = ENV_ENTRY_NAME_MESSAGE_DIGEST_ALGORITHM)
    private String messageDigestAlgorithm;

    /**
     * Digest used for one-way hashing
     */
    private MessageDigest messageDigest;

    /**
     * Cipher used for symmetric encryption
     */
    private Cipher encryptionCipher;

    /**
     * Cipher used for symmetric decryption
     */
    private Cipher decryptionCipher;

    // ---------------------------------------------------------------------------||
    // Lifecycle -----------------------------------------------------------------||
    // ---------------------------------------------------------------------------||

    /**
     * Initializes this service before it may handle requests
     * 
     * @throws Exception If some unexpected error occurred
     */
    @PostConstruct
    public void initialize() throws Exception {
        // Log that we're here
        log.info("Initializing, part of " + PostConstruct.class.getName() + " lifecycle");

        /*
         * Symmetric Encryption
         */

        // Obtain parameters used in initializing the ciphers
        final String cipherAlgorithm = DEFAULT_ALGORITHM_CIPHER;
        final byte[] ciphersSalt = DEFAULT_SALT_CIPHERS;
        final int ciphersIterationCount = DEFAULT_ITERATION_COUNT_CIPHERS;
        final String ciphersPassphrase = this.getCiphersPassphrase();

        // Obtain key and param spec for the ciphers
        final KeySpec ciphersKeySpec = new PBEKeySpec(ciphersPassphrase.toCharArray(), ciphersSalt,
                ciphersIterationCount);
        final SecretKey ciphersKey = SecretKeyFactory.getInstance(cipherAlgorithm).generateSecret(ciphersKeySpec);
        final AlgorithmParameterSpec paramSpec = new PBEParameterSpec(ciphersSalt, ciphersIterationCount);

        // Create and init the ciphers
        this.encryptionCipher = Cipher.getInstance(ciphersKey.getAlgorithm());
        this.decryptionCipher = Cipher.getInstance(ciphersKey.getAlgorithm());
        encryptionCipher.init(Cipher.ENCRYPT_MODE, ciphersKey, paramSpec);
        decryptionCipher.init(Cipher.DECRYPT_MODE, ciphersKey, paramSpec);

        // Log
        log.info("Initialized encryption cipher: " + this.encryptionCipher);
        log.info("Initialized decryption cipher: " + this.decryptionCipher);

        /*
         * One-way Hashing
         */

        // Get the algorithm for the MessageDigest
        final String messageDigestAlgorithm = this.getMessageDigestAlgorithm();

        // Create the MessageDigest
        try {
            this.messageDigest = MessageDigest.getInstance(messageDigestAlgorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Could not obtain the " + MessageDigest.class.getSimpleName()
                    + " for algorithm: " + messageDigestAlgorithm, e);
        }
        log.info("Initialized MessageDigest for one-way hashing: " + this.messageDigest);
    }

    // ---------------------------------------------------------------------------||
    // Required Implementations --------------------------------------------------||
    // ---------------------------------------------------------------------------||

    /**
     * {@inheritDoc}
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionCommonBusiness#compare(java.lang.String, java.lang.String)
     */
    @Override
    public boolean compare(final String hash, final String input)
            throws IllegalArgumentException, EncryptionException {
        // Precondition checks
        if (hash == null) {
            throw new IllegalArgumentException("hash is required.");
        }
        if (input == null) {
            throw new IllegalArgumentException("Input is required.");
        }

        // Get the hash of the supplied input
        final String hashOfInput = this.hash(input);

        // Determine whether equal
        final boolean equal = hash.equals(hashOfInput);

        // Return
        return equal;
    }

    /**
     * {@inheritDoc}
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionCommonBusiness#decrypt(java.lang.String)
     */
    @Override
    public String decrypt(final String input)
            throws IllegalArgumentException, IllegalStateException, EncryptionException {
        // Get the cipher
        final Cipher cipher = this.decryptionCipher;
        if (cipher == null) {
            throw new IllegalStateException("Decyrption cipher not available, has this service been initialized?");
        }

        // Run the cipher
        byte[] resultBytes = null;
        ;
        try {
            final byte[] inputBytes = this.stringToByteArray(input);
            resultBytes = cipher.doFinal(Base64.decodeBase64(inputBytes));
        } catch (final Throwable t) {
            throw new EncryptionException("Error in decryption", t);
        }
        final String result = this.byteArrayToString(resultBytes);

        // Log
        log.info("Decryption on \"" + input + "\": " + result);

        // Return
        return result;
    }

    /**
     * {@inheritDoc}
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionCommonBusiness#encrypt(java.lang.String)
     */
    @Override
    public String encrypt(final String input) throws IllegalArgumentException, EncryptionException {
        // Get the cipher
        final Cipher cipher = this.encryptionCipher;
        if (cipher == null) {
            throw new IllegalStateException("Encyrption cipher not available, has this service been initialized?");
        }

        // Get bytes from the String
        byte[] inputBytes = this.stringToByteArray(input);

        // Run the cipher
        byte[] resultBytes = null;
        try {
            resultBytes = Base64.encodeBase64(cipher.doFinal(inputBytes));
        } catch (final Throwable t) {
            throw new EncryptionException("Error in encryption of: " + input, t);
        }

        // Log
        log.info("Encryption on \"" + input + "\": " + this.byteArrayToString(resultBytes));

        // Return
        final String result = this.byteArrayToString(resultBytes);
        return result;
    }

    /**
     * Note:
     * 
     * This is a weak implementation, but is enough to satisfy the example.
     * If considering real-world stresses, we would be, at a minimum:
     * 
     * 1) Incorporating a random salt and storing it alongside the hashed result
     * 2) Additionally implementing an iteration count to re-hash N times  
     */
    /* (non-Javadoc)
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionCommonBusiness#hash(java.lang.String)
     */
    @Override
    public String hash(final String input) throws IllegalArgumentException, EncryptionException {
        // Precondition check
        if (input == null) {
            throw new IllegalArgumentException("Input is required.");
        }

        // Get bytes from the input
        byte[] inputBytes = this.stringToByteArray(input);

        // Obtain the MessageDigest
        final MessageDigest digest = this.messageDigest;

        // Update with our input, and obtain the hash, resetting the messageDigest
        digest.update(inputBytes, 0, inputBytes.length);
        final byte[] hashBytes = digest.digest();
        final byte[] encodedBytes = Base64.encodeBase64(hashBytes);

        // Get the input back in some readable format
        final String hash = this.byteArrayToString(encodedBytes);
        log.info("One-way hash of \"" + input + "\": " + hash);

        // Return
        return hash;
    }

    /**
     * {@inheritDoc}
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionCommonBusiness#hashAsync(java.lang.String)
     */
    @Asynchronous
    @Override
    public Future<String> hashAsync(final String input) throws IllegalArgumentException, EncryptionException {
        // Get the real hash
        final String hash = this.hash(input);

        // Wrap and return
        return new AsyncResult<String>(hash);
    }

    /**
     * Override the way we get the ciphers' passphrase so that we may 
     * define it in a secure location on the server.  Now our production
     * systems will use a different key for encoding than our development
     * servers, and we may limit the likelihood of a security breach 
     * while still allowing our programmer to use the default passphrase
     * transparently during development.  
     * 
     * If not provided as an env-entry, fall back upon the default.
     * 
     * Note that a real system won't expose this method in the public API, ever.  We
     * do here for testing and to illustrate the example.
     * 
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionBeanBase#getCiphersPassphrase()
     */
    @Override
    public String getCiphersPassphrase() {
        // Obtain current
        String passphrase = this.ciphersPassphrase;

        // If not set
        if (passphrase == null) {

            // Do a lookup via SessionContext
            passphrase = this.getEnvironmentEntryAsString(ENV_ENTRY_NAME_CIPHERS_PASSPHRASE);

            // See if provided
            if (passphrase == null) {

                // Log a warning
                log.warning("No encryption passphrase has been supplied explicitly via "
                        + "an env-entry, falling back on the default...");

                // Set
                passphrase = DEFAULT_PASSPHRASE;
            }

            // Set the passphrase to be used so we don't have to do this lazy init again
            this.ciphersPassphrase = passphrase;
        }

        // In a secure system, we don't log this. ;)
        log.info("Using encryption passphrase for ciphers keys: " + passphrase);

        // Return 
        return passphrase;
    }

    /**
     * Obtains the message digest algorithm as injected from the env-entry element
     * defined in ejb-jar.xml.  If not specified, fall back onto the default, logging a warn 
     * message
     * 
     * @see org.jboss.ejb3.examples.ch05.encryption.EncryptionRemoteBusiness#getMessageDigestAlgorithm()
     */
    @Override
    public String getMessageDigestAlgorithm() {
        // First see if this has been injected/set
        if (this.messageDigestAlgorithm == null) {
            // Log a warning
            log.warning("No message digest algorithm has been supplied explicitly via "
                    + "an env-entry, falling back on the default...");

            // Set
            this.messageDigestAlgorithm = DEFAULT_ALGORITHM_MESSAGE_DIGEST;
        }

        // Log
        log.info("Configured MessageDigest one-way hash algorithm is: " + this.messageDigestAlgorithm);

        // Return
        return this.messageDigestAlgorithm;
    }

    // ---------------------------------------------------------------------------||
    // Internal Helper Methods ---------------------------------------------------||
    // ---------------------------------------------------------------------------||

    /**
     * Obtains the environment entry with the specified name, casting to a String,
     * and returning the result.  If the entry is not assignable 
     * to a String, an {@link IllegalStateException} will be raised.  In the event that the 
     * specified environment entry cannot be found, a warning message will be logged
     * and we'll return null.
     * 
     * @param envEntryName
     * @return
     * @throws IllegalStateException
     */
    private String getEnvironmentEntryAsString(final String envEntryName) throws IllegalStateException {
        // See if we have a SessionContext
        final SessionContext context = this.context;
        if (context == null) {
            log.warning("No SessionContext, bypassing request to obtain environment entry: " + envEntryName);
            return null;
        }

        // Lookup in the Private JNDI ENC via the injected SessionContext
        Object lookupValue = null;
        try {
            lookupValue = context.lookup(envEntryName);
            log.fine("Obtained environment entry \"" + envEntryName + "\": " + lookupValue);
        } catch (final IllegalArgumentException iae) {
            // Not found defined within this EJB's Component Environment, 
            // so return null and let the caller handle it
            log.warning("Could not find environment entry with name: " + envEntryName);
            return null;
        }

        // Cast
        String returnValue = null;
        try {
            returnValue = String.class.cast(lookupValue);
        } catch (final ClassCastException cce) {
            throw new IllegalStateException("The specified environment entry, " + lookupValue
                    + ", was not able to be represented as a " + String.class.getName(), cce);
        }

        // Return
        return returnValue;
    }

    /**
     * Returns a String representation of the specified byte array
     * using the charset from {@link EncryptionBeanBase#getCharset()}.  Wraps 
     * any {@link UnsupportedEncodingException} as a result of using an invalid
     * charset in a {@link RuntimeException}.
     * 
     * @param bytes
     * @return
     * @throws RuntimeException If the charset was invalid, or some otehr unknown error occurred 
     * @throws IllegalArgumentException If the byte array was not specified
     */
    private String byteArrayToString(final byte[] bytes) throws RuntimeException, IllegalArgumentException {
        // Precondition check
        if (bytes == null) {
            throw new IllegalArgumentException("Byte array is required.");
        }

        // Represent as a String
        String result = null;
        final String charset = this.getCharset();
        try {
            result = new String(bytes, charset);
        } catch (final UnsupportedEncodingException e) {
            throw new RuntimeException("Specified charset is invalid: " + charset, e);
        }

        // Return
        return result;
    }

    /**
     * Returns a byte array representation of the specified String
     * using the charset from {@link EncryptionBeanBase#getCharset()}.  Wraps 
     * any {@link UnsupportedEncodingException} as a result of using an invalid
     * charset in a {@link RuntimeException}.
     * 
     * @param input
     * @return
     * @throws RuntimeException If the charset was invalid, or some otehr unknown error occurred 
     * @throws IllegalArgumentException If the input was not specified (null)
     */
    private byte[] stringToByteArray(final String input) throws RuntimeException, IllegalArgumentException {
        // Precondition check
        if (input == null) {
            throw new IllegalArgumentException("Input is required.");
        }

        // Represent as a String
        byte[] result = null;
        final String charset = this.getCharset();
        try {
            result = input.getBytes(charset);
        } catch (final UnsupportedEncodingException e) {
            throw new RuntimeException("Specified charset is invalid: " + charset, e);
        }

        // Return
        return result;
    }

    /**
     * Obtains the charset used in encoding/decoding Strings 
     * to/from byte representation
     * 
     * @return The charset
     */
    private String getCharset() {
        return CHARSET;
    }

}