Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Extensible RustCrypto

The RustCrypto crates share traits such as Digest, DynDigest and Mac, so you can write code that works with any algorithm, or picks one at runtime. These recipes cover jobs that are awkward with a C-style API, where the algorithm is fixed by a function name; see Choosing between ring and RustCrypto for when to use them. The simple one-shot versions are on the Hashing page.

Choose a hash algorithm at runtime

sha2 data-encoding Cryptography

Use DynDigest when the algorithm name comes from a config file, a command line flag or a protocol field and is not known at compile time; this is the job of EVP_get_digestbyname in OpenSSL. Every hash in the RustCrypto family can be boxed as a dyn DynDigest, so hasher_by_name returns one value type for all of them. Unsupported names return None instead of a null pointer.

use data_encoding::HEXLOWER;
use sha2::digest::DynDigest;
use sha2::{Sha256, Sha384, Sha512};

/// Looks up a hash function by name, like `EVP_get_digestbyname` in OpenSSL.
fn hasher_by_name(name: &str) -> Option<Box<dyn DynDigest>> {
    match name {
        "sha256" => Some(Box::new(Sha256::default())),
        "sha384" => Some(Box::new(Sha384::default())),
        "sha512" => Some(Box::new(Sha512::default())),
        _ => None,
    }
}

fn main() {
    // The algorithm names could just as well come from a config file.
    for name in ["sha256", "sha384", "sha512", "md5"] {
        match hasher_by_name(name) {
            Some(mut hasher) => {
                hasher.update(b"hello ");
                hasher.update(b"world");
                println!("{name}: {}", HEXLOWER.encode(&hasher.finalize()));
            }
            None => println!("{name}: unsupported algorithm"),
        }
    }
}

Hash data while writing it

sha2 data-encoding Cryptography

Use a wrapper around Write when you want the digest of a stream you are already writing, such as a download or an archive, without reading it back. HashingWriter works with any Digest, passes writes through to the inner writer and hashes only the bytes it accepted, so partial writes stay correct. Because it is a Write, it plugs into io::copy and anything else that takes a writer.

use anyhow::Result;
use data_encoding::HEXLOWER;
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::{self, Write};

/// A writer that hashes everything written through it.
struct HashingWriter<W: Write, D: Digest> {
    inner: W,
    hasher: D,
}

impl<W: Write, D: Digest> HashingWriter<W, D> {
    fn new(inner: W) -> Self {
        Self {
            inner,
            hasher: D::new(),
        }
    }

    fn finish(self) -> (W, Vec<u8>) {
        (self.inner, self.hasher.finalize().to_vec())
    }
}

impl<W: Write, D: Digest> Write for HashingWriter<W, D> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        // Only hash the bytes the inner writer actually accepted.
        let written = self.inner.write(buf)?;
        self.hasher.update(&buf[..written]);
        Ok(written)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.inner.flush()
    }
}

fn main() -> Result<()> {
    let dir = tempfile::tempdir()?;
    let path = dir.path().join("copy.txt");

    // Copy a reader into a file and get its digest without reading it back.
    let mut source = &b"We will generate a digest of this text"[..];
    let mut writer = HashingWriter::<_, Sha256>::new(File::create(&path)?);
    io::copy(&mut source, &mut writer)?;
    let (_file, digest) = writer.finish();

    println!(
        "Wrote {} and its SHA-256 digest is {}",
        path.display(),
        HEXLOWER.encode(&digest)
    );
    Ok(())
}

Upgrade stored password hashes on login

argon2 Cryptography

Use this when you raise the cost of password hashing but cannot rehash users who are not logged in. A PHC string records its own parameters, so verify_and_upgrade can verify an old hash with the parameters stored in it, read them back with Params::try_from and, if they are weaker than the current ones, return a new hash to save. It returns None when the stored hash is already current, and an error when the password is wrong.

use anyhow::Result;
use argon2::password_hash::phc::PasswordHash;
use argon2::password_hash::{PasswordHasher, PasswordVerifier};
use argon2::{Algorithm, Argon2, Params, Version};

/// The cost parameters new hashes should use.
fn current_argon2() -> Result<Argon2<'static>> {
    let params = Params::new(32 * 1024, 3, 1, None)?;
    Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
}

/// Checks a login attempt. If the password is right but the stored hash was
/// made with weaker parameters, returns a replacement hash to store.
fn verify_and_upgrade(password: &str, stored: &str) -> Result<Option<String>> {
    let parsed = PasswordHash::new(stored)?;
    // The parameters in the stored hash are used to verify it.
    Argon2::default().verify_password(password.as_bytes(), &parsed)?;

    let current = current_argon2()?;
    let used = Params::try_from(&parsed)?;
    if used.m_cost() < 32 * 1024 || used.t_cost() < 3 {
        let upgraded = current.hash_password(password.as_bytes())?.to_string();
        return Ok(Some(upgraded));
    }
    Ok(None)
}

fn main() -> Result<()> {
    let password = "Guess Me If You Can!";

    // A hash from before the parameters were raised.
    let weak = Params::new(8 * 1024, 1, 1, None)?;
    let old_hash = Argon2::new(Algorithm::Argon2id, Version::V0x13, weak)
        .hash_password(password.as_bytes())?
        .to_string();
    println!("Old hash: {old_hash}");

    match verify_and_upgrade(password, &old_hash)? {
        Some(new_hash) => println!("Upgraded: {new_hash}"),
        None => println!("Hash already up to date"),
    }
    Ok(())
}

Write code that is generic over the MAC

hmac sha2 Cryptography

Use the Mac trait when the algorithm should be chosen by the caller, for example to migrate from HMAC-SHA256 to HMAC-SHA512. sign and verify take the MAC as a type parameter, so there is one implementation for every algorithm that implements Mac and KeyInit. Mac::verify_slice compares in constant time. A tag only verifies with the algorithm that made it, which the second assert! shows.

use hmac::{Hmac, KeyInit, Mac};
use sha2::{Sha256, Sha512};

fn sign<M: Mac + KeyInit>(
    key: &[u8],
    message: &[u8],
) -> Result<Vec<u8>, hmac::digest::InvalidLength> {
    let mut mac = <M as KeyInit>::new_from_slice(key)?;
    mac.update(message);
    Ok(mac.finalize().into_bytes().to_vec())
}

/// Verifies a tag in constant time with whichever MAC `M` names.
fn verify<M: Mac + KeyInit>(key: &[u8], message: &[u8], tag: &[u8]) -> bool {
    match <M as KeyInit>::new_from_slice(key) {
        Ok(mut mac) => {
            mac.update(message);
            mac.verify_slice(tag).is_ok()
        }
        Err(_) => false,
    }
}

fn main() -> Result<(), hmac::digest::InvalidLength> {
    let key = b"my secret and secure key";
    let message = b"input message";

    let tag256 = sign::<Hmac<Sha256>>(key, message)?;
    let tag512 = sign::<Hmac<Sha512>>(key, message)?;
    println!("HMAC-SHA256 tag is {} bytes", tag256.len());
    println!("HMAC-SHA512 tag is {} bytes", tag512.len());

    assert!(verify::<Hmac<Sha256>>(key, message, &tag256));
    // A tag only verifies with the algorithm that made it.
    assert!(!verify::<Hmac<Sha512>>(key, message, &tag256));
    Ok(())
}