Search code examples
python-3.xcryptographyaesaes-gcm

Different generated Ciphertext from AES-CTR & AES-GCM


To verify my understanding on AES-GCM & AES-CTR modes, I'm creating a simple example using python Crpyto.Cipher library. I'm expecting the same ciphertext is generated from both modes where both are using CTR method.

Since my intention is just to compare the encrypted result from the encryption engines, hence, I'm setting the message to all 0 (Hex format) for both GCM & CTR. Anything XOR with 0 will remain as the original Ciphertext.

On AES-CTR side, I'm setting the nonce to "00". This mean no nonce will be used, and by default the counter number will start from value 0.

On AES-GCM side, I'm setting the nonce (IV) to 16 bytes "00". I'm assuming this is equivalent to 0 starting value for counter.

Looking at the AES-GCM block diagram below, the first ciphertext I should get from AES-GCM should be simply the encryption result from the counter value 1.

However, I couldn't to get the same encryption result from AES-CTR & AES-GCM. Please enlighten me which part I'm making mistake? Lastly, I'm using the same 256-AES-key for the both encryption modes.

enter image description here

Here is the code:

key  = bytes.fromhex('0123456789ABCDEF11113333555577770123456789ABCDEF1111333355557777')
msg  = bytes.fromhex('00000000000000000000000000000000')
msg1 = bytes.fromhex('00000000000000000000000000000001')

###### AES-256 ECB Mode ######
aes1 = AES.new(key,AES.MODE_ECB)
print("AES-ECB Result, Counter 1: "+str(binascii.hexlify(aes1.encrypt(msg1)))+"\n")

###### AES-256 CTR Mode ######
aes1 = AES.new(key,AES.MODE_CTR,nonce=bytes.fromhex('00'))
print("AES-CTR Result, Counter 0: "+str(binascii.hexlify(aes1.encrypt(msg))))
print("AES-CTR Result, Counter 1: "+str(binascii.hexlify(aes1.encrypt(msg)))+"\n")

###### AES-256 GCM Mode ######
aes1 = AES.new(key, AES.MODE_GCM, nonce=bytes.fromhex('00000000000000000000000000000000'))
ciphertext, authTag = aes1.encrypt_and_digest(msg)
print("AES-GCM Result, Counter 0: "+str(binascii.hexlify(ciphertext)))
print("AES-GCM Initialization Vector: "+str(binascii.hexlify(aes1.nonce)))

Python Result:

AES-ECB Result, Counter 1: b'24c82c75b5546a77d20c9868503767b4'

AES-CTR Result, Counter 0: b'4a85984511e5ca3f03297d84c69584c4'
AES-CTR Result, Counter 1: b'24c82c75b5546a77d20c9868503767b4'

AES-GCM Result: b'dfff0d463d8254d7eb23887729b22a85'
AES-GCM Initialization Vector: b'00000000000000000000000000000000'

Solution

  • The problem is that Wikipedia is a bit misleading about how GCM works. "Counter 0" isn't a bunch of zeros like it is in CTR. It's the "pre-counter" block (J0). As described in NIST 800-38d:

    In Step 2, the pre-counter block (J0) is generated from the IV. In particular, when the length of the IV is 96 bits, then the padding string 0³¹||1 is appended to the IV to form the pre-counter block. Otherwise, the IV is padded with the minimum number of ‘0’ bits, possibly none, so that the length of the resulting string is a multiple of 128 bits (the block size); this string in turn is appended with 64 additional ‘0’ bits, followed by the 64-bit representation of the length of the IV, and the GHASH function is applied to the resulting string to form the pre- counter block.

    In your example, you pass a 128-bit block, so:

    • Append no padding (it's already 128 bits)
    • Append 8 bytes of 0
    • Append the length of the IV (16): 0x0000000000000010

    Then run all of that through the GHASH function. That gives you J0, called "Counter 0" on the graphic.

    To get Counter 1:

    In Step 3, the 32-bit incrementing function is applied to the pre-counter block to produce the initial counter block for an invocation of the GCTR function on the plaintext.

    The 32-bit incrementing function basically means "increment the right-most 32 bits."

    That's what gets fed into AES. This is going to be something very random, and certainly not "1" like in your CTR case. Doing it this way allows for nonces of arbitrary length.

    But there's another special case: a 96-bit nonce for GCM, which is the recommended configuration. In that case, J0 is much simpler. It's just the nonce with a 32-bit 01 appended. That's passed through the increment function, which means the "first" block will be 02.

    If you use a 96-bit nonce for GCM, and compare it to the third block of CTR, they'll match.

    aes1 = AES.new(key,AES.MODE_CTR,nonce=bytes.fromhex('000000000000000000000000000000'))
    print("AES-CTR Result, Counter 0: "+str(binascii.hexlify(aes1.encrypt(msg))))
    print("AES-CTR Result, Counter 1: "+str(binascii.hexlify(aes1.encrypt(msg))))
    print("AES-CTR Result, Counter 2: "+str(binascii.hexlify(aes1.encrypt(msg)))+"\n")
    
    aes1 = AES.new(key, AES.MODE_GCM, nonce=bytes.fromhex('000000000000000000000000'))
    print("AES-GCM Result, Counter 0: "+str(binascii.hexlify(aes1.encrypt(msg))))
    
    ==>
    
    AES-CTR Result, Counter 0: b'4a85984511e5ca3f03297d84c69584c4'
    AES-CTR Result, Counter 1: b'24c82c75b5546a77d20c9868503767b4'
    AES-CTR Result, Counter 2: b'c8f656193e3bb5b6117d49e3c6799864'  <===
    
    AES-GCM Result, Counter 0: b'c8f656193e3bb5b6117d49e3c6799864'  <===