/**
 * END USER LICENSE AGREEMENT (“EULA”)
 *
 * READ THIS AGREEMENT CAREFULLY (date: 9/13/2011):
 * http://www.akiban.com/licensing/20110913
 *
 * BY INSTALLING OR USING ALL OR ANY PORTION OF THE SOFTWARE, YOU ARE ACCEPTING
 * ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. YOU AGREE THAT THIS
 * AGREEMENT IS ENFORCEABLE LIKE ANY WRITTEN AGREEMENT SIGNED BY YOU.
 *
 * IF YOU HAVE PAID A LICENSE FEE FOR USE OF THE SOFTWARE AND DO NOT AGREE TO
 * THESE TERMS, YOU MAY RETURN THE SOFTWARE FOR A FULL REFUND PROVIDED YOU (A) DO
 * NOT USE THE SOFTWARE AND (B) RETURN THE SOFTWARE WITHIN THIRTY (30) DAYS OF
 * YOUR INITIAL PURCHASE.
 *
 * IF YOU WISH TO USE THE SOFTWARE AS AN EMPLOYEE, CONTRACTOR, OR AGENT OF A
 * CORPORATION, PARTNERSHIP OR SIMILAR ENTITY, THEN YOU MUST BE AUTHORIZED TO SIGN
 * FOR AND BIND THE ENTITY IN ORDER TO ACCEPT THE TERMS OF THIS AGREEMENT. THE
 * LICENSES GRANTED UNDER THIS AGREEMENT ARE EXPRESSLY CONDITIONED UPON ACCEPTANCE
 * BY SUCH AUTHORIZED PERSONNEL.
 *
 * IF YOU HAVE ENTERED INTO A SEPARATE WRITTEN LICENSE AGREEMENT WITH AKIBAN FOR
 * USE OF THE SOFTWARE, THE TERMS AND CONDITIONS OF SUCH OTHER AGREEMENT SHALL
 * PREVAIL OVER ANY CONFLICTING TERMS OR CONDITIONS IN THIS AGREEMENT.
 */
package com.akiban.server.expression.std;

import com.akiban.qp.operator.QueryContext;
import com.akiban.server.error.InvalidParameterValueException;
import com.akiban.server.error.WrongExpressionArityException;
import com.akiban.server.expression.Expression;
import com.akiban.server.expression.ExpressionComposer;
import com.akiban.server.expression.ExpressionEvaluation;
import com.akiban.server.expression.ExpressionType;
import com.akiban.server.expression.TypesList;
import com.akiban.server.service.functions.Scalar;
import com.akiban.server.types.AkType;
import com.akiban.server.types.NullValueSource;
import com.akiban.server.types.ValueSource;
import com.akiban.server.types.util.ValueHolder;
import com.akiban.sql.StandardException;
import com.akiban.util.ByteSource;
import com.akiban.util.WrappingByteSource;
import com.persistit.exception.InvalidKeyException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;

public class EncryptExpression extends AbstractBinaryExpression
{

    @Scalar("aes_encrypt")
    public static final ExpressionComposer ENCRYPT 
            = new InnerComposer(Cipher.ENCRYPT_MODE, AkType.VARBINARY);
    
    @Scalar("aes_decrypt")
    public static final ExpressionComposer DECRYPT 
            = new InnerComposer(Cipher.DECRYPT_MODE, AkType.VARCHAR);
    
    private static final class InnerComposer extends BinaryComposer
    {
        private final int MODE;
        private final AkType TOP;
        InnerComposer (int mode, AkType t)
        {
            MODE = mode;
            TOP = t;
        }
        
        @Override
        protected Expression compose(Expression first, Expression second)
        {
            return new EncryptExpression(first, second, MODE, TOP);
        }

        @Override
        public ExpressionType composeType(TypesList argumentTypes) throws StandardException
        {
            if (argumentTypes.size() != 2)
                throw new WrongExpressionArityException(2, argumentTypes.size());
            
            argumentTypes.setType(0, AkType.VARCHAR);
            argumentTypes.setType(1, AkType.VARCHAR);
            
            int l = argumentTypes.get(0).getPrecision();
            return ExpressionTypes.varchar((l / 16 + 1) * 16);
        }    
    }
    
