Skip to main content

auth/
user_provider.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub(crate) mod static_user_provider;
16pub(crate) mod watch_file_user_provider;
17
18use std::collections::HashMap;
19use std::fs::File;
20use std::io::BufRead;
21use std::path::Path;
22use std::{fmt, io};
23
24use common_base::secrets::ExposeSecret;
25use pbkdf2::pbkdf2_hmac;
26use sha2::Sha256;
27use snafu::{OptionExt, ResultExt, ensure};
28use subtle::ConstantTimeEq;
29
30use crate::common::{
31    DEFAULT_PBKDF2_SHA256_SALT_LEN, Identity, MAX_PBKDF2_SHA256_ITERATIONS,
32    MAX_PBKDF2_SHA256_SALT_LEN, PBKDF2_SHA256_HASH_LEN, PG_SCRAM_SHA256_KEY_LEN, Password,
33    PgScramSha256Verifier, auth_mysql_with_hash_stage_2,
34};
35use crate::error::{
36    IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedPasswordTypeSnafu,
37    UserNotFoundSnafu, UserPasswordMismatchSnafu,
38};
39use crate::user_info::{DefaultUserInfo, PermissionMode};
40use crate::{UserInfoRef, auth_mysql};
41
42#[async_trait::async_trait]
43pub trait UserProvider: Send + Sync {
44    fn name(&self) -> &str;
45
46    /// Checks whether a user is valid and allowed to access the database.
47    async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
48
49    /// Checks whether a connection request
50    /// from a certain user to a certain catalog/schema is legal.
51    /// This method should be called after [authenticate()](UserProvider::authenticate()).
52    async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
53
54    /// Combination of [authenticate()](UserProvider::authenticate()) and [authorize()](UserProvider::authorize()).
55    /// In most cases it's preferred for both convenience and performance.
56    async fn auth(
57        &self,
58        id: Identity<'_>,
59        password: Password<'_>,
60        catalog: &str,
61        schema: &str,
62    ) -> Result<UserInfoRef> {
63        let user_info = self.authenticate(id, password).await?;
64        self.authorize(catalog, schema, &user_info).await?;
65        Ok(user_info)
66    }
67
68    async fn postgres_auth_info(&self, _id: Identity<'_>) -> Result<PgAuthInfo> {
69        Ok(PgAuthInfo::Cleartext)
70    }
71
72    /// Returns whether this user provider implementation is backed by an external system.
73    fn external(&self) -> bool {
74        false
75    }
76}
77
78pub enum PgAuthInfo {
79    ScramSha256 {
80        verifier: PgScramSha256Verifier,
81        user_info: Option<UserInfoRef>,
82    },
83    Cleartext,
84}
85
86#[derive(Clone)]
87pub(crate) enum PasswordVerifier {
88    PlainText {
89        password: String,
90        /// SCRAM verifier derived once at load time. Precomputing it keeps the
91        /// Postgres SCRAM `server-first-message` (stable salt, fixed iterations)
92        /// and per-connection cost indistinguishable from stored-hash and
93        /// unknown users, instead of running PBKDF2 with a fresh salt on every
94        /// connection.
95        scram: PgScramSha256Verifier,
96    },
97    Pbkdf2Sha256 {
98        iterations: u32,
99        salt: Vec<u8>,
100        hash: Vec<u8>,
101    },
102    MysqlNativePassword {
103        hash_stage_2: Vec<u8>,
104    },
105    PgScramSha256(PgScramSha256Verifier),
106}
107
108impl PartialEq for PasswordVerifier {
109    fn eq(&self, other: &Self) -> bool {
110        // The precomputed SCRAM verifier is a cache derived from the password, so
111        // two plaintext verifiers are equal iff their passwords match.
112        match (self, other) {
113            (
114                PasswordVerifier::PlainText { password: a, .. },
115                PasswordVerifier::PlainText { password: b, .. },
116            ) => a == b,
117            (
118                PasswordVerifier::Pbkdf2Sha256 {
119                    iterations: i1,
120                    salt: s1,
121                    hash: h1,
122                },
123                PasswordVerifier::Pbkdf2Sha256 {
124                    iterations: i2,
125                    salt: s2,
126                    hash: h2,
127                },
128            ) => i1 == i2 && s1 == s2 && h1 == h2,
129            (
130                PasswordVerifier::MysqlNativePassword { hash_stage_2: a },
131                PasswordVerifier::MysqlNativePassword { hash_stage_2: b },
132            ) => a == b,
133            (PasswordVerifier::PgScramSha256(a), PasswordVerifier::PgScramSha256(b)) => a == b,
134            _ => false,
135        }
136    }
137}
138
139impl Eq for PasswordVerifier {}
140
141impl fmt::Debug for PasswordVerifier {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            PasswordVerifier::PlainText { .. } => {
145                f.debug_tuple("PlainText").field(&"<REDACTED>").finish()
146            }
147            PasswordVerifier::Pbkdf2Sha256 { iterations, .. } => f
148                .debug_struct("Pbkdf2Sha256")
149                .field("iterations", iterations)
150                .field("salt", &"<REDACTED>")
151                .field("hash", &"<REDACTED>")
152                .finish(),
153            PasswordVerifier::MysqlNativePassword { .. } => f
154                .debug_struct("MysqlNativePassword")
155                .field("hash_stage_2", &"<REDACTED>")
156                .finish(),
157            PasswordVerifier::PgScramSha256(_) => f
158                .debug_struct("PgScramSha256")
159                .field("verifier", &"<REDACTED>")
160                .finish(),
161        }
162    }
163}
164
165impl PasswordVerifier {
166    /// Builds a plaintext verifier, precomputing a stable SCRAM verifier so the
167    /// Postgres SCRAM handshake for this user carries a stable salt and runs no
168    /// per-connection PBKDF2. Uses [`DEFAULT_PBKDF2_SHA256_ITERATIONS`] to match
169    /// the mock verifier handed to unknown users.
170    fn plain_text(password: String) -> Option<Self> {
171        let salt = rand::random::<[u8; DEFAULT_PBKDF2_SHA256_SALT_LEN]>();
172        let scram = PgScramSha256Verifier::from_password(
173            password.as_bytes(),
174            &salt,
175            crate::DEFAULT_PBKDF2_SHA256_ITERATIONS,
176        )
177        .ok()?;
178        Some(Self::PlainText { password, scram })
179    }
180
181    fn parse(input: &str) -> Option<Self> {
182        if let Some(password) = input.strip_prefix("plain:") {
183            return Self::plain_text(password.to_string());
184        }
185
186        if let Some(verifier) = input.strip_prefix("pbkdf2_sha256:") {
187            let mut parts = verifier.split(':');
188            let iterations = parts.next()?.parse::<u32>().ok()?;
189            let salt = hex::decode(parts.next()?).ok()?;
190            let hash = hex::decode(parts.next()?).ok()?;
191            if parts.next().is_some()
192                || iterations == 0
193                || iterations > MAX_PBKDF2_SHA256_ITERATIONS
194                || salt.is_empty()
195                || salt.len() > MAX_PBKDF2_SHA256_SALT_LEN
196                || hash.len() != PBKDF2_SHA256_HASH_LEN
197            {
198                return None;
199            }
200
201            return Some(Self::Pbkdf2Sha256 {
202                iterations,
203                salt,
204                hash,
205            });
206        }
207
208        if let Some(verifier) = input.strip_prefix("mysql_native_password:") {
209            let hash_stage_2 = hex::decode(verifier).ok()?;
210            if hash_stage_2.len() != 20 {
211                return None;
212            }
213
214            return Some(Self::MysqlNativePassword { hash_stage_2 });
215        }
216
217        if let Some(verifier) = input.strip_prefix("pg_scram_sha256:") {
218            let mut parts = verifier.split(':');
219            let iterations = parts.next()?.parse::<u32>().ok()?;
220            let salt = hex::decode(parts.next()?).ok()?;
221            let stored_key = hex::decode(parts.next()?).ok()?;
222            let server_key = hex::decode(parts.next()?).ok()?;
223            if parts.next().is_some()
224                || stored_key.len() != PG_SCRAM_SHA256_KEY_LEN
225                || server_key.len() != PG_SCRAM_SHA256_KEY_LEN
226            {
227                return None;
228            }
229
230            return PgScramSha256Verifier::new(iterations, salt, stored_key, server_key)
231                .ok()
232                .map(Self::PgScramSha256);
233        }
234
235        Self::plain_text(input.to_string())
236    }
237
238    fn supports_pg_scram_sha256(&self) -> bool {
239        matches!(
240            self,
241            PasswordVerifier::PlainText { .. } | PasswordVerifier::PgScramSha256(_)
242        )
243    }
244
245    fn to_pg_scram_sha256_verifier(&self) -> Option<PgScramSha256Verifier> {
246        match self {
247            PasswordVerifier::PlainText { scram, .. } => Some(scram.clone()),
248            // Legacy PBKDF2 verifiers were derived without SASLprep, and the
249            // original password is unavailable to normalize them safely.
250            PasswordVerifier::Pbkdf2Sha256 { .. } => None,
251            PasswordVerifier::PgScramSha256(verifier) => Some(verifier.clone()),
252            PasswordVerifier::MysqlNativePassword { .. } => None,
253        }
254    }
255
256    fn verify_plain_text(&self, password: &str) -> bool {
257        match self {
258            PasswordVerifier::PlainText {
259                password: expected, ..
260            } => expected.as_bytes().ct_eq(password.as_bytes()).into(),
261            PasswordVerifier::Pbkdf2Sha256 {
262                iterations,
263                salt,
264                hash,
265            } => {
266                if hash.len() != PBKDF2_SHA256_HASH_LEN {
267                    return false;
268                }
269                let mut actual = [0u8; PBKDF2_SHA256_HASH_LEN];
270                pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, *iterations, &mut actual);
271                hash.as_slice().ct_eq(&actual[..]).into()
272            }
273            PasswordVerifier::MysqlNativePassword { .. } => false,
274            PasswordVerifier::PgScramSha256(verifier) => verifier
275                .verify_plain_password(password.as_bytes())
276                .unwrap_or(false),
277        }
278    }
279
280    fn verify_mysql_native_password(
281        &self,
282        auth_data: &[u8],
283        salt: &[u8],
284        username: &str,
285    ) -> Result<()> {
286        match self {
287            PasswordVerifier::PlainText { password, .. } => {
288                auth_mysql(auth_data, salt, username, password.as_bytes())
289            }
290            PasswordVerifier::MysqlNativePassword { hash_stage_2 } => {
291                auth_mysql_with_hash_stage_2(auth_data, salt, username, hash_stage_2)
292            }
293            PasswordVerifier::Pbkdf2Sha256 { .. } => UnsupportedPasswordTypeSnafu {
294                password_type: "mysql_native_password_with_pbkdf2_sha256_verifier",
295            }
296            .fail(),
297            PasswordVerifier::PgScramSha256(_) => UnsupportedPasswordTypeSnafu {
298                password_type: "mysql_native_password_with_pg_scram_sha256_verifier",
299            }
300            .fail(),
301        }
302    }
303}
304
305/// Type alias for user info map.
306/// Key is username, value is (password verifier, permission_mode).
307pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
308
309fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
310    // check valid path
311    let path = Path::new(filepath);
312    if !path.exists() {
313        return InvalidConfigSnafu {
314            value: filepath.to_string(),
315            msg: "UserProvider file must exist",
316        }
317        .fail();
318    }
319
320    ensure!(
321        path.is_file(),
322        InvalidConfigSnafu {
323            value: filepath,
324            msg: "UserProvider file must be a file",
325        }
326    );
327    let file = File::open(path).context(IoSnafu)?;
328    let credential = io::BufReader::new(file)
329        .lines()
330        .map_while(std::result::Result::ok)
331        .filter_map(|line| {
332            // The line format is:
333            // - `username=password` - Basic user with default permissions
334            // - `username:permission_mode=password` - User with specific permission mode
335            // - Lines starting with '#' are treated as comments and ignored
336            // - Empty lines are ignored
337            let line = line.trim();
338            if line.is_empty() || line.starts_with('#') {
339                return None;
340            }
341
342            parse_credential_line(line)
343        })
344        .collect::<HashMap<String, _>>();
345
346    ensure!(
347        !credential.is_empty(),
348        InvalidConfigSnafu {
349            value: filepath,
350            msg: "UserProvider's file must contains at least one valid credential",
351        }
352    );
353
354    Ok(credential)
355}
356
357/// Parse a line of credential in the format of `username=password` or `username:permission_mode=password`.
358///
359/// The password part accepts legacy plain text and explicit verifier formats:
360/// - `plain:<password>`
361/// - `pbkdf2_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-hash>`
362/// - `mysql_native_password:<hex-encoded-sha1-sha1-password>`
363/// - `pg_scram_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-stored-key>:<hex-encoded-server-key>`
364pub(crate) fn parse_credential_line(
365    line: &str,
366) -> Option<(String, (PasswordVerifier, PermissionMode))> {
367    let parts = line.split('=').collect::<Vec<&str>>();
368    if parts.len() != 2 {
369        return None;
370    }
371
372    let (username_part, password) = (parts[0], parts[1]);
373    let (username, permission_mode) = if let Some((user, perm)) = username_part.split_once(':') {
374        (user, PermissionMode::from_str(perm))
375    } else {
376        (username_part, PermissionMode::default())
377    };
378
379    let verifier = PasswordVerifier::parse(password)?;
380
381    Some((username.to_string(), (verifier, permission_mode)))
382}
383
384pub(crate) fn postgres_auth_info_with_credential(
385    users: &UserInfoMap,
386    input_id: Identity<'_>,
387) -> Result<PgAuthInfo> {
388    match input_id {
389        Identity::UserId(username, _) => {
390            ensure!(
391                !username.is_empty(),
392                IllegalParamSnafu {
393                    msg: "blank username"
394                }
395            );
396
397            if !users
398                .values()
399                .all(|(verifier, _)| verifier.supports_pg_scram_sha256())
400            {
401                // PostgreSQL chooses one auth method during startup. Selecting it
402                // per username would expose user or verifier existence.
403                return Ok(PgAuthInfo::Cleartext);
404            }
405
406            if let Some((verifier, permission_mode)) = users.get(username) {
407                if let Some(verifier) = verifier.to_pg_scram_sha256_verifier() {
408                    return Ok(PgAuthInfo::ScramSha256 {
409                        verifier,
410                        user_info: Some(DefaultUserInfo::with_name_and_permission(
411                            username,
412                            *permission_mode,
413                        )),
414                    });
415                }
416
417                return Ok(PgAuthInfo::Cleartext);
418            }
419
420            // Unknown user: hand back a deterministic mock verifier so the SCRAM
421            // handshake is indistinguishable from a real user, without running
422            // PBKDF2 or leaking existence through an unstable salt.
423            Ok(PgAuthInfo::ScramSha256 {
424                verifier: PgScramSha256Verifier::mock_for_unknown_user(username.as_bytes()),
425                user_info: None,
426            })
427        }
428    }
429}
430
431fn authenticate_with_credential(
432    users: &UserInfoMap,
433    input_id: Identity<'_>,
434    input_pwd: Password<'_>,
435) -> Result<UserInfoRef> {
436    match input_id {
437        Identity::UserId(username, _) => {
438            ensure!(
439                !username.is_empty(),
440                IllegalParamSnafu {
441                    msg: "blank username"
442                }
443            );
444            let (verifier, permission_mode) = users.get(username).context(UserNotFoundSnafu {
445                username: username.to_string(),
446            })?;
447
448            match input_pwd {
449                Password::PlainText(pwd) => {
450                    ensure!(
451                        !pwd.expose_secret().is_empty(),
452                        IllegalParamSnafu {
453                            msg: "blank password"
454                        }
455                    );
456                    if verifier.verify_plain_text(pwd.expose_secret()) {
457                        Ok(DefaultUserInfo::with_name_and_permission(
458                            username,
459                            *permission_mode,
460                        ))
461                    } else {
462                        UserPasswordMismatchSnafu {
463                            username: username.to_string(),
464                        }
465                        .fail()
466                    }
467                }
468                Password::MysqlNativePassword(auth_data, salt) => verifier
469                    .verify_mysql_native_password(auth_data, salt, username)
470                    .map(|_| DefaultUserInfo::with_name_and_permission(username, *permission_mode)),
471                Password::PgMD5(_, _) => UnsupportedPasswordTypeSnafu {
472                    password_type: "pg_md5",
473                }
474                .fail(),
475            }
476        }
477    }
478}
479#[cfg(test)]
480mod tests {
481    use digest::Digest;
482    use sha1::Sha1;
483
484    use super::*;
485    use crate::common::{format_pg_scram_sha256_password_verifier, mysql_native_password_hash};
486
487    fn plain(password: &str) -> PasswordVerifier {
488        PasswordVerifier::plain_text(password.to_string()).unwrap()
489    }
490
491    fn sha1_one(data: &[u8]) -> Vec<u8> {
492        let mut hasher = Sha1::new();
493        hasher.update(data);
494        hasher.finalize().to_vec()
495    }
496
497    fn mysql_native_password_auth_data(password: &str, salt: &[u8]) -> Vec<u8> {
498        let hash_stage_1 = sha1_one(password.as_bytes());
499        let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
500        let mut hasher = Sha1::new();
501        hasher.update(salt);
502        hasher.update(hash_stage_2);
503        let scramble = hasher.finalize();
504
505        hash_stage_1
506            .iter()
507            .zip(scramble.iter())
508            .map(|(lhs, rhs)| lhs ^ rhs)
509            .collect()
510    }
511
512    #[test]
513    fn test_parse_credential_line() {
514        // Basic username=password format
515        let result = parse_credential_line("admin=password123");
516        assert_eq!(
517            result,
518            Some((
519                "admin".to_string(),
520                (plain("password123"), PermissionMode::default())
521            ))
522        );
523
524        // Username with permission mode
525        let result = parse_credential_line("user:ReadOnly=secret");
526        assert_eq!(
527            result,
528            Some((
529                "user".to_string(),
530                (plain("secret"), PermissionMode::ReadOnly)
531            ))
532        );
533        let result = parse_credential_line("user:ro=secret");
534        assert_eq!(
535            result,
536            Some((
537                "user".to_string(),
538                (plain("secret"), PermissionMode::ReadOnly)
539            ))
540        );
541        // Username with WriteOnly permission mode
542        let result = parse_credential_line("writer:WriteOnly=mypass");
543        assert_eq!(
544            result,
545            Some((
546                "writer".to_string(),
547                (plain("mypass"), PermissionMode::WriteOnly)
548            ))
549        );
550
551        // Username with 'wo' as WriteOnly permission shorthand
552        let result = parse_credential_line("writer:wo=mypass");
553        assert_eq!(
554            result,
555            Some((
556                "writer".to_string(),
557                (plain("mypass"), PermissionMode::WriteOnly)
558            ))
559        );
560
561        // Username with complex password containing special characters
562        let result = parse_credential_line("admin:rw=p@ssw0rd!123");
563        assert_eq!(
564            result,
565            Some((
566                "admin".to_string(),
567                (plain("p@ssw0rd!123"), PermissionMode::ReadWrite)
568            ))
569        );
570
571        // Username with spaces should be preserved
572        let result = parse_credential_line("user name:WriteOnly=password");
573        assert_eq!(
574            result,
575            Some((
576                "user name".to_string(),
577                (plain("password"), PermissionMode::WriteOnly)
578            ))
579        );
580
581        let result = parse_credential_line("user=plain:password");
582        assert_eq!(
583            result,
584            Some((
585                "user".to_string(),
586                (plain("password"), PermissionMode::default())
587            ))
588        );
589
590        let iterations = 4096;
591        let salt = b"salt";
592        let mut hash = [0u8; 32];
593        pbkdf2_hmac::<Sha256>("password".as_bytes(), salt, iterations, &mut hash);
594        let result = parse_credential_line(&format!(
595            "user=pbkdf2_sha256:{iterations}:{}:{}",
596            hex::encode(salt),
597            hex::encode(hash)
598        ));
599        assert_eq!(
600            result,
601            Some((
602                "user".to_string(),
603                (
604                    PasswordVerifier::Pbkdf2Sha256 {
605                        iterations,
606                        salt: salt.to_vec(),
607                        hash: hash.to_vec(),
608                    },
609                    PermissionMode::default()
610                )
611            ))
612        );
613
614        let result = parse_credential_line("user=pbkdf2_sha256:4096:not-hex:abcd");
615        assert_eq!(result, None);
616
617        // A well-formed but truncated hash must be rejected: a short hash would let
618        // many wrong passwords pass by matching only a few derived bytes.
619        let result = parse_credential_line(&format!(
620            "user=pbkdf2_sha256:4096:{}:abcd",
621            hex::encode(salt)
622        ));
623        assert_eq!(result, None);
624
625        let result = parse_credential_line(&format!(
626            "user=pbkdf2_sha256:{}:{}:{}",
627            MAX_PBKDF2_SHA256_ITERATIONS + 1,
628            hex::encode(salt),
629            hex::encode(hash)
630        ));
631        assert_eq!(result, None);
632
633        let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
634        let result = parse_credential_line(&format!(
635            "user=mysql_native_password:{}",
636            hex::encode(&hash_stage_2)
637        ));
638        assert_eq!(
639            result,
640            Some((
641                "user".to_string(),
642                (
643                    PasswordVerifier::MysqlNativePassword { hash_stage_2 },
644                    PermissionMode::default()
645                )
646            ))
647        );
648
649        let result = parse_credential_line("user=mysql_native_password:abcd");
650        assert_eq!(result, None);
651
652        let verifier =
653            format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
654        let result = parse_credential_line(&format!("user={verifier}"));
655        assert!(matches!(
656            result,
657            Some((
658                _,
659                (
660                    PasswordVerifier::PgScramSha256(PgScramSha256Verifier { .. }),
661                    PermissionMode::ReadWrite
662                )
663            ))
664        ));
665
666        let result = parse_credential_line("user=pg_scram_sha256:4096:73616c74:abcd:abcd");
667        assert_eq!(result, None);
668
669        // Invalid format - no equals sign
670        let result = parse_credential_line("invalid_line");
671        assert_eq!(result, None);
672
673        // Invalid format - multiple equals signs
674        let result = parse_credential_line("user=pass=word");
675        assert_eq!(result, None);
676
677        // Empty password
678        let result = parse_credential_line("user=");
679        assert_eq!(
680            result,
681            Some(("user".to_string(), (plain(""), PermissionMode::default())))
682        );
683
684        // Empty username
685        let result = parse_credential_line("=password");
686        assert_eq!(
687            result,
688            Some((
689                "".to_string(),
690                (plain("password"), PermissionMode::default())
691            ))
692        );
693    }
694
695    #[test]
696    fn test_authenticate_with_mysql_native_password_verifier() {
697        let password = "password";
698        let salt = b"12345678901234567890";
699        let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
700        let auth_data = mysql_native_password_auth_data(password, salt);
701        let users = HashMap::from([(
702            "user".to_string(),
703            (
704                PasswordVerifier::MysqlNativePassword { hash_stage_2 },
705                PermissionMode::default(),
706            ),
707        )]);
708
709        let result = authenticate_with_credential(
710            &users,
711            Identity::UserId("user", None),
712            Password::MysqlNativePassword(&auth_data, salt),
713        );
714
715        assert!(result.is_ok());
716    }
717
718    #[test]
719    fn test_authenticate_with_plain_text_mysql_native_password() {
720        let password = "password";
721        let salt = b"12345678901234567890";
722        let auth_data = mysql_native_password_auth_data(password, salt);
723        let users = HashMap::from([(
724            "user".to_string(),
725            (
726                PasswordVerifier::plain_text(password.to_string()).unwrap(),
727                PermissionMode::default(),
728            ),
729        )]);
730
731        let result = authenticate_with_credential(
732            &users,
733            Identity::UserId("user", None),
734            Password::MysqlNativePassword(&auth_data, salt),
735        );
736
737        assert!(result.is_ok());
738    }
739
740    #[test]
741    fn test_pbkdf2_sha256_rejects_mysql_native_password() {
742        let password = "password";
743        let salt = b"salt";
744        let iterations = 4096;
745        let mut hash = [0u8; 32];
746        pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
747        let users = HashMap::from([(
748            "user".to_string(),
749            (
750                PasswordVerifier::Pbkdf2Sha256 {
751                    iterations,
752                    salt: salt.to_vec(),
753                    hash: hash.to_vec(),
754                },
755                PermissionMode::default(),
756            ),
757        )]);
758        let mysql_salt = b"12345678901234567890";
759        let auth_data = mysql_native_password_auth_data(password, mysql_salt);
760
761        let result = authenticate_with_credential(
762            &users,
763            Identity::UserId("user", None),
764            Password::MysqlNativePassword(&auth_data, mysql_salt),
765        );
766
767        assert!(result.is_err());
768    }
769
770    #[test]
771    fn test_authenticate_with_pg_scram_sha256_verifier_plain_text() {
772        let verifier =
773            format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
774        let (_, user) = parse_credential_line(&format!("user={verifier}")).unwrap();
775        let users = HashMap::from([("user".to_string(), user)]);
776
777        let result = authenticate_with_credential(
778            &users,
779            Identity::UserId("user", None),
780            Password::PlainText("password".to_string().into()),
781        );
782        assert!(result.is_ok());
783
784        let result = authenticate_with_credential(
785            &users,
786            Identity::UserId("user", None),
787            Password::PlainText("wrong".to_string().into()),
788        );
789        assert!(result.is_err());
790    }
791
792    #[test]
793    fn test_plain_text_scram_verifier_is_stable() {
794        let verifier = PasswordVerifier::plain_text("password".to_string()).unwrap();
795        let first = verifier.to_pg_scram_sha256_verifier().unwrap();
796        let second = verifier.to_pg_scram_sha256_verifier().unwrap();
797
798        // A plaintext-backed user must present a stable salt and fixed iterations
799        // across connections without re-running PBKDF2, otherwise the SCRAM
800        // server-first message (and its timing) leaks that the user exists.
801        assert_eq!(first.salt(), second.salt());
802        assert_eq!(first.salt().len(), DEFAULT_PBKDF2_SHA256_SALT_LEN);
803        assert_eq!(first.iterations(), crate::DEFAULT_PBKDF2_SHA256_ITERATIONS);
804
805        // The derived verifier must still accept the real password.
806        assert!(first.verify_plain_password(b"password").unwrap());
807        assert!(!first.verify_plain_password(b"wrong").unwrap());
808    }
809
810    #[test]
811    fn test_postgres_auth_info_uses_scram_for_unknown_user() {
812        let verifier =
813            format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
814        let (_, user) = parse_credential_line(&format!("user={verifier}")).unwrap();
815        let users = HashMap::from([("user".to_string(), user)]);
816
817        let auth_info =
818            postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
819        assert!(matches!(
820            auth_info,
821            PgAuthInfo::ScramSha256 {
822                user_info: None,
823                ..
824            }
825        ));
826    }
827
828    #[test]
829    fn test_postgres_auth_info_falls_back_to_cleartext() {
830        let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
831        let users = HashMap::from([(
832            "user".to_string(),
833            (
834                PasswordVerifier::MysqlNativePassword { hash_stage_2 },
835                PermissionMode::default(),
836            ),
837        )]);
838
839        let auth_info =
840            postgres_auth_info_with_credential(&users, Identity::UserId("user", None)).unwrap();
841        assert!(matches!(auth_info, PgAuthInfo::Cleartext));
842
843        let auth_info =
844            postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
845        assert!(matches!(auth_info, PgAuthInfo::Cleartext));
846    }
847
848    #[test]
849    fn test_postgres_auth_info_with_pbkdf2_falls_back_to_cleartext() {
850        let iterations = 4096;
851        let salt = b"salt";
852        let password = "pass\u{00a0}word";
853        let mut hash = [0u8; PBKDF2_SHA256_HASH_LEN];
854        pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
855        let users = HashMap::from([(
856            "user".to_string(),
857            (
858                PasswordVerifier::Pbkdf2Sha256 {
859                    iterations,
860                    salt: salt.to_vec(),
861                    hash: hash.to_vec(),
862                },
863                PermissionMode::default(),
864            ),
865        )]);
866
867        let auth_info =
868            postgres_auth_info_with_credential(&users, Identity::UserId("user", None)).unwrap();
869        assert!(matches!(auth_info, PgAuthInfo::Cleartext));
870
871        assert!(
872            authenticate_with_credential(
873                &users,
874                Identity::UserId("user", None),
875                Password::PlainText(password.to_string().into()),
876            )
877            .is_ok()
878        );
879    }
880
881    #[test]
882    fn test_postgres_auth_info_mixed_verifiers_fall_back_to_cleartext() {
883        let verifier =
884            format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
885        let (_, scram_user) = parse_credential_line(&format!("scram={verifier}")).unwrap();
886        let mysql_user = (
887            PasswordVerifier::MysqlNativePassword {
888                hash_stage_2: mysql_native_password_hash("password".as_bytes()),
889            },
890            PermissionMode::default(),
891        );
892        let users = HashMap::from([
893            ("scram".to_string(), scram_user),
894            ("mysql".to_string(), mysql_user),
895        ]);
896
897        let auth_info =
898            postgres_auth_info_with_credential(&users, Identity::UserId("scram", None)).unwrap();
899        assert!(matches!(auth_info, PgAuthInfo::Cleartext));
900
901        let auth_info =
902            postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
903        assert!(matches!(auth_info, PgAuthInfo::Cleartext));
904    }
905
906    #[test]
907    fn test_password_verifier_debug_redacts_secrets() {
908        let debug = format!(
909            "{:?}",
910            PasswordVerifier::plain_text("secret".to_string()).unwrap()
911        );
912        assert!(debug.contains("<REDACTED>"));
913        assert!(!debug.contains("secret"));
914
915        let debug = format!(
916            "{:?}",
917            PasswordVerifier::Pbkdf2Sha256 {
918                iterations: 4096,
919                salt: b"super-secret-salt".to_vec(),
920                hash: b"super-secret-hash".to_vec(),
921            }
922        );
923        assert!(debug.contains("Pbkdf2Sha256"));
924        assert!(debug.contains("4096"));
925        assert!(!debug.contains("super-secret-salt"));
926        assert!(!debug.contains("super-secret-hash"));
927
928        let debug = format!(
929            "{:?}",
930            PasswordVerifier::MysqlNativePassword {
931                hash_stage_2: b"super-secret-hash".to_vec(),
932            }
933        );
934        assert!(debug.contains("MysqlNativePassword"));
935        assert!(!debug.contains("super-secret-hash"));
936    }
937}