Mimic MySQL AES_ENCRYPT in Apex

I am trying to mimic the MySQL AES_ENCRYPT function in Apex code. However the issue I am having is that MySQL doesn’t use an initialization vector. Is there a way to replicate this behavior in Apex?

In MySQL the text would be encoded as follows:

select HEX(AES_ENCRYPT('some random text', 'a16characterkey!')) as encoded_text;

In Apex I have tried the following but it doesn’t produce the same output. Is there an initialize vector I can use that will produce the same output?

String key = 'a16characterkey!';
String iv = 'a16characterkey!';

Blob cryptoKey = Blob.valueOf(key);
Blob initializationVector = Blob.valueOf(iv);
Blob data = Blob.valueOf('some random text');

Blob encryptedData = Crypto.encrypt('AES128', cryptoKey, initializationVector, data);

system.debug(EncodingUtil.convertToHex(encryptedData));

Any thoughts?

Answer

UPDATE: Apex is still not the best tool for byte-twiddling, but the addition of EncodingUtil.ConvertFromHex() in Spring ’14 makes this MUCH easier. Here’s a better version of my implementation:

public class MySQLCrypto2 {
    // 16 bytes of zeroes
    static Blob iv = EncodingUtil.convertFromHex('00000000000000000000000000000000');

    public class MySQLCryptoException extends Exception {
    }

    public class BlobSplitter {
        String hexEncoding;
        Integer bytes; // size of the hex encoding
        Integer ptr = 0; // offset into the base64 encoding

        public BlobSplitter(Blob b) {
            hexEncoding = EncodingUtil.convertToHex(b);
            bytes = hexEncoding.length();
        }

        public Blob getBlock() {
            if (ptr >= bytes) {
                return null;
            }

            Integer charsToConsume = Math.min(32, (bytes - ptr));
            String hexBlock = hexEncoding.substring(ptr, ptr + charsToConsume);
            ptr += charsToConsume;

            return EncodingUtil.convertFromHex(hexBlock);
        }
    }

    private static Blob joinBlocks(List<Blob> blocks) {
        String hexEncoded = '';
        for (Integer i = 0; i < blocks.size(); i++) {
            hexEncoded += EncodingUtil.convertToHex(blocks[i]);
        }
        return EncodingUtil.convertFromHex(hexEncoded);
    }

    public static Blob encrypt(String algorithmName, Blob privateKey, Blob plainText) {
        if (algorithmName != 'AES128') {
            throw new MySQLCryptoException('Algorithm '+algorithmName+' not supported');
        }

        BlobSplitter bs = new BlobSplitter(plainText);

        List<Blob> cipherTextBlocks = new List<Blob>();
        Integer i;
        Blob block;
        for (i = 0; (block = bs.getBlock()) != null; i++) {
            System.debug('PT: '+EncodingUtil.convertToHex(block));

            cipherTextBlocks.add(Crypto.encrypt('AES128', privateKey, iv, block));
            if (cipherTextBlocks[i].size() > 16) {
                BlobSplitter bsc = new BlobSplitter(cipherTextBlocks[i]);
                cipherTextBlocks[i] = bsc.getBlock();
            }

            System.debug('CT: '+EncodingUtil.convertToHex(cipherTextBlocks[i]).toUpperCase());
        }

        Blob cipherText = joinBlocks(cipherTextBlocks);

        // System.debug('XX: '+EncodingUtil.convertToHex(cipherText).toUpperCase());

        return cipherText;
    }
}

Test class:

