Garry Welding
Garry Welding

Reputation: 3609

AES encryption inconsistency in PHP using the Open SSL library and coding CBC mode from scratch

I suppose I should deal with my first statement first. Is there an inconsistency in the way PHP does AES-128-CBC encryption when you use the Open SSL library?

I am asking this because if you look through RFC3602 for AES then you'll find some test vectors in section 4. The first one I tried was:

Case #1: Encrypting 16 bytes (1 block) using AES-CBC with 128-bit key

Key : 0x06a9214036b8a15b512e03d534120006

IV : 0x3dafba429d9eb430b422da802c9fac41

Paintext : "Single block msg"

Cphertext: 0xe353779c1079aeb82708942dbe77181a

Using the open SSL library with the following code:

echo bin2hex(openssl_encrypt($str, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $iv));

You actually get back:

e353779c1079aeb82708942dbe77181ab97c825e1c785146542d396941bce55d

Now this is all well and good and decrypts back just fine, and the first 32 bytes are the expected cipher text "e353779c1079aeb82708942dbe77181a". However, there appears to be another 32 bytes at the end of the cipher text returned from the Open SSL library. Why is this? This question becomes relevant next.

As part of something I am doing for fun I am trying to implement AES CBC mode encryption sort of from scratch. The code I currently have for doing this is (based on the RFC previously referenced and on CBC diagrams at the "Block cipher mode of operation" Wikipedia page):

public function cbcEncrypt($str, $iv, $key) {
    $bl = 16;
    
    $bs = str_split($str,$bl);
    $pv = null;
    $r = '';
    
    foreach($bs as $b) {
        if($pv === null) $pv = $iv;
        if(strlen($b)<$bl) $b = $this->pkcs7Pad($b, $bl);
        $pv = openssl_encrypt($b^$pv, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);
        $r .= $pv;
    }
    
    return $r;
}

At the moment I'm pretty sure this code wont work on strings longer than the current test 16 byte string anyway, however, I ran this code side by side with the Open SSL implementation to compare. The return value from by CBC implementation is:

e353779c1079aeb82708942dbe77181a8b1ccc6f8cd525ffe22d6327d891a063

When called via:

$crypto = new CryptoTools();
$enc = $crypto->cbcEncrypt('Single block msg',hex2bin("3dafba429d9eb430b422da802c9fac41"),hex2bin("06a9214036b8a15b512e03d534120006"));

Again, the first 32 bytes are correct, but the second 32 bytes are again added but completely different from the Open SSL implementation of CBC. Any clues as to why?

Also, the Open SSL implementation of ECB mode returns a 32 byte string, which means CBC encrypting strings currently greater than 16 bytes in length the new value of $pv will be 32 bytes in length and then it will be XOR'd against the second plain text block when in reality shouldn't $pv always be 16 bytes in length? Is it usually just accepted that you truncate $pv from 32 to 16 bytes?

Just in case it helps, the pkcs7Pad method in the code above looks like:

public function pkcs7Pad($str, $b = 8) {
    if(trim($str) == '') return false;
    if((int)$b < 1)  return false;
    $p = $b - (strlen($str) % $b);
    return $str . str_repeat(chr($p),$p);
}

Any help would be greatly appreciated. Needless to say there's not much documentation on this as people aren't usually stupid enough to try and reinvent the wheel, certainly in PHP...

Upvotes: 1

Views: 2930

Answers (2)

Garry Welding
Garry Welding

Reputation: 3609

Ah ha! Got it! Thanks to ntoskrnl's answer and this Stackoverflow question I managed to come up with a solution! If you do:

echo bin2hex(base64_decode(openssl_encrypt($str, 'aes-128-cbc', $key, OPENSSL_ZERO_PADDING, $iv)));

You will match the test case provided by the RFC! You just need to remember that because you're now not doing OPENSSL_RAW_DATA you will get back a base64 encoded string and so will need to base64 decode it before converting it to hex to match the test case!

TLDR: it was a padding issue, just not an obvious one!

Upvotes: 1

ntoskrnl
ntoskrnl

Reputation: 5744

This has to do with padding. The test vectors don't use any padding at all, while the openssl_encrypt function and your function apply PKCS#7 padding.

PKCS#7 padding is specified so that padding is added in all cases. When the plaintext length is a multiple of the block size, a full block of padding is added, which is why the ciphertext is 32 bytes long when the plaintext is 16 bytes.

Can't immediately see anything wrong with your function (though I'm not familiar with PHP), but you could try padding $str before splitting it instead of checking whether each block requires padding.

Upvotes: 4

Related Questions