Skip to main content

cmd/
user.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
15#![allow(clippy::print_stdout)]
16
17use std::io;
18
19use auth::{
20    DEFAULT_PBKDF2_SHA256_ITERATIONS, DEFAULT_PBKDF2_SHA256_SALT_LEN, MAX_PBKDF2_SHA256_SALT_LEN,
21    format_mysql_native_password_verifier, format_pbkdf2_sha256_password_verifier,
22    format_pg_scram_sha256_password_verifier,
23};
24use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
25use rand::RngCore;
26use snafu::ResultExt;
27
28use crate::error::{self, Result};
29
30#[derive(Debug, Parser)]
31pub struct Command {
32    #[clap(subcommand)]
33    pub subcmd: SubCommand,
34}
35
36#[derive(Debug, Subcommand)]
37pub enum SubCommand {
38    /// Generate a password verifier for user_provider.
39    HashPassword(HashPasswordCommand),
40}
41
42impl Command {
43    pub fn run(self) -> Result<()> {
44        match self.subcmd {
45            SubCommand::HashPassword(cmd) => cmd.run(),
46        }
47    }
48}
49
50#[derive(Debug, Parser)]
51#[clap(group(
52    ArgGroup::new("password-input")
53        .required(true)
54        .args(["password", "password_stdin"])
55))]
56pub struct HashPasswordCommand {
57    /// Password verifier format to generate.
58    #[clap(long, value_enum, default_value = "pbkdf2_sha256")]
59    format: PasswordFormat,
60
61    /// Plaintext password. Prefer --password-stdin to avoid shell history leaks.
62    #[clap(long)]
63    password: Option<String>,
64
65    /// Read the plaintext password from stdin.
66    #[clap(long)]
67    password_stdin: bool,
68
69    /// PBKDF2-SHA256 / SCRAM-SHA-256 iteration count.
70    #[clap(long, default_value_t = DEFAULT_PBKDF2_SHA256_ITERATIONS)]
71    iterations: u32,
72
73    /// PBKDF2-SHA256 / SCRAM-SHA-256 random salt length in bytes.
74    #[clap(long, default_value_t = DEFAULT_PBKDF2_SHA256_SALT_LEN)]
75    salt_len: usize,
76
77    /// PBKDF2-SHA256 / SCRAM-SHA-256 salt as hex. Mainly useful for deterministic automation.
78    #[clap(long)]
79    salt_hex: Option<String>,
80}
81
82#[derive(Clone, Copy, Debug, ValueEnum)]
83#[clap(rename_all = "snake_case")]
84enum PasswordFormat {
85    Pbkdf2Sha256,
86    MysqlNativePassword,
87    PgScramSha256,
88}
89
90impl HashPasswordCommand {
91    fn run(self) -> Result<()> {
92        let password = self.read_password()?;
93        let verifier = match self.format {
94            PasswordFormat::Pbkdf2Sha256 => {
95                let salt = self.pbkdf2_salt()?;
96                format_pbkdf2_sha256_password_verifier(password.as_bytes(), &salt, self.iterations)
97                    .map_err(common_error::ext::BoxedError::new)
98                    .context(error::OtherSnafu)?
99            }
100            PasswordFormat::MysqlNativePassword => {
101                format_mysql_native_password_verifier(password.as_bytes())
102            }
103            PasswordFormat::PgScramSha256 => {
104                let salt = self.pbkdf2_salt()?;
105                format_pg_scram_sha256_password_verifier(
106                    password.as_bytes(),
107                    &salt,
108                    self.iterations,
109                )
110                .map_err(common_error::ext::BoxedError::new)
111                .context(error::OtherSnafu)?
112            }
113        };
114
115        println!("{verifier}");
116        Ok(())
117    }
118
119    fn read_password(&self) -> Result<String> {
120        let password = if let Some(password) = self.password.as_ref() {
121            password.clone()
122        } else {
123            let mut password = String::new();
124            io::stdin()
125                .read_line(&mut password)
126                .context(error::FileIoSnafu)?;
127            password.trim_end_matches(['\r', '\n']).to_string()
128        };
129
130        // A blank password is rejected by the user provider before verifier
131        // comparison, so a verifier built from it would be unusable. Fail fast
132        // instead of emitting a dead verifier (e.g. on EOF or an empty line).
133        if password.is_empty() {
134            return error::IllegalConfigSnafu {
135                msg: "password must not be empty",
136            }
137            .fail();
138        }
139
140        Ok(password)
141    }
142
143    fn pbkdf2_salt(&self) -> Result<Vec<u8>> {
144        if let Some(salt_hex) = self.salt_hex.as_ref() {
145            let salt = hex::decode(salt_hex).map_err(|err| {
146                error::IllegalConfigSnafu {
147                    msg: format!("invalid --salt-hex: {err}"),
148                }
149                .build()
150            })?;
151            Self::ensure_salt_len(salt.len())?;
152            return Ok(salt);
153        }
154
155        Self::ensure_salt_len(self.salt_len)?;
156        let mut salt = vec![0u8; self.salt_len];
157        rand::rng().fill_bytes(&mut salt);
158        Ok(salt)
159    }
160
161    fn ensure_salt_len(salt_len: usize) -> Result<()> {
162        if salt_len == 0 || salt_len > MAX_PBKDF2_SHA256_SALT_LEN {
163            return error::IllegalConfigSnafu {
164                msg: format!("salt length must be in 1..={}", MAX_PBKDF2_SHA256_SALT_LEN),
165            }
166            .fail();
167        }
168        Ok(())
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use auth::MAX_PBKDF2_SHA256_ITERATIONS;
175
176    use super::*;
177
178    #[test]
179    fn test_hash_password_command_with_pbkdf2_sha256() {
180        let cmd = HashPasswordCommand {
181            format: PasswordFormat::Pbkdf2Sha256,
182            password: Some("password".to_string()),
183            password_stdin: false,
184            iterations: 4096,
185            salt_len: 16,
186            salt_hex: Some("73616c74".to_string()),
187        };
188
189        let password = cmd.read_password().unwrap();
190        let salt = cmd.pbkdf2_salt().unwrap();
191        let verifier =
192            format_pbkdf2_sha256_password_verifier(password.as_bytes(), &salt, cmd.iterations)
193                .unwrap();
194
195        assert_eq!(
196            "pbkdf2_sha256:4096:73616c74:c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a",
197            verifier
198        );
199    }
200
201    #[test]
202    fn test_hash_password_command_with_pg_scram_sha256() {
203        let cmd = HashPasswordCommand {
204            format: PasswordFormat::PgScramSha256,
205            password: Some("password".to_string()),
206            password_stdin: false,
207            iterations: 4096,
208            salt_len: 16,
209            salt_hex: Some("73616c74".to_string()),
210        };
211
212        let password = cmd.read_password().unwrap();
213        let salt = cmd.pbkdf2_salt().unwrap();
214        let verifier =
215            format_pg_scram_sha256_password_verifier(password.as_bytes(), &salt, cmd.iterations)
216                .unwrap();
217
218        assert_eq!(
219            "pg_scram_sha256:4096:73616c74:945e1c466fc9932efadc23781edc5d1e78d5e10f005933652af1a6105154f084:b9bf0e811b1fb6793671c0cc3adedf7c75cd72291191092ad65878c5a02aad2c",
220            verifier
221        );
222    }
223
224    #[test]
225    fn test_hash_password_command_with_mysql_native_password() {
226        let verifier = format_mysql_native_password_verifier("123456".as_bytes());
227
228        assert_eq!(
229            "mysql_native_password:6bb4837eb74329105ee4568dda7dc67ed2ca2ad9",
230            verifier
231        );
232    }
233
234    #[test]
235    fn test_reject_empty_salt() {
236        let cmd = HashPasswordCommand {
237            format: PasswordFormat::Pbkdf2Sha256,
238            password: Some("password".to_string()),
239            password_stdin: false,
240            iterations: 4096,
241            salt_len: 0,
242            salt_hex: None,
243        };
244
245        assert!(cmd.pbkdf2_salt().is_err());
246    }
247
248    #[test]
249    fn test_reject_empty_password() {
250        let cmd = HashPasswordCommand {
251            format: PasswordFormat::Pbkdf2Sha256,
252            password: Some(String::new()),
253            password_stdin: false,
254            iterations: 4096,
255            salt_len: 16,
256            salt_hex: Some("73616c74".to_string()),
257        };
258
259        assert!(cmd.read_password().is_err());
260    }
261
262    #[test]
263    fn test_reject_too_many_iterations() {
264        let result = format_pbkdf2_sha256_password_verifier(
265            b"password",
266            b"salt",
267            MAX_PBKDF2_SHA256_ITERATIONS + 1,
268        );
269
270        assert!(result.is_err());
271    }
272}