dnscrypt 0.1.0

A pure-Rust DNSCrypt v2 client library — sync and async support.
Documentation
//! Raw XChaCha20-Poly1305 AEAD and HChaCha20 key derivation.
//!
//! These primitives implement the DNSCrypt v2 "XSalsa20Poly1305" wire format,
//! which DNSCrypt labels "X25519-XChacha20Poly1305".  The construction is:
//!
//! 1. Derive a 64-byte keystream block from the key and nonce using XChaCha20.
//! 2. Use the first 32 bytes as the Poly1305 one-time key.
//! 3. Encrypt/decrypt the payload using the remaining keystream bytes.
//! 4. Authenticate the ciphertext with Poly1305.

use chacha20::XChaCha20;
use chacha20::cipher::{KeyIvInit, StreamCipher};
use poly1305::Poly1305;
use poly1305::universal_hash::KeyInit;
use subtle::ConstantTimeEq;

/// Compute HChaCha20(key, input) and return the 32-byte derived subkey.
///
/// In the DNSCrypt session setup the raw X25519 shared secret is passed through
/// HChaCha20 with a zero nonce to produce the symmetric session key used for
/// all subsequent query/response encryption.
pub fn run_hchacha20(key: &[u8; 32], input: &[u8; 16]) -> [u8; 32] {
    use chacha20::{R20, hchacha};
    let out = hchacha::<R20>(&(*key).into(), &(*input).into());
    let mut result = [0u8; 32];
    result.copy_from_slice(&out);
    result
}

/// Encrypt `plaintext` with XChaCha20-Poly1305 and prepend the 16-byte tag.
///
/// The returned buffer layout is `[tag (16 bytes) | ciphertext]`.
pub fn xchacha20_djb_poly1305_encrypt(
    key: &[u8; 32],
    nonce: &[u8; 24],
    plaintext: &[u8],
) -> Vec<u8> {
    // Generate the full keystream: first 32 bytes become the Poly1305 key,
    // the rest XOR with the plaintext.
    #[cfg(feature = "zeroize")]
    let mut keystream = zeroize::Zeroizing::new(vec![0u8; 32 + plaintext.len()]);
    #[cfg(not(feature = "zeroize"))]
    let mut keystream = vec![0u8; 32 + plaintext.len()];

    XChaCha20::new(key.into(), nonce.into()).apply_keystream(&mut keystream);

    let (poly_key_bytes, keystream_body) = keystream.split_at(32);

    #[cfg(feature = "zeroize")]
    let mut poly_key = zeroize::Zeroizing::new([0u8; 32]);
    #[cfg(not(feature = "zeroize"))]
    let mut poly_key = [0u8; 32];

    poly_key.copy_from_slice(poly_key_bytes);

    // XOR plaintext with keystream to produce ciphertext.
    let ciphertext: Vec<u8> = plaintext
        .iter()
        .zip(keystream_body.iter())
        .map(|(&p, &k)| p ^ k)
        .collect();

    #[cfg(feature = "zeroize")]
    let tag = Poly1305::new(&(*poly_key).into()).compute_unpadded(&ciphertext);
    #[cfg(not(feature = "zeroize"))]
    let tag = Poly1305::new(&poly_key.into()).compute_unpadded(&ciphertext);

    let mut result = Vec::with_capacity(16 + ciphertext.len());
    result.extend_from_slice(&tag[..]);
    result.extend_from_slice(&ciphertext);
    result
}

/// Verify and decrypt a XChaCha20-Poly1305 ciphertext.
///
/// `ciphertext_with_tag` must begin with the 16-byte Poly1305 tag followed
/// by the ciphertext produced by [`xchacha20_djb_poly1305_encrypt`].
///
/// # Errors
///
/// Returns `Err` if the input is shorter than 16 bytes or if the Poly1305
/// tag does not match (constant-time comparison).
pub fn xchacha20_djb_poly1305_decrypt(
    key: &[u8; 32],
    nonce: &[u8; 24],
    ciphertext_with_tag: &[u8],
) -> Result<Vec<u8>, String> {
    if ciphertext_with_tag.len() < 16 {
        return Err("Ciphertext too short".to_string());
    }
    let (tag, ciphertext) = ciphertext_with_tag.split_at(16);

    #[cfg(feature = "zeroize")]
    let mut keystream = zeroize::Zeroizing::new(vec![0u8; 32 + ciphertext.len()]);
    #[cfg(not(feature = "zeroize"))]
    let mut keystream = vec![0u8; 32 + ciphertext.len()];

    XChaCha20::new(key.into(), nonce.into()).apply_keystream(&mut keystream);

    let (poly_key_bytes, keystream_body) = keystream.split_at(32);

    #[cfg(feature = "zeroize")]
    let mut poly_key = zeroize::Zeroizing::new([0u8; 32]);
    #[cfg(not(feature = "zeroize"))]
    let mut poly_key = [0u8; 32];

    poly_key.copy_from_slice(poly_key_bytes);

    #[cfg(feature = "zeroize")]
    let computed_tag = Poly1305::new(&(*poly_key).into()).compute_unpadded(ciphertext);
    #[cfg(not(feature = "zeroize"))]
    let computed_tag = Poly1305::new(&poly_key.into()).compute_unpadded(ciphertext);

    // Constant-time comparison to prevent timing oracle attacks.
    if computed_tag[..].ct_eq(tag).unwrap_u8() == 0 {
        return Err("Poly1305 authentication failed".to_string());
    }

    let plaintext: Vec<u8> = ciphertext
        .iter()
        .zip(keystream_body.iter())
        .map(|(&c, &k)| c ^ k)
        .collect();

    Ok(plaintext)
}