phpcryptographyaeslockbox-3cbc-mode

PHP implementing Ciphertext Stealing (CTS) with CBC


I have been trying to implement Ciphertext Stealing(CTS) in PHP for CBC.

Referring below two links

How can I encrypt/decrypt data using AES CBC+CTS (ciphertext stealing) mode in PHP?

and

http://en.wikipedia.org/wiki/Ciphertext_stealing

I am confused and stuck on the last and simplest step of XOR. I know this is silly but having tried all the combinations, i don't know what am i missing. Code follows.

// 1. Decrypt the second to last ciphertext block, using zeros as IV.       
$second_to_last_cipher_block = substr($cipher_text, strlen($cipher_text) - 32, 16);     
$second_to_last_plain = @mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $second_to_last_cipher_block, MCRYPT_MODE_CBC);

// 2. Pad the ciphertext to the nearest multiple of the block size using the last B-M 
//    bits of block cipher decryption of the second-to-last ciphertext block.
$n = 16 - (strlen($cipher_text) % 16);
$cipher_text .= substr($second_to_last_plain, -$n);

// 3. Swap the last two ciphertext blocks.
$cipher_block_last = substr($cipher_text, -16);
$cipher_block_second_last = substr($cipher_text, -32, 16);
$cipher_text = substr($cipher_text, 0, -32) . $cipher_block_last . $cipher_block_second_last;

// 4. Decrypt the ciphertext using the standard CBC mode up to the last block.
$cipher = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($cipher, $key, $iv);
$plain_text = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $cipher_text, MCRYPT_MODE_CBC , $iv);

// 5. Exclusive-OR the last ciphertext (was already decrypted in step 1) with the second last ciphertext.
//    ???
//    echo $??? ^ $???;

