Skip to main content

auth/
common.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
15use std::sync::{Arc, LazyLock};
16
17use common_base::secrets::{ExposeSecret, SecretString};
18use digest::Digest;
19use hmac::{Hmac, Mac};
20use pbkdf2::pbkdf2_hmac;
21use serde::Serializer;
22use sha1::Sha1;
23use sha2::Sha256;
24use snafu::{OptionExt, ensure};
25use subtle::ConstantTimeEq;
26
27use crate::error::{IllegalParamSnafu, InvalidConfigSnafu, Result, UserPasswordMismatchSnafu};
28use crate::user_info::DefaultUserInfo;
29use crate::user_provider::static_user_provider::{STATIC_USER_PROVIDER, StaticUserProvider};
30use crate::user_provider::watch_file_user_provider::{
31    WATCH_FILE_USER_PROVIDER, WatchFileUserProvider,
32};
33use crate::{UserInfoRef, UserProviderRef};
34
35pub(crate) const DEFAULT_USERNAME: &str = "greptime";
36pub const DEFAULT_PBKDF2_SHA256_ITERATIONS: u32 = 4096;
37pub const DEFAULT_PBKDF2_SHA256_SALT_LEN: usize = 16;
38pub const PBKDF2_SHA256_HASH_LEN: usize = 32;
39pub const MAX_PBKDF2_SHA256_ITERATIONS: u32 = 1_000_000;
40pub const MAX_PBKDF2_SHA256_SALT_LEN: usize = 1024;
41pub const PG_SCRAM_SHA256_KEY_LEN: usize = 32;
42
43/// Placeholder used when rendering potentially secret user-provider options.
44pub(crate) const REDACTED_USER_PROVIDER_OPTION: &str = "[REDACTED]";
45
46type HmacSha256 = Hmac<Sha256>;
47
48/// Process-wide secret used to derive a stable mock salt for unknown users, so
49/// the SCRAM `server-first-message` can't be used to enumerate usernames.
50static PG_SCRAM_MOCK_SECRET: LazyLock<[u8; PG_SCRAM_SHA256_KEY_LEN]> = LazyLock::new(rand::random);
51
52/// construct a [`UserInfo`](crate::user_info::UserInfo) impl with name
53/// use default username `greptime` if None is provided
54pub fn userinfo_by_name(username: Option<String>) -> UserInfoRef {
55    DefaultUserInfo::with_name(username.unwrap_or_else(|| DEFAULT_USERNAME.to_string()))
56}
57
58pub fn user_provider_from_option(opt: &str) -> Result<UserProviderRef> {
59    let (name, content) = opt.split_once(':').context(InvalidConfigSnafu {
60        value: REDACTED_USER_PROVIDER_OPTION,
61        msg: "UserProviderOption must be in format `<option>:<value>`",
62    })?;
63    match name {
64        STATIC_USER_PROVIDER => {
65            let provider =
66                StaticUserProvider::new(content).map(|p| Arc::new(p) as UserProviderRef)?;
67            Ok(provider)
68        }
69        WATCH_FILE_USER_PROVIDER => {
70            WatchFileUserProvider::new(content).map(|p| Arc::new(p) as UserProviderRef)
71        }
72        _ => InvalidConfigSnafu {
73            value: REDACTED_USER_PROVIDER_OPTION,
74            msg: "Invalid UserProviderOption",
75        }
76        .fail(),
77    }
78}
79
80/// Serializes a `user_provider` option, exposing only known file-based forms.
81///
82/// The option is held as a [`SecretString`] so `Debug` and the default
83/// serializer never print it. This serializer exposes the value only to keep
84/// provider names and file paths visible for those forms in configuration dumps
85/// such as the `/config` endpoint.
86pub fn serialize_user_provider_option<S>(
87    value: &Option<SecretString>,
88    serializer: S,
89) -> std::result::Result<S::Ok, S::Error>
90where
91    S: Serializer,
92{
93    match value {
94        Some(value) => {
95            serializer.serialize_some(&redact_user_provider_option(value.expose_secret()))
96        }
97        None => serializer.serialize_none(),
98    }
99}
100
101/// Redacts user-provider options except known file-based forms with nonempty paths.
102///
103/// Only `static_user_provider:file:<path>` and `watch_file_user_provider:<path>`
104/// are returned unchanged. Inline credentials, unknown providers and malformed
105/// options are replaced entirely with a placeholder.
106pub fn redact_user_provider_option(value: &str) -> String {
107    let is_file_provider = match value.split_once(':') {
108        Some((STATIC_USER_PROVIDER, content)) => content
109            .strip_prefix("file:")
110            .is_some_and(|path| !path.is_empty()),
111        Some((WATCH_FILE_USER_PROVIDER, path)) => !path.is_empty(),
112        _ => false,
113    };
114    if is_file_provider {
115        value.to_string()
116    } else {
117        REDACTED_USER_PROVIDER_OPTION.to_string()
118    }
119}
120
121pub fn static_user_provider_from_option(opt: &str) -> Result<StaticUserProvider> {
122    let (name, content) = opt.split_once(':').context(InvalidConfigSnafu {
123        value: REDACTED_USER_PROVIDER_OPTION,
124        msg: "UserProviderOption must be in format `<option>:<value>`",
125    })?;
126    match name {
127        STATIC_USER_PROVIDER => {
128            let provider = StaticUserProvider::new(content)?;
129            Ok(provider)
130        }
131        _ => InvalidConfigSnafu {
132            value: REDACTED_USER_PROVIDER_OPTION,
133            msg: format!("Invalid UserProviderOption, expect only {STATIC_USER_PROVIDER}"),
134        }
135        .fail(),
136    }
137}
138
139type Username<'a> = &'a str;
140type HostOrIp<'a> = &'a str;
141
142#[derive(Debug, Clone)]
143pub enum Identity<'a> {
144    UserId(Username<'a>, Option<HostOrIp<'a>>),
145}
146
147pub type HashedPassword<'a> = &'a [u8];
148pub type Salt<'a> = &'a [u8];
149
150/// Authentication information sent by the client.
151pub enum Password<'a> {
152    PlainText(SecretString),
153    MysqlNativePassword(HashedPassword<'a>, Salt<'a>),
154    PgMD5(HashedPassword<'a>, Salt<'a>),
155}
156
157impl Password<'_> {
158    pub fn r#type(&self) -> &str {
159        match self {
160            Password::PlainText(_) => "plain_text",
161            Password::MysqlNativePassword(_, _) => "mysql_native_password",
162            Password::PgMD5(_, _) => "pg_md5",
163        }
164    }
165}
166
167pub fn auth_mysql(
168    auth_data: HashedPassword,
169    salt: Salt,
170    username: &str,
171    save_pwd: &[u8],
172) -> Result<()> {
173    let hash_stage_2 = mysql_native_password_hash(save_pwd);
174    auth_mysql_with_hash_stage_2(auth_data, salt, username, &hash_stage_2)
175}
176
177/// Authenticates a MySQL native password response against an encoded verifier.
178pub fn auth_mysql_with_verifier(
179    auth_data: HashedPassword,
180    salt: Salt,
181    username: &str,
182    verifier: &str,
183) -> Result<()> {
184    let hash_stage_2 = parse_mysql_native_password_verifier(verifier)?;
185    auth_mysql_with_hash_stage_2(auth_data, salt, username, &hash_stage_2)
186}
187
188pub fn validate_mysql_native_password_verifier(verifier: &str) -> Result<()> {
189    parse_mysql_native_password_verifier(verifier).map(|_| ())
190}
191
192pub(crate) fn parse_mysql_native_password_verifier(verifier: &str) -> Result<Vec<u8>> {
193    let Some(verifier) = verifier.strip_prefix("mysql_native_password:") else {
194        return InvalidConfigSnafu {
195            value: "mysql_native_password".to_string(),
196            msg: "Invalid mysql native password verifier format",
197        }
198        .fail();
199    };
200    let Ok(hash_stage_2) = hex::decode(verifier) else {
201        return InvalidConfigSnafu {
202            value: "mysql_native_password".to_string(),
203            msg: "Invalid mysql native password verifier encoding",
204        }
205        .fail();
206    };
207    ensure!(
208        hash_stage_2.len() == 20,
209        InvalidConfigSnafu {
210            value: "mysql_native_password".to_string(),
211            msg: "Illegal mysql native password verifier length",
212        }
213    );
214    Ok(hash_stage_2)
215}
216
217pub(crate) fn auth_mysql_with_hash_stage_2(
218    auth_data: HashedPassword,
219    salt: Salt,
220    username: &str,
221    hash_stage_2: &[u8],
222) -> Result<()> {
223    ensure!(
224        auth_data.len() == 20,
225        IllegalParamSnafu {
226            msg: "Illegal mysql password length"
227        }
228    );
229    ensure!(
230        hash_stage_2.len() == 20,
231        InvalidConfigSnafu {
232            value: hash_stage_2.len().to_string(),
233            msg: "Illegal mysql native password verifier length",
234        }
235    );
236    // ref: https://github.com/mysql/mysql-server/blob/a246bad76b9271cb4333634e954040a970222e0a/sql/auth/password.cc#L62
237    let tmp = sha1_two(salt, hash_stage_2);
238    // xor auth_data and tmp
239    let mut xor_result = [0u8; 20];
240    for i in 0..20 {
241        xor_result[i] = auth_data[i] ^ tmp[i];
242    }
243    let candidate_stage_2 = sha1_one(&xor_result);
244    if candidate_stage_2 == hash_stage_2 {
245        Ok(())
246    } else {
247        UserPasswordMismatchSnafu {
248            username: username.to_string(),
249        }
250        .fail()
251    }
252}
253
254pub fn mysql_native_password_hash(save_pwd: &[u8]) -> Vec<u8> {
255    double_sha1(save_pwd)
256}
257
258pub fn format_mysql_native_password_verifier(password: &[u8]) -> String {
259    format!(
260        "mysql_native_password:{}",
261        hex::encode(mysql_native_password_hash(password))
262    )
263}
264
265pub fn format_pbkdf2_sha256_password_verifier(
266    password: &[u8],
267    salt: &[u8],
268    iterations: u32,
269) -> Result<String> {
270    ensure!(
271        iterations > 0 && iterations <= MAX_PBKDF2_SHA256_ITERATIONS,
272        IllegalParamSnafu {
273            msg: format!(
274                "pbkdf2_sha256 iterations must be in 1..={}",
275                MAX_PBKDF2_SHA256_ITERATIONS
276            )
277        }
278    );
279    ensure!(
280        !salt.is_empty() && salt.len() <= MAX_PBKDF2_SHA256_SALT_LEN,
281        IllegalParamSnafu {
282            msg: format!(
283                "pbkdf2_sha256 salt length must be in 1..={}",
284                MAX_PBKDF2_SHA256_SALT_LEN
285            )
286        }
287    );
288
289    let mut hash = [0u8; PBKDF2_SHA256_HASH_LEN];
290    pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut hash);
291    Ok(format!(
292        "pbkdf2_sha256:{iterations}:{}:{}",
293        hex::encode(salt),
294        hex::encode(hash)
295    ))
296}
297
298#[derive(Clone, PartialEq, Eq)]
299pub struct PgScramSha256Verifier {
300    iterations: u32,
301    salt: Vec<u8>,
302    stored_key: Vec<u8>,
303    server_key: Vec<u8>,
304}
305
306impl std::fmt::Debug for PgScramSha256Verifier {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        f.debug_struct("PgScramSha256Verifier")
309            .field("iterations", &self.iterations)
310            .field("salt", &"<REDACTED>")
311            .field("stored_key", &"<REDACTED>")
312            .field("server_key", &"<REDACTED>")
313            .finish()
314    }
315}
316
317impl PgScramSha256Verifier {
318    pub fn new(
319        iterations: u32,
320        salt: Vec<u8>,
321        stored_key: Vec<u8>,
322        server_key: Vec<u8>,
323    ) -> Result<Self> {
324        ensure!(
325            iterations > 0 && iterations <= MAX_PBKDF2_SHA256_ITERATIONS,
326            IllegalParamSnafu {
327                msg: format!(
328                    "pg_scram_sha256 iterations must be in 1..={}",
329                    MAX_PBKDF2_SHA256_ITERATIONS
330                )
331            }
332        );
333        ensure!(
334            !salt.is_empty() && salt.len() <= MAX_PBKDF2_SHA256_SALT_LEN,
335            IllegalParamSnafu {
336                msg: format!(
337                    "pg_scram_sha256 salt length must be in 1..={}",
338                    MAX_PBKDF2_SHA256_SALT_LEN
339                )
340            }
341        );
342        ensure!(
343            stored_key.len() == PG_SCRAM_SHA256_KEY_LEN,
344            IllegalParamSnafu {
345                msg: "pg_scram_sha256 stored key must be 32 bytes"
346            }
347        );
348        ensure!(
349            server_key.len() == PG_SCRAM_SHA256_KEY_LEN,
350            IllegalParamSnafu {
351                msg: "pg_scram_sha256 server key must be 32 bytes"
352            }
353        );
354
355        Ok(Self {
356            iterations,
357            salt,
358            stored_key,
359            server_key,
360        })
361    }
362
363    pub fn from_password(password: &[u8], salt: &[u8], iterations: u32) -> Result<Self> {
364        let salted_password = pg_scram_sha256_salted_password(password, salt, iterations)?;
365        Self::from_salted_password(salted_password, salt.to_vec(), iterations)
366    }
367
368    pub fn from_salted_password(
369        salted_password: Vec<u8>,
370        salt: Vec<u8>,
371        iterations: u32,
372    ) -> Result<Self> {
373        ensure!(
374            salted_password.len() == PG_SCRAM_SHA256_KEY_LEN,
375            IllegalParamSnafu {
376                msg: "pg_scram_sha256 salted password must be 32 bytes"
377            }
378        );
379        let client_key = hmac_sha256(&salted_password, b"Client Key");
380        let stored_key = sha256(&client_key);
381        let server_key = hmac_sha256(&salted_password, b"Server Key");
382        Self::new(iterations, salt, stored_key, server_key)
383    }
384
385    /// Builds a throwaway verifier for an unknown user. The salt is derived
386    /// deterministically from the username and a process-wide secret, so the
387    /// SCRAM `server-first-message` (salt + iterations) stays stable per
388    /// username and indistinguishable from a real user across reconnects. No
389    /// PBKDF2 is run (avoids a CPU-exhaustion DoS keyed on unknown usernames),
390    /// and the random keys guarantee the client proof never matches.
391    pub fn mock_for_unknown_user(username: &[u8]) -> Self {
392        let mock_salt = hmac_sha256(PG_SCRAM_MOCK_SECRET.as_slice(), username);
393        Self {
394            iterations: DEFAULT_PBKDF2_SHA256_ITERATIONS,
395            salt: mock_salt[..DEFAULT_PBKDF2_SHA256_SALT_LEN].to_vec(),
396            stored_key: rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>().to_vec(),
397            server_key: rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>().to_vec(),
398        }
399    }
400
401    pub fn iterations(&self) -> u32 {
402        self.iterations
403    }
404
405    pub fn salt(&self) -> &[u8] {
406        &self.salt
407    }
408
409    /// `SHA256(ClientKey)`, used to verify the client proof in SCRAM.
410    pub fn stored_key(&self) -> &[u8] {
411        &self.stored_key
412    }
413
414    /// `HMAC(SaltedPassword, "Server Key")`, used to compute the SCRAM
415    /// server signature. This is secret material and must not be exposed to
416    /// clients.
417    pub fn server_key(&self) -> &[u8] {
418        &self.server_key
419    }
420
421    pub fn verify_plain_password(&self, password: &[u8]) -> Result<bool> {
422        let salted_password =
423            pg_scram_sha256_salted_password(password, &self.salt, self.iterations)?;
424        let client_key = hmac_sha256(&salted_password, b"Client Key");
425        let stored_key = sha256(&client_key);
426        Ok(self.stored_key.ct_eq(&stored_key).into())
427    }
428
429    pub fn verify_client_proof(
430        &self,
431        auth_message: &[u8],
432        client_proof: &[u8],
433    ) -> Result<Option<Vec<u8>>> {
434        if client_proof.len() != PG_SCRAM_SHA256_KEY_LEN {
435            return Ok(None);
436        }
437
438        let client_signature = hmac_sha256(&self.stored_key, auth_message);
439        let client_key = xor(client_proof, &client_signature);
440        let stored_key = sha256(&client_key);
441        if self.stored_key.ct_eq(&stored_key).into() {
442            Ok(Some(hmac_sha256(&self.server_key, auth_message)))
443        } else {
444            Ok(None)
445        }
446    }
447}
448
449pub fn format_pg_scram_sha256_password_verifier(
450    password: &[u8],
451    salt: &[u8],
452    iterations: u32,
453) -> Result<String> {
454    let verifier = PgScramSha256Verifier::from_password(password, salt, iterations)?;
455    Ok(format!(
456        "pg_scram_sha256:{iterations}:{}:{}:{}",
457        hex::encode(verifier.salt),
458        hex::encode(verifier.stored_key),
459        hex::encode(verifier.server_key)
460    ))
461}
462
463/// Parses and validates an encoded PostgreSQL SCRAM-SHA-256 verifier.
464pub fn parse_pg_scram_sha256_password_verifier(verifier: &str) -> Result<PgScramSha256Verifier> {
465    let Some(verifier) = verifier.strip_prefix("pg_scram_sha256:") else {
466        return InvalidConfigSnafu {
467            value: "pg_scram_sha256".to_string(),
468            msg: "Invalid pg scram sha256 verifier format",
469        }
470        .fail();
471    };
472    let mut parts = verifier.split(':');
473    let (Some(iterations), Some(salt), Some(stored_key), Some(server_key), None) = (
474        parts.next(),
475        parts.next(),
476        parts.next(),
477        parts.next(),
478        parts.next(),
479    ) else {
480        return InvalidConfigSnafu {
481            value: "pg_scram_sha256".to_string(),
482            msg: "Invalid pg scram sha256 verifier format",
483        }
484        .fail();
485    };
486    let (Ok(iterations), Ok(salt), Ok(stored_key), Ok(server_key)) = (
487        iterations.parse::<u32>(),
488        hex::decode(salt),
489        hex::decode(stored_key),
490        hex::decode(server_key),
491    ) else {
492        return InvalidConfigSnafu {
493            value: "pg_scram_sha256".to_string(),
494            msg: "Invalid pg scram sha256 verifier encoding",
495        }
496        .fail();
497    };
498
499    PgScramSha256Verifier::new(iterations, salt, stored_key, server_key)
500}
501
502fn pg_scram_sha256_salted_password(
503    password: &[u8],
504    salt: &[u8],
505    iterations: u32,
506) -> Result<Vec<u8>> {
507    ensure!(
508        iterations > 0 && iterations <= MAX_PBKDF2_SHA256_ITERATIONS,
509        IllegalParamSnafu {
510            msg: format!(
511                "pg_scram_sha256 iterations must be in 1..={}",
512                MAX_PBKDF2_SHA256_ITERATIONS
513            )
514        }
515    );
516    ensure!(
517        !salt.is_empty() && salt.len() <= MAX_PBKDF2_SHA256_SALT_LEN,
518        IllegalParamSnafu {
519            msg: format!(
520                "pg_scram_sha256 salt length must be in 1..={}",
521                MAX_PBKDF2_SHA256_SALT_LEN
522            )
523        }
524    );
525
526    let prepared_password = std::str::from_utf8(password)
527        .ok()
528        .and_then(|password| stringprep::saslprep(password).ok());
529    let password = prepared_password
530        .as_deref()
531        .map(str::as_bytes)
532        .unwrap_or(password);
533
534    let mut salted_password = [0u8; PG_SCRAM_SHA256_KEY_LEN];
535    pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut salted_password);
536    Ok(salted_password.to_vec())
537}
538
539fn hmac_sha256(key: &[u8], msg: &[u8]) -> Vec<u8> {
540    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
541    mac.update(msg);
542    mac.finalize().into_bytes().to_vec()
543}
544
545fn sha256(data: &[u8]) -> Vec<u8> {
546    let mut hasher = Sha256::new();
547    hasher.update(data);
548    hasher.finalize().to_vec()
549}
550
551fn xor(lhs: &[u8], rhs: &[u8]) -> Vec<u8> {
552    lhs.iter().zip(rhs).map(|(l, r)| l ^ r).collect()
553}
554
555fn sha1_two(input_1: &[u8], input_2: &[u8]) -> Vec<u8> {
556    let mut hasher = Sha1::new();
557    hasher.update(input_1);
558    hasher.update(input_2);
559    hasher.finalize().to_vec()
560}
561
562fn sha1_one(data: &[u8]) -> Vec<u8> {
563    let mut hasher = Sha1::new();
564    hasher.update(data);
565    hasher.finalize().to_vec()
566}
567
568fn double_sha1(data: &[u8]) -> Vec<u8> {
569    sha1_one(&sha1_one(data))
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575    use crate::error::Error;
576
577    fn mysql_native_password_auth_data(password: &[u8], salt: &[u8]) -> Vec<u8> {
578        let hash_stage_1 = sha1_one(password);
579        let hash_stage_2 = mysql_native_password_hash(password);
580        let scramble = sha1_two(salt, &hash_stage_2);
581        hash_stage_1
582            .iter()
583            .zip(scramble)
584            .map(|(lhs, rhs)| lhs ^ rhs)
585            .collect()
586    }
587
588    #[test]
589    fn test_sha() {
590        let sha_1_answer: Vec<u8> = vec![
591            124, 74, 141, 9, 202, 55, 98, 175, 97, 229, 149, 32, 148, 61, 194, 100, 148, 248, 148,
592            27,
593        ];
594        let sha_1 = sha1_one("123456".as_bytes());
595        assert_eq!(sha_1, sha_1_answer);
596
597        let double_sha1_answer: Vec<u8> = vec![
598            107, 180, 131, 126, 183, 67, 41, 16, 94, 228, 86, 141, 218, 125, 198, 126, 210, 202,
599            42, 217,
600        ];
601        let double_sha1 = double_sha1("123456".as_bytes());
602        assert_eq!(double_sha1, double_sha1_answer);
603
604        let sha1_2_answer: Vec<u8> = vec![
605            132, 115, 215, 211, 99, 186, 164, 206, 168, 152, 217, 192, 117, 47, 240, 252, 142, 244,
606            37, 204,
607        ];
608        let sha1_2 = sha1_two("123456".as_bytes(), "654321".as_bytes());
609        assert_eq!(sha1_2, sha1_2_answer);
610    }
611
612    #[test]
613    fn test_format_mysql_native_password_verifier() {
614        let verifier = format_mysql_native_password_verifier("123456".as_bytes());
615        assert_eq!(
616            "mysql_native_password:6bb4837eb74329105ee4568dda7dc67ed2ca2ad9",
617            verifier
618        );
619        assert_eq!(
620            mysql_native_password_hash(b"123456"),
621            parse_mysql_native_password_verifier(&verifier).unwrap()
622        );
623        assert!(parse_mysql_native_password_verifier("mysql_native_password:00").is_err());
624
625        let salt = b"01234567890123456789";
626        let auth_data = mysql_native_password_auth_data(b"123456", salt);
627        auth_mysql_with_verifier(&auth_data, salt, "greptime", &verifier).unwrap();
628        assert!(
629            auth_mysql_with_verifier(
630                &auth_data,
631                salt,
632                "greptime",
633                &format_mysql_native_password_verifier(b"wrong")
634            )
635            .is_err()
636        );
637    }
638
639    #[test]
640    fn test_format_pbkdf2_sha256_password_verifier() {
641        let verifier =
642            format_pbkdf2_sha256_password_verifier("password".as_bytes(), b"salt", 4096).unwrap();
643        assert_eq!(
644            "pbkdf2_sha256:4096:73616c74:c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a",
645            verifier
646        );
647
648        assert!(format_pbkdf2_sha256_password_verifier(b"password", b"", 4096).is_err());
649        assert!(format_pbkdf2_sha256_password_verifier(b"password", b"salt", 0).is_err());
650        assert!(
651            format_pbkdf2_sha256_password_verifier(
652                b"password",
653                b"salt",
654                MAX_PBKDF2_SHA256_ITERATIONS + 1,
655            )
656            .is_err()
657        );
658    }
659
660    #[test]
661    fn test_format_pg_scram_sha256_password_verifier() {
662        let verifier =
663            format_pg_scram_sha256_password_verifier("password".as_bytes(), b"salt", 4096).unwrap();
664        assert_eq!(
665            "pg_scram_sha256:4096:73616c74:945e1c466fc9932efadc23781edc5d1e78d5e10f005933652af1a6105154f084:b9bf0e811b1fb6793671c0cc3adedf7c75cd72291191092ad65878c5a02aad2c",
666            verifier
667        );
668        let parsed = parse_pg_scram_sha256_password_verifier(&verifier).unwrap();
669        assert_eq!(4096, parsed.iterations());
670        assert_eq!(b"salt", parsed.salt());
671        assert!(parse_pg_scram_sha256_password_verifier("pg_scram_sha256:bad").is_err());
672    }
673
674    #[test]
675    fn test_pg_scram_sha256_applies_saslprep() {
676        let normalized = PgScramSha256Verifier::from_password(b"pass word", b"salt", 4096).unwrap();
677        let non_breaking_space =
678            PgScramSha256Verifier::from_password("pass\u{00a0}word".as_bytes(), b"salt", 4096)
679                .unwrap();
680
681        assert_eq!(normalized, non_breaking_space);
682        assert!(
683            non_breaking_space
684                .verify_plain_password(b"pass word")
685                .unwrap()
686        );
687    }
688
689    #[test]
690    fn test_pg_scram_sha256_uses_original_bytes_when_saslprep_is_not_possible() {
691        let invalid_utf8 = b"password\xff";
692        let prohibited = b"password\x07";
693
694        for password in [invalid_utf8.as_slice(), prohibited.as_slice()] {
695            let verifier = PgScramSha256Verifier::from_password(password, b"salt", 4096).unwrap();
696            assert!(verifier.verify_plain_password(password).unwrap());
697        }
698    }
699
700    #[test]
701    fn test_redact_user_provider_option() {
702        for option in [
703            "static_user_provider:file:/etc/greptimedb/users",
704            "watch_file_user_provider:/etc/greptimedb/users",
705            "static_user_provider:file:users.txt",
706            "watch_file_user_provider:users.txt",
707        ] {
708            assert_eq!(redact_user_provider_option(option), option);
709        }
710
711        for option in [
712            "",
713            "secret",
714            "alice=secret",
715            "static_user_provider",
716            "static_user_provider:",
717            "static_user_provider:file",
718            "static_user_provider:file:",
719            "static_user_provider:filex:/etc/greptimedb/users",
720            "static_user_provider:cmd:alice=secret,bob=pbkdf2_sha256:4096:aa:bb",
721            "static_user_provider:cmd:alice",
722            "static_user_provider:cmd:alice:secret",
723            "static_user_provider:cmd:alice=plain:prefix,secret-tail",
724            "static_user_provider:cmdx:alice=secret",
725            "watch_file_user_provider",
726            "watch_file_user_provider:",
727            "ldap_user_provider:/etc/greptimedb/ldap.toml",
728            "unknown_user_provider:cmd:alice=secret",
729        ] {
730            assert_eq!(redact_user_provider_option(option), "[REDACTED]");
731        }
732    }
733
734    #[test]
735    fn test_user_provider_errors_redact_credentials() {
736        for option in [
737            "secret",
738            "alice=secret",
739            "alice=secret,bob=other_secret",
740            "alice=secret:cmd:alice=secret",
741            "static_user_provider:alice=secret",
742            "static_user_provider:alice=secret:cmd",
743            "static_user_provider:cmdx:alice=secret",
744            "unknown_user_provider:cmd:alice=secret",
745            "static_user_provider:cmd:alice:invalid=secret",
746            "static_user_provider:cmd:alice=secret=tail",
747            "static_user_provider:cmd:alice=pbkdf2_sha256:secret",
748            "static_user_provider:cmd:alice=mysql_native_password:secret",
749            "static_user_provider:cmd:alice=pg_scram_sha256:secret",
750            "static_user_provider:cmd:bob=secret,alice=pbkdf2_sha256:secret",
751            "static_user_provider:cmd:alice:secret",
752            "static_user_provider:cmd:alice=plain:prefix,secret-tail",
753            "static_user_provider:cmd:watch_file_user_provider:secret",
754        ] {
755            for error in [
756                user_provider_from_option(option).err().unwrap(),
757                static_user_provider_from_option(option).err().unwrap(),
758            ] {
759                assert!(!error.to_string().contains("secret"));
760                assert!(!format!("{error:?}").contains("secret"));
761                let Error::InvalidConfig { value, .. } = error else {
762                    panic!("Expected an invalid config error");
763                };
764                assert_eq!(value, "[REDACTED]");
765            }
766        }
767    }
768
769    #[test]
770    fn test_mock_verifier_is_stable_per_username() {
771        let alice = PgScramSha256Verifier::mock_for_unknown_user(b"alice");
772        let alice_again = PgScramSha256Verifier::mock_for_unknown_user(b"alice");
773        let bob = PgScramSha256Verifier::mock_for_unknown_user(b"bob");
774
775        // Same username must yield the same salt and iterations across reconnects,
776        // otherwise the SCRAM server-first message leaks user (non-)existence.
777        assert_eq!(alice.salt(), alice_again.salt());
778        assert_ne!(alice.salt(), bob.salt());
779        assert_eq!(alice.iterations(), DEFAULT_PBKDF2_SHA256_ITERATIONS);
780        assert_eq!(alice.salt().len(), DEFAULT_PBKDF2_SHA256_SALT_LEN);
781
782        // The mock verifier must never accept a client proof.
783        assert!(
784            alice
785                .verify_client_proof(b"auth-message", &[0u8; PG_SCRAM_SHA256_KEY_LEN])
786                .unwrap()
787                .is_none()
788        );
789    }
790}