feat: add password hash generation command (#8288)

* feat: add password hash generation command

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* fix: reject empty password in hash-password command

A blank plaintext password is rejected by the user provider before
verifier comparison, so a verifier generated from an empty password is
unusable. Fail fast on EOF or an empty line from --password-stdin (and on
an empty --password) instead of printing a dead verifier.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
Co-authored-by: Yingwen <realevenyag@gmail.com>
This commit is contained in:
dennis zhuang
2026-06-12 13:28:38 +00:00
committed by GitHub
co-authored by Yingwen
parent f6f56e6bc5
commit c9062fe042
8 changed files with 332 additions and 11 deletions
Generated
+1
View File
@@ -2128,6 +2128,7 @@ dependencies = [
"flow",
"frontend",
"futures",
"hex",
"human-panic",
"humantime",
"lazy_static",
+77 -1
View File
@@ -16,7 +16,9 @@ use std::sync::Arc;
use common_base::secrets::SecretString;
use digest::Digest;
use pbkdf2::pbkdf2_hmac;
use sha1::Sha1;
use sha2::Sha256;
use snafu::{OptionExt, ensure};
use crate::error::{IllegalParamSnafu, InvalidConfigSnafu, Result, UserPasswordMismatchSnafu};
@@ -28,6 +30,10 @@ use crate::user_provider::watch_file_user_provider::{
use crate::{UserInfoRef, UserProviderRef};
pub(crate) const DEFAULT_USERNAME: &str = "greptime";
pub const DEFAULT_PBKDF2_SHA256_ITERATIONS: u32 = 4096;
pub const PBKDF2_SHA256_HASH_LEN: usize = 32;
pub const MAX_PBKDF2_SHA256_ITERATIONS: u32 = 1_000_000;
pub const MAX_PBKDF2_SHA256_SALT_LEN: usize = 1024;
/// construct a [`UserInfo`](crate::user_info::UserInfo) impl with name
/// use default username `greptime` if None is provided
@@ -150,10 +156,50 @@ pub(crate) fn auth_mysql_with_hash_stage_2(
}
}
pub(crate) fn mysql_native_password_hash(save_pwd: &[u8]) -> Vec<u8> {
pub fn mysql_native_password_hash(save_pwd: &[u8]) -> Vec<u8> {
double_sha1(save_pwd)
}
pub fn format_mysql_native_password_verifier(password: &[u8]) -> String {
format!(
"mysql_native_password:{}",
hex::encode(mysql_native_password_hash(password))
)
}
pub fn format_pbkdf2_sha256_password_verifier(
password: &[u8],
salt: &[u8],
iterations: u32,
) -> Result<String> {
ensure!(
iterations > 0 && iterations <= MAX_PBKDF2_SHA256_ITERATIONS,
IllegalParamSnafu {
msg: format!(
"pbkdf2_sha256 iterations must be in 1..={}",
MAX_PBKDF2_SHA256_ITERATIONS
)
}
);
ensure!(
!salt.is_empty() && salt.len() <= MAX_PBKDF2_SHA256_SALT_LEN,
IllegalParamSnafu {
msg: format!(
"pbkdf2_sha256 salt length must be in 1..={}",
MAX_PBKDF2_SHA256_SALT_LEN
)
}
);
let mut hash = [0u8; PBKDF2_SHA256_HASH_LEN];
pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut hash);
Ok(format!(
"pbkdf2_sha256:{iterations}:{}:{}",
hex::encode(salt),
hex::encode(hash)
))
}
fn sha1_two(input_1: &[u8], input_2: &[u8]) -> Vec<u8> {
let mut hasher = Sha1::new();
hasher.update(input_1);
@@ -198,4 +244,34 @@ mod tests {
let sha1_2 = sha1_two("123456".as_bytes(), "654321".as_bytes());
assert_eq!(sha1_2, sha1_2_answer);
}
#[test]
fn test_format_mysql_native_password_verifier() {
let verifier = format_mysql_native_password_verifier("123456".as_bytes());
assert_eq!(
"mysql_native_password:6bb4837eb74329105ee4568dda7dc67ed2ca2ad9",
verifier
);
}
#[test]
fn test_format_pbkdf2_sha256_password_verifier() {
let verifier =
format_pbkdf2_sha256_password_verifier("password".as_bytes(), b"salt", 4096).unwrap();
assert_eq!(
"pbkdf2_sha256:4096:73616c74:c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a",
verifier
);
assert!(format_pbkdf2_sha256_password_verifier(b"password", b"", 4096).is_err());
assert!(format_pbkdf2_sha256_password_verifier(b"password", b"salt", 0).is_err());
assert!(
format_pbkdf2_sha256_password_verifier(
b"password",
b"salt",
MAX_PBKDF2_SHA256_ITERATIONS + 1,
)
.is_err()
);
}
}
+5 -2
View File
@@ -22,8 +22,11 @@ mod user_provider;
pub mod tests;
pub use common::{
HashedPassword, Identity, Password, auth_mysql, static_user_provider_from_option,
user_provider_from_option, userinfo_by_name,
DEFAULT_PBKDF2_SHA256_ITERATIONS, HashedPassword, Identity, MAX_PBKDF2_SHA256_ITERATIONS,
MAX_PBKDF2_SHA256_SALT_LEN, PBKDF2_SHA256_HASH_LEN, Password, auth_mysql,
format_mysql_native_password_verifier, format_pbkdf2_sha256_password_verifier,
mysql_native_password_hash, static_user_provider_from_option, user_provider_from_option,
userinfo_by_name,
};
pub use permission::{DefaultPermissionChecker, PermissionChecker, PermissionReq, PermissionResp};
pub use user_info::UserInfo;
+4 -7
View File
@@ -27,7 +27,10 @@ use sha2::Sha256;
use snafu::{OptionExt, ResultExt, ensure};
use subtle::ConstantTimeEq;
use crate::common::{Identity, Password, auth_mysql_with_hash_stage_2};
use crate::common::{
Identity, MAX_PBKDF2_SHA256_ITERATIONS, MAX_PBKDF2_SHA256_SALT_LEN, PBKDF2_SHA256_HASH_LEN,
Password, auth_mysql_with_hash_stage_2,
};
use crate::error::{
IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedPasswordTypeSnafu,
UserNotFoundSnafu, UserPasswordMismatchSnafu,
@@ -35,12 +38,6 @@ use crate::error::{
use crate::user_info::{DefaultUserInfo, PermissionMode};
use crate::{UserInfoRef, auth_mysql};
const MAX_PBKDF2_SHA256_ITERATIONS: u32 = 1_000_000;
/// PBKDF2-SHA256 derived key length, fixed to the native SHA-256 output size.
const PBKDF2_SHA256_HASH_LEN: usize = 32;
/// Upper bound on the salt length to reject misconfigured credentials.
const MAX_PBKDF2_SHA256_SALT_LEN: usize = 1024;
#[async_trait::async_trait]
pub trait UserProvider: Send + Sync {
fn name(&self) -> &str;
+1
View File
@@ -67,6 +67,7 @@ etcd-client.workspace = true
flow.workspace = true
frontend = { workspace = true, default-features = false }
futures.workspace = true
hex.workspace = true
human-panic = "2.0"
humantime.workspace = true
lazy_static.workspace = true
+6 -1
View File
@@ -19,7 +19,7 @@ use clap::{Parser, Subcommand};
use cmd::datanode::builder::InstanceBuilder;
use cmd::error::{InitTlsProviderSnafu, Result};
use cmd::options::GlobalOptions;
use cmd::{App, cli, datanode, flownode, frontend, metasrv, standalone};
use cmd::{App, cli, datanode, flownode, frontend, metasrv, standalone, user};
use common_base::Plugins;
use common_version::{product_name, verbose_version, version};
use servers::install_default_crypto_provider;
@@ -60,6 +60,10 @@ enum SubCommand {
/// Execute the cli tools.
#[clap(name = "cli")]
Cli(cli::Command),
/// Manage user credentials.
#[clap(name = "user")]
User(user::Command),
}
#[cfg(not(windows))]
@@ -146,6 +150,7 @@ async fn start(cli: Command) -> Result<()> {
.run()
.await
}
SubCommand::User(cmd) => cmd.run(),
}
}
+1
View File
@@ -31,6 +31,7 @@ pub mod frontend;
pub mod metasrv;
pub mod options;
pub mod standalone;
pub mod user;
lazy_static::lazy_static! {
static ref APP_VERSION: prometheus::IntGaugeVec =
+237
View File
@@ -0,0 +1,237 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(clippy::print_stdout)]
use std::io;
use auth::{
DEFAULT_PBKDF2_SHA256_ITERATIONS, MAX_PBKDF2_SHA256_SALT_LEN,
format_mysql_native_password_verifier, format_pbkdf2_sha256_password_verifier,
};
use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
use rand::RngCore;
use snafu::ResultExt;
use crate::error::{self, Result};
#[derive(Debug, Parser)]
pub struct Command {
#[clap(subcommand)]
pub subcmd: SubCommand,
}
#[derive(Debug, Subcommand)]
pub enum SubCommand {
/// Generate a password verifier for user_provider.
HashPassword(HashPasswordCommand),
}
impl Command {
pub fn run(self) -> Result<()> {
match self.subcmd {
SubCommand::HashPassword(cmd) => cmd.run(),
}
}
}
#[derive(Debug, Parser)]
#[clap(group(
ArgGroup::new("password-input")
.required(true)
.args(["password", "password_stdin"])
))]
pub struct HashPasswordCommand {
/// Password verifier format to generate.
#[clap(long, value_enum, default_value = "pbkdf2_sha256")]
format: PasswordFormat,
/// Plaintext password. Prefer --password-stdin to avoid shell history leaks.
#[clap(long)]
password: Option<String>,
/// Read the plaintext password from stdin.
#[clap(long)]
password_stdin: bool,
/// PBKDF2-SHA256 iteration count.
#[clap(long, default_value_t = DEFAULT_PBKDF2_SHA256_ITERATIONS)]
iterations: u32,
/// PBKDF2-SHA256 random salt length in bytes.
#[clap(long, default_value_t = 16)]
salt_len: usize,
/// PBKDF2-SHA256 salt as hex. Mainly useful for deterministic automation.
#[clap(long)]
salt_hex: Option<String>,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[clap(rename_all = "snake_case")]
enum PasswordFormat {
Pbkdf2Sha256,
MysqlNativePassword,
}
impl HashPasswordCommand {
fn run(self) -> Result<()> {
let password = self.read_password()?;
let verifier = match self.format {
PasswordFormat::Pbkdf2Sha256 => {
let salt = self.pbkdf2_salt()?;
format_pbkdf2_sha256_password_verifier(password.as_bytes(), &salt, self.iterations)
.map_err(common_error::ext::BoxedError::new)
.context(error::OtherSnafu)?
}
PasswordFormat::MysqlNativePassword => {
format_mysql_native_password_verifier(password.as_bytes())
}
};
println!("{verifier}");
Ok(())
}
fn read_password(&self) -> Result<String> {
let password = if let Some(password) = self.password.as_ref() {
password.clone()
} else {
let mut password = String::new();
io::stdin()
.read_line(&mut password)
.context(error::FileIoSnafu)?;
password.trim_end_matches(['\r', '\n']).to_string()
};
// A blank password is rejected by the user provider before verifier
// comparison, so a verifier built from it would be unusable. Fail fast
// instead of emitting a dead verifier (e.g. on EOF or an empty line).
if password.is_empty() {
return error::IllegalConfigSnafu {
msg: "password must not be empty",
}
.fail();
}
Ok(password)
}
fn pbkdf2_salt(&self) -> Result<Vec<u8>> {
if let Some(salt_hex) = self.salt_hex.as_ref() {
let salt = hex::decode(salt_hex).map_err(|err| {
error::IllegalConfigSnafu {
msg: format!("invalid --salt-hex: {err}"),
}
.build()
})?;
Self::ensure_salt_len(salt.len())?;
return Ok(salt);
}
Self::ensure_salt_len(self.salt_len)?;
let mut salt = vec![0u8; self.salt_len];
rand::rng().fill_bytes(&mut salt);
Ok(salt)
}
fn ensure_salt_len(salt_len: usize) -> Result<()> {
if salt_len == 0 || salt_len > MAX_PBKDF2_SHA256_SALT_LEN {
return error::IllegalConfigSnafu {
msg: format!("salt length must be in 1..={}", MAX_PBKDF2_SHA256_SALT_LEN),
}
.fail();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use auth::MAX_PBKDF2_SHA256_ITERATIONS;
use super::*;
#[test]
fn test_hash_password_command_with_pbkdf2_sha256() {
let cmd = HashPasswordCommand {
format: PasswordFormat::Pbkdf2Sha256,
password: Some("password".to_string()),
password_stdin: false,
iterations: 4096,
salt_len: 16,
salt_hex: Some("73616c74".to_string()),
};
let password = cmd.read_password().unwrap();
let salt = cmd.pbkdf2_salt().unwrap();
let verifier =
format_pbkdf2_sha256_password_verifier(password.as_bytes(), &salt, cmd.iterations)
.unwrap();
assert_eq!(
"pbkdf2_sha256:4096:73616c74:c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a",
verifier
);
}
#[test]
fn test_hash_password_command_with_mysql_native_password() {
let verifier = format_mysql_native_password_verifier("123456".as_bytes());
assert_eq!(
"mysql_native_password:6bb4837eb74329105ee4568dda7dc67ed2ca2ad9",
verifier
);
}
#[test]
fn test_reject_empty_salt() {
let cmd = HashPasswordCommand {
format: PasswordFormat::Pbkdf2Sha256,
password: Some("password".to_string()),
password_stdin: false,
iterations: 4096,
salt_len: 0,
salt_hex: None,
};
assert!(cmd.pbkdf2_salt().is_err());
}
#[test]
fn test_reject_empty_password() {
let cmd = HashPasswordCommand {
format: PasswordFormat::Pbkdf2Sha256,
password: Some(String::new()),
password_stdin: false,
iterations: 4096,
salt_len: 16,
salt_hex: Some("73616c74".to_string()),
};
assert!(cmd.read_password().is_err());
}
#[test]
fn test_reject_too_many_iterations() {
let result = format_pbkdf2_sha256_password_verifier(
b"password",
b"salt",
MAX_PBKDF2_SHA256_ITERATIONS + 1,
);
assert!(result.is_err());
}
}