openstack_keystone_core/common/
password_hashing.rs1use std::cmp::max;
16use std::str;
17use thiserror::Error;
18use tokio::task;
19use tracing::warn;
20
21use crate::config::{Config, PasswordHashingAlgo};
22
23#[derive(Error, Debug)]
25pub enum PasswordHashError {
26 #[error(transparent)]
28 BCrypt {
29 #[from]
31 source: bcrypt::BcryptError,
32 },
33
34 #[error(transparent)]
36 Join {
37 #[from]
39 source: tokio::task::JoinError,
40 },
41
42 #[error(transparent)]
44 Utf8 {
45 #[from]
47 source: str::Utf8Error,
48 },
49}
50
51fn verify_length_and_trunc_password(password: &[u8], max_length: usize) -> &[u8] {
52 if password.len() > max_length {
53 warn!("Truncating password to the specified value");
54 return &password[..max_length];
55 }
56 password
57}
58
59pub async fn hash_password<S: AsRef<[u8]>>(
61 conf: &Config,
62 password: S,
63) -> Result<String, PasswordHashError> {
64 match conf.identity.password_hashing_algorithm {
65 PasswordHashingAlgo::Bcrypt => {
66 let password_bytes = verify_length_and_trunc_password(
67 password.as_ref(),
68 max(conf.identity.max_password_length, 72),
69 )
70 .to_owned();
71 let rounds = conf.identity.password_hash_rounds.unwrap_or(12);
72 let hash =
73 task::spawn_blocking(move || bcrypt::hash(password_bytes, rounds as u32)).await??;
74 Ok(hash)
75 }
76 PasswordHashingAlgo::None => Ok(str::from_utf8(password.as_ref())?.to_string()),
78 }
79}
80
81pub async fn verify_password<P: AsRef<[u8]>, H: AsRef<str>>(
83 conf: &Config,
84 password: P,
85 hash: H,
86) -> Result<bool, PasswordHashError> {
87 match conf.identity.password_hashing_algorithm {
88 PasswordHashingAlgo::Bcrypt => {
89 let password_bytes = verify_length_and_trunc_password(
90 password.as_ref(),
91 max(conf.identity.max_password_length, 72),
92 )
93 .to_owned();
94 let password_hash = hash.as_ref().to_string();
95 match task::spawn_blocking(move || bcrypt::verify(password_bytes, &password_hash))
97 .await?
98 {
99 Ok(res) => Ok(res),
100 Err(bcrypt::BcryptError::InvalidHash(..)) => {
101 warn!("Bcrypt hash verification error: bad hash");
103 Ok(false)
104 }
105 other => {
106 warn!("Bcrypt hash verification error: {other:?}");
107 Ok(false)
108 }
109 }
110 }
111 PasswordHashingAlgo::None => Ok(str::from_utf8(password.as_ref())?.eq(hash.as_ref())),
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use rand::distr::{Alphanumeric, SampleString};
120 use tracing_test::traced_test;
121
122 #[test]
123 fn test_verify_length_and_trunc_password() {
124 assert_eq!(
125 b"abcdefg",
126 verify_length_and_trunc_password("abcdefg".as_bytes(), 70)
127 );
128 assert_eq!(
129 b"abcd",
130 verify_length_and_trunc_password("abcdefg".as_bytes(), 4)
131 );
132 assert_eq!(
134 b"\xE2\x98\x81a",
135 verify_length_and_trunc_password("☁abcdefg".as_bytes(), 4)
136 );
137 }
138
139 #[tokio::test]
140 #[traced_test]
141 async fn test_hash_bcrypt() {
142 let builder = config::Config::builder()
143 .set_override("auth.methods", "")
144 .unwrap()
145 .set_override("database.connection", "dummy")
146 .unwrap();
147 let conf: Config = Config::try_from(builder).expect("can build a valid config");
148 let pass = "abcdefg";
149 let hashed = hash_password(&conf, &pass).await.unwrap();
150 assert!(!logs_contain(pass));
151 assert!(!logs_contain(&hashed));
152 }
153
154 #[tokio::test]
155 #[traced_test]
156 async fn test_roundtrip_bcrypt() {
157 let builder = config::Config::builder()
158 .set_override("auth.methods", "")
159 .unwrap()
160 .set_override("database.connection", "dummy")
161 .unwrap();
162 let conf: Config = Config::try_from(builder).expect("can build a valid config");
163 let pass = "abcdefg";
164 let hashed = hash_password(&conf, &pass).await.unwrap();
165 assert!(verify_password(&conf, &pass, &hashed).await.unwrap());
166 assert!(!logs_contain(pass));
167 assert!(!logs_contain(&hashed));
168 }
169
170 #[tokio::test]
171 #[traced_test]
172 async fn test_roundtrip_bcrypt_longer_than_72() {
173 let builder = config::Config::builder()
174 .set_override("auth.methods", "")
175 .unwrap()
176 .set_override("database.connection", "dummy")
177 .unwrap();
178 let conf: Config = Config::try_from(builder).expect("can build a valid config");
179 let pass = Alphanumeric.sample_string(&mut rand::rng(), 80);
180 let hashed = hash_password(&conf, &pass).await.unwrap();
181 assert!(verify_password(&conf, &pass, &hashed).await.unwrap());
182 assert!(!logs_contain(&pass));
183 assert!(!logs_contain(&hashed));
184 }
185
186 #[tokio::test]
187 #[traced_test]
188 async fn test_roundtrip_bcrypt_mismatch() {
189 let builder = config::Config::builder()
190 .set_override("auth.methods", "")
191 .unwrap()
192 .set_override("database.connection", "dummy")
193 .unwrap();
194 let conf: Config = Config::try_from(builder).expect("can build a valid config");
195 let pass = Alphanumeric.sample_string(&mut rand::rng(), 80);
196 let hashed = hash_password(&conf, "other password").await.unwrap();
197 assert!(!verify_password(&conf, &pass, &hashed).await.unwrap());
198 assert!(!logs_contain(&pass));
199 assert!(!logs_contain(&hashed));
200 }
201
202 #[tokio::test]
203 #[traced_test]
204 async fn test_roundtrip_bcrypt_bad_hash() {
205 let builder = config::Config::builder()
206 .set_override("auth.methods", "")
207 .unwrap()
208 .set_override("database.connection", "dummy")
209 .unwrap();
210 let conf: Config = Config::try_from(builder).expect("can build a valid config");
211 let pass = Alphanumeric.sample_string(&mut rand::rng(), 80);
212 assert!(!verify_password(&conf, &pass, "foobar").await.unwrap());
213 assert!(!logs_contain("foobar"));
214 assert!(!logs_contain(&pass));
215 }
216}