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 ⚠️

  1. Nonce Reuse Fatal: NEVER reuse nonce with same key - breaks confidentiality
  2. Not Authenticated: ChaCha20 alone provides no authenticity - use ChaCha20-Poly1305
  3. Counter Limits: 32-bit counter limits to 256 GB per nonce
  4. Malleable: Without authentication, ciphertext can be modified
  5. 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:

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:

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:

Security Notes:

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:

Security Notes:

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

Security Auditing

Verification Checklist

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:

For complete security analysis and risk assessment, see the dedicated threat model documentation.

References

  1. RFC 8439 - ChaCha20 and Poly1305
  2. ChaCha Family Paper
  3. RFC 7905 - ChaCha20-Poly1305 in TLS

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