AES 256 Encryption -> Changing inititialization vector slightly changes decrypted message without knowing key
Asked Answered
G

1

5

I made the following observation with an AES encrypt/decrypt example which is very counter intuitive for me.

I tried to encrypt and the decrypt a simple payload with AES in CBC mode. My understanding is/was that the initialization vector does not have to be secret, according to this answer: https://security.stackexchange.com/a/17046. And in most examples that I have seen the initialization vector is a non random part of the encrypted payload.

But by changing the initialization vector I was able to change the message during encryption.

See for example this python example which I copied and adapted from https://mcmap.net/q/125539/-encrypt-and-decrypt-using-pycrypto-aes-256. I set a hardcoded iv for encrypt and I slightly adapted the iv for decrypt. With this change I could change the message from "hello world" to "hello!world".

import base64
import hashlib

from Crypto.Cipher import AES


class AESCipher(object):

    def __init__(self, key):
        self.bs = AES.block_size
        self.key = hashlib.sha256(key.encode()).digest()

    def encrypt(self, raw):
        raw = self._pad(raw)
        #iv = Random.new().read(AES.block_size)
        #                    | here is the difference to the iv from decrypt
        iv = b'\xe2\xe0l3H\xc42*N\xb0\x152\x98\x9cBh'
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        code = cipher.encrypt((raw.encode()))
        return base64.b64encode(iv + code)

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        #iv = enc[:AES.block_size]
        #                    | here is the difference to the iv from encrypt
        iv = b'\xe2\xe0l3H\xc52*N\xb0\x152\x98\x9cBh'
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s) - 1:])]


if __name__ == '__main__':
    text = "hello world"
    print(text) # -> "hello world"
    aes = AESCipher("F56hnXWaUWMh6ThQZ5l3mBg9zHFx6vQg")
    payload = aes.encrypt(text)
    print(aes.decrypt(payload)) # -> "hello!world"

The result of this simple example is completly counter-intuitive for me. It seems that someone in the middle can take the payload, change the iv slightly and by doing so change the decrypted message without even knowing the secret key!

In my understanding it should not be that easy to change the content of the encrypted message by just changing the initialization vector. Changing the initialization vector should result in a completly different result!

Is there something wrong with my thinking?

Could you help me clarify my misunderstanding?

Gratianna answered 26/11, 2019 at 22:15 Comment(0)
B
5

AES, and block ciphers in general, usually only provide "secrecy" - they make no guarantees about integrity.

Your observations are correct - changing the IV does change the resulting plaintext after decrypting. You'll also note that, in my cases, changing the bytes of the ciphertext itself can still allow a successful decryption (albeit, a different plaintext) under AES-CBC.

What you want is a way to verify that the IV and ciphertext have not been modified since the initial encryption operation took place.

The two most common ways to achieve this are:

  • A MAC (HMAC is common)
  • An Authenticated mode of encryption, like GCM, which is preferred.

You might find this example of AES-GCM encryption in Python useful. I've included it below:

from Crypto.Hash import SHA256, HMAC
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Protocol.KDF import PBKDF2
import base64

ALGORITHM_NONCE_SIZE = 12
ALGORITHM_TAG_SIZE = 16
ALGORITHM_KEY_SIZE = 16
PBKDF2_SALT_SIZE = 16
PBKDF2_ITERATIONS = 32767
PBKDF2_LAMBDA = lambda x, y: HMAC.new(x, y, SHA256).digest()

def encryptString(plaintext, password):
    # Generate a 128-bit salt using a CSPRNG.
    salt = get_random_bytes(PBKDF2_SALT_SIZE)

    # Derive a key using PBKDF2.
    key = PBKDF2(password, salt, ALGORITHM_KEY_SIZE, PBKDF2_ITERATIONS, PBKDF2_LAMBDA)

    # Encrypt and prepend salt.
    ciphertextAndNonce = encrypt(plaintext.encode('utf-8'), key)
    ciphertextAndNonceAndSalt = salt + ciphertextAndNonce

    # Return as base64 string.
    return base64.b64encode(ciphertextAndNonceAndSalt)

def decryptString(base64CiphertextAndNonceAndSalt, password):
    # Decode the base64.
    ciphertextAndNonceAndSalt = base64.b64decode(base64CiphertextAndNonceAndSalt)

    # Get the salt and ciphertextAndNonce.
    salt = ciphertextAndNonceAndSalt[:PBKDF2_SALT_SIZE]
    ciphertextAndNonce = ciphertextAndNonceAndSalt[PBKDF2_SALT_SIZE:]

    # Derive the key using PBKDF2.
    key = PBKDF2(password, salt, ALGORITHM_KEY_SIZE, PBKDF2_ITERATIONS, PBKDF2_LAMBDA)

    # Decrypt and return result.
    plaintext = decrypt(ciphertextAndNonce, key)

    return plaintext.decode('utf-8')

def encrypt(plaintext, key):
    # Generate a 96-bit nonce using a CSPRNG.
    nonce = get_random_bytes(ALGORITHM_NONCE_SIZE)

    # Create the cipher.
    cipher = AES.new(key, AES.MODE_GCM, nonce)

    # Encrypt and prepend nonce.
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    ciphertextAndNonce = nonce + ciphertext + tag

    return ciphertextAndNonce

def decrypt(ciphertextAndNonce, key):
    # Get the nonce, ciphertext and tag.
    nonce = ciphertextAndNonce[:ALGORITHM_NONCE_SIZE]
    ciphertext = ciphertextAndNonce[ALGORITHM_NONCE_SIZE:len(ciphertextAndNonce) - ALGORITHM_TAG_SIZE]
    tag = ciphertextAndNonce[len(ciphertextAndNonce) - ALGORITHM_TAG_SIZE:]

    # Create the cipher.
    cipher = AES.new(key, AES.MODE_GCM, nonce)

    # Decrypt and return result.
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)

    return plaintext
Bay answered 26/11, 2019 at 22:19 Comment(2)
🙏 the mention of the keyword “integrity” helped me find this answer security.stackexchange.com/a/9448 which clarifies things for me.Gratianna
Here some more interesting details: security.stackexchange.com/q/63132/222412Gratianna

© 2022 - 2024 — McMap. All rights reserved.