unix_crypt/
apr1.rs

1use md5::{Digest, Md5};
2
3const HASH_PREFIX: &[u8] = b"$apr1$";
4
5const DIGEST_SIZE: usize = 16;
6
7fn extract_salt(mut hash: &[u8]) -> Option<&[u8]> {
8    use std::cmp::min;
9
10    if !hash.starts_with(HASH_PREFIX) {
11        return None;
12    }
13
14    // Skip prefix
15    hash = &hash[HASH_PREFIX.len()..];
16
17    // Up to 8 characters or until end
18    hash = &hash[..min(hash.len(), 8)];
19
20    // Or until $
21    let end = hash.iter().position(|&x| x == b'$').unwrap_or(8);
22
23    Some(&hash[..end])
24}
25
26pub fn verify(password: &[u8], hash: &[u8]) -> bool {
27    let Some(salt) = extract_salt(hash) else {
28        return false;
29    };
30
31    assert!(salt.len() == 8);
32
33    let salt = salt.try_into().unwrap();
34
35    let h2 = to_hash_string(encode(password, salt), salt);
36
37    return hash == h2;
38}
39
40pub fn to_hash_string(digest: [u8; DIGEST_SIZE], salt: [u8; 8]) -> Vec<u8> {
41    // 6 (prefix) + 1..=8 (salt) + 1 ($) + 22 (hash)
42    const LEN_STRING: usize = 37;
43
44    // ceil(128 / 6)
45    const LEN_HASH: usize = 22;
46
47    let mut out = Vec::with_capacity(LEN_STRING);
48
49    out.extend_from_slice(HASH_PREFIX);
50    out.extend_from_slice(&salt);
51    out.push(b'$');
52
53    let alpha = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
54
55    let n = u128::from_le_bytes(digest);
56
57    for i in 0..LEN_HASH {
58        let idx = (n >> i * 6) as usize & 0x3F;
59        out.push(alpha[idx]);
60    }
61
62    out
63}
64
65pub fn encode(password: &[u8], salt: [u8; 8]) -> [u8; DIGEST_SIZE] {
66    let mut md5 = Md5::new();
67
68    md5.update(password);
69    md5.update(HASH_PREFIX);
70    md5.update(&salt);
71
72    // md5(password, salt, password)
73    let extra: [u8; DIGEST_SIZE] = {
74        let mut md5 = Md5::new();
75        md5.update(password);
76        md5.update(&salt);
77        md5.update(password);
78        md5.finalize().into()
79    };
80
81    // password.len() many bytes of `extra` repeated
82    for _ in 0..password.len() / DIGEST_SIZE {
83        md5.update(&extra);
84    }
85
86    md5.update(&extra[..password.len() % DIGEST_SIZE]);
87
88    let mut i = password.len();
89
90    while i != 0 {
91        if i & 1 != 0 {
92            md5.update(&[0]);
93        }
94        else {
95            // If `password` is empty, we pass in
96            // what would be the null terminator in the C version.
97            md5.update(password.get(..1).unwrap_or(&[0]));
98        }
99        i >>= 1;
100    }
101
102    let mut digest: [u8; DIGEST_SIZE] = md5.finalize().into();
103
104    for i in 0..1000 {
105        let mut md5 = Md5::new();
106
107        if i & 1 != 0 {
108            md5.update(password);
109        }
110        else {
111            md5.update(&digest);
112        }
113
114        if i % 3 != 0 {
115            md5.update(&salt);
116        }
117
118        if i % 7 != 0 {
119            md5.update(password);
120        }
121
122        if i & 1 != 0 {
123            md5.update(&digest)
124        }
125        else {
126            md5.update(password);
127        }
128
129        digest = md5.finalize().into();
130    }
131
132    let output = [
133        digest[12], digest[ 6], digest[0],
134        digest[13], digest[ 7], digest[1],
135        digest[14], digest[ 8], digest[2],
136        digest[15], digest[ 9], digest[3],
137        digest[ 5], digest[10], digest[4],
138        digest[11]
139    ];
140
141    output
142}