1#![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 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 #[clap(long, value_enum, default_value = "pbkdf2_sha256")]
59 format: PasswordFormat,
60
61 #[clap(long)]
63 password: Option<String>,
64
65 #[clap(long)]
67 password_stdin: bool,
68
69 #[clap(long, default_value_t = DEFAULT_PBKDF2_SHA256_ITERATIONS)]
71 iterations: u32,
72
73 #[clap(long, default_value_t = DEFAULT_PBKDF2_SHA256_SALT_LEN)]
75 salt_len: usize,
76
77 #[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 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}