@isTest
public class TestMySQLCrypto2{
    private static testmethod void testMySQLCrypto(){
        // From http://salesforce.stackexchange.com/questions/860/mimic-mysql-aes-encrpt-in-apex#comment1010_860
        // and generated in MySQL
        Blob cipherText = null;
        Blob privateKey = Blob.valueOf('a16characterkey!');
        List<Blob> plainTexts = new Blob[]{
            Blob.valueOf('Here is about fifty random bytes of data to be encoded'),
            Blob.valueOf('short'),
            Blob.valueOf('short1'),
            Blob.valueOf('short12'),
            Blob.valueOf('short123'),
            Blob.valueOf('012345678901234560'),
            Blob.valueOf('0123456789012345601'),
            Blob.valueOf('01234567890123456012'),
            Blob.valueOf('012345678901234560123'),
            Blob.valueOf('0123456789012345601234567890123456'),
            Blob.valueOf('01234567890123456012345678901234560'),
            Blob.valueOf('012345678901234560123456789012345601'),
            Blob.valueOf('0123456789012345601234567890123456012'),
            Blob.valueOf('01234567890123456012345678901234560123')
        };
        List<String> expectedHexes = new String[]{
            'C36E987E6B24F4A088ACD4A076165E90FD52616A3004D30F11700AC9FE70D3512F8A0962A2F5CD3BE0432114E8F3C8E2EE31674BE37015456040EB80E76EECF7',
            'DDF01D81CB2BFA522840E09DDC506C5E',
            '2551643CA0CE1508DF032DE82C1D432A',
            '92EEDAE9DC0618ED9E09065BF49BB536',
            'BA0F44AEF91549BC3D706C32265C7CA4',
            '9676A69026D36E6EAFFCDE1538C32BAE270AE9ACDB041268494601D90B63B3B8',
            '9676A69026D36E6EAFFCDE1538C32BAEC7A62B760AD0F2E6992C241DE1A0F7BD',
            '9676A69026D36E6EAFFCDE1538C32BAE77822C557C04BBF020D5A6E7EF6D8E1F',
            '9676A69026D36E6EAFFCDE1538C32BAE47FCCCB0BD9F367B460DB9808A6573B0',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B6D9A9A1AC5952484BB3BDFBDC710DDA47',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B63886EB402D276E7BF58853FE08A134DF',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B69AC2D7128E1E2A82032708BD83558579',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B60243F9CB463F23D517BF53E2F92A9835',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B68925B57FD46C9806B095BB70D3500A5A'
        };

        for (Integer i = 0; i < plainTexts.size(); i++) {
            cipherText = MySQLCrypto2.encrypt('AES128', privateKey, plainTexts[i]);
            System.assertEquals(expectedHexes[i], EncodingUtil.convertToHex(cipherText).toUpperCase(), 'Test 1-'+i);
        }        

        // From http://www.inconteam.com/software-development/41-encryption/55-aes-test-vectors#aes-ecb-128
        privateKey = EncodingUtil.base64Decode('K34VFiiu0qar9xWICc9PPA==');
        plainTexts = new Blob[]{
            EncodingUtil.base64Decode('a8G+4i5An5bpPX4Rc5MXKg=='),
            EncodingUtil.base64Decode('ri2KVx4DrJyet2+sRa+OUQ=='),
            EncodingUtil.base64Decode('MMgcRqNc5BHl+8EZGgpS7w=='),
            EncodingUtil.base64Decode('9p8kRd9PmxetK0F75mw3EA==')
        };
        expectedHexes = new String[]{
            '3ad77bb40d7a3660a89ecaf32466ef97',
            'f5d3d58503b9699de785895a96fdbaaf',
            '43b1cd7f598ece23881b00e3ed030688',
            '7b0c785e27e8ad3f8223207104725dd4'
        };

        for (Integer i = 0; i < plainTexts.size(); i++) {
            cipherText = MySQLCrypto2.encrypt('AES128', privateKey, plainTexts[i]);
            System.assertEquals(expectedHexes[i], EncodingUtil.convertToHex(cipherText), 'Test 2-'+i);
        }        
    }
}

OLD VERSION (kept for posterity, since it’s the gnarliest Apex code you’ll probably ever encounter!)

Apex is really not well suited to this task. The crypto was very straightforward, but the only way I was able to break the plaintext into 128-bit (16-byte) blocks was by base64 encoding it, twiddling bits, then base64 decoding the result. The same for combining ciphertext blocks into the final output.

Anyway – here is the code. It works for a bunch of test vectors that attempt to hit various cases in terms of numbers of blocks and amount of data in the last block, including your 54 byte example plaintext. It is a bit messy and, since it is stepping through most of the plaintext byte by byte, horribly inefficient, with about 10 script statements per byte of plaintext. You would not be able to encrypt more than about 20k of plaintext before you ran up against the script statement governor limit. I have no idea how this will perform under CPU time limits. Would be interesting to find out!

