unix-crypt 0.1.0

Rust reimplementation of various unix crypt(3) algorithms.
Documentation
use md5::{Digest, Md5};

const HASH_PREFIX: &[u8] = b"$apr1$";

const DIGEST_SIZE: usize = 16;

fn extract_salt(mut hash: &[u8]) -> Option<&[u8]> {
    use std::cmp::min;

    if !hash.starts_with(HASH_PREFIX) {
        return None;
    }

    // Skip prefix
    hash = &hash[HASH_PREFIX.len()..];

    // Up to 8 characters or until end
    hash = &hash[..min(hash.len(), 8)];

    // Or until $
    let end = hash.iter().position(|&x| x == b'$').unwrap_or(8);

    Some(&hash[..end])
}

pub fn verify(password: &[u8], hash: &[u8]) -> bool {
    let Some(salt) = extract_salt(hash) else {
        return false;
    };

    assert!(salt.len() == 8);

    let salt = salt.try_into().unwrap();

    let h2 = to_hash_string(encode(password, salt), salt);

    return hash == h2;
}

pub fn to_hash_string(digest: [u8; DIGEST_SIZE], salt: [u8; 8]) -> Vec<u8> {
    // 6 (prefix) + 1..=8 (salt) + 1 ($) + 22 (hash)
    const LEN_STRING: usize = 37;

    // ceil(128 / 6)
    const LEN_HASH: usize = 22;

    let mut out = Vec::with_capacity(LEN_STRING);

    out.extend_from_slice(HASH_PREFIX);
    out.extend_from_slice(&salt);
    out.push(b'$');

    let alpha = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    let n = u128::from_le_bytes(digest);

    for i in 0..LEN_HASH {
        let idx = (n >> i * 6) as usize & 0x3F;
        out.push(alpha[idx]);
    }

    out
}

pub fn encode(password: &[u8], salt: [u8; 8]) -> [u8; DIGEST_SIZE] {
    let mut md5 = Md5::new();

    md5.update(password);
    md5.update(HASH_PREFIX);
    md5.update(&salt);

    // md5(password, salt, password)
    let extra: [u8; DIGEST_SIZE] = {
        let mut md5 = Md5::new();
        md5.update(password);
        md5.update(&salt);
        md5.update(password);
        md5.finalize().into()
    };

    // password.len() many bytes of `extra` repeated
    for _ in 0..password.len() / DIGEST_SIZE {
        md5.update(&extra);
    }

    md5.update(&extra[..password.len() % DIGEST_SIZE]);

    let mut i = password.len();

    while i != 0 {
        if i & 1 != 0 {
            md5.update(&[0]);
        }
        else {
            // If `password` is empty, we pass in
            // what would be the null terminator in the C version.
            md5.update(password.get(..1).unwrap_or(&[0]));
        }
        i >>= 1;
    }

    let mut digest: [u8; DIGEST_SIZE] = md5.finalize().into();

    for i in 0..1000 {
        let mut md5 = Md5::new();

        if i & 1 != 0 {
            md5.update(password);
        }
        else {
            md5.update(&digest);
        }

        if i % 3 != 0 {
            md5.update(&salt);
        }

        if i % 7 != 0 {
            md5.update(password);
        }

        if i & 1 != 0 {
            md5.update(&digest)
        }
        else {
            md5.update(password);
        }

        digest = md5.finalize().into();
    }

    let output = [
        digest[12], digest[ 6], digest[0],
        digest[13], digest[ 7], digest[1],
        digest[14], digest[ 8], digest[2],
        digest[15], digest[ 9], digest[3],
        digest[ 5], digest[10], digest[4],
        digest[11]
    ];

    output
}