14

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?

  • It might be my unfamiliarity with the mysql function but I don't really understand your question. You want to get the same encrypted output from apex and MySQL if you pass in the same key and text and/or be able to encrypt something in mySQL and decrypt in salesforce and vice-versa? – Greg Grinberg Aug 24 '12 at 17:17
  • I don't have a MySQL handy to test against, but if you can provide the hex encoding of the encryption of about 50 bytes of data with a key, I have some ideas on how I might be able to use the Apex Crypto class to come up with the same result. – metadaddy Aug 24 '12 at 20:28
  • @grigriforce The problem is the MySQL function doesn't provide a way to pass in an initialization vector however the APEX function does require an initialization vector. My first thought was to try using the APEX function encodeWithManagedIV but that oroduced different results. It seems the MySQL function doesn't generate an IV and prefix the encode text with it as the encodeWithManagedIV apex function does. From what I have been able to gather the MySQL function just doesn't use an IV. – sfelf Aug 25 '12 at 16:19
  • @metadaddy Thank you so much for your offer of help. Here is the input data and key I used to encode with MySQl: SELECT HEX( AES_ENCRYPT( 'Here is about fifty random bytes of data to be encoded', 'a16characterkey!' ) ) AS encoded_text;. Then here is the encoded output: C36E987E6B24F4A088ACD4A076165E90FD52616A3004D30F11700AC9FE70D3512F8A0962A2F5CD3BE0432114E8F3C8E2EE31674BE37015456040EB80E76EECF7 – sfelf Aug 25 '12 at 16:32
  • I'm not familiar with the Apex call, but can you use an empty string as the IV? – Mike Chale Aug 25 '12 at 17:19
25

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);
        }        
    }
}
  • Needless to say, no warranty, express or implied on the above code. I would be amazed if there isn't some combination of block count and final block size that trips it up! – metadaddy Aug 27 '12 at 23:40
  • Thank you for posting this code! The data that I need to encrypt is relatively small ( <30 characters ) so it seems this solution should meet that need. If I find any cases that trip this code up I will definitely post them as well as look for solutions that will resolve the issue. Thanks again! – sfelf Aug 28 '12 at 20:33
  • Excellent. If you could click the 'mark this as the answer' check mark button thingy, that would be awesome :-) – metadaddy Aug 28 '12 at 21:04
  • I have a follow up question that I think I already know the answer for but just want to verify. It seems the complexity in your solution lies in extracting 128 bits out of the Base64 encoding of the Blob. This leads me to wonder why not use Hex encoding of the Blob as that would make it much easier to extract 128 bit blocks? I suspect the reason you didn't is the difficulty of converting Hex encoded strings back to a Blob. Is this the case or was there another reason? – sfelf Aug 29 '12 at 17:48
  • You are correct - it's easy to get the hex encoding from the blob, but difficult to turn that hex back into a blob - there is no convertFromHex(). I've filed a 'user story' to make Blobs easier to work with, so, hopefully, this sort of thing will be easier in future. – metadaddy Aug 29 '12 at 17:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.