gel_auth/
md5.rs

1//! MD5 password hashing.
2
3/// Computes the MD5 password hash used in PostgreSQL authentication.
4///
5/// This function implements the MD5 password hashing algorithm as specified in the
6/// PostgreSQL documentation for MD5 authentication.
7///
8/// # Algorithm
9///
10/// 1. Concatenate the password and username.
11/// 2. Calculate the MD5 hash of this concatenated string.
12/// 3. Concatenate the hexadecimal representation of the hash from step 2 with the salt.
13/// 4. Calculate the MD5 hash of the result from step 3.
14/// 5. Return the final hash as hex, prefixed with "md5".
15///
16/// # Example
17///
18/// ```
19/// # use gel_auth::md5::*;
20/// let password = "secret";
21/// let username = "user";
22/// let salt = [0x01, 0x02, 0x03, 0x04];
23/// let hash = md5_password(password, username, salt);
24/// assert_eq!(hash, "md5fccef98e4f1cf6cbe96b743fad4e8bd0");
25/// ```
26pub fn md5_password(password: &str, username: &str, salt: [u8; 4]) -> String {
27    StoredHash::generate(password.as_bytes(), username).salted(salt)
28}
29
30/// Converts a byte slice to a hexadecimal string.
31fn to_hex_string(bytes: &[u8]) -> String {
32    let mut hex = String::with_capacity(bytes.len() * 2);
33    for &byte in bytes {
34        hex.push_str(&format!("{byte:02x}"));
35    }
36    hex
37}
38
39/// Postgres stores `MD5(username || password)`.
40#[derive(Clone, Copy, Debug)]
41pub struct StoredHash {
42    pub hash: [u8; 16],
43}
44
45impl StoredHash {
46    pub fn generate(password: &[u8], username: &str) -> Self {
47        // First MD5 hash of password + username
48        let mut hasher = md5::Context::new();
49        hasher.consume(password);
50        hasher.consume(username.as_bytes());
51        let first_hash = hasher.compute();
52        Self { hash: first_hash.0 }
53    }
54
55    pub fn matches(&self, client_exchange: &[u8], salt: [u8; 4]) -> bool {
56        let server_exchange = self.salted(salt);
57        constant_time_eq::constant_time_eq(client_exchange, server_exchange.as_bytes())
58    }
59
60    pub fn salted(&self, salt: [u8; 4]) -> String {
61        let this = &self;
62        let salt: &[u8; 4] = &salt;
63        // Convert first hash to hex string
64        let first_hash_hex = to_hex_string(&this.hash);
65
66        // Second MD5 hash of first hash + salt
67        let mut hasher = md5::Context::new();
68        hasher.consume(first_hash_hex.as_bytes());
69        hasher.consume(salt);
70        let second_hash = hasher.compute();
71
72        // Convert second hash to hex string
73        let second_hash_hex = to_hex_string(&second_hash.0);
74
75        format!("md5{second_hash_hex}")
76    }
77}
78
79impl PartialEq for StoredHash {
80    fn eq(&self, other: &Self) -> bool {
81        constant_time_eq::constant_time_eq(&self.hash, &other.hash)
82    }
83}
84
85impl Eq for StoredHash {}