I’m posting it to show a solution as best I can see it, and to solicit better approaches, if they exist. If you are looking to encrypt small amounts of data, this might be useful.

public class MySQLCrypto {
    // 16 bytes of zeroes
    static Blob iv = EncodingUtil.base64Decode('AAAAAAAAAAAAAAAAAAAAAA==');
    static String base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    static Map<string, Integer> base64 = new map<string, Integer>{'A'=>0,'B'=>1,'C'=>2,'D'=>3,'E'=>4,'F'=>5,'G'=>6,'H'=>7,'I'=>8,'J'=>9,'K'=>10,'L'=>11,'M'=>12,'N'=>13,'O'=>14,'P'=>15,'Q'=>16,'R'=>17,'S'=>18,'T'=>19,'U'=>20,'V'=>21,'W'=>22,'X'=>23,'Y'=>24,'Z'=>25,'a'=>26,'b'=>27,'c'=>28,'d'=>29,'e'=>30,'f'=>31,'g'=>32,'h'=>33,'i'=>34,'j'=>35,'k'=>36,'l'=>37,'m'=>38,'n'=>39,'o'=>40,'p'=>41,'q'=>42,'r'=>43,'s'=>44,'t'=>45,'u'=>46,'v'=>47,'w'=>48,'x'=>49,'y'=>50,'z'=>51,'0'=>52,'1'=>53,'2'=>54,'3'=>55,'4'=>56,'5'=>57,'6'=>58,'7'=>59,'8'=>60,'9'=>61,'+'=>62,'/'=>63};

    public class MySQLCryptoException extends Exception {
    }

    public class BlobSplitter {
        String base64encoding;
        Integer bytes; // size of the base 64 encoding
        Integer ptr = 0; // offset into the base64 encoding
        Integer numBits = 0; // offset into first byte of next block

        public BlobSplitter(Blob b) {
            base64encoding = EncodingUtil.base64Encode(b);
            // Remove any padding
            if (base64encoding.endsWith('==')){
                base64encoding = base64encoding.substring(0, base64encoding.length() - 2);
            } else if (base64encoding.endsWith('=')){
                base64encoding = base64encoding.substring(0, base64encoding.length() - 1);
            }
            bytes = base64encoding.length();
        }

        public Blob getBlock() {
            if (ptr >= bytes) {
                return null;
            }

            String base64Block = '';
            String lastChar;
            String nextChar;
            Integer thisCharIndex;
            Integer charsToConsume = Math.min(21, (bytes - ptr) - 1);
            Integer nextCharIndex;
            if (numBits == 0){
                base64Block = base64encoding.substring(ptr, ptr + charsToConsume);
                nextChar = base64encoding.substring(ptr + charsToConsume, ptr + charsToConsume + 1);
                ptr += charsToConsume + 1;

                nextCharIndex = (base64.get(nextChar) & 48);
                numBits = 4;
            } else if (numBits == 4){
                Integer endIdx = ptr + charsToConsume;
                for (; ptr < endIdx; ptr++) {
                    lastChar = base64encoding.substring(ptr - 1, ptr);
                    nextChar = base64encoding.substring(ptr, ptr + 1);
                    thisCharIndex = ((base64.get(lastChar) & 15) << 2) | ((base64.get(nextChar) & 48) >>> 4);
                    base64Block += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                }
                nextCharIndex = ((base64.get(nextChar) & 15) << 2);
                numBits = 2;
            } else if (numBits == 2){
                Integer endIdx = ptr + charsToConsume;
                for (; ptr < endIdx; ptr++) {
                    lastChar = base64encoding.substring(ptr - 1, ptr);
                    nextChar = base64encoding.substring(ptr, ptr + 1);
                    thisCharIndex = ((base64.get(lastChar) & 3) << 4) | ((base64.get(nextChar) & 60) >>> 2);
                    base64Block += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                }
                nextCharIndex = (base64.get(nextChar) & 3) << 4;
                numBits = 0;
            }

            if (charsToConsume < 21) {
                // More logic required here to handle the various cases of
                // partial blocks!!!
                System.debug('charsToConsume '+charsToConsume);
                System.debug('numBits '+numBits);
                System.debug('charsLeft '+(bytes-ptr));
                if (numBits == 4){
                    base64Block += base64encoding.substring(ptr - 1, ptr);
                    ptr++;
                } else if (numBits == 2){
                    lastChar = base64encoding.substring(ptr - 1, ptr);
                    nextChar = base64encoding.substring(ptr, ptr + 1);
                    thisCharIndex = ((base64.get(lastChar) & 15) << 2) | ((base64.get(nextChar) & 48) >>> 4);
                    base64Block += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                    ptr++;
                    thisCharIndex = (base64.get(nextChar) & 15) << 2;
                    base64Block += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                } else if (numBits == 0){
                    lastChar = base64encoding.substring(ptr - 1, ptr);
                    nextChar = base64encoding.substring(ptr, ptr + 1);
                    thisCharIndex = ((base64.get(lastChar) & 3) << 4) | ((base64.get(nextChar) & 60) >>> 2);
                    base64Block += base64Chars.substring(thisCharIndex, thisCharIndex + 1);                    
                    ptr++;
                    if (Math.mod(charsToConsume,4) == 0) {
                        thisCharIndex = (base64.get(nextChar) & 3) << 4;
                        base64Block += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                        ptr++;
                    }
                }
            } else {
                base64Block += base64Chars.substring(nextCharIndex, nextCharIndex + 1);
            }

            return EncodingUtil.base64Decode(base64Block);
        }
    }

