From 170516572ea3ce663c3b994bdde287f271cdabae Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:49:54 +0800 Subject: [PATCH] refact(password): Store permanent password as hashed verifier (#14619) * refact(password): Store permanent password as hashed verifier Signed-off-by: fufesou * fix(password): remove unused code Signed-off-by: fufesou * fix(password): mobile, password dialog, width 500 Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 5 +- .../lib/desktop/pages/desktop_home_page.dart | 139 +++++++++++++++--- .../desktop/pages/desktop_setting_page.dart | 5 +- flutter/lib/mobile/pages/server_page.dart | 3 +- flutter/lib/mobile/widgets/dialog.dart | 94 ------------ flutter/lib/models/server_model.dart | 11 -- flutter/lib/web/bridge.dart | 8 +- libs/hbb_common | 2 +- src/flutter_ffi.rs | 33 +++-- src/ipc.rs | 135 +++++++++++++++-- src/lang/ar.rs | 2 + src/lang/be.rs | 2 + src/lang/bg.rs | 2 + src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/el.rs | 2 + src/lang/en.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/et.rs | 2 + src/lang/eu.rs | 2 + src/lang/fa.rs | 2 + src/lang/fi.rs | 2 + src/lang/fr.rs | 2 + src/lang/ge.rs | 2 + src/lang/he.rs | 2 + src/lang/hr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/lt.rs | 2 + src/lang/lv.rs | 2 + src/lang/nb.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sc.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/ta.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/uk.rs | 2 + src/lang/vi.rs | 2 + src/server/connection.rs | 83 +++++++++-- src/ui.rs | 15 +- src/ui/common.css | 7 +- src/ui/index.tis | 63 +++++++- src/ui/msgbox.tis | 6 +- src/ui_interface.rs | 50 ++++++- 64 files changed, 563 insertions(+), 192 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index af87f980f..ad3bbc9f6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2377,8 +2377,9 @@ List? urlLinkToCmdArgs(Uri uri) { final password = uri.path.substring("/".length); if (password.isNotEmpty) { Timer(Duration(seconds: 1), () async { - await bind.mainSetPermanentPassword(password: password); - showToast(translate('Successful')); + final ok = + await bind.mainSetPermanentPasswordWithResult(password: password); + showToast(translate(ok ? 'Successful' : 'Failed')); }); } } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 339ecddb0..42ec10032 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -908,12 +908,17 @@ class _DesktopHomePageState extends State } void setPasswordDialog({VoidCallback? notEmptyCallback}) async { - final pw = await bind.mainGetPermanentPassword(); - final p0 = TextEditingController(text: pw); - final p1 = TextEditingController(text: pw); + final p0 = TextEditingController(text: ""); + final p1 = TextEditingController(text: ""); var errMsg0 = ""; var errMsg1 = ""; - final RxString rxPass = pw.trim().obs; + final localPasswordSet = + (await bind.mainGetCommon(key: "local-permanent-password-set")) == "true"; + final permanentPasswordSet = + (await bind.mainGetCommon(key: "permanent-password-set")) == "true"; + final presetPassword = permanentPasswordSet && !localPasswordSet; + var canSubmit = false; + final RxString rxPass = "".obs; final rules = [ DigitValidationRule(), UppercaseValidationRule(), @@ -922,9 +927,21 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { MinCharactersValidationRule(8), ]; final maxLength = bind.mainMaxEncryptLen(); + final statusTip = localPasswordSet + ? translate('password-hidden-tip') + : (presetPassword ? translate('preset-password-in-use-tip') : ''); + final showStatusTipOnMobile = + statusTip.isNotEmpty && !isDesktop && !isWebDesktop; gFFI.dialogManager.show((setState, close, context) { - submit() { + updateCanSubmit() { + canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty; + } + + submit() async { + if (!canSubmit) { + return; + } setState(() { errMsg0 = ""; errMsg1 = ""; @@ -947,7 +964,13 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); return; } - bind.mainSetPermanentPassword(password: pass); + final ok = await bind.mainSetPermanentPasswordWithResult(password: pass); + if (!ok) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; + }); + return; + } if (pass.isNotEmpty) { notEmptyCallback?.call(); } @@ -955,14 +978,20 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { } return CustomAlertDialog( - title: Text(translate("Set Password")), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.key, color: MyTheme.accent), + Text(translate("Set Password")).paddingOnly(left: 10), + ], + ), content: ConstrainedBox( constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 8.0, + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 6.0, ), Row( children: [ @@ -978,6 +1007,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { rxPass.value = value.trim(); setState(() { errMsg0 = ''; + updateCanSubmit(); }); }, maxLength: maxLength, @@ -989,9 +1019,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { children: [ Expanded(child: PasswordStrengthIndicator(password: rxPass)), ], - ).marginSymmetric(vertical: 8), - const SizedBox( - height: 8.0, + ).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8), + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 8.0, ), Row( children: [ @@ -1005,6 +1035,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { onChanged: (value) { setState(() { errMsg1 = ''; + updateCanSubmit(); }); }, maxLength: maxLength, @@ -1012,11 +1043,23 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { ), ], ), - const SizedBox( - height: 8.0, + if (statusTip.isNotEmpty) + Row( + children: [ + Icon(Icons.info, color: Colors.amber, size: 18) + .marginOnly(right: 6), + Expanded( + child: Text( + statusTip, + style: const TextStyle(fontSize: 13, height: 1.1), + )) + ], + ).marginOnly(top: 6, bottom: 2), + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 8.0, ), Obx(() => Wrap( - runSpacing: 8, + runSpacing: showStatusTipOnMobile ? 2.0 : 8.0, spacing: 4, children: rules.map((e) { var checked = e.validate(rxPass.value.trim()); @@ -1036,11 +1079,67 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { ], ), ), - actions: [ - dialogButton("Cancel", onPressed: close, isOutline: true), - dialogButton("OK", onPressed: submit), - ], - onSubmit: submit, + actions: (() { + final cancelButton = dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ); + final removeButton = dialogButton( + "Remove", + icon: Icon(Icons.delete_outline_rounded), + onPressed: () async { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final ok = + await bind.mainSetPermanentPasswordWithResult(password: ""); + if (!ok) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; + }); + return; + } + close(); + }, + buttonStyle: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red)), + ); + final okButton = dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: canSubmit ? submit : null, + ); + if (!isDesktop && !isWebDesktop && localPasswordSet) { + return [ + Align( + alignment: Alignment.centerRight, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + cancelButton, + const SizedBox(width: 4), + removeButton, + const SizedBox(width: 4), + okButton, + ], + ), + ), + ), + ]; + } + return [ + cancelButton, + if (localPasswordSet) removeButton, + okButton, + ]; + })(), + onSubmit: canSubmit ? submit : null, onCancel: close, ); }); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 029629b24..d118b6793 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1109,8 +1109,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (value == passwordValues[passwordKeys .indexOf(kUsePermanentPassword)] && - (await bind.mainGetPermanentPassword()) - .isEmpty) { + (await bind.mainGetCommon( + key: "permanent-password-set")) != + "true") { if (isChangePermanentPasswordDisabled()) { await callback(); return; diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 57856a4d7..2c8b0f2d6 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -150,7 +150,8 @@ class _DropDownAction extends StatelessWidget { } if (value == kUsePermanentPassword && - (await bind.mainGetPermanentPassword()).isEmpty) { + (await bind.mainGetCommon(key: "permanent-password-set")) != + "true") { if (isChangePermanentPasswordDisabled()) { callback(); return; diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index f6900e5dd..8b645bb88 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -12,100 +12,6 @@ void _showSuccess() { showToast(translate("Successful")); } -void _showError() { - showToast(translate("Error")); -} - -void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { - final pw = await bind.mainGetPermanentPassword(); - final p0 = TextEditingController(text: pw); - final p1 = TextEditingController(text: pw); - var validateLength = false; - var validateSame = false; - dialogManager.show((setState, close, context) { - submit() async { - close(); - dialogManager.showLoading(translate("Waiting")); - if (await gFFI.serverModel.setPermanentPassword(p0.text)) { - dialogManager.dismissAll(); - _showSuccess(); - } else { - dialogManager.dismissAll(); - _showError(); - } - } - - return CustomAlertDialog( - title: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.password_rounded, color: MyTheme.accent), - Text(translate('Set your own password')).paddingOnly(left: 10), - ], - ), - content: Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - TextFormField( - autofocus: true, - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Password'), - ), - controller: p0, - validator: (v) { - if (v == null) return null; - final val = v.trim().length > 5; - if (validateLength != val) { - // use delay to make setState success - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateLength = val)); - } - return val - ? null - : translate('Too short, at least 6 characters.'); - }, - ).workaroundFreezeLinuxMint(), - TextFormField( - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Confirmation'), - ), - controller: p1, - validator: (v) { - if (v == null) return null; - final val = p0.text == v; - if (validateSame != val) { - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateSame = val)); - } - return val - ? null - : translate('The confirmation is not identical.'); - }, - ).workaroundFreezeLinuxMint(), - ])), - onCancel: close, - onSubmit: (validateLength && validateSame) ? submit : null, - actions: [ - dialogButton( - 'Cancel', - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ), - dialogButton( - 'OK', - icon: Icon(Icons.done_rounded), - onPressed: (validateLength && validateSame) ? submit : null, - ), - ], - ); - }); -} - void setTemporaryPasswordLengthDialog( OverlayDialogManager dialogManager) async { List lengths = ['6', '8', '10']; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 5892ed0fe..78e334d4f 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -471,17 +471,6 @@ class ServerModel with ChangeNotifier { WakelockManager.disable(_wakelockKey); } - Future setPermanentPassword(String newPW) async { - await bind.mainSetPermanentPassword(password: newPW); - await Future.delayed(Duration(milliseconds: 500)); - final pw = await bind.mainGetPermanentPassword(); - if (newPW == pw) { - return true; - } else { - return false; - } - } - fetchID() async { final id = await bind.mainGetMyId(); if (id != _serverId.id) { diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 66191d004..1cfce661b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1159,10 +1159,6 @@ class RustdeskImpl { return Future.value(''); } - Future mainGetPermanentPassword({dynamic hint}) { - return Future.value(''); - } - Future mainGetFingerprint({dynamic hint}) { return Future.value(''); } @@ -1346,9 +1342,9 @@ class RustdeskImpl { throw UnimplementedError("mainUpdateTemporaryPassword"); } - Future mainSetPermanentPassword( + Future mainSetPermanentPasswordWithResult( {required String password, dynamic hint}) { - throw UnimplementedError("mainSetPermanentPassword"); + throw UnimplementedError("mainSetPermanentPasswordWithResult"); } Future mainCheckSuperUserPermission({dynamic hint}) { diff --git a/libs/hbb_common b/libs/hbb_common index 6fb03d076..f08ce5d6d 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 6fb03d076eae81e244db72e87474eee149a0fb85 +Subproject commit f08ce5d6d07cd200713418ce2932769d14ff21d2 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 092e6d295..e29133687 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1693,8 +1693,8 @@ pub fn main_get_temporary_password() -> String { ui_interface::temporary_password() } -pub fn main_get_permanent_password() -> String { - ui_interface::permanent_password() +pub fn main_set_permanent_password_with_result(password: String) -> bool { + ui_interface::set_permanent_password_with_result(password) } pub fn main_get_fingerprint() -> String { @@ -2072,10 +2072,6 @@ pub fn main_update_temporary_password() { update_temporary_password(); } -pub fn main_set_permanent_password(password: String) { - set_permanent_password(password); -} - pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } @@ -2423,16 +2419,23 @@ pub fn is_disable_installation() -> SyncReturn { } pub fn is_preset_password() -> bool { - config::HARD_SETTINGS + let hard = config::HARD_SETTINGS .read() .unwrap() .get("password") - .map_or(false, |p| { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return p == &crate::ipc::get_permanent_password(); - #[cfg(any(target_os = "android", target_os = "ios"))] - return p == &config::Config::get_permanent_password(); - }) + .cloned() + .unwrap_or_default(); + if hard.is_empty() { + return false; + } + + // On desktop, service owns the authoritative config; query it via IPC and return only a boolean. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::ipc::is_permanent_password_preset(); + + // On mobile, we have no service IPC; verify against local storage. + #[cfg(any(target_os = "android", target_os = "ios"))] + return config::Config::matches_permanent_password_plain(&hard); } // Don't call this function for desktop version. @@ -2768,6 +2771,10 @@ pub fn main_get_common(key: String) -> String { return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string(); #[cfg(not(target_os = "linux"))] return false.to_string(); + } else if key == "permanent-password-set" { + return ui_interface::is_permanent_password_set().to_string(); + } else if key == "local-permanent-password-set" { + return ui_interface::is_local_permanent_password_set().to_string(); } else { if key.starts_with("download-data-") { let id = key.replace("download-data-", ""); diff --git a/src/ipc.rs b/src/ipc.rs index 891ec81dd..099c24d34 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -632,8 +632,29 @@ async fn handle(data: Data, stream: &mut Connection) { value = Some(Config::get_id()); } else if name == "temporary-password" { value = Some(password::temporary_password()); - } else if name == "permanent-password" { - value = Some(Config::get_permanent_password()); + } else if name == "permanent-password-storage-and-salt" { + let (storage, salt) = Config::get_local_permanent_password_storage_and_salt(); + value = Some(storage + "\n" + &salt); + } else if name == "permanent-password-set" { + value = Some(if Config::has_permanent_password() { + "Y".to_owned() + } else { + "N".to_owned() + }); + } else if name == "permanent-password-is-preset" { + let hard = config::HARD_SETTINGS + .read() + .unwrap() + .get("password") + .cloned() + .unwrap_or_default(); + let is_preset = + !hard.is_empty() && Config::matches_permanent_password_plain(&hard); + value = Some(if is_preset { + "Y".to_owned() + } else { + "N".to_owned() + }); } else if name == "salt" { value = Some(Config::get_salt()); } else if name == "rendezvous_server" { @@ -669,13 +690,24 @@ async fn handle(data: Data, stream: &mut Connection) { allow_err!(stream.send(&Data::Config((name, value))).await); } Some(value) => { + let mut updated = true; if name == "id" { Config::set_key_confirmed(false); Config::set_id(&value); } else if name == "temporary-password" { password::update_temporary_password(); } else if name == "permanent-password" { - Config::set_permanent_password(&value); + if Config::is_disable_change_permanent_password() { + log::warn!("Changing permanent password is disabled"); + updated = false; + } else { + Config::set_permanent_password(&value); + } + // Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to + // distinguish "accepted by daemon" vs "IPC send succeeded" without + // reading back any secret. + let ack = if updated { "Y" } else { "N" }.to_owned(); + allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await); } else if name == "salt" { Config::set_salt(&value); } else if name == "voice-call-input" { @@ -685,7 +717,9 @@ async fn handle(data: Data, stream: &mut Connection) { } else { return; } - log::info!("{} updated", name); + if updated { + log::info!("{} updated", name); + } } }, Data::Options(value) => match value { @@ -1143,13 +1177,57 @@ pub fn update_temporary_password() -> ResultType<()> { set_config("temporary-password", "".to_owned()) } -pub fn get_permanent_password() -> String { - if let Ok(Some(v)) = get_config("permanent-password") { - Config::set_permanent_password(&v); - v - } else { - Config::get_permanent_password() +fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> { + let Some(payload) = payload else { + return Ok(()); + }; + let Some((storage, salt)) = payload.split_once('\n') else { + bail!("Invalid permanent-password-storage-and-salt payload"); + }; + + if storage.is_empty() { + Config::set_permanent_password_storage_for_sync("", "")?; + return Ok(()); } + + Config::set_permanent_password_storage_for_sync(storage, salt)?; + Ok(()) +} + +pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> { + let v = get_config("permanent-password-storage-and-salt")?; + apply_permanent_password_storage_and_salt_payload(v.as_deref()) +} + +async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> { + let ms_timeout = 1_000; + let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?; + apply_permanent_password_storage_and_salt_payload(v.as_deref()) +} + +pub fn is_permanent_password_set() -> bool { + match get_config("permanent-password-set") { + Ok(Some(v)) => { + let v = v.trim(); + return v == "Y"; + } + Ok(None) => { + // No response/value (timeout). + } + Err(_) => { + // Connection error. + } + } + log::warn!("Failed to query permanent password state from daemon"); + false +} + +pub fn is_permanent_password_preset() -> bool { + if let Ok(Some(v)) = get_config("permanent-password-is-preset") { + let v = v.trim(); + return v == "Y"; + } + false } pub fn get_fingerprint() -> String { @@ -1159,8 +1237,41 @@ pub fn get_fingerprint() -> String { } pub fn set_permanent_password(v: String) -> ResultType<()> { - Config::set_permanent_password(&v); - set_config("permanent-password", v) + if Config::is_disable_change_permanent_password() { + bail!("Changing permanent password is disabled"); + } + if set_permanent_password_with_ack(v)? { + Ok(()) + } else { + bail!("Changing permanent password was rejected by daemon"); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_permanent_password_with_ack(v: String) -> ResultType { + set_permanent_password_with_ack_async(v).await +} + +async fn set_permanent_password_with_ack_async(v: String) -> ResultType { + // The daemon ACK/NACK is expected quickly since it applies the config in-process. + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send_config("permanent-password", v).await?; + if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? { + if name2 == "permanent-password" { + let v = v.trim(); + let ok = v == "Y"; + if ok { + // Ensure the hashed permanent password storage is written to the user config file. + // This sync must not affect the daemon ACK outcome. + if let Err(err) = sync_permanent_password_storage_from_daemon_async().await { + log::warn!("Failed to sync permanent password storage from daemon: {err}"); + } + } + return Ok(ok); + } + } + Ok(false) } #[cfg(feature = "flutter")] diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8af320864..8204da6fd 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "متابعة مع {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 6735d3eff..6c6a13315 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Працягнуць з {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index e87322b8b..218070291 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продължи с {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index fd78c3ae6..2f1cc8734 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continua amb {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index b4026bdf9..75d16ff92 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"), ("Continue with {}", "使用 {} 登录"), ("Display Name", "显示名称"), + ("password-hidden-tip", "永久密码已设置(已隐藏)"), + ("preset-password-in-use-tip", "当前使用预设密码"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 952a55b6c..7b3dc7908 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovat s {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index d309fff3f..06ad254c7 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsæt med {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ff4139559..7eca199cb 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ("Continue with {}", "Fortfahren mit {}"), ("Display Name", "Anzeigename"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index ab5c6dfa7..38e11bfce 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"), ("Continue with {}", "Συνέχεια με {}"), ("Display Name", "Εμφανιζόμενο όνομα"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index d8190bde0..73974a2e5 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -272,5 +272,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."), ("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"), ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), + ("password-hidden-tip", "Permanent password is set (hidden)."), + ("preset-password-in-use-tip", "Preset password is currently in use."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index be7d8a751..921f79612 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 524c9a98e..0f49079a2 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuar con {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 3a90a1bd7..d65cd31c5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jätka koos {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 04bed674a..f12ecf371 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} honekin jarraitu"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6de3960a9..5f6d5f005 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ادامه با {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 3dc01b4d5..43c033a11 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jatka käyttäen {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 56b19a33d..0dda7817f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ("Continue with {}", "Continuer avec {}"), ("Display Name", "Nom d’affichage"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 8afb46704..dc78bc0d9 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{}-ით გაგრძელება"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 1e2d84b71..741805e25 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "המשך עם {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 8ae5d2d96..2d596bacc 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5486d16b4..e69514e45 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), ("Continue with {}", "Folytatás ezzel: {}"), ("Display Name", "Kijelző név"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a19d9ad85..356a9ee2d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Lanjutkan dengan {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index bc2b98eb6..a577971a9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ("Continue with {}", "Continua con {}"), ("Display Name", "Visualizza nome"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c933c8018..805898ef9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} で続行"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 15cfd10ef..51a18ceb7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("Continue with {}", "{}(으)로 계속"), ("Display Name", "표시 이름"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e3d31cde6..e943ff4cd 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 28451dc6f..a4f39f1e4 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tęsti su {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9c03bf8fc..838984207 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Turpināt ar {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index daf1c90d2..d9cf6ad38 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsett med {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index e999e99ff..77da4f79e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ("Continue with {}", "Ga verder met {}"), ("Display Name", "Naam Weergeven"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 96cf22d22..51611c9b3 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), ("Continue with {}", "Kontynuuj z {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 8637ce63c..0cdcf93b4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b642cd75a..f9bae32b1 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("Continue with {}", "Continuar com {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 69f72f316..0a5ab0299 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuă cu {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c28baf600..5712c1fcd 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ("Continue with {}", "Продолжить с {}"), ("Display Name", "Отображаемое имя"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index c67d88f99..f2c4fbfa2 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Sighi cun {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 9132485ac..d0e99b2a4 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovať s {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6dfb5d572..aef6b7c66 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nadaljuj z {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0cfdb03be..5f9d5505b 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Vazhdo me {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 743aacc2c..19ae6896f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 52a451474..7ad257fcb 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsätt med {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index e4be24259..2cee45268 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} உடன் தொடர்"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b70bb616b..ff755768c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index f4bf65798..2d3eb1d34 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ทำต่อด้วย {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 3f7c21c2b..d69995b5f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), ("Display Name", "Görünen Ad"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 1172fe2cc..4089257cc 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), ("Continue with {}", "使用 {} 登入"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 146b89569..2594b7cc3 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продовжити з {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 6ba287912..6939b2ea1 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tiếp tục với {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1ffb1a25e..afa40a25b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -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::>> = 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>> = 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; diff --git a/src/ui.rs b/src/ui.rs index fc59cffd2..154319ce4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -212,12 +212,16 @@ impl UI { update_temporary_password() } - fn permanent_password(&self) -> String { - permanent_password() + fn set_permanent_password(&self, password: String) { + let _ = set_permanent_password_with_result(password); } - fn set_permanent_password(&self, password: String) { - set_permanent_password(password); + fn is_local_permanent_password_set(&self) -> bool { + is_local_permanent_password_set() + } + + fn is_permanent_password_set(&self) -> bool { + is_permanent_password_set() } fn get_remote_id(&mut self) -> String { @@ -726,8 +730,9 @@ impl sciter::EventHandler for UI { fn get_id(); fn temporary_password(); fn update_temporary_password(); - fn permanent_password(); fn set_permanent_password(String); + fn is_local_permanent_password_set(); + fn is_permanent_password_set(); fn get_remote_id(); fn set_remote_id(String); fn closing(i32, i32, i32, i32); diff --git a/src/ui/common.css b/src/ui/common.css index 3307e0965..16dd6ca9f 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -72,6 +72,11 @@ button.button:hover, button.outline:hover { border-color: color(hover-border); } +button:disabled, +button:disabled:hover { + opacity: 0.3; +} + button.link { background: none !important; border: none; @@ -484,4 +489,4 @@ div.user-session select { background: color(bg); color: color(text); padding-left: 0.5em; -} \ No newline at end of file +} diff --git a/src/ui/index.tis b/src/ui/index.tis index acec6a2b5..be826529d 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1072,6 +1072,7 @@ class PasswordArea: Reactor.Component { var method = handler.get_option('verification-method'); var approve_mode= handler.get_option('approve-mode'); var show_password = approve_mode != 'click'; + var has_local_password = handler.is_local_permanent_password_set(); return
  • {svg_checkmark}{translate('Accept sessions via password')}
  • {svg_checkmark}{translate('Accept sessions via click')}
  • @@ -1082,6 +1083,7 @@ class PasswordArea: Reactor.Component { { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } { !show_password ? '' :
    } { !show_password || disable_change_permanent_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Clear permanent password')}
  • } { !show_password ? '' : }
  • {svg_checkmark}{translate('enable-2fa-title')}
  • @@ -1114,6 +1116,10 @@ class PasswordArea: Reactor.Component { el.state.disabled = true; } } + if (el.id == "clear-password") { + var has_local_password = handler.is_local_permanent_password_set(); + el.state.disabled = !has_local_password; + } if (el.id == "tfa") el.attributes.toggleClass("selected", has_valid_2fa); } @@ -1129,16 +1135,28 @@ class PasswordArea: Reactor.Component { event click $(li#set-password) { var me = this; - var password = handler.permanent_password(); - var value_field = password.length == 0 ? "" : "value=" + password; + var has_local_password = handler.is_local_permanent_password_set(); + var permanent_password_set = handler.is_permanent_password_set(); + var password_hidden_tip = translate('password-hidden-tip'); + var preset_password_tip = translate('preset-password-in-use-tip'); + var password_tip = ""; + if (has_local_password) { + password_tip = "
    [!] " + password_hidden_tip + "
    "; + } else if (permanent_password_set) { + password_tip = "
    [!] " + preset_password_tip + "
    "; + } msgbox("custom-password", translate("Set Password"), "
    \ -
    " + translate('Password') + ":
    \ -
    " + translate('Confirmation') + ":
    \ +
    " + translate('Password') + ":
    \ +
    " + translate('Confirmation') + ":
    \ + " + password_tip + " \
    \ ", "", function(res=null) { if (!res) return; var p0 = (res.password || "").trim(); var p1 = (res.confirmation || "").trim(); + if (p0.length == 0 && p1.length == 0) { + return " "; + } if (p0.length < 6 && p0.length != 0) { return translate("Too short, at least 6 characters."); } @@ -1148,6 +1166,15 @@ class PasswordArea: Reactor.Component { handler.set_permanent_password(p0); me.update(); }, msgbox_default_height, get_msgbox_width()); + self.timer(30ms, function() { + updateSetPasswordSubmitState(); + }); + } + + event click $(li#clear-password) { + if (this.$(li#clear-password).state.disabled) return; + handler.set_permanent_password(""); + this.update(); } event click $(menu#edit-password-context>li) (_, me) { @@ -1227,6 +1254,18 @@ function updatePasswordArea() { } if (!outgoing_only) updatePasswordArea(); +function updateSetPasswordSubmitState() { + var dialog = $(#msgbox); + if (!dialog) return; + var password = dialog.$(input[name='password']); + var confirmation = dialog.$(input[name='confirmation']); + var submit = dialog.$(button#submit); + if (!password || !confirmation || !submit) return; + var can_submit = (password.value || "").trim().length > 0 || + (confirmation.value || "").trim().length > 0; + submit.state.disabled = !can_submit; +} + class ID: Reactor.Component { function render() { return
    ); event click $(#powered-by) { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 542691f5f..6e6b6a62f 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -193,8 +193,10 @@ class MsgboxComponent: Reactor.Component { } function submit() { - if (this.$(button#submit)) { - this.$(button#submit).sendEvent("click"); + var submit_btn = this.$(button#submit); + if (submit_btn) { + if (submit_btn.state.disabled) return; + submit_btn.sendEvent("click"); } } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 49098f2db..1645b242d 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -609,19 +609,57 @@ pub fn update_temporary_password() { } #[inline] -pub fn permanent_password() -> String { +pub fn is_permanent_password_set() -> bool { #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::get_permanent_password(); + return Config::has_permanent_password(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - return ipc::get_permanent_password(); + { + let daemon_is_set = ipc::is_permanent_password_set(); + // `daemon_is_set` is authoritative for the return value. Local storage is only used to + // decide whether we should attempt a sync to clear stale user-side state. + let local_storage_is_empty = if daemon_is_set { + true + } else { + let (storage, _) = Config::get_local_permanent_password_storage_and_salt(); + storage.is_empty() + }; + if daemon_is_set || !local_storage_is_empty { + allow_err!(ipc::sync_permanent_password_storage_from_daemon()); + } + daemon_is_set + } } #[inline] -pub fn set_permanent_password(password: String) { +pub fn is_local_permanent_password_set() -> bool { #[cfg(any(target_os = "android", target_os = "ios"))] - Config::set_permanent_password(&password); + return Config::has_local_permanent_password(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - allow_err!(ipc::set_permanent_password(password)); + { + allow_err!(ipc::sync_permanent_password_storage_from_daemon()); + Config::has_local_permanent_password() + } +} + +pub fn set_permanent_password_with_result(password: String) -> bool { + if config::Config::is_disable_change_permanent_password() { + return false; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + config::Config::set_permanent_password(&password); + return true; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + match crate::ipc::set_permanent_password_with_ack(password) { + Ok(ok) => ok, + Err(err) => { + log::warn!("Failed to set permanent password via IPC: {err}"); + false + } + } + } } #[inline]