ChaCha20 and XChaCha20

ChaCha20 is a stream cipher designed by Daniel J. Bernstein. The secret key is 256 bits long (32 bytes). The cipher requires a nonce, which must not be reused across encryptions performed with the same key.

There are three variants, defined by the length of the nonce:

Nonce length

Description

Max data

If random nonce and same key

8 bytes (default)

The original ChaCha20 designed by Bernstein (64-bit block counter).

No limitations

Max 200 000 messages

12 bytes

The ChaCha20 used in TLS, specified in RFC8439 (32-bit block counter).

256 GB

Max 13 billion messages

24 bytes

XChaCha20, specified in the expired draft draft-irtf-cfrg-xchacha-03 (it never became an RFC).

256 GB

No limitations

XChaCha20 follows the draft: it derives a subkey with HChaCha20, and then it is the ChaCha20 of RFC8439, with a 32-bit block counter. Some implementations, like libsodium, use instead a 64-bit block counter: the key stream is the same for the first 256 GB, after which PyCryptodome raises Crypto.MessageTooLongError.

This is an example of how ChaCha20 (Bernstein’s version) can encrypt data:

>>> import json
>>> from base64 import b64encode
>>> from Crypto.Cipher import ChaCha20
>>> from Crypto.Random import get_random_bytes
>>>
>>> plaintext = b'Attack at dawn'
>>> key = get_random_bytes(32)
>>> cipher = ChaCha20.new(key=key)
>>> ciphertext = cipher.encrypt(plaintext)
>>>
>>> nonce = b64encode(cipher.nonce).decode('utf-8')
>>> ct = b64encode(ciphertext).decode('utf-8')
>>> result = json.dumps({'nonce':nonce, 'ciphertext':ct})
>>> print(result)
{"nonce": "IZScZh28fDo=", "ciphertext": "ZatgU1f30WDHriaN8ts="}

And this is how you decrypt it:

>>> import json
>>> from base64 import b64decode
>>> from Crypto.Cipher import ChaCha20
>>>
>>> # We assume that the key was somehow securely shared
>>> try:
>>>     b64 = json.loads(json_input)
>>>     nonce = b64decode(b64['nonce'])
>>>     ciphertext = b64decode(b64['ciphertext'])
>>>     cipher = ChaCha20.new(key=key, nonce=nonce)
>>>     plaintext = cipher.decrypt(ciphertext)
>>>     print("The message was " + plaintext)
>>> except (ValueError, KeyError):
>>>     print("Incorrect decryption")

In order to have a RFC8439-compliant ChaCha20 cipher, you need to explicitly generate and pass a 96 bit (12 byte) nonce parameter to new():

nonce_rfc8439 = get_random_bytes(12)
cipher = ChaCha20.new(key=key, nonce=nonce_rfc8439)

To encrypt or decrypt long data faster on a machine with several cores, pass the maximum number of threads to new() with the parameter threads (0 means as many as the cores available); each thread processes at least 1 MiB:

cipher = ChaCha20.new(key=key, nonce=nonce, threads=0)

Warning

ChaCha20 does not guarantee authenticity of the data you decrypt! In other words, an attacker may manipulate the data in transit. In order to prevent that, you must also use a Message Authentication Code (such as HMAC) to authenticate the ciphertext (encrypt-then-mac). Alternatively, you can use ChaCha20_Poly1305.

class Crypto.Cipher.ChaCha20.ChaCha20Cipher(key, nonce, threads=1)

ChaCha20 (or XChaCha20) cipher object. Do not create it directly. Use new() instead.

Variables:

nonce (bytes) – The nonce with length 8, 12 or 24 bytes

decrypt(ciphertext, output=None)

Decrypt a piece of data.

Parameters:

ciphertext (bytes/bytearray/memoryview) – The data to decrypt, of any size.

Keyword Arguments:

output (bytes/bytearray/memoryview) – The location where the plaintext is written to. If None, the plaintext is returned.

Return type:

Optional[bytes]

Returns:

If output is None, the plaintext is returned as bytes. Otherwise, None.

encrypt(plaintext, output=None)

Encrypt a piece of data.

Parameters:

plaintext (bytes/bytearray/memoryview) – The data to encrypt, of any size.

Keyword Arguments:

output (bytes/bytearray/memoryview) – The location where the ciphertext is written to. If None, the ciphertext is returned.

Return type:

Optional[bytes]

Returns:

If output is None, the ciphertext is returned as bytes. Otherwise, None.

seek(position)

Seek to a certain position in the key stream.

If you want to seek to a certain block, use seek(block_number * 64).

Parameters:

position (integer) – The absolute position within the key stream, in bytes.

Return type:

None

Crypto.Cipher.ChaCha20.new(*, key, nonce=None, threads=1)

Create a new ChaCha20 or XChaCha20 cipher

Keyword Arguments:
  • key (bytes/bytearray/memoryview) – The secret key to use. It must be 32 bytes long.

  • nonce (bytes/bytearray/memoryview) –

    A mandatory value that must never be reused for any other encryption done with this key.

    For ChaCha20, it must be 8 or 12 bytes long.

    For XChaCha20, it must be 24 bytes long.

    If not provided, 8 bytes will be randomly generated (you can find them back in the nonce attribute).

  • threads (integer) – The maximum number of threads used to encrypt or decrypt long data (default: 1, no extra threads; 0 for as many threads as the CPU cores available to the process). Each thread processes at least 1 MiB.

Return:

a Crypto.Cipher.ChaCha20.ChaCha20Cipher object

Return type:

ChaCha20Cipher