    private static final class Encryption
    {
        private static final Base64 ENCODER = new Base64();
        
        public static void decrypt_encrypt (ValueSource text, ValueSource key, 
                                            ValueHolder ret,
                                            int length, int mode) throws
                    NoSuchAlgorithmException, NoSuchPaddingException, 
                    InvalidKeyException, IllegalBlockSizeException, 
                    BadPaddingException, UnsupportedEncodingException, 
                    NoSuchProviderException, java.security.InvalidKeyException
        {            
            SecretKey skey = new SecretKeySpec(adjustKey(key.getString(), length), "AES"); 
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(mode, skey);
            
            switch(mode)
            {
                case Cipher.ENCRYPT_MODE:
                    ret.putVarBinary(new WrappingByteSource(cipher.doFinal(text.getString().getBytes())));
                    break;
                case Cipher.DECRYPT_MODE:
                    ret.putString(new String(cipher.doFinal(text.getVarBinary().byteArray())));
                    break;
                default:
                    throw new IllegalArgumentException("Unexpected MODE: " + mode);
            }
        }
        
        
        /**
         * adjust the key into a byte array of [length] bytes.
         * If key.length() is >= key length, then it wraps around
         *
         * This is a 'bad' cryptographic hash algorithm because it's not hard at all to find
         * two keys that would hash to the same value, but it's what MySQL does.
         * 
         * 
         * Try: 
         * SELECT aes_decrypt(aes_encrypt('abc', 'abcdefghijklmnoprst'), 'rstdefghijklmnopabc');
         *  
         * => returns 'abc'!
         * 
         * @param key
         * @return the key in byte array
         * @throws UnsupportedEncodingException 
         */
        private static byte[] adjustKey(String key, int length) throws UnsupportedEncodingException
        {
            byte keyBytes[] = new byte[length];
            Arrays.fill(keyBytes, (byte) 0);
            byte realKey[] = key.getBytes();

            int n = 0;
            for (byte b : realKey)
                keyBytes[n++ % length] ^= b;

            return keyBytes;
        }
    }
    
    private static final class InnerEvaluation extends AbstractTwoArgExpressionEvaluation
    {
        private final int MODE;
        
        // KEY LENTH in BYTES
        private final int DEFAULT_KEY_LENGTH = 16; // someone might want to change this 
        
        InnerEvaluation(List<? extends ExpressionEvaluation> args, int mode)
        {
            super(args);
            MODE = mode;
        }
        
        @Override
        public ValueSource eval()
        {
            ValueSource text = left();
            ValueSource key =  right();
            if (text.isNull())
                if (text.isNull() || key.isNull())
                    return NullValueSource.only();
                
            try
            {
                Encryption.decrypt_encrypt(text, key, valueHolder(),
                                            DEFAULT_KEY_LENGTH, 
                                            MODE);
                return valueHolder();
            }
            catch (Exception e)
            {
                QueryContext context = queryContext();
                if (context != null)
                    context.warnClient(new InvalidParameterValueException(e.getMessage()));
                return NullValueSource.only();
            }
        }
    }
    
    private final int MODE;
    EncryptExpression(Expression text, Expression key, int mode, AkType top)
    {
        super(top, text, key);
        MODE = mode;
    }
    
       
    @Override
    protected boolean nullIsContaminating()
    {
        return true;
    }

    @Override
    protected void describe(StringBuilder sb)
    {
        sb.append(MODE == Cipher.DECRYPT_MODE ? "DECRYPT" : "ENCRYPT");
    }

    @Override
    public ExpressionEvaluation evaluation()
    {
        return new InnerEvaluation(childrenEvaluations(), MODE);
    }
}

@@ -1975,43 +1975,8 @@
 on;%0A
-import com.akiban.util.ByteSource;%0A
 impo
@@ -3922,74 +3922,8 @@
    %7B
-%0A        private static final Base64 ENCODER = new Base64();%0A     
    %0A

remove unused fields