mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-26 21:56:17 +00:00
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:
Generated
+1
@@ -2128,6 +2128,7 @@ dependencies = [
|
||||
"flow",
|
||||
"frontend",
|
||||
"futures",
|
||||
"hex",
|
||||
"human-panic",
|
||||
"humantime",
|
||||
"lazy_static",
|
||||
|
||||
+77
-1
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user