Skip to content

Commit e4e48a0

Browse files
committed
Add password module for hashing on the client side.
Hashing a password on the client side is useful so that you can set a user's password without ever sending it in plain text to the server. This avoids leaking passwords in the log or elsewhere.
1 parent f6fedb8 commit e4e48a0

File tree

4 files changed

+125
-1
lines changed

4 files changed

+125
-1
lines changed

postgres-protocol/src/authentication/sasl.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ fn normalize(pass: &[u8]) -> Vec<u8> {
3232
}
3333
}
3434

35-
fn hi(str: &[u8], salt: &[u8], i: u32) -> [u8; 32] {
35+
pub(crate) fn hi(str: &[u8], salt: &[u8], i: u32) -> [u8; 32] {
3636
let mut hmac = Hmac::<Sha256>::new_varkey(str).expect("HMAC is able to accept all key sizes");
3737
hmac.update(salt);
3838
hmac.update(&[0, 0, 0, 1]);

postgres-protocol/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::io;
1919
pub mod authentication;
2020
pub mod escape;
2121
pub mod message;
22+
pub mod password;
2223
pub mod types;
2324

2425
/// A Postgres OID.

postgres-protocol/src/password/mod.rs

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use crate::password;
2+
3+
#[test]
4+
fn test_encrypt_scram_sha_256() {
5+
// Specify the salt to make the test deterministic. Any bytes will do.
6+
let salt: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
7+
assert_eq!(
8+
password::scram_sha_256_salt(b"secret", salt),
9+
"SCRAM-SHA-256$4096:AQIDBAUGBwgJCgsMDQ4PEA==$8rrDg00OqaiWXJ7p+sCgHEIaBSHY89ZJl3mfIsf32oY=:05L1f+yZbiN8O0AnO40Og85NNRhvzTS57naKRWCcsIA="
10+
);
11+
}
12+
13+
#[test]
14+
fn test_encrypt_md5() {
15+
assert_eq!(
16+
password::md5(b"secret", "foo"),
17+
"md54ab2c5d00339c4b2a4e921d2dc4edec7"
18+
);
19+
}

0 commit comments

Comments
 (0)