Skip to content
Go back

Known-plaintext Attack With Key+Nonce Reuse on ChaCha20

Published:  at  07:41 PM

Table of Contents

Open Table of Contents

How Does ChaCha20 Work?

ChaCha20 is a stream cipher designed by Daniel J. Bernstein in 2008, known for its high performance and security.

It operates by generating a pseudorandom stream of bits (the keystream) derived from the key and a nonce (supposedly used once). This keystream, as long as the plaintext, is then XORed with the plaintext to produce the ciphertext.

Pretty simple, right?

The challenge

In this example we have the following code :

from Crypto.Cipher import ChaCha20
from Crypto.Random import get_random_bytes
from secret import FLAG

def encrypt(msg, key, iv):
    cipher = ChaCha20.new(key=key, nonce=iv)
    ciphertext = cipher.encrypt(msg.encode())  # Encode string to bytes
    return ciphertext

msg = "Hey agent, heres a secret message for you :"
key = get_random_bytes(32)  # 256-bit key for ChaCha20
iv = get_random_bytes(12)   # 96-bit IV for ChaCha20

encrypted_message = encrypt(msg, key, iv)
encrypted_flag = encrypt(FLAG, key, iv)

encrypted_flag = encrypt(FLAG, key, iv)
print(iv.hex() + "\n" + encrypted_message.hex() + "\n" + encrypted_flag.hex())

And the following ciphertext :

4a441144b78987964e097222
7f3e729b73d0f526c441ca45b9391be6d823ff1da412e612d23940c462d65ec346940ce84511c969e5bb3f39
71174afc69cea03de608b56fb90e109eec6dae509213f8

From the code, we can identify three different parts of the ciphertext :

We observe that both the key and the nonce were generated once but used twice, which is a major security flaw!

Known-plaintext attack on key+nonce reuse

Since the keystream generation is deterministic, the same keystream was XORed with the plaintext to produce the ciphertext we have.

Given a ciphertext with the corresponding plaintext, we can extract the keystream.

Because XOR is an associative operation, Cipher XOR Plain = KeyStream (where the original operation is KeyStream XOR Plain = Cipher).

This gives us the keystream: 375b0bbb12b79048b06dea2ddc4b7ec1ab039e3dd77785 (shortened for brevity, matching the length of the encrypted flag).

XORing this keystream with the encrypted flag uncovers the flag : FLAG{y0uVe_BeEn_Gn0mEd}

Suggested Implementation to Break a ChaCha20 Cipher

iv = "4a441144b78987964e097222"
plaintext = "Hey agent, here's a secret message for you :"
encrypted_plaintext = "7f3e729b73d0f526c441ca45b9391be6d823ff1da412e612d23940c462d65ec346940ce84511c969e5bb3f39"
flag = "71174afc69cea03de608b56fb90e109eec6dae509213f8"

def xor_bytes(bytes1, bytes2):
   
    # Perform XOR byte by byte
    result = bytes([a ^ b for a, b in zip(bytes1, bytes2)])
    
    return result

plaintext_bytes = plaintext.encode('utf-8')
cipher_bytes = bytes.fromhex(encrypted_plaintext)
flag_bytes = bytes.fromhex(flag)

# Ensure the length matches that of 'flag'
short_keystream = xor_bytes(cipher_bytes[:len(flag_bytes)], plaintext_bytes[:len(flag_bytes)])

finalxor = xor_bytes(flag_bytes, short_keystream)
print(finalxor.decode('utf-8'))

Thank you for reading through to the end! Have a wonderful day :)


Share this post on:

Previous Post
Quick & Easy Bitwarden Setup on Your Own Infrastructure
Next Post
Here's my actual first post