Solution

  • I find that concrete use cases are very helpful in understanding algorithms. Here are 2 use cases, and a step-by-step walk-through.

    Starting point for both Use Cases.

    These Use Cases assume that you are decrypting messages uses AES-256 with CBC chaining mode and ciphertext stealing for block quantisation. To generate these Use Cases, I used Delphi 2010 compiler and the TurboPower LockBox3 library (SVN revision 243). In what follows, I use a notation like so...

    IV := [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    

    ... to mean that some variable named 'IV' is assigned to be equal to an array of 16 bytes. The left most byte, is the rendering of the Least Signficant (lowest address) byte of the array, and the right-most byte, the most significant. These bytes are written in hexadecimal, so for example if one puts...

    X := [2] 03 10
    

    ... it means that the LSB is 3 and the MSB is 16.

    Use Case One

    1. Let the AES-256 32 byte compressed key (as defined in the AES standard) be...

      key = [32] 0D EE 8F 9F 8B 0B D4 A1 17 59 FA 05 FA 2B 65 4F 23 00 29 26 0D EE 8F 9F 8B 0B D4 A1 17 59 FA 05
      

      With TurboPower LockBox 3, this can be achieved by setting the password ('UTF8Password') property of the TCodec component to...

      password = (UTF-8) 'Your lips are smoother than vasoline.'
      
    2. The plaintext message to be sent will be

      Message = (UTF-8) 'Leeeeeeeeeroy Jenkins!'
      

      Encoded this is 22 bytes long. AES-256 has a 16 byte block size, so this is some-where between 1 and 2 blocks long.

    3. Let the IV be 1. (Aside: On the Delphi side, this can be achieved by setting

      TRandomStream.Instance.Seed := 1;
      

      just before encryption). Thus the ciphertext message to be decrypted by PHP will be (with 8 byte IV prepended a la LockBox3) ...

      ciphertext = [30] 01 00 00 00 00 00 00 00 17 5C C0 97 FF EF 63 5A 88 83 6C 00 62 BF 87 E5 1D 66 DB 97 2E 2C
      (base64 equivalent ='AQAAAAAAAAAXXMCX/+9jWoiDbABiv4flHWbbly4s')
      

      Breaking this down into IV, first ciphertext block (c[0]) and last (partial) ciphertext block (c[ 1])...

      IV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
      c[0] = [16] 17 5C C0 97 FF EF 63 5A 88 83 6C 00 62 BF 87 E5
      c[1] = [6] 1D 66 DB 97 2E 2C
      
    4. Now let's walk-through the decryption with ciphertext stealing.

      • CV := IV

        CV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
        
      • In general, the for n'th block (except for the last 2 blocks), our normal CBC algorithm is...

        m[n]    := Decrypt( c[n]) XOR CV;
        CV[n+1] := c[n]
        

        where:

        • m is the output plaintext block;
        • Decrypt() means AES-256 ECB decryption on that block;
        • CV is our Carry-Vector. The chaining mode defines how this changes from block to block.
      • but for the second last block (N-1) (N=2 in Use Case One), the transformation changes to ... (This exception is made due to the selection of ciphertext stealing)

        m[n]    := Decrypt( c[n]) XOR CV;
        CV[n+1] := CV[n] // Unchanged!
        
      • Applying to our use case:

        CV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
        c[0] = [16] 17 5C C0 97 FF EF 63 5A 88 83 6C 00 62 BF 87 E5
        Decrypt(c[0]) = [16] 6F 6B 69 6E 73 21 F0 7B 79 F2 AF 27 B1 52 D6 0B
        m[0] := Decrypt(c[0]) XOR CV = [16] 6E 6B 69 6E 73 21 F0 7B 79 F2 AF 27 B1 52 D6 0B
        
    5. Now to process the last block. It is a partial one, 6 bytes long. In general, the processing of the last block goes like this...

      y := c[N-1] | LastBytes( m[N-2], BlockSize-Length(c[N-1]));
      m[N-1] := Decrypt( y) XOR CV 
      

      Applying to Use Case One:

      c[1] = [6] 1D 66 DB 97 2E 2C
      y := c[1] | LastBytes( m[0], 10)
      y = [16] 1D 66 DB 97 2E 2C F0 7B 79 F2 AF 27 B1 52 D6 0B
      Decrypt( y) = [16]= 4D 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65
      m[1] := Decrypt(y) XOR CV
      m[1] = [16] 4C 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65
      
    6. The last step in the decryption process is the emission of the last two blocks. We reverse the order, emitting m[N-1] first, and then emit the first part of m[N-2] (the length of which is equal to the length of c[N-1]). Applying to Use Case One...

      • Emit m[ 1]

        m[1] = [16] 4C 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65
        
      • Emit the first 6 bytes of m[0]

        FirstBytes( m[0], 6) = 6E 6B 69 6E 73 21
        
      • Putting it altogether, we get a reconstructed plaintext of ...

        [22] 4C 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65 6E 6B 69 6E 73 21
        

      which is the UTF-8 encoding of 'Leeeeeeeeeroy Jenkins!'

    Use Case Two

    In this use case, the message is precisely 2 blocks long. This is called the round case. In round cases, there is no partial block to quantise, so it proceeds as if it were normal CBC. The password, key and IV are the same as in Use Case One. The ciphertext message to be decrypted (included prepended 8 byte IV) is...

    1. Set-up

      Ciphertext = [40] 01 00 00 00 00 00 00 00 70 76 12 58 4E 38 1C E1 92 CA 34 FB 9A 37 C5 0A 75 F2 0B 46 A1 DF 56 60 D4 5C 76 4B 52 19 DA 83
      which is encoded base64 as 'AQAAAAAAAABwdhJYTjgc4ZLKNPuaN8UKdfILRqHfVmDUXHZLUhnagw=='
      

      This breaks down into IV, first block and second block, like so...

      IV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
      c[0] = [16] 70 76 12 58 4E 38 1C E1 92 CA 34 FB 9A 37 C5 0A
      c[1] = [16] 75 F2 0B 46 A1 DF 56 60 D4 5C 76 4B 52 19 DA 83
      
    2. General and 2nd last block

      Decrypt(c[0]) = [16] 45 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72
      m[0] := Decrypt(c[0]) XOR CV = [16] 44 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72
      Next CV := c[0] = [16] 70 76 12 58 4E 38 1C E1 92 CA 34 FB 9A 37 C5 0A
      
    3. Last Block:

      Our last block is round in this use case.

      Decrypt(c[1]) = [16] 75 F2 0B 46 A1 DF 56 60 D4 5C 76 4B 52 19 DA 83
      m[1] := Decrypt(c[1]) XOR CV = [16] 65 65 76 65 72 20 79 6F 75 20 6D 61 79 20 62 65
      
    4. The last step in the decryption process is the emission of the last two blocks. In the round case, we don't reverse the order. We emit m[N-2] first, and then m[N-1]. Applying to Use Case Two...

      • Emit m[0]

        m[0] = [16] 44 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72
        
      • Emit the whole of m1

        m[1] = [16] 65 65 76 65 72 20 79 6F 75 20 6D 61 79 20 62 65
        
      • Putting it altogether, we get a reconstructed plaintext of ...

        [32] 44 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72 65 65 76 65 72 20 79 6F 75 20 6D 61 79 20 62 65
        

      which is the UTF-8 encoding of 'Dance then, whereever you may be'

    5. Edge Cases to consider. There are two edge cases, not illustrated by the two Use Cases provided here.

      • Short messages. A short message is a message, whose length in bytes is:

        • Not zero; and
        • Less than one block;
      • Zero length messages.

    In the case of short messages, technically one could still implement ciphertext stealing by using the the IV as the prior block of ciphertext. However, IMHO, this use of ciphertext stealing, in this way, is not justified by lack of research into the impact on cryptographic strength, not to mention the added implementation complexity. In TurboPower LockBox 3, when the message is a short message, and the chaining mode is not a key-streaming one, then the chaining mode is treated as CFB-8bit. CFB-8 bit is a key-streaming mode.

    In the case of zero-length messages, its really simple. Zero-length plaintext message maps one-to-one to zero-length ciphertext messages. No IV is needed, generated nor prepended. This mapping is independent of chaining mode and cipher (in the case of block mode ciphers).

    Notes on PHP Implementation

    Caveat

    I am not a PHP programmer. I don't know PHP. Any thing I say here should be taken with a grain of salt.

    Arrays of bytes

    It looks like you are using PHP strings to store arrays of bytes. This looks dangerous to me. What if one of the byte values was zero? Would that shorten the string? How would strlen() behave in that case? If PHP has a native data type which was an array of byte, then this probably would be safer. But I don't really know. I am just bringing this point to your attention, if you are not already aware of it. Possibly it is not really an issue.

    mcrypt_decrypt library

    I am not familiar with this library. Does it natively support ciphertext stealing? I assume not. So there are two possible strategies for you.

    1. Call the library's decrypt for all but the last two blocks with CBC mode. Process the last two blocks as I have described to you. But this requires access to the CV. Does the API expose this? If not, the this strategy is not a viable option for you.

    2. Call the library's decrypt for all but the last two blocks with ECB mode, and roll your CBC chaining. Fairly easy to implement, and be definition, you have access to the CV.

    How to do XOR in PHP

    Some-one else posted an answer to this question, but has currently withdrawn it. But he was right. It looks like to do an XOR in PHP on an array of bytes, iterate through the characters, one by one, and do a byte level XOR. The technique is shown here.