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:
fufesou
2026-03-26 14:49:54 +08:00
committed by GitHub
parent 285e29d2dc
commit 170516572e
64 changed files with 563 additions and 192 deletions

View File

@@ -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;