    private static Blob joinBlocks(List<Blob> blocks) {
        String base64encoded = '';
        Integer numBits = 0;
        Integer lastBits;
        for (Integer i = 0; i < blocks.size(); i++) {
            if (numBits == 0) {
                String encodedBlock = EncodingUtil.base64encode(blocks[i]);
                base64encoded += encodedBlock.substring(0,21);
                lastBits = base64.get(encodedBlock.substring(21,22)) & 48;
                numBits = 4;
            } else if (numBits == 4) {
                String encodedBlock = EncodingUtil.base64encode(blocks[i]);
                for (Integer j = 0; j < 21; j++) {
                    String thisChar = encodedBlock.substring(j, j + 1);
                    Integer thisCharIndex = lastBits | ((base64.get(thisChar) & 60) >>> 2);
                    base64encoded += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                    lastBits = (base64.get(thisChar) & 3) << 4;
                }
                lastBits = lastBits | ((base64.get(encodedBlock.substring(21,22)) & 48) >>> 2);
                numBits = 2;                
            } else if (numBits == 2) {
                String encodedBlock = EncodingUtil.base64encode(blocks[i]);
                for (Integer j = 0; j < 22; j++) {
                    String thisChar = encodedBlock.substring(j, j + 1);
                    Integer thisCharIndex = lastBits | ((base64.get(thisChar) & 48) >>> 4);
                    base64encoded += base64Chars.substring(thisCharIndex, thisCharIndex + 1);
                    lastBits = (base64.get(thisChar) & 15) << 2;
                }
                numBits = 0;                
            }
        }
        base64encoded += base64Chars.substring(lastBits, lastBits + 1);

        return EncodingUtil.base64decode(base64encoded);
    }

    public static Blob encrypt(String algorithmName, Blob privateKey, Blob plainText) {
        if (algorithmName != 'AES128') {
            throw new MySQLCryptoException('Algorithm '+algorithmName+' not supported');
        }

        BlobSplitter bs = new BlobSplitter(plainText);

        List<Blob> cipherTextBlocks = new List<Blob>();
        Integer i;
        Blob block;
        for (i = 0; (block = bs.getBlock()) != null; i++) {
            System.debug('PT: '+EncodingUtil.convertToHex(block));

            cipherTextBlocks.add(Crypto.encrypt('AES128', privateKey, iv, block));
            if (cipherTextBlocks[i].size() > 16) {
                BlobSplitter bsc = new BlobSplitter(cipherTextBlocks[i]);
                cipherTextBlocks[i] = bsc.getBlock();
            }

            System.debug('CT: '+EncodingUtil.convertToHex(cipherTextBlocks[i]).toUpperCase());
        }

        Blob cipherText = joinBlocks(cipherTextBlocks);

        // System.debug('XX: '+EncodingUtil.convertToHex(cipherText).toUpperCase());

        return cipherText;
    }

