diff --git a/core/src/main/java/org/acegisecurity/util/EncryptionUtils.java b/core/src/main/java/org/acegisecurity/util/EncryptionUtils.java new file mode 100644 index 0000000000..39b07b3d98 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/util/EncryptionUtils.java @@ -0,0 +1,123 @@ +package org.acegisecurity.util; + +import java.io.UnsupportedEncodingException; +import java.security.spec.KeySpec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESedeKeySpec; + +import org.acegisecurity.AcegiSecurityException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.Validate; +import org.springframework.util.Assert; + +/** + * A static utility class that can encrypt and decrypt text. + * + *

This class is useful if you have simple needs and wish to use the DESede + * encryption cipher. More sophisticated requirements will need to use the + * Java crypto libraries directly. + * + * @author Alan Stewart + * @author Ben Alex + */ +public class EncryptionUtils { + + /** + * This is a static class that should not be instantiated. + */ + private EncryptionUtils() {} + + /** + * Converts a String into a byte array using UTF-8, falling back to the + * platform's default character set if UTF-8 fails. + * + * @param input the input (required) + * @return a byte array representation of the input string + */ + public static byte[] stringToByteArray(String input) { + Assert.hasLength(input, "Input required"); + try { + return input.getBytes("UTF-8"); + } catch (UnsupportedEncodingException fallbackToDefault) { + return input.getBytes(); + } + } + + /** + * Converts a byte array into a String using UTF-8, falling back to the + * platform's default character set if UTF-8 fails. + * + * @param byteArray the byte array to convert (required) + * @return a string representation of the byte array + */ + public static String byteArrayToString(byte[] byteArray) { + Assert.notNull(byteArray, "ByteArray required"); + Assert.isTrue(byteArray.length > 0, "ByteArray cannot be empty"); + try { + return new String(byteArray, "UTF8"); + } catch (final UnsupportedEncodingException e) { + return new String(byteArray); + } + } + + private static byte[] cipher(String key, byte[] passedBytes, int cipherMode) throws EncryptionException { + try { + final KeySpec keySpec = new DESedeKeySpec(stringToByteArray(key)); + final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede"); + final Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding"); + final SecretKey secretKey = keyFactory.generateSecret(keySpec); + cipher.init(cipherMode, secretKey); + return cipher.doFinal(passedBytes); + } catch (final Exception e) { + throw new EncryptionException(e.getMessage(), e); + } + } + + /** + * Encrypts the inputString using the key. + * + * @param key at least 24 character long key (required) + * @param inputString the string to encrypt (required) + * @return the encrypted version of the inputString + * @throws EncryptionException in the event of an encryption failure + */ + public static String encrypt(String key, String inputString) throws EncryptionException { + isValidKey(key); + final byte[] cipherText = cipher(key, stringToByteArray(inputString), Cipher.ENCRYPT_MODE); + return byteArrayToString(Base64.encodeBase64(cipherText)); + } + + /** + * Decrypts the inputString using the key. + * + * @param key the key used to originally encrypt the string (required) + * @param inputString the encrypted string (required) + * @return the decrypted version of inputString + * @throws EncryptionException in the event of an encryption failure + */ + public static String decrypt(String key, String inputString) throws EncryptionException { + Assert.hasText(key, "A key is required to attempt decryption"); + final byte[] cipherText = cipher(key, Base64.decodeBase64(stringToByteArray(inputString)), Cipher.DECRYPT_MODE); + return byteArrayToString(cipherText); + } + + private static void isValidKey(String key) { + Assert.hasText(key, "A key to perform the encryption is required"); + Validate.isTrue(key.length() >= 24, "Key must be at least 24 characters long"); + } + + public static class EncryptionException extends AcegiSecurityException { + private static final long serialVersionUID = 1L; + + public EncryptionException(String message, Throwable t) { + super(message, t); + } + + public EncryptionException(String message) { + super(message); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/util/EncryptionUtilsTests.java b/core/src/test/java/org/acegisecurity/util/EncryptionUtilsTests.java new file mode 100644 index 0000000000..f29c3ffa85 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/util/EncryptionUtilsTests.java @@ -0,0 +1,93 @@ +package org.acegisecurity.util; + +import junit.framework.TestCase; + +import org.acegisecurity.util.EncryptionUtils.EncryptionException; + +/** + * JUnit tests for EncryptionUtils. + * + * @author Alan Stewart + * @author Ben Alex + */ +public class EncryptionUtilsTests extends TestCase { + private final static String STRING_TO_ENCRYPT = "Alan K Stewart"; + private final static String ENCRYPTION_KEY = "123456789012345678901234567890"; + + public void testEncryptsUsingDESEde() throws EncryptionException { + final String encryptedString = EncryptionUtils.encrypt(ENCRYPTION_KEY, STRING_TO_ENCRYPT); + assertEquals("3YIE8sIbaEoqGZZrHamFGQ==", encryptedString); + } + + public void testEncryptionKeyCanContainLetters() throws EncryptionException { + final String encryptedString = EncryptionUtils.encrypt("ASDF asdf 1234 8983 jklasdf J2Jaf8", STRING_TO_ENCRYPT); + assertEquals("v4+DQoClx6qm5tJwBcRrkw==", encryptedString); + } + + public void testDecryptsUsingDESEde() throws EncryptionException { + final String encryptedString = "3YIE8sIbaEoqGZZrHamFGQ=="; + final String decryptedString = EncryptionUtils.decrypt(ENCRYPTION_KEY, encryptedString); + assertEquals(STRING_TO_ENCRYPT, decryptedString); + } + + public void testCantEncryptWithNullEncryptionKey() throws EncryptionException { + try { + EncryptionUtils.encrypt(null, ""); + fail("Should have thrown IAE"); + } catch (final IllegalArgumentException e) { + assertTrue(true); + } + } + + public void testCantEncryptWithEmptyEncryptionKey() throws EncryptionException { + try { + EncryptionUtils.encrypt("", ""); + fail("Should have thrown IAE"); + } catch (final IllegalArgumentException e) { + assertTrue(true); + } + } + + public void testCantEncryptWithShortEncryptionKey() throws EncryptionException { + try { + EncryptionUtils.encrypt("01234567890123456789012", ""); + fail("Should have thrown IAE"); + } catch (final IllegalArgumentException e) { + assertTrue(true); + } + } + + public void testCantDecryptWithEmptyString() throws EncryptionException { + try { + EncryptionUtils.decrypt(ENCRYPTION_KEY, ""); + fail("Should have thrown IAE"); + } catch (final IllegalArgumentException e) { + assertTrue(true); + } + } + + public void testCantEncryptWithEmptyString() throws EncryptionException { + try { + EncryptionUtils.encrypt(ENCRYPTION_KEY, ""); + fail("Should have thrown IAE"); + } catch (final IllegalArgumentException e) { + assertTrue(true); + } + } + + public void testCantEncryptWithNullString() throws EncryptionException { + try { + EncryptionUtils.encrypt(ENCRYPTION_KEY, null); + fail("Should have thrown IAE"); + } catch (final IllegalArgumentException e) { + assertTrue(true); + } + } + + public void testEncryptAndDecrypt() throws EncryptionException { + final String stringToEncrypt = "Alan Stewart"; + final String encryptedString = EncryptionUtils.encrypt(ENCRYPTION_KEY, stringToEncrypt); + final String decryptedString = EncryptionUtils.decrypt(ENCRYPTION_KEY, encryptedString); + assertEquals(stringToEncrypt, decryptedString); + } +}