Skip to main content

Overview

ChaCha20Poly1305 is an authenticated encryption algorithm combining ChaCha20 stream cipher with Poly1305 MAC, optimized for software implementations. Ethereum context: Not on Ethereum - High-performance alternative to AES-GCM for encrypted communications and storage. Key advantages over AES-GCM:
  • Fast in software (no hardware requirements)
  • Constant-time operations (side-channel resistant)
  • Simpler implementation (easier to audit)
  • Better mobile/embedded performance
Use ChaCha20-Poly1305 when:
  • No AES hardware acceleration available
  • Constant-time execution is critical
  • Running on mobile/embedded devices
  • Simplicity and auditability matter

Status Note

ChaCha20-Poly1305 is not yet implemented in Tevm. This documentation describes the specification and planned implementation based on RFC 8439. For production use, consider:
  • AES-GCM (currently implemented in Tevm)
  • @noble/ciphers - Pure TypeScript implementation
  • libsodium.js - WebAssembly wrapper for libsodium

Specification

Standard: RFC 8439 (June 2018) Parameters:
  • Key size: 256 bits (32 bytes) only
  • Nonce size: 96 bits (12 bytes)
  • Tag size: 128 bits (16 bytes)
  • Block size: 64 bytes (ChaCha20)
Algorithm:
  1. Encrypt plaintext with ChaCha20 stream cipher
  2. Compute Poly1305 MAC over ciphertext and AAD
  3. Output ciphertext + 16-byte authentication tag

How It Works

ChaCha20 Stream Cipher

ChaCha20 generates a pseudorandom keystream from:
  • 256-bit key
  • 96-bit nonce
  • 32-bit counter (starts at 1)
Key advantages:
  • Fast in software (bitwise operations)
  • Constant-time (no table lookups)
  • Designed by Daniel J. Bernstein
Keystream generation:
ChaCha20 Block:
  Input: key[32], nonce[12], counter[4]
  Output: 64-byte keystream block

  1. Initialize 4x4 matrix with constants, key, counter, nonce
  2. Apply 20 rounds of quarter-round function
  3. Add initial state to final state
  4. Output 64-byte block

Poly1305 MAC

Poly1305 is a one-time authenticator:
  • 256-bit one-time key (derived from ChaCha20)
  • Processes message in 16-byte chunks
  • Computes MAC using modular arithmetic (mod 2¹³⁰ - 5)
Tag computation:
Poly1305 MAC:
  Input: message, one-time-key[32]
  Output: 16-byte tag

  1. Derive r, s from one-time-key
  2. Accumulate message blocks: acc = (acc + block) * r mod (2^130 - 5)
  3. Add s to accumulator
  4. Output 16-byte tag

Combined AEAD Construction

ChaCha20-Poly1305:
  Input: plaintext, key[32], nonce[12], aad
  Output: ciphertext || tag[16]

  1. Generate Poly1305 key from ChaCha20(key, nonce, counter=0)
  2. Encrypt plaintext with ChaCha20(key, nonce, counter=1...)
  3. Construct Poly1305 input:
     - AAD || padding
     - ciphertext || padding
     - len(AAD) || len(ciphertext) (8 bytes each, little-endian)
  4. Compute Poly1305 MAC with one-time key
  5. Output ciphertext || tag

Planned API

import * as ChaCha20Poly1305 from '@tevm/voltaire/crypto/chacha20poly1305';

// Generate key (256-bit only)
const key = ChaCha20Poly1305.generateKey();
console.log(key.length); // 32 bytes

// Generate nonce (96-bit)
const nonce = ChaCha20Poly1305.generateNonce();
console.log(nonce.length); // 12 bytes

// Encrypt
const plaintext = new TextEncoder().encode('Secret message');
const ciphertext = ChaCha20Poly1305.encrypt(plaintext, key, nonce);
console.log(ciphertext.length); // plaintext.length + 16 (tag)

// Decrypt
const decrypted = ChaCha20Poly1305.decrypt(ciphertext, key, nonce);
const message = new TextDecoder().decode(decrypted);
console.log(message); // "Secret message"

// With Additional Authenticated Data (AAD)
const aad = new TextEncoder().encode('metadata');
const ciphertextWithAAD = ChaCha20Poly1305.encrypt(plaintext, key, nonce, aad);
const decryptedWithAAD = ChaCha20Poly1305.decrypt(ciphertextWithAAD, key, nonce, aad);

