Skillversitas

Stream Ciphers and ChaCha20

A stream cipher generates a keystream from a key and a nonce, then XORs it with the plaintext byte for byte - fast, parallel, and friendly to environments without AES hardware (phones, embedded devices, some servers). The modern standard pairing is ChaCha20 for encryption plus Poly1305 for authentication: the same authenticated-encryption story as AES-GCM, with a purely 32-bit arithmetic core that runs well even where AES is in software.

ChaCha20-Poly1305

import os
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305

key = ChaCha20Poly1305.generate_key(bit_length=256)   # 32 bytes
cipher = ChaCha20Poly1305(key)

nonce = os.urandom(12)                                 # 12 bytes, unique per key
ct = cipher.encrypt(nonce, b"wireless payload", b"msg-id:77")
print(len(ct))      # 16 (payload) + 16 (tag)

pt = cipher.decrypt(nonce, ct, b"msg-id:77")
print(pt)           # b"wireless payload"

The call shape is identical to AES-GCM - key, 12-byte nonce, optional AAD - because both are AEAD ciphers. Swapping AES for ChaCha20 is a one-line change in most codebases.

Where ChaCha20 Wins

  • Mobile and embedded: no AES-NI needed; ChaCha20 stays fast on generic 32-bit CPUs, which is why it is the default in many mobile TLS profiles.
  • Software-only servers: on hardware without AES instructions, ChaCha20-Poly1305 can be faster than AES-GCM in software.
  • TLS 1.3: the cipher suite TLS_CHACHA20_POLY1305_SHA256 is standard; browsers and servers negotiate it automatically.

Nonce Handling

Same rule as GCM: the nonce must be unique for a given key, ever. With a 12-byte random nonce the collision risk becomes noticeable only after tens of billions of messages per key, so random nonces are fine for message-level encryption; for counter-based streams (bulk data) derive nonces from a starting value plus a message counter and store that counter with the key. Reusing a ChaCha20 nonce with the same key leaks the XOR of the two plaintexts - a severe failure.

Raw ChaCha20 (without Poly1305)

The library also exposes plain ChaCha20 as a stream cipher. Use it only if you add your own MAC (encrypt-then-MAC) - an unauthenticated keystream is malleable, and an attacker who can flip ciphertext bits can flip predictable plaintext bits:

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

cipher = Cipher(algorithms.ChaCha20(key, os.urandom(16)), mode=None)
enc = cipher.encryptor()
ct = enc.update(b"plaintext") + enc.finalize()   # no tag - add an HMAC

Summary

Prefer ChaCha20-Poly1305 over raw stream ciphers: it is an AEAD with the same interface as AES-GCM and it shines where AES hardware is absent. Keep nonces unique per key, treat the tag as mandatory, and use plain ChaCha20 only behind your own MAC.