mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-07 14:48:11 +03:00
refact(password): Store permanent password as hashed verifier (#14619)
* refact(password): Store permanent password as hashed verifier Signed-off-by: fufesou <linlong1266@gmail.com> * fix(password): remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(password): mobile, password dialog, width 500 Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -27,6 +27,7 @@ use hbb_common::platform::linux::run_cmds;
|
||||
#[cfg(target_os = "android")]
|
||||
use hbb_common::protobuf::EnumOrUnknown;
|
||||
use hbb_common::{
|
||||
config::decode_permanent_password_h1_from_storage,
|
||||
config::{self, keys, Config, TrustedDevice},
|
||||
fs::{self, can_enable_overwrite_detection, JobType},
|
||||
futures::{SinkExt, StreamExt},
|
||||
@@ -77,6 +78,18 @@ lazy_static::lazy_static! {
|
||||
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
|
||||
}
|
||||
|
||||
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
// Avoid data-dependent early exits.
|
||||
let mut x: u8 = 0;
|
||||
for i in 0..a.len() {
|
||||
x |= a[i] ^ b[i];
|
||||
}
|
||||
x == 0
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref WALLPAPER_REMOVER: Arc<Mutex<Option<WallPaperRemover>>> = Default::default();
|
||||
@@ -1969,23 +1982,53 @@ impl Connection {
|
||||
self.tx_input.send(MessageInput::Key((msg, press))).ok();
|
||||
}
|
||||
|
||||
fn validate_one_password(&self, password: String) -> bool {
|
||||
if password.len() == 0 {
|
||||
fn verify_h1(&self, h1: &[u8]) -> bool {
|
||||
let mut hasher2 = Sha256::new();
|
||||
hasher2.update(h1);
|
||||
hasher2.update(self.hash.challenge.as_bytes());
|
||||
// A normal `==` on slices may short-circuit on the first mismatch, which can leak how many leading
|
||||
// bytes matched via timing. In typical remote scenarios this is difficult to exploit due to network
|
||||
// jitter, changing challenges, and login attempt throttling, but a constant-time comparison here is
|
||||
// low-cost defensive programming.
|
||||
constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn validate_one_password(&self, password: &str) -> bool {
|
||||
self.validate_password_plain(password)
|
||||
}
|
||||
|
||||
fn validate_password_plain(&self, password: &str) -> bool {
|
||||
if password.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password);
|
||||
hasher.update(&self.hash.salt);
|
||||
let mut hasher2 = Sha256::new();
|
||||
hasher2.update(&hasher.finalize()[..]);
|
||||
hasher2.update(&self.hash.challenge);
|
||||
hasher2.finalize()[..] == self.lr.password[..]
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(self.hash.salt.as_bytes());
|
||||
let h1_plain = hasher.finalize();
|
||||
self.verify_h1(&h1_plain[..])
|
||||
}
|
||||
|
||||
fn validate_password_storage(&self, storage: &str) -> bool {
|
||||
if storage.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use strict decode success to detect hashed storage.
|
||||
// If decode fails, treat as legacy plaintext storage for compatibility.
|
||||
if let Some(h1) = decode_permanent_password_h1_from_storage(storage) {
|
||||
return self.verify_h1(&h1[..]);
|
||||
}
|
||||
|
||||
// Legacy plaintext storage path.
|
||||
self.validate_password_plain(storage)
|
||||
}
|
||||
|
||||
fn validate_password(&mut self) -> bool {
|
||||
if password::temporary_enabled() {
|
||||
let password = password::temporary_password();
|
||||
if self.validate_one_password(password.clone()) {
|
||||
if self.validate_one_password(&password) {
|
||||
raii::AuthedConnID::update_or_insert_session(
|
||||
self.session_key(),
|
||||
Some(password),
|
||||
@@ -1995,8 +2038,24 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
if password::permanent_enabled() {
|
||||
if self.validate_one_password(Config::get_permanent_password()) {
|
||||
return true;
|
||||
// Since hashed storage uses a prefix-based encoding, a hard plaintext that
|
||||
// happens to look like hashed storage could be mis-detected. Validate local storage
|
||||
// and hard/preset plaintext via separate paths to avoid that ambiguity.
|
||||
let (local_storage, _) = Config::get_local_permanent_password_storage_and_salt();
|
||||
if !local_storage.is_empty() {
|
||||
if self.validate_password_storage(&local_storage) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
let hard = config::HARD_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get("password")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !hard.is_empty() && self.validate_password_plain(&hard) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -2016,7 +2075,7 @@ impl Connection {
|
||||
if let Some(session) = session {
|
||||
if !self.lr.password.is_empty()
|
||||
&& (tfa && session.tfa
|
||||
|| !tfa && self.validate_one_password(session.random_password.clone()))
|
||||
|| !tfa && self.validate_password_plain(&session.random_password))
|
||||
{
|
||||
log::info!("is recent session");
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user