Security Properties

Confidentiality

IND-CPA Security:
  • Ciphertext reveals no plaintext information
  • Requires unique nonces (never reuse!)
  • 256-bit key provides strong security
Resistance:
  • No known attacks better than brute-force (2²⁵⁶ operations)
  • Post-quantum: Reduced to ~2¹²⁸ (Grover’s algorithm)

Authentication

Unforgeability:
  • 128-bit authentication tag
  • Poly1305 is provably secure (one-time MAC)
  • Forgery probability: ~2⁻¹²⁸ per attempt

Side-Channel Resistance

Constant-Time Operations:
  • No secret-dependent branches
  • No table lookups (unlike AES without hardware)
  • Resistant to cache-timing attacks
Why this matters:
  • AES (software): Vulnerable to cache-timing attacks
  • ChaCha20: All operations constant-time by design

Comparison with AES-GCM

ChaCha20-Poly1305AES-GCM
StandardRFC 8439 (IETF)NIST SP 800-38D
Key Size256-bit only128, 192, 256-bit
Nonce Size96-bit (12 bytes)96-bit recommended
Tag Size128-bit (16 bytes)128-bit (can truncate)
Speed (HW)SlowerFaster (AES-NI)
Speed (SW)FasterSlower
MobileExcellentGood (if AES support)
Side-ChannelsResistantVulnerable (without AES-NI)
SimplicitySimplerMore complex
AdoptionTLS 1.3, WireGuardTLS, IPsec, widespread
When to use ChaCha20-Poly1305:
  • Mobile/embedded systems
  • Software-only environments
  • Constant-time requirements
  • Prefer simplicity/auditability
When to use AES-GCM:
  • Hardware acceleration available (AES-NI)
  • NIST compliance required
  • Legacy system compatibility
  • Slightly faster with hardware

Nonce Management

CRITICAL: Never reuse nonces! Same nonce reuse attack as AES-GCM:
  • Exposes XOR of plaintexts
  • Breaks authentication (Poly1305 key reuse)
  • Complete security failure
Safe nonce strategies: 1. Random nonces (default):
const nonce = ChaCha20Poly1305.generateNonce();
2. Counter-based:
class NonceCounter {
  constructor() {
    this.counter = 0n;
  }

  next() {
    const nonce = new Uint8Array(12);
    const view = new DataView(nonce.buffer);
    view.setBigUint64(0, this.counter, false); // Big-endian
    this.counter++;

    if (this.counter >= (1n << 96n)) {
      throw new Error('Nonce space exhausted');
    }

    return nonce;
  }
}
3. Hybrid (random + counter):
// 4 bytes random prefix + 8 bytes counter
const prefix = crypto.getRandomValues(Bytes4());
const counter = 0n;

function generateNonce() {
  const nonce = new Uint8Array(12);
  nonce.set(prefix, 0);

  const view = new DataView(nonce.buffer, 4);
  view.setBigUint64(0, counter, false);

  counter++;
  return nonce;
}

Security Considerations

Critical Requirements

  1. Unique nonces: Never reuse with same key
  2. Cryptographically secure random: Use crypto.getRandomValues()
  3. Key protection: Store keys securely (encrypted, HSM, KMS)
  4. Key rotation: Rotate before 2⁴⁸ messages (random nonces)

Nonce Collision Risk

Random nonces:
  • 96-bit nonce space: 2⁹⁶ possible values
  • Birthday paradox: ~50% collision after 2⁴⁸ messages
  • Safe for: <2³² messages per key (~4 billion)
Counter nonces:
  • No collisions (deterministic)
  • Safe for: Up to 2⁹⁶ messages (practically unlimited)

Common Vulnerabilities

1. Nonce reuse:
// DANGEROUS
const nonce = ChaCha20Poly1305.generateNonce();
const ct1 = encrypt(msg1, key, nonce);
const ct2 = encrypt(msg2, key, nonce); // BREAKS SECURITY!
2. Predictable nonces:
// WRONG - Timestamp alone
const nonce = new Uint8Array(12);
const view = new DataView(nonce.buffer);
view.setBigUint64(0, BigInt(Date.now()), false); // Predictable!
3. Non-cryptographic random:
// WRONG - Math.random()
const badNonce = new Uint8Array(12);
for (let i = 0; i < 12; i++) {
  badNonce[i] = Math.floor(Math.random() * 256); // NOT SECURE!
}

Use Cases

VPN/WireGuard

WireGuard uses ChaCha20-Poly1305 for:
  • Fast encryption on all platforms
  • Constant-time operations (security)
  • Simple implementation (fewer bugs)

TLS 1.3

ChaCha20-Poly1305 is mandatory cipher suite in TLS 1.3:
  • TLS_CHACHA20_POLY1305_SHA256
  • Used when AES hardware unavailable
  • Better mobile performance

Mobile Apps

Ideal for mobile encryption:
  • Fast on ARM processors
  • Low battery consumption
  • Constant-time (security)

Secure Messaging

Used by Signal, WhatsApp for:
  • End-to-end encryption
  • Fast message encryption
  • Strong authentication

Cryptocurrency Wallets

Encrypt private keys with user password:
  • Derive key from password (PBKDF2/Argon2)
  • Encrypt private key
  • Store encrypted wallet

Performance

Throughput (typical)

Desktop (Intel/AMD):
  • ChaCha20-Poly1305: ~1-2 GB/s (software)
  • AES-GCM (AES-NI): ~3-5 GB/s (hardware)
Mobile (ARM):
  • ChaCha20-Poly1305: ~500 MB/s - 1 GB/s
  • AES-GCM (NEON): ~300 MB/s - 800 MB/s
Embedded (no crypto HW):
  • ChaCha20-Poly1305: ~10-50 MB/s
  • AES-GCM: ~5-20 MB/s
Key insight: ChaCha20-Poly1305 faster in software, AES-GCM faster with hardware.

Implementation Status

Current: Not yet implemented in Tevm Planned:
  • Pure TypeScript implementation
  • WASM implementation (performance)
  • Zig implementation (native library)
Alternatives (available now):
// @noble/ciphers - Pure TypeScript
import { chacha20poly1305 } from '@noble/ciphers/chacha';

const key = Bytes32();
crypto.getRandomValues(key);

const nonce = new Uint8Array(12);
crypto.getRandomValues(nonce);

const plaintext = new TextEncoder().encode('Secret');
const ciphertext = chacha20poly1305(key, nonce).encrypt(plaintext);

// libsodium.js - WebAssembly
import sodium from 'libsodium-wrappers';

await sodium.ready;

const key = sodium.crypto_aead_chacha20poly1305_ietf_keygen();
const nonce = sodium.randombytes_buf(sodium.crypto_aead_chacha20poly1305_IETF_NPUBBYTES);

const ciphertext = sodium.crypto_aead_chacha20poly1305_ietf_encrypt(
  plaintext,
  null, // AAD
  null,
  nonce,
  key
);

RFC 8439 Test Vectors

Test Vector 1: Basic Encryption

// Key (hex): 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f
// Nonce (hex): 070000004041424344454647
// Plaintext: "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."

const key = new Uint8Array([
  0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
  0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
  0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
  0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f
]);

const nonce = new Uint8Array([
  0x07, 0x00, 0x00, 0x00,
  0x40, 0x41, 0x42, 0x43,
  0x44, 0x45, 0x46, 0x47
]);

const plaintext = new TextEncoder().encode(
  "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."
);

const ciphertext = ChaCha20Poly1305.encrypt(plaintext, key, nonce);

// Expected ciphertext + tag (hex):
// d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd0600691

Test Vector 2: With AAD

// AAD (hex): 50515253c0c1c2c3c4c5c6c7

const aad = new Uint8Array([
  0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7
]);

const ciphertext = ChaCha20Poly1305.encrypt(plaintext, key, nonce, aad);
// Verify against RFC 8439 expected output

Best Practices

DO

✓ Use unique nonces for each encryption ✓ Use crypto.getRandomValues() for nonces/keys ✓ Store nonce with ciphertext (not secret) ✓ Rotate keys periodically (<2⁴⁸ messages) ✓ Handle decryption errors gracefully ✓ Use strong passwords for key derivation ✓ Clear sensitive data from memory

DON’T

✗ Never reuse nonces with same key ✗ Never use predictable nonces (timestamp only) ✗ Never use Math.random() for crypto ✗ Never store keys in plaintext ✗ Never ignore decryption errors ✗ Never exceed 2⁴⁸ messages per key ✗ Never commit keys to version control

References