mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-12-11 13:56:27 +08:00
263 lines
10 KiB
Rust
263 lines
10 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::Principal;
|
|
use crate::PrincipalData;
|
|
use argon2::Argon2;
|
|
use compact_str::ToCompactString;
|
|
use mail_builder::encoders::base64::base64_encode;
|
|
use mail_parser::decoders::base64::base64_decode;
|
|
use password_hash::PasswordHash;
|
|
use pbkdf2::Pbkdf2;
|
|
use pwhash::{bcrypt, bsdi_crypt, md5_crypt, sha1_crypt, sha256_crypt, sha512_crypt, unix_crypt};
|
|
use scrypt::Scrypt;
|
|
use sha1::Digest;
|
|
use sha1::Sha1;
|
|
use sha2::Sha256;
|
|
use sha2::Sha512;
|
|
use tokio::sync::oneshot;
|
|
use totp_rs::TOTP;
|
|
|
|
impl Principal {
|
|
pub async fn verify_secret(
|
|
&self,
|
|
code: &str,
|
|
only_app_pass: bool,
|
|
is_ordered: bool,
|
|
) -> trc::Result<bool> {
|
|
let mut seen_password = false;
|
|
let mut password = None;
|
|
let mut otp_auth = None;
|
|
|
|
for item in &self.data {
|
|
match item {
|
|
PrincipalData::OtpAuth(secret) => {
|
|
if !only_app_pass {
|
|
otp_auth = Some(secret);
|
|
}
|
|
seen_password = true;
|
|
}
|
|
PrincipalData::Password(secret) => {
|
|
if !only_app_pass {
|
|
password = Some(secret);
|
|
}
|
|
seen_password = true;
|
|
}
|
|
PrincipalData::AppPassword(secret) => {
|
|
// App passwords do not require TOTP
|
|
if let Some((_, app_secret)) =
|
|
secret.strip_prefix("$app$").and_then(|s| s.split_once('$'))
|
|
&& verify_secret_hash(app_secret, code).await?
|
|
{
|
|
return Ok(true);
|
|
}
|
|
|
|
seen_password = true;
|
|
}
|
|
_ => {
|
|
if seen_password && is_ordered {
|
|
// Password-related secrets are expected to be at the beginning of the list
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate TOTP
|
|
match (otp_auth, password) {
|
|
(Some(otp_auth), Some(password)) => {
|
|
if let Some((code, totp_token)) = code.rsplit_once('$').filter(|(c, t)| {
|
|
!c.is_empty()
|
|
&& (6..=8).contains(&t.len())
|
|
&& t.as_bytes().iter().all(|b| b.is_ascii_digit())
|
|
}) {
|
|
let result = verify_secret_hash(password, code).await?
|
|
&& TOTP::from_url(otp_auth)
|
|
.map_err(|err| {
|
|
trc::AuthEvent::Error
|
|
.reason(err)
|
|
.details(otp_auth.to_compact_string())
|
|
})?
|
|
.check_current(totp_token)
|
|
.unwrap_or(false);
|
|
Ok(result)
|
|
} else if verify_secret_hash(password, code).await? {
|
|
// Only let the client know if the TOTP code is missing
|
|
// if the password is correct
|
|
|
|
Err(trc::AuthEvent::MissingTotp.into_err())
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
(None, Some(password)) => verify_secret_hash(password, code).await,
|
|
_ => Ok(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result<bool> {
|
|
if hashed_secret.starts_with("$argon2")
|
|
|| hashed_secret.starts_with("$pbkdf2")
|
|
|| hashed_secret.starts_with("$scrypt")
|
|
{
|
|
let (tx, rx) = oneshot::channel();
|
|
let secret = secret.to_string();
|
|
let hashed_secret = hashed_secret.to_string();
|
|
|
|
tokio::task::spawn_blocking(move || match PasswordHash::new(&hashed_secret) {
|
|
Ok(hash) => {
|
|
tx.send(Ok(hash
|
|
.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret)
|
|
.is_ok()))
|
|
.ok();
|
|
}
|
|
Err(err) => {
|
|
tx.send(Err(trc::AuthEvent::Error
|
|
.reason(err)
|
|
.details(hashed_secret)))
|
|
.ok();
|
|
}
|
|
});
|
|
|
|
match rx.await {
|
|
Ok(result) => result,
|
|
Err(err) => Err(trc::EventType::Server(trc::ServerEvent::ThreadError)
|
|
.caused_by(trc::location!())
|
|
.reason(err)),
|
|
}
|
|
} else if hashed_secret.starts_with("$2") {
|
|
// Blowfish crypt
|
|
Ok(bcrypt::verify(secret, hashed_secret))
|
|
} else if hashed_secret.starts_with("$6$") {
|
|
// SHA-512 crypt
|
|
Ok(sha512_crypt::verify(secret, hashed_secret))
|
|
} else if hashed_secret.starts_with("$5$") {
|
|
// SHA-256 crypt
|
|
Ok(sha256_crypt::verify(secret, hashed_secret))
|
|
} else if hashed_secret.starts_with("$sha1") {
|
|
// SHA-1 crypt
|
|
Ok(sha1_crypt::verify(secret, hashed_secret))
|
|
} else if hashed_secret.starts_with("$1") {
|
|
// MD5 based hash
|
|
Ok(md5_crypt::verify(secret, hashed_secret))
|
|
} else {
|
|
Err(trc::AuthEvent::Error
|
|
.into_err()
|
|
.details(hashed_secret.to_string()))
|
|
}
|
|
}
|
|
|
|
pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> trc::Result<bool> {
|
|
if hashed_secret.starts_with('$') {
|
|
verify_hash_prefix(hashed_secret, secret).await
|
|
} else if hashed_secret.starts_with('_') {
|
|
// Enhanced DES-based hash
|
|
Ok(bsdi_crypt::verify(secret, hashed_secret))
|
|
} else if let Some(hashed_secret) = hashed_secret.strip_prefix('{') {
|
|
if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') {
|
|
match algo {
|
|
"ARGON2" | "ARGON2I" | "ARGON2ID" | "PBKDF2" => {
|
|
verify_hash_prefix(hashed_secret, secret).await
|
|
}
|
|
"SHA" => {
|
|
// SHA-1
|
|
let mut hasher = Sha1::new();
|
|
hasher.update(secret.as_bytes());
|
|
Ok(
|
|
String::from_utf8(
|
|
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
|
|
)
|
|
.unwrap()
|
|
== hashed_secret,
|
|
)
|
|
}
|
|
"SSHA" => {
|
|
// Salted SHA-1
|
|
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
|
|
let hash = decoded.get(..20).unwrap_or_default();
|
|
let salt = decoded.get(20..).unwrap_or_default();
|
|
let mut hasher = Sha1::new();
|
|
hasher.update(secret.as_bytes());
|
|
hasher.update(salt);
|
|
Ok(&hasher.finalize()[..] == hash)
|
|
}
|
|
"SHA256" => {
|
|
// Verify hash
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(secret.as_bytes());
|
|
Ok(
|
|
String::from_utf8(
|
|
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
|
|
)
|
|
.unwrap()
|
|
== hashed_secret,
|
|
)
|
|
}
|
|
"SSHA256" => {
|
|
// Salted SHA-256
|
|
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
|
|
let hash = decoded.get(..32).unwrap_or_default();
|
|
let salt = decoded.get(32..).unwrap_or_default();
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(secret.as_bytes());
|
|
hasher.update(salt);
|
|
Ok(&hasher.finalize()[..] == hash)
|
|
}
|
|
"SHA512" => {
|
|
// SHA-512
|
|
let mut hasher = Sha512::new();
|
|
hasher.update(secret.as_bytes());
|
|
Ok(
|
|
String::from_utf8(
|
|
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
|
|
)
|
|
.unwrap()
|
|
== hashed_secret,
|
|
)
|
|
}
|
|
"SSHA512" => {
|
|
// Salted SHA-512
|
|
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
|
|
let hash = decoded.get(..64).unwrap_or_default();
|
|
let salt = decoded.get(64..).unwrap_or_default();
|
|
let mut hasher = Sha512::new();
|
|
hasher.update(secret.as_bytes());
|
|
hasher.update(salt);
|
|
Ok(&hasher.finalize()[..] == hash)
|
|
}
|
|
"MD5" => {
|
|
// MD5
|
|
let digest = md5::compute(secret.as_bytes());
|
|
Ok(
|
|
String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap()
|
|
== hashed_secret,
|
|
)
|
|
}
|
|
"CRYPT" | "crypt" => {
|
|
if hashed_secret.starts_with('$') {
|
|
verify_hash_prefix(hashed_secret, secret).await
|
|
} else {
|
|
// Unix crypt
|
|
Ok(unix_crypt::verify(secret, hashed_secret))
|
|
}
|
|
}
|
|
"PLAIN" | "plain" | "CLEAR" | "clear" => Ok(hashed_secret == secret),
|
|
_ => Err(trc::AuthEvent::Error
|
|
.ctx(trc::Key::Reason, "Unsupported algorithm")
|
|
.details(hashed_secret.to_string())),
|
|
}
|
|
} else {
|
|
Err(trc::AuthEvent::Error
|
|
.into_err()
|
|
.details(hashed_secret.to_string()))
|
|
}
|
|
} else if !hashed_secret.is_empty() {
|
|
Ok(hashed_secret == secret)
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|