    @isTest
    private static void testMySQLCrypto(){
        // From http://salesforce.stackexchange.com/questions/860/mimic-mysql-aes-encrpt-in-apex#comment1010_860
        // and generated in MySQL
        Blob cipherText = null;
        Blob privateKey = Blob.valueOf('a16characterkey!');
        List<Blob> plainTexts = new Blob[]{
            Blob.valueOf('Here is about fifty random bytes of data to be encoded'),
            Blob.valueOf('short'),
            Blob.valueOf('short1'),
            Blob.valueOf('short12'),
            Blob.valueOf('short123'),
            Blob.valueOf('012345678901234560'),
            Blob.valueOf('0123456789012345601'),
            Blob.valueOf('01234567890123456012'),
            Blob.valueOf('012345678901234560123'),
            Blob.valueOf('0123456789012345601234567890123456'),
            Blob.valueOf('01234567890123456012345678901234560'),
            Blob.valueOf('012345678901234560123456789012345601'),
            Blob.valueOf('0123456789012345601234567890123456012'),
            Blob.valueOf('01234567890123456012345678901234560123')
        };
        List<String> expectedHexes = new String[]{
            'C36E987E6B24F4A088ACD4A076165E90FD52616A3004D30F11700AC9FE70D3512F8A0962A2F5CD3BE0432114E8F3C8E2EE31674BE37015456040EB80E76EECF7',
            'DDF01D81CB2BFA522840E09DDC506C5E',
            '2551643CA0CE1508DF032DE82C1D432A',
            '92EEDAE9DC0618ED9E09065BF49BB536',
            'BA0F44AEF91549BC3D706C32265C7CA4',
            '9676A69026D36E6EAFFCDE1538C32BAE270AE9ACDB041268494601D90B63B3B8',
            '9676A69026D36E6EAFFCDE1538C32BAEC7A62B760AD0F2E6992C241DE1A0F7BD',
            '9676A69026D36E6EAFFCDE1538C32BAE77822C557C04BBF020D5A6E7EF6D8E1F',
            '9676A69026D36E6EAFFCDE1538C32BAE47FCCCB0BD9F367B460DB9808A6573B0',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B6D9A9A1AC5952484BB3BDFBDC710DDA47',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B63886EB402D276E7BF58853FE08A134DF',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B69AC2D7128E1E2A82032708BD83558579',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B60243F9CB463F23D517BF53E2F92A9835',
            '9676A69026D36E6EAFFCDE1538C32BAEC381D4ACBA23ED3416F629703E3758B68925B57FD46C9806B095BB70D3500A5A'
        };

        for (Integer i = 0; i < plainTexts.size(); i++) {
            cipherText = MySQLCrypto.encrypt('AES128', privateKey, plainTexts[i]);
            System.assertEquals(expectedHexes[i], EncodingUtil.convertToHex(cipherText).toUpperCase(), 'Test 1-'+i);
        }        

        // From http://www.inconteam.com/software-development/41-encryption/55-aes-test-vectors#aes-ecb-128
        privateKey = EncodingUtil.base64Decode('K34VFiiu0qar9xWICc9PPA==');
        plainTexts = new Blob[]{
            EncodingUtil.base64Decode('a8G+4i5An5bpPX4Rc5MXKg=='),
            EncodingUtil.base64Decode('ri2KVx4DrJyet2+sRa+OUQ=='),
            EncodingUtil.base64Decode('MMgcRqNc5BHl+8EZGgpS7w=='),
            EncodingUtil.base64Decode('9p8kRd9PmxetK0F75mw3EA==')
        };
        expectedHexes = new String[]{
            '3ad77bb40d7a3660a89ecaf32466ef97',
            'f5d3d58503b9699de785895a96fdbaaf',
            '43b1cd7f598ece23881b00e3ed030688',
            '7b0c785e27e8ad3f8223207104725dd4'
        };

        for (Integer i = 0; i < plainTexts.size(); i++) {
            cipherText = MySQLCrypto.encrypt('AES128', privateKey, plainTexts[i]);
            System.assertEquals(expectedHexes[i], EncodingUtil.convertToHex(cipherText), 'Test 2-'+i);
        }        
    }
}

Attribution
Source : Link , Question Author : sfelf , Answer Author : metadaddy

Leave a Comment