ChaCha20 Security-Focused API Documentation
Version: 1.0
Last Updated: 2025-07-05
**Security Classification: PUBLIC
Author: Phantom (phantom@metamui.id)
Overview
ChaCha20 is a high-speed stream cipher designed by Daniel J. Bernstein. It operates on 512-bit blocks using 256-bit keys and provides excellent performance across all platforms, especially those without AES hardware acceleration. ChaCha20 is the core of ChaCha20-Poly1305 AEAD and is widely deployed in TLS 1.3 and other security protocols.
Security Level: 256-bit key security
Key Size: 32 bytes (256 bits)
Nonce Size: 12 bytes (96 bits) or 8 bytes (64 bits)
Block Size: 64 bytes (512 bits)
Counter: 32-bit or 64-bit
Security Warnings ⚠️
- Nonce Reuse Fatal: NEVER reuse nonce with same key - breaks confidentiality
- Not Authenticated: ChaCha20 alone provides no authenticity - use ChaCha20-Poly1305
- Counter Limits: 32-bit counter limits to 256 GB per nonce
- Malleable: Without authentication, ciphertext can be modified
- Key Management: Keys must be securely generated and managed
API Functions
chacha20_encrypt(key: bytes[32], nonce: bytes[12], data: bytes, counter: int = 0) -> bytes
Security Contract:
- Preconditions:
keymust be 32 bytes of high entropynoncemust be unique per key usagecountershould be 0 for new streams- Total data ≤ 2^38 bytes per key-nonce pair
- Postconditions:
- Returns ciphertext same length as plaintext
- Provides confidentiality only
- Deterministic for same inputs
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Key Recovery | ✅ | 2^256 operations required | | Plaintext Recovery | ✅ | Without nonce reuse | | Distinguishing | ✅ | Indistinguishable from random | | Nonce Reuse | ❌ | Reveals XOR of plaintexts | | Bit Flipping | ❌ | No integrity protection | | Replay | ❌ | No authenticity |
Security Requirements:
- Generate cryptographically random keys
- Ensure nonce uniqueness per key
- Add authentication (Poly1305 or HMAC)
- Implement proper key rotation
- Use ChaCha20-Poly1305 when possible
Secure Usage Example:
import secrets
import struct
import time
from typing import Tuple, Optional
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import hmac
import hashlib
class SecureChaCha20:
"""Secure ChaCha20 implementation with authentication"""
def __init__(self, master_key: bytes):
if len(master_key) != 32:
raise ValueError("Master key must be 32 bytes")
self.master_key = master_key
self.nonce_counter = 0
self.key_usage = {}
def encrypt_with_auth(
self,
plaintext: bytes,
associated_data: bytes = b"",
nonce: Optional[bytes] = None
) -> dict:
"""Encrypt with authentication using ChaCha20+HMAC"""
# Generate or validate nonce
if nonce is None:
nonce = self._generate_nonce()
elif len(nonce) != 12:
raise ValueError("Nonce must be 12 bytes")
# Check usage limits
self._check_usage_limits(nonce)
# Derive stream cipher and MAC keys
enc_key, mac_key = self._derive_keys(nonce)
# Encrypt with ChaCha20
cipher = Cipher(
algorithms.ChaCha20(enc_key, nonce),
mode=None,
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# Compute HMAC
h = hmac.new(mac_key, digestmod=hashlib.sha256)
h.update(nonce)
h.update(associated_data)
h.update(ciphertext)
auth_tag = h.digest()
# Track usage
self._track_usage(nonce, len(plaintext))
# Clear derived keys
secure_zero(enc_key)
secure_zero(mac_key)
return {
'ciphertext': ciphertext,
'nonce': nonce,
'tag': auth_tag,
'algorithm': 'ChaCha20-HMAC-SHA256'
}
def decrypt_with_auth(
self,
encrypted_data: dict,
associated_data: bytes = b""
) -> bytes:
"""Decrypt and verify authentication"""
ciphertext = encrypted_data['ciphertext']
nonce = encrypted_data['nonce']
tag = encrypted_data['tag']
# Derive keys
enc_key, mac_key = self._derive_keys(nonce)
# Verify HMAC first
h = hmac.new(mac_key, digestmod=hashlib.sha256)
h.update(nonce)
h.update(associated_data)
h.update(ciphertext)
expected_tag = h.digest()
if not hmac.compare_digest(tag, expected_tag):
# Clear keys and reject
secure_zero(enc_key)
secure_zero(mac_key)
raise ValueError("Authentication failed")
# Decrypt
cipher = Cipher(
algorithms.ChaCha20(enc_key, nonce),
mode=None,
backend=default_backend()
)
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
# Clear derived keys
secure_zero(enc_key)
secure_zero(mac_key)
return plaintext
def _generate_nonce(self) -> bytes:
"""Generate unique nonce"""
# Combine counter with random data
counter_bytes = struct.pack('<I', self.nonce_counter)
random_bytes = secrets.token_bytes(8)
nonce = counter_bytes + random_bytes
self.nonce_counter += 1
# Check for wrap-around
if self.nonce_counter >= 2**32:
raise RuntimeError("Nonce counter exhausted - rotate master key")
return nonce
def _derive_keys(self, nonce: bytes) -> Tuple[bytes, bytes]:
"""Derive encryption and MAC keys"""
# Use HKDF-like expansion
context = b"ChaCha20-Keys-v1" + nonce
# Derive encryption key
enc_key = hmac.new(
self.master_key,
b"encrypt" + context,
hashlib.sha256
).digest()[:32]
# Derive MAC key
mac_key = hmac.new(
self.master_key,
b"authenticate" + context,
hashlib.sha256
).digest()
return enc_key, mac_key
def _check_usage_limits(self, nonce: bytes):
"""Check if usage limits are exceeded"""
nonce_hex = nonce.hex()
usage = self.key_usage.get(nonce_hex, {'bytes': 0, 'operations': 0})
# ChaCha20 limits: 2^38 bytes per key-nonce
if usage['bytes'] > 2**38:
raise RuntimeError("Data limit exceeded for this nonce")
# Practical limit: 2^32 operations
if usage['operations'] > 2**32:
raise RuntimeError("Operation limit exceeded for this nonce")
def _track_usage(self, nonce: bytes, data_len: int):
"""Track usage for limits"""
nonce_hex = nonce.hex()
if nonce_hex not in self.key_usage:
self.key_usage[nonce_hex] = {'bytes': 0, 'operations': 0}
self.key_usage[nonce_hex]['bytes'] += data_len
self.key_usage[nonce_hex]['operations'] += 1
# SECURE: Stream encryption for large files
class ChaCha20FileEncryption:
def __init__(self, key: bytes):
self.key = key
def encrypt_file_stream(
self,
input_file,
output_file,
chunk_size: int = 64 * 1024
) -> dict:
"""Encrypt large file with ChaCha20"""
# Generate file nonce and MAC key
file_nonce = secrets.token_bytes(12)
mac_key = hmac.new(
self.key,
b"file-mac" + file_nonce,
hashlib.sha256
).digest()
# Initialize ChaCha20 with file nonce
cipher = Cipher(
algorithms.ChaCha20(self.key, file_nonce),
mode=None,
backend=default_backend()
)
encryptor = cipher.encryptor()
# Initialize HMAC
h = hmac.new(mac_key, digestmod=hashlib.sha256)
h.update(file_nonce)
# Write header
output_file.write(b"ChaCha20File")
output_file.write(file_nonce)
total_bytes = 0
# Process file in chunks
while True:
chunk = input_file.read(chunk_size)
if not chunk:
break
# Encrypt chunk
encrypted_chunk = encryptor.update(chunk)
# Update HMAC
h.update(encrypted_chunk)
# Write encrypted chunk
output_file.write(encrypted_chunk)
total_bytes += len(chunk)
# Finalize encryption
final_chunk = encryptor.finalize()
if final_chunk:
h.update(final_chunk)
output_file.write(final_chunk)
# Write MAC
file_mac = h.digest()
output_file.write(file_mac)
# Clear MAC key
secure_zero(mac_key)
return {
'nonce': file_nonce,
'total_bytes': total_bytes,
'algorithm': 'ChaCha20-HMAC-SHA256'
}
# SECURE: Key derivation using ChaCha20
def derive_keys_chacha20(master_key: bytes, context: str, count: int) -> list:
"""Derive multiple keys using ChaCha20 as PRF"""
keys = []
for i in range(count):
# Create nonce from context and counter
context_bytes = context.encode()[:8].ljust(8, b'\x00')
counter_bytes = struct.pack('<I', i)
nonce = context_bytes + counter_bytes
# Use ChaCha20 to generate key material
cipher = Cipher(
algorithms.ChaCha20(master_key, nonce),
mode=None,
backend=default_backend()
)
encryptor = cipher.encryptor()
# Generate 32 bytes of key material
key_material = encryptor.update(b'\x00' * 32) + encryptor.finalize()
keys.append(key_material)
return keys
Common Mistakes:
# CATASTROPHIC: Nonce reuse
nonce = b'\x00' * 12 # Same nonce every time!
ct1 = chacha20_encrypt(key, nonce, msg1)
ct2 = chacha20_encrypt(key, nonce, msg2) # BROKEN!
# INSECURE: No authentication
ciphertext = chacha20_encrypt(key, nonce, plaintext) # Malleable!
# INSECURE: Predictable nonce
nonce = struct.pack('<Q', int(time.time())) + b'\x00\x00\x00\x00'
# INSECURE: Weak key
key = hashlib.sha256(b"password").digest() # Predictable!
# INSECURE: Counter manipulation
# Starting with high counter values
chacha20_keystream(key: bytes[32], nonce: bytes[12], counter: int, length: int) -> bytes
Security Contract:
- Preconditions:
keymust be 32 bytesnoncemust be uniquecountersets starting positionlengthspecifies output bytes
- Postconditions:
- Returns keystream of specified length
- Output appears random to adversaries
- Deterministic for same inputs
Security Notes:
- Keystream can be XORed with data
- Don’t reuse keystream bytes
- Ensure counter doesn’t wrap
Secure Usage Example:
use chacha20::{ChaCha20, cipher::{KeyIvInit, StreamCipher}};
use rand::{RngCore, thread_rng};
use zeroize::Zeroize;
/// Secure ChaCha20 keystream generator
pub struct SecureKeystream {
key: [u8; 32],
base_nonce: [u8; 12],
stream_counter: u64,
}
impl SecureKeystream {
pub fn new() -> Self {
let mut key = [0u8; 32];
let mut base_nonce = [0u8; 12];
thread_rng().fill_bytes(&mut key);
thread_rng().fill_bytes(&mut base_nonce);
Self {
key,
base_nonce,
stream_counter: 0,
}
}
pub fn generate_stream(&mut self, length: usize) -> Vec<u8> {
// Create nonce with stream counter
let mut nonce = self.base_nonce;
let counter_bytes = self.stream_counter.to_le_bytes();
nonce[4..12].copy_from_slice(&counter_bytes);
// Generate keystream
let mut cipher = ChaCha20::new(&self.key.into(), &nonce.into());
let mut keystream = vec![0u8; length];
cipher.apply_keystream(&mut keystream);
// Increment stream counter
self.stream_counter += 1;
keystream
}
pub fn encrypt_data(&mut self, data: &[u8]) -> (Vec<u8>, [u8; 12]) {
// Generate unique nonce
let mut nonce = self.base_nonce;
let counter_bytes = self.stream_counter.to_le_bytes();
nonce[4..12].copy_from_slice(&counter_bytes);
// Encrypt
let mut cipher = ChaCha20::new(&self.key.into(), &nonce.into());
let mut ciphertext = data.to_vec();
cipher.apply_keystream(&mut ciphertext);
self.stream_counter += 1;
(ciphertext, nonce)
}
}
impl Drop for SecureKeystream {
fn drop(&mut self) {
self.key.zeroize();
}
}
/// One-time pad using ChaCha20
pub struct ChaCha20OTP {
key: [u8; 32],
used_nonces: std::collections::HashSet<[u8; 12]>,
}
impl ChaCha20OTP {
pub fn new() -> Self {
let mut key = [0u8; 32];
thread_rng().fill_bytes(&mut key);
Self {
key,
used_nonces: std::collections::HashSet::new(),
}
}
pub fn encrypt_once(&mut self, plaintext: &[u8]) -> Result<(Vec<u8>, [u8; 12]), Error> {
// Generate unique nonce
let mut nonce = [0u8; 12];
loop {
thread_rng().fill_bytes(&mut nonce);
if !self.used_nonces.contains(&nonce) {
break;
}
}
// Mark nonce as used
self.used_nonces.insert(nonce);
// Generate one-time keystream
let mut cipher = ChaCha20::new(&self.key.into(), &nonce.into());
let mut ciphertext = plaintext.to_vec();
cipher.apply_keystream(&mut ciphertext);
Ok((ciphertext, nonce))
}
pub fn decrypt_once(&self, ciphertext: &[u8], nonce: &[u8; 12]) -> Vec<u8> {
// Check if nonce was used
if !self.used_nonces.contains(nonce) {
panic!("Nonce not found - potential forgery");
}
// Decrypt (same as encrypt for stream cipher)
let mut cipher = ChaCha20::new(&self.key.into(), nonce.into());
let mut plaintext = ciphertext.to_vec();
cipher.apply_keystream(&mut plaintext);
plaintext
}
}
chacha20_block(key: bytes[32], nonce: bytes[12], counter: int) -> bytes[64]
Security Contract:
- Preconditions:
keymust be 32 bytesnoncemust be 12 bytescounterspecifies block number
- Postconditions:
- Returns exactly 64 bytes
- Output is one ChaCha20 block
- Suitable for custom protocols
Security Notes:
- Low-level interface for experts
- Ensure proper usage in protocols
- Maintain nonce uniqueness
Security Best Practices
Nonce Management
class NonceManager:
"""Secure nonce management for ChaCha20"""
def __init__(self):
self.counter_nonces = {}
self.random_nonces = set()
def generate_counter_nonce(self, context: str) -> bytes:
"""Generate sequential nonce for context"""
if context not in self.counter_nonces:
# Initialize with random base
base = secrets.token_bytes(8)
self.counter_nonces[context] = {
'base': base,
'counter': 0
}
ctx_data = self.counter_nonces[context]
# Check counter limit
if ctx_data['counter'] >= 2**32:
raise RuntimeError(f"Counter exhausted for context: {context}")
# Create nonce: base + counter
nonce = ctx_data['base'] + struct.pack('<I', ctx_data['counter'])
ctx_data['counter'] += 1
return nonce
def generate_random_nonce(self) -> bytes:
"""Generate random nonce with collision detection"""
max_attempts = 1000
for _ in range(max_attempts):
nonce = secrets.token_bytes(12)
if nonce not in self.random_nonces:
self.random_nonces.add(nonce)
return nonce
raise RuntimeError("Failed to generate unique random nonce")
def verify_nonce_uniqueness(self, nonce: bytes, context: str) -> bool:
"""Verify nonce hasn't been used"""
# For counter nonces, check if within expected range
if context in self.counter_nonces:
ctx_data = self.counter_nonces[context]
if nonce.startswith(ctx_data['base']):
counter = struct.unpack('<I', nonce[8:])[0]
return counter < ctx_data['counter']
# For random nonces, check set
return nonce not in self.random_nonces
Secure Key Rotation
/// Key rotation for ChaCha20
pub struct ChaCha20KeyRotation {
current_key: [u8; 32],
previous_keys: Vec<RotatedKey>,
rotation_interval: std::time::Duration,
last_rotation: std::time::Instant,
}
struct RotatedKey {
key: [u8; 32],
valid_until: std::time::Instant,
}
impl ChaCha20KeyRotation {
pub fn new(rotation_interval_hours: u64) -> Self {
let mut key = [0u8; 32];
thread_rng().fill_bytes(&mut key);
Self {
current_key: key,
previous_keys: Vec::new(),
rotation_interval: std::time::Duration::from_secs(rotation_interval_hours * 3600),
last_rotation: std::time::Instant::now(),
}
}
pub fn maybe_rotate(&mut self) -> bool {
if self.last_rotation.elapsed() > self.rotation_interval {
self.rotate_key();
true
} else {
false
}
}
fn rotate_key(&mut self) {
// Move current key to previous keys
let old_key = RotatedKey {
key: self.current_key,
valid_until: std::time::Instant::now() + self.rotation_interval,
};
self.previous_keys.push(old_key);
// Generate new key
thread_rng().fill_bytes(&mut self.current_key);
self.last_rotation = std::time::Instant::now();
// Clean up expired keys
let now = std::time::Instant::now();
self.previous_keys.retain(|k| k.valid_until > now);
}
pub fn get_current_key(&self) -> &[u8; 32] {
&self.current_key
}
pub fn find_decryption_key(&self, encrypted_time: std::time::Instant) -> Option<&[u8; 32]> {
// Try current key first
if encrypted_time >= self.last_rotation {
return Some(&self.current_key);
}
// Try previous keys
for key in &self.previous_keys {
if encrypted_time <= key.valid_until {
return Some(&key.key);
}
}
None
}
}
impl Drop for ChaCha20KeyRotation {
fn drop(&mut self) {
self.current_key.zeroize();
for key in &mut self.previous_keys {
key.key.zeroize();
}
}
}
Protocol Integration
class ChaCha20TLSRecord:
"""ChaCha20 in TLS-like record protocol"""
def __init__(self, read_key: bytes, write_key: bytes):
self.read_key = read_key
self.write_key = write_key
self.read_seq = 0
self.write_seq = 0
def encrypt_record(
self,
record_type: int,
payload: bytes
) -> bytes:
"""Encrypt TLS record with ChaCha20"""
# Create nonce from sequence number
nonce = struct.pack('<Q', self.write_seq) + b'\x00\x00\x00\x00'
# Create additional data
additional_data = (
struct.pack('<Q', self.write_seq) + # Sequence
struct.pack('B', record_type) + # Type
struct.pack('>H', len(payload)) # Length
)
# Encrypt with ChaCha20-Poly1305
encrypted = self._chacha20_poly1305_encrypt(
self.write_key,
nonce,
payload,
additional_data
)
self.write_seq += 1
# Return record: type + version + length + encrypted
record = (
struct.pack('B', record_type) +
b'\x03\x04' + # TLS 1.3
struct.pack('>H', len(encrypted)) +
encrypted
)
return record
Common Integration Patterns
Disk Encryption
class ChaCha20DiskEncryption:
"""ChaCha20 for disk encryption (educational)"""
def __init__(self, master_key: bytes):
self.master_key = master_key
def encrypt_sector(
self,
sector_data: bytes,
sector_number: int,
sector_size: int = 512
) -> bytes:
"""Encrypt disk sector"""
if len(sector_data) != sector_size:
raise ValueError(f"Sector must be {sector_size} bytes")
# Derive sector key from master key and sector number
sector_key = hmac.new(
self.master_key,
f"sector-{sector_number}".encode(),
hashlib.sha256
).digest()[:32]
# Use sector number as nonce (with padding)
nonce = struct.pack('<Q', sector_number) + b'\x00\x00\x00\x00'
# Encrypt sector
cipher = Cipher(
algorithms.ChaCha20(sector_key, nonce),
mode=None,
backend=default_backend()
)
encryptor = cipher.encryptor()
encrypted = encryptor.update(sector_data) + encryptor.finalize()
# Clear sector key
secure_zero(sector_key)
return encrypted
Secure Communication
/// ChaCha20 for secure messaging
pub struct SecureMessaging {
shared_secret: [u8; 32],
send_nonce: u64,
recv_nonce: u64,
}
impl SecureMessaging {
pub fn new(shared_secret: [u8; 32]) -> Self {
Self {
shared_secret,
send_nonce: 0,
recv_nonce: 0,
}
}
pub fn send_message(&mut self, message: &[u8]) -> Vec<u8> {
// Create nonce with direction bit
let mut nonce = [0u8; 12];
nonce[0] = 0x01; // Send direction
nonce[4..12].copy_from_slice(&self.send_nonce.to_le_bytes());
// Encrypt message
let mut cipher = ChaCha20::new(&self.shared_secret.into(), &nonce.into());
let mut encrypted = message.to_vec();
cipher.apply_keystream(&mut encrypted);
// Increment nonce
self.send_nonce += 1;
// Format: nonce + encrypted message
let mut packet = nonce.to_vec();
packet.extend_from_slice(&encrypted);
packet
}
pub fn recv_message(&mut self, packet: &[u8]) -> Result<Vec<u8>, Error> {
if packet.len() < 12 {
return Err(Error::PacketTooShort);
}
let nonce = &packet[..12];
let encrypted = &packet[12..];
// Verify direction bit
if nonce[0] != 0x02 {
return Err(Error::WrongDirection);
}
// Extract and verify sequence
let seq = u64::from_le_bytes(nonce[4..12].try_into().unwrap());
if seq != self.recv_nonce {
return Err(Error::SequenceError);
}
// Decrypt message
let mut cipher = ChaCha20::new(&self.shared_secret.into(), nonce.try_into().unwrap());
let mut message = encrypted.to_vec();
cipher.apply_keystream(&mut message);
self.recv_nonce += 1;
Ok(message)
}
}
Performance Considerations
| Platform | ChaCha20 Speed | AES-128 Speed | Notes |
|---|---|---|---|
| x86-64 (no AES-NI) | 3.5 cycles/byte | 15 cycles/byte | ChaCha20 faster |
| x86-64 (AES-NI) | 3.5 cycles/byte | 0.8 cycles/byte | AES faster |
| ARM64 | 4.2 cycles/byte | 6.5 cycles/byte | ChaCha20 faster |
| ARM32 | 8.5 cycles/byte | 20 cycles/byte | ChaCha20 faster |
| RISC-V | 5.1 cycles/byte | 18 cycles/byte | ChaCha20 faster |
Optimization Strategies
- Use vectorized implementations (SIMD)
- Parallelize multiple streams
- Buffer large operations
- Consider ChaCha8 for extreme performance needs
Security Auditing
Verification Checklist
- Never reusing nonces with same key
- Adding authentication (Poly1305/HMAC)
- Using strong key generation
- Implementing proper key rotation
- Respecting counter/size limits
- Protecting key material in memory
Common Vulnerabilities
# AUDIT: Look for these patterns
# ❌ Static nonce
NONCE = b'\x00' * 12 # Same every time!
# ❌ No authentication
def encrypt(key, data):
return chacha20(key, generate_nonce(), data) # Malleable!
# ❌ Predictable key
key = hashlib.sha256(password.encode()).digest()
# ❌ Nonce from timestamp
nonce = struct.pack('<Q', int(time.time())) + b'\x00\x00\x00\x00'
# ❌ High counter start
cipher = ChaCha20(key, nonce, counter=1000000) # Why so high?
Security Analysis
Threat Model: ChaCha20 Threat Model
The comprehensive threat analysis covers:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
Support
Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md
Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC