package er.extensions.crypting;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Key;
import javax.crypto.Cipher;
import com.webobjects.foundation.NSForwardException;
import er.extensions.foundation.ERXStringUtilities;
/**
* ERXAbstractAESCrypter is an AES implementation of the crypter
* interface that allows subclasses to override the source of the cipher key.
* The AES cipher is a two-way cipher meaning the original string that was
* encrypted can be retrieved. The way that this version of the AES cipher
* is encrypted it is safe to use as a form value.
*
* @author qdolan
*/
public abstract class ERXAbstractAESCrypter implements ERXCrypterInterface {
/**
* Cipher instances are not thread safe so we need to create one
* for each thread using a ThreadLocal object
*/
class ThreadLocalCipher extends ThreadLocal {
private final int _mode;
public ThreadLocalCipher(int mode) {
_mode = mode;
}
@Override
protected Object initialValue() {
return createCipher(_mode);
}
@Override
public Cipher get() {
return (Cipher) super.get();
}
}
/** Block size of encrypted strings */
private int _blockSize;
/** Used to cache the encryption cipher */
private ThreadLocalCipher _encryptCipher;
/** Used to cache the decryption cipher */
private ThreadLocalCipher _decryptCipher;
/** Used to cache the secret key */
private Key _secretKey;
public ERXAbstractAESCrypter() {
_blockSize = 16;
}
/**
* Sets the block size to use for this cipher.
*
* @param blockSize
* the block size to use for this cipher
*/
public void setBlockSize(int blockSize) {
_blockSize = blockSize;
}
/**
* Returns the block size for this cipher.
*
* @return the block size for this cipher
*/
public int blockSize() {
return _blockSize;
}
/**
* Returns the secret key to use for this cipher.
*
* @return a secret key for the cipher
*/
protected abstract Key secretKey() throws Exception;
private Key _secretKey() {
if (_secretKey == null) {
try {
_secretKey = secretKey();
}
catch (Exception e) {
throw new NSForwardException(e);
}
}
return _secretKey;
}
/**
* Creates an AES cipher for a given mode. The two possible modes for a
* cipher are: ENCRYPT and DECRYPT.
*
* @param mode
* of the cipher (encrypting or decrypting)
* @return an AES cipher initialized with the given mode and with the
* <code>secretKey</code> from the above method.
*/
protected Cipher createCipher(int mode) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(mode, _secretKey());
return cipher;
}
catch (java.security.NoSuchAlgorithmException ex) {
throw new NSForwardException(ex, "Couldn't find the AES algorithm; perhaps you do not have the SunJCE security provider installed properly?");
}
catch (Exception e) {
throw new NSForwardException(e);
}
}
/**
* Method used to return the shared instance of the encryption cipher.
*
* @return AES encryption cipher
*/
protected Cipher encryptCipher() {
if (_encryptCipher == null) {
_encryptCipher = new ThreadLocalCipher(Cipher.ENCRYPT_MODE);
}
return _encryptCipher.get();
}
/**
* Method used to return the shared instance of the decryption cipher.
*
* @return decryption cipher
*/
protected Cipher decryptCipher() {
if (_decryptCipher == null) {
_decryptCipher = new ThreadLocalCipher(Cipher.DECRYPT_MODE);
}
return _decryptCipher.get();
}
/**
* Decodes an AES encoded string. Note that the originally encoded
* string should have been encoded with the same secret key as is used for
* the decoding cipher or else you are going to get garbage. To encode a
* string have a look at <code>encrypt</code>.
*
* @param cryptedText
* AES encoded string to be decoded
* @return decode clear text string
*/
public String decrypt(String cryptedText) {
if (cryptedText == null) {
return null;
}
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] clearText = null;
byte[] decodedBytes = null;
try {
decodedBytes = ERXCrypto.base64Decode(cryptedText);
}
catch (IOException e1) {
e1.printStackTrace();
return null;
}
int length = decodedBytes.length;
for (int j = 0; j < length;) {
byte[] encryptedBytes = new byte[_blockSize];
System.arraycopy(decodedBytes, j, encryptedBytes, 0, Math.min(length - j, _blockSize));
try {
clearText = decryptCipher().doFinal(encryptedBytes);
}
catch (Exception e) {
throw new NSForwardException(e);
}
for (int k = 0; k < _blockSize; k++) {
if (clearText[k] != 0) {
result.write(clearText[k]);
}
}
j+= _blockSize;
}
return ERXStringUtilities.fromUTF8Bytes(result.toByteArray());
}
/**
* AES encodes a given string using the secret key specified in the
* System property: <b>er.extensions.ERXAESCipherKey</b>. The AES cipher is a
* two way cipher meaning that given the secret key you can de-cipher what
* the original string is. For one-way encryption look at methods dealing
* with the SHA algorithm. To decode an AES encoded string use the
* method: <code>decrypt</code>. The resultant string from encoding
* a string is base64url encoded and safe for use in urls and cookies.
*
* @param clearText
* string to be encrypted
* @return encrypted string
*/
public String encrypt(String clearText) {
if (clearText == null) {
return null;
}
byte clearTextBytes[] = ERXStringUtilities.toUTF8Bytes(clearText);
ByteArrayOutputStream result = new ByteArrayOutputStream();
int pos = 0, length = clearTextBytes.length;
byte[] encryptedBytes;
while (pos < length) {
byte[] bytesToEncrypt = new byte[_blockSize];
System.arraycopy(clearTextBytes, pos, bytesToEncrypt, 0,
Math.min(length - pos, _blockSize));
try {
encryptedBytes = encryptCipher().doFinal(bytesToEncrypt);
result.write(encryptedBytes);
}
catch (Exception e) {
throw new NSForwardException(e);
}
pos += _blockSize;
}
return ERXCrypto.base64urlEncode(result.toByteArray());
}
}