ChaCha20-Poly1305 Explained: When to Choose It Over AES
ChaCha20-Poly1305 is up to eight times faster than AES-GCM on devices without hardware acceleration. Learn when to choose it and why TLS 1.3 includes it.
Introduction
On a smartphone without hardware AES support, ChaCha20-Poly1305 encrypts data up to eight times faster than AES-GCM. That is why Google, WireGuard, and TLS 1.3 all include it as a first-class cipher — and why understanding when to choose ChaCha20-Poly1305 versus AES-GCM is a real engineering decision, not a theoretical one.
AES-GCM is the dominant symmetric AEAD cipher on modern servers and desktop CPUs. Intel and AMD have included AES-NI hardware acceleration since 2010; ARM’s Cortex-A series added it around 2012. On these platforms, AES-256-GCM achieves 3–5 GB/s in software — practical for any workload.
But billions of devices do not have AES acceleration: low-power IoT chips running MIPS or older ARM Cortex-M, embedded microcontrollers in industrial equipment, legacy Android devices, and any hardware that predates widespread AES-NI adoption. On these platforms, software-only AES-GCM achieves 100–300 MB/s — and worse, it is vulnerable to cache-timing side channels when implemented with lookup tables, because table access patterns leak information about the key.
ChaCha20-Poly1305 was designed to solve exactly this problem. Its core operations are addition, rotation, and XOR — integer operations available on every processor, requiring no lookup tables, and inherently constant-time. On hardware without AES acceleration, it outperforms AES-GCM by 3–8×. On hardware with AES acceleration, it performs comparably or slightly slower, but remains a valid and secure choice.
ChaCha20-Poly1305: What It Is and When to Use It
ChaCha20-Poly1305 is an AEAD (Authenticated Encryption with Associated Data) algorithm that combines two components:
- ChaCha20: A native stream cipher designed by Daniel J. Bernstein (2008), based on the Salsa20 cipher. It generates a keystream of pseudorandom bytes using only integer addition, 32-bit rotation, and XOR — the ARX (Add-Rotate-XOR) construction.
- Poly1305: A one-time message authentication code (MAC) that computes a 128-bit authentication tag over the ciphertext, ensuring any modification is detected during decryption.
The two components are specified together in RFC 8439 (2018) and are mandated as a single, inseparable unit — ChaCha20 without Poly1305, or Poly1305 with a different cipher, is not the same algorithm.
The combination produces AEAD output: encrypted data with a 16-byte authentication tag. Decryption verifies the tag first. If it fails, no plaintext is returned — the same guarantee as AES-256-GCM.
How ChaCha20-Poly1305 Works
The ChaCha20 Stream Cipher
ChaCha20 generates a keystream from a 512-bit state matrix, organized as a 4×4 grid of 32-bit words:
- Row 0: Four fixed constants (“expand 32-byte k”)
- Rows 1–2: Your 256-bit key (eight 32-bit words)
- Row 3a: A 32-bit block counter (starts at 1 for message encryption)
- Row 3b: A 96-bit nonce (three 32-bit words)
The quarter round is the algorithm’s only operation, applied 20 times:
- Add: Two words are added together.
- XOR: The sum is XORed with another word.
- Rotate: The XOR result is rotated left by a fixed number of bits.
This sequence — no lookup tables, no data-dependent branches — is what makes ChaCha20 naturally constant-time. There are no memory access patterns that vary based on key or plaintext values, which is how cache-timing attacks extract key material from AES table-based implementations.
After 20 rounds, the 512-bit result is added back to the original state, producing 64 bytes of keystream. The plaintext is XORed with keystream bytes — producing ciphertext.
The Poly1305 MAC
Poly1305 uses a one-time 256-bit key derived from ChaCha20’s output for block counter 0 (the first 64 bytes generated before encryption begins). It evaluates a polynomial over the message bytes modulo 2¹³⁰ − 5 (a prime), producing a 128-bit authentication tag.
The tag covers the ciphertext and any associated data (unencrypted metadata you want to authenticate alongside the ciphertext). During decryption, the receiver computes the same polynomial and compares. A mismatch — caused by any modification, however small — causes decryption to abort immediately.
The Critical Rule: Never Reuse a Nonce
ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce that must be unique for every encryption with the same key. Reusing a nonce with the same key means two plaintexts are XORed with the same keystream — XORing the ciphertexts together cancels the keystream and reveals the XOR of both plaintexts. An attacker who can observe two encrypted messages under the same key and nonce recovers significant plaintext information.
Generate a fresh random 96-bit nonce for every encryption. With random nonces, the probability of a collision after one billion encryptions is approximately 1 in 4 billion — acceptable for most applications. High-volume applications should implement nonce counters or rekey more frequently.
Trace the quarter round: Add → XOR → Rotate, repeated 20 times across a 4×4 state matrix. No lookup tables appear anywhere in this path — which is exactly why ChaCha20 is constant-time by construction.
ChaCha20-Poly1305 vs AES-256-GCM
| Feature | ChaCha20-Poly1305 | AES-256-GCM |
|---|---|---|
| Cipher type | Stream cipher (ARX design) | Block cipher in CTR mode |
| Key size | 256-bit | 128 or 256-bit |
| Nonce size | 96-bit | 96-bit |
| Authentication tag | 128-bit (Poly1305) | 128-bit (GHASH) |
| Constant-time by design | Yes | Only with AES-NI |
| Performance without AES-NI | Fast (1–2 GB/s in software) | Slow (100–300 MB/s; timing risk) |
| Performance with AES-NI | Slower (~1.7 GB/s) | Fast (3–5 GB/s) |
| TLS 1.3 support | Mandatory cipher suite | Mandatory cipher suite |
| WireGuard | Only cipher used | Not supported |
| Nonce reuse safety | Catastrophic — avoid | Catastrophic — avoid |
The table reveals the deployment decision framework: if you know your hardware has AES-NI, prefer AES-256-GCM for raw performance. If you cannot guarantee AES-NI availability — or if you prioritize constant-time security properties over peak throughput — ChaCha20-Poly1305 is the correct choice.
Real-World Use Cases
TLS 1.3 on mobile devices: TLS 1.3 includes both AES-256-GCM and ChaCha20-Poly1305 as mandatory cipher suites. When a TLS 1.3 connection is negotiated between a server with AES-NI and a client without it (an older Android device, for example), the client can signal its cipher preference. Google Chrome on Android uses ChaCha20-Poly1305 by default when it determines the device lacks AES acceleration — resulting in measurably better battery life on those devices.
WireGuard VPN: WireGuard is the modern VPN protocol included in the Linux kernel since 5.6. It uses ChaCha20-Poly1305 as its only symmetric cipher — no cipher negotiation, no configuration. The single-cipher approach eliminates an entire class of downgrade attacks and simplifies the codebase dramatically. WireGuard’s use of ChaCha20-Poly1305 is validated by the formal security analysis in the WireGuard whitepaper (2020).
Signal messaging: The Signal Protocol uses ChaCha20-Poly1305 in its Double Ratchet algorithm for per-message encryption. Signal targets the full range of Android hardware — including low-end devices without AES acceleration in markets like South Asia and sub-Saharan Africa. ChaCha20-Poly1305’s consistent performance across hardware generations makes it the natural choice for a privacy-critical application that must work well on budget hardware.
Common Mistakes to Avoid
Using ChaCha20 without Poly1305. ChaCha20 alone is a stream cipher — it provides confidentiality but no authentication. Unauthenticated encryption means an attacker can flip bits in the ciphertext and produce ciphertext that decrypts to modified plaintext with no error. Always use ChaCha20-Poly1305 together as a single AEAD algorithm. The RFC 8439 combined construction is the correct specification.
Implementing ChaCha20 from scratch. The algorithm’s simplicity is deceptive. Correct implementation requires careful handling of the counter, nonce, and key material. Use a vetted library: libsodium (secretbox API), the Go stdlib crypto/chacha20poly1305, or Rust’s chacha20poly1305 crate. Cryptographic implementation errors are subtle and often not detectable from functional tests.
Treating AES-GCM and ChaCha20-Poly1305 as interchangeable without testing. If you switch from AES-256-GCM to ChaCha20-Poly1305 in an existing system that stores encrypted data, old ciphertexts cannot be decrypted with the new algorithm. Cipher migration requires decrypting existing data with the old algorithm and re-encrypting with the new one, or maintaining algorithm metadata alongside ciphertexts. Plan this before changing algorithms in production.
Assuming all TLS libraries default to ChaCha20 on appropriate hardware. Browser behavior is well-documented (Chrome prefers ChaCha20 when client lacks AES-NI), but server-side TLS libraries vary. OpenSSL’s server preference ordering may put AES-GCM first regardless of the client’s hardware. Configure your TLS server to respect client cipher order preference (SSL_OP_CIPHER_SERVER_PREFERENCE off in OpenSSL) if your clients are primarily low-power hardware.
Getting Started
Determine whether ChaCha20-Poly1305 or AES-256-GCM is the right choice for your deployment context. If your application runs entirely on modern server hardware with guaranteed AES-NI (any Intel Ivy Bridge+ or AMD Zen+ processor), AES-256-GCM is typically faster. If your application reaches low-power ARM, IoT, or embedded hardware, ChaCha20-Poly1305 is faster and safer.
For TLS configuration, both cipher suites are included in the TLS 1.3 mandatory set — you do not need to explicitly enable ChaCha20-Poly1305. The negotiation is handled automatically based on client and server preferences. The important step is ensuring both cipher suites are allowed, not restricted, in your TLS configuration.
For application-level encryption (encrypting files, database fields, or messages outside of TLS), use libsodium’s crypto_secretbox_easy function, which uses XSalsa20-Poly1305 (a close relative), or use crypto_aead_chacha20poly1305_ietf_encrypt for the IETF-standard construction from RFC 8439. Both handle nonce management correctly when used with the library’s recommended API.
Verify your nonce generation. The nonce must be unique per encryption with the same key — use a cryptographically random nonce generator (OS getrandom, crypto.getRandomValues()) rather than a sequential counter starting at zero. If you encrypt at high volume (millions of messages per day with the same key), implement automatic key rotation to reduce nonce collision probability.
For a detailed comparison with AES-256-GCM and when each is the correct choice in practice, see AES-256-GCM: ChaCha20’s main alternative. To understand how TLS 1.3 selects between these two cipher suites during handshake negotiation, read TLS 1.3 vs TLS 1.2.
FAQ
Common questions — answered in plain English.
What is ChaCha20-Poly1305?
Is ChaCha20-Poly1305 more secure than AES-GCM?
When should I use ChaCha20 instead of AES?
What is Poly1305 used for?
Does TLS 1.3 use ChaCha20?
Why does WireGuard use ChaCha20-Poly1305?
References
- [1]
- [2]RFC 8446: The TLS Protocol Version 1.3IETF, 2018
- [3]
- [4]Bernstein, ChaCha a variant of Salsa20 (ECRYPT2 Workshop)cr.yp.to / IACR, 2008
- [5]WireGuard: Next Generation Kernel Network TunnelWireGuard Project, 2020