|
| 1 | +//! Functions to encrypt a password in the client. |
| 2 | +//! |
| 3 | +//! This is intended to be used by client applications that wish to |
| 4 | +//! send commands like `ALTER USER joe PASSWORD 'pwd'`. The password |
| 5 | +//! need not be sent in cleartext if it is encrypted on the client |
| 6 | +//! side. This is good because it ensures the cleartext password won't |
| 7 | +//! end up in logs pg_stat displays, etc. |
| 8 | +
|
| 9 | +use crate::authentication::sasl; |
| 10 | +use hmac::{Hmac, Mac, NewMac}; |
| 11 | +use md5::Md5; |
| 12 | +use rand::RngCore; |
| 13 | +use sha2::digest::FixedOutput; |
| 14 | +use sha2::{Digest, Sha256}; |
| 15 | + |
| 16 | +#[cfg(test)] |
| 17 | +mod test; |
| 18 | + |
| 19 | +const SCRAM_DEFAULT_ITERATIONS: u32 = 4096; |
| 20 | +const SCRAM_DEFAULT_SALT_LEN: usize = 16; |
| 21 | + |
| 22 | +/// Hash password using SCRAM-SHA-256 with a randomly-generated |
| 23 | +/// salt. |
| 24 | +/// |
| 25 | +/// The client may assume the returned string doesn't contain any |
| 26 | +/// special characters that would require escaping in an SQL command. |
| 27 | +pub fn scram_sha_256(password: &[u8]) -> String { |
| 28 | + let mut salt: [u8; SCRAM_DEFAULT_SALT_LEN] = [0; SCRAM_DEFAULT_SALT_LEN]; |
| 29 | + let mut rng = rand::thread_rng(); |
| 30 | + rng.fill_bytes(&mut salt); |
| 31 | + scram_sha_256_salt(password, salt) |
| 32 | +} |
| 33 | + |
| 34 | +// Internal implementation of scram_sha_256 with a caller-provided |
| 35 | +// salt. This is useful for testing. |
| 36 | +pub(crate) fn scram_sha_256_salt(password: &[u8], salt: [u8; SCRAM_DEFAULT_SALT_LEN]) -> String { |
| 37 | + // Prepare the password, per [RFC |
| 38 | + // 4013](https://tools.ietf.org/html/rfc4013), if possible. |
| 39 | + // |
| 40 | + // Postgres treats passwords as byte strings (without embedded NUL |
| 41 | + // bytes), but SASL expects passwords to be valid UTF-8. |
| 42 | + // |
| 43 | + // Follow the behavior of libpq's PQencryptPasswordConn(), and |
| 44 | + // also the backend. If the password is not valid UTF-8, or if it |
| 45 | + // contains prohibited characters (such as non-ASCII whitespace), |
| 46 | + // just skip the SASLprep step and use the original byte |
| 47 | + // sequence. |
| 48 | + let prepared: Vec<u8> = match std::str::from_utf8(password) { |
| 49 | + Ok(password_str) => { |
| 50 | + match stringprep::saslprep(password_str) { |
| 51 | + Ok(p) => p.into_owned().into_bytes(), |
| 52 | + // contains invalid characters; skip saslprep |
| 53 | + Err(_) => Vec::from(password), |
| 54 | + } |
| 55 | + } |
| 56 | + // not valid UTF-8; skip saslprep |
| 57 | + Err(_) => Vec::from(password), |
| 58 | + }; |
| 59 | + |
| 60 | + // salt password |
| 61 | + let salted_password = sasl::hi(&prepared, &salt, SCRAM_DEFAULT_ITERATIONS); |
| 62 | + |
| 63 | + // client key |
| 64 | + let mut hmac = |
| 65 | + Hmac::<Sha256>::new_varkey(&salted_password).expect("HMAC is able to accept all key sizes"); |
| 66 | + hmac.update(b"Client Key"); |
| 67 | + let client_key = hmac.finalize().into_bytes(); |
| 68 | + |
| 69 | + // stored key |
| 70 | + let mut hash = Sha256::default(); |
| 71 | + hash.update(client_key.as_slice()); |
| 72 | + let stored_key = hash.finalize_fixed(); |
| 73 | + |
| 74 | + // server key |
| 75 | + let mut hmac = |
| 76 | + Hmac::<Sha256>::new_varkey(&salted_password).expect("HMAC is able to accept all key sizes"); |
| 77 | + hmac.update(b"Server Key"); |
| 78 | + let server_key = hmac.finalize().into_bytes(); |
| 79 | + |
| 80 | + format!( |
| 81 | + "SCRAM-SHA-256${}:{}${}:{}", |
| 82 | + SCRAM_DEFAULT_ITERATIONS, |
| 83 | + base64::encode(salt), |
| 84 | + base64::encode(stored_key), |
| 85 | + base64::encode(server_key) |
| 86 | + ) |
| 87 | +} |
| 88 | + |
| 89 | +/// **Not recommended, as MD5 is not considered to be secure.** |
| 90 | +/// |
| 91 | +/// Hash password using MD5 with the username as the salt. |
| 92 | +/// |
| 93 | +/// The client may assume the returned string doesn't contain any |
| 94 | +/// special characters that would require escaping. |
| 95 | +pub fn md5(password: &[u8], username: &str) -> String { |
| 96 | + // salt password with username |
| 97 | + let mut salted_password = Vec::from(password); |
| 98 | + salted_password.extend_from_slice(username.as_bytes()); |
| 99 | + |
| 100 | + let mut hash = Md5::new(); |
| 101 | + hash.update(&salted_password); |
| 102 | + let digest = hash.finalize(); |
| 103 | + format!("md5{:x}", digest) |
| 104 | +} |
0 commit comments