Understanding Cryptographic Primitives in Rust

Cryptography is fundamental to building secure systems. In this post, we'll explore how to work with cryptographic primitives in Rust using the ecosystem's excellent libraries.

Choosing the Right Crypto Library

The Rust ecosystem offers several high-quality cryptography crates:

  • ring: Fast, safe, and thoroughly audited
  • RustCrypto: Pure Rust implementations with extensive algorithm support
  • sodiumoxide: Rust bindings to libsodium

For this example, we'll use ring for its performance and security track record.

Hashing with BLAKE2

BLAKE2 is a cryptographic hash function faster than MD5, SHA-1, SHA-2, and SHA-3, yet is at least as secure as the latest standard SHA-3:

use ring::digest;

fn hash_data(data: &[u8]) -> Vec<u8> {
    let digest = digest::digest(&digest::BLAKE2B_256, data);
    digest.as_ref().to_vec()
}

fn main() {
    let data = b"Hello, AnyVec!";
    let hash = hash_data(data);
    println!("Hash: {:?}", hash);
}

Authenticated Encryption with ChaCha20-Poly1305

For encrypting data, always use authenticated encryption (AEAD):

use ring::aead;
use ring::rand::{SecureRandom, SystemRandom};

fn encrypt_message(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
    let rng = SystemRandom::new();

    // Generate a unique nonce
    let mut nonce_bytes = vec![0u8; 12];
    rng.fill(&mut nonce_bytes)?;

    let unbound_key = aead::UnboundKey::new(
        &aead::CHACHA20_POLY1305,
        key
    )?;

    let sealing_key = aead::LessSafeKey::new(unbound_key);
    let nonce = aead::Nonce::try_assume_unique_for_key(&nonce_bytes)?;

    let mut ciphertext = plaintext.to_vec();
    sealing_key.seal_in_place_append_tag(
        nonce,
        aead::Aad::empty(),
        &mut ciphertext
    )?;

    // Prepend nonce to ciphertext
    let mut result = nonce_bytes;
    result.extend_from_slice(&ciphertext);

    Ok(result)
}

Key Derivation with PBKDF2

Never use passwords directly as encryption keys. Always use a key derivation function:

use ring::pbkdf2;
use std::num::NonZeroU32;

fn derive_key(password: &str, salt: &[u8]) -> [u8; 32] {
    let iterations = NonZeroU32::new(100_000).unwrap();
    let mut key = [0u8; 32];

    pbkdf2::derive(
        pbkdf2::PBKDF2_HMAC_SHA256,
        iterations,
        salt,
        password.as_bytes(),
        &mut key,
    );

    key
}

Best Practices

1. Use High-Level APIs

Prefer high-level APIs that make it harder to misuse cryptography:

// Good: High-level API
let encrypted = encrypt_aead(key, plaintext)?;

// Avoid: Low-level primitives require expert knowledge
let cipher = Cipher::new(key);
let encrypted = cipher.encrypt_block(plaintext); // Easy to misuse!

2. Generate Random Keys Properly

Always use cryptographically secure random number generators:

use ring::rand::{SecureRandom, SystemRandom};

let rng = SystemRandom::new();
let mut key = vec![0u8; 32];
rng.fill(&mut key)?; // Cryptographically secure

3. Constant-Time Comparisons

Prevent timing attacks when comparing secrets:

use ring::constant_time;

fn verify_mac(expected: &[u8], actual: &[u8]) -> bool {
    constant_time::verify_slices_are_equal(expected, actual).is_ok()
}

Common Mistakes to Avoid

  1. Reusing nonces with the same key
  2. Using ECB mode (never use block ciphers without a mode)
  3. Rolling your own crypto (use audited libraries)
  4. Ignoring authentication (encryption without authentication is insecure)
  5. Hardcoding keys (use key management systems)

Conclusion

Rust's type system and memory safety make it an excellent choice for implementing cryptographic systems. Combined with well-audited libraries like ring, you can build secure applications with confidence.

In our next post, we'll explore cryptographic protocol implementation and how to avoid common pitfalls when building secure communication systems.


Want to learn more? Check out our GitHub for example projects.