From ffbab698b790dff3ea354588466c8f33de400f05 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 1 Aug 2022 20:42:30 +0800 Subject: [PATCH] password Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 287 +++++++++++++++--- flutter/lib/models/server_model.dart | 47 ++- src/flutter_ffi.rs | 18 +- src/ui.rs | 8 +- src/ui_interface.rs | 63 ++-- 5 files changed, 353 insertions(+), 70 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index a162c3535..54a52b774 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -127,9 +128,8 @@ class _DesktopHomePageState extends State with TrayListener { ), TextFormField( controller: model.serverId, - decoration: InputDecoration( - enabled: false, - ), + enableInteractiveSelection: true, + readOnly: true, ), ], ), @@ -248,8 +248,34 @@ class _DesktopHomePageState extends State with TrayListener { translate("Password"), style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), ), - TextFormField( - controller: model.serverPasswd, + Row( + children: [ + Expanded( + child: TextFormField( + controller: model.serverPasswd, + enableInteractiveSelection: true, + readOnly: true, + ), + ), + IconButton( + icon: Icon(Icons.refresh), + onPressed: () { + gFFI.setByName("temporary_password"); + }, + ), + FutureBuilder( + future: buildPasswordPopupMenu(context), + builder: (context, snapshot) { + if (snapshot.hasError) { + print("${snapshot.error}"); + } + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }) + ], ), ], ), @@ -260,6 +286,83 @@ class _DesktopHomePageState extends State with TrayListener { ); } + Future buildPasswordPopupMenu(BuildContext context) async { + var position; + return GestureDetector( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + var method = (String text, String value) => PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: gFFI.serverModel.verificationMethod != value, + child: Icon(Icons.check)), + Text( + text, + ), + ], + ), + value: value, + onTap: () => gFFI.serverModel.verificationMethod = value, + ); + final temporary_enabled = + gFFI.serverModel.verificationMethod != kUsePermanentPassword; + var menu = [ + method(translate("Use temporary password"), kUseTemporaryPassword), + method(translate("Use permanent password"), kUsePermanentPassword), + method(translate("Use both passwords"), kUseBothPasswords), + PopupMenuItem( + child: Text(translate("Set permanent password")), + value: 'set-permanent-password', + enabled: gFFI.serverModel.verificationMethod != + kUseTemporaryPassword), + PopupMenuItem( + child: PopupMenuButton( + child: Text("Set temporary password length"), + itemBuilder: (context) => ["6", "8", "10"] + .map((e) => PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: gFFI.serverModel + .temporaryPasswordLength != + e, + child: Icon(Icons.check)), + Text( + e, + ), + ], + ), + value: e, + onTap: () { + if (gFFI.serverModel.temporaryPasswordLength != + e) { + gFFI.serverModel.temporaryPasswordLength = e; + gFFI.setByName("temporary_password"); + } + }, + )) + .toList(), + enabled: temporary_enabled, + ), + value: 'set-temporary-password-length', + enabled: temporary_enabled), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + if (v == "set-permanent-password") { + setPasswordDialog(); + } + } + }, + child: Icon(Icons.edit)); + } + buildTip(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), @@ -295,15 +398,15 @@ class _DesktopHomePageState extends State with TrayListener { Text(translate("Control Remote Desktop")), Form( child: Column( - children: [ - TextFormField( - controller: TextEditingController(), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) - ], - ) + children: [ + TextFormField( + controller: TextEditingController(), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) ], - )) + ) + ], + )) ], ), ); @@ -320,7 +423,7 @@ class _DesktopHomePageState extends State with TrayListener { case "quit": exit(0); case "show": - // windowManager.show(); + // windowManager.show(); break; default: break; @@ -398,7 +501,7 @@ class _DesktopHomePageState extends State with TrayListener { return isPositive ? TextStyle() : TextStyle( - color: Colors.redAccent, decoration: TextDecoration.lineThrough); + color: Colors.redAccent, decoration: TextDecoration.lineThrough); } PopupMenuItem genAudioInputPopupMenuItem() { @@ -410,29 +513,29 @@ class _DesktopHomePageState extends State with TrayListener { future: gFFI.getAudioInputs(), builder: (context, snapshot) { if (snapshot.hasData) { - final inputs = snapshot.data!; + final inputs = snapshot.data!.toList(); if (Platform.isWindows) { inputs.insert(0, translate("System Sound")); } var inputList = inputs .map((e) => PopupMenuItem( - child: Row( - children: [ - Obx(() => Offstage( - offstage: defaultInput.value != e, - child: Icon(Icons.check))), - Expanded( - child: Tooltip( - message: e, - child: Text( - "$e", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ))), - ], - ), - value: e, - )) + child: Row( + children: [ + Obx(() => Offstage( + offstage: defaultInput.value != e, + child: Icon(Icons.check))), + Expanded( + child: Tooltip( + message: e, + child: Text( + "$e", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ))), + ], + ), + value: e, + )) .toList(); inputList.insert( 0, @@ -553,7 +656,7 @@ class _DesktopHomePageState extends State with TrayListener { void changeServer() async { Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + jsonDecode(await gFFI.bind.mainGetOptions()); print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; @@ -592,7 +695,7 @@ class _DesktopHomePageState extends State with TrayListener { decoration: InputDecoration( border: OutlineInputBorder(), errorText: - idServerMsg.isNotEmpty ? idServerMsg : null), + idServerMsg.isNotEmpty ? idServerMsg : null), controller: TextEditingController(text: idServer), ), ), @@ -645,7 +748,7 @@ class _DesktopHomePageState extends State with TrayListener { decoration: InputDecoration( border: OutlineInputBorder(), errorText: - apiServerMsg.isNotEmpty ? apiServerMsg : null), + apiServerMsg.isNotEmpty ? apiServerMsg : null), controller: TextEditingController(text: apiServer), ), ), @@ -761,7 +864,7 @@ class _DesktopHomePageState extends State with TrayListener { void changeWhiteList() async { Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + jsonDecode(await gFFI.bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); var msg = ""; @@ -817,7 +920,7 @@ class _DesktopHomePageState extends State with TrayListener { // pass } else { final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); // test ip final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); for (final ip in ips) { @@ -977,7 +1080,8 @@ class _DesktopHomePageState extends State with TrayListener { return; } } - await gFFI.bind.mainSetSocks(proxy: proxy, username: username, password: password); + await gFFI.bind.mainSetSocks( + proxy: proxy, username: username, password: password); close(); }, child: Text(translate("OK"))), @@ -1204,4 +1308,107 @@ Future loginDialog() async { ); }); return completer.future; -} \ No newline at end of file +} + +void setPasswordDialog() { + final pw = gFFI.getByName("permanent_password"); + final p0 = TextEditingController(text: pw); + final p1 = TextEditingController(text: pw); + var errMsg0 = ""; + var errMsg1 = ""; + + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Set Password")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: errMsg0.isNotEmpty ? errMsg0 : null), + controller: p0, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Confirmation')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + if (pass.length < 6) { + setState(() { + errMsg0 = translate("Too short, at least 6 characters."); + }); + return; + } + if (p1.text.trim() != pass) { + setState(() { + errMsg1 = translate("The confirmation is not identical."); + }); + return; + } + gFFI.setByName("permanent_password", pass); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 8ea9e1c93..c9147441e 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -23,6 +23,7 @@ class ServerModel with ChangeNotifier { bool _fileOk = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; + String _temporaryPasswordLength = ""; late String _emptyIdShow; late final TextEditingController _serverId; @@ -42,7 +43,35 @@ class ServerModel with ChangeNotifier { int get connectStatus => _connectStatus; - String get verificationMethod => _verificationMethod; + String get verificationMethod { + final index = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords + ].indexOf(_verificationMethod); + if (index < 0) { + _verificationMethod = kUseBothPasswords; + } + return _verificationMethod; + } + + set verificationMethod(String method) { + _verificationMethod = method; + gFFI.setOption("verification-method", method); + } + + String get temporaryPasswordLength { + final lengthIndex = ["6", "8", "10"].indexOf(_temporaryPasswordLength); + if (lengthIndex < 0) { + _temporaryPasswordLength = "6"; + } + return _temporaryPasswordLength; + } + + set temporaryPasswordLength(String length) { + _temporaryPasswordLength = length; + gFFI.setOption("temporary-password-length", length); + } TextEditingController get serverId => _serverId; @@ -127,16 +156,26 @@ class ServerModel with ChangeNotifier { updatePasswordModel() { var update = false; final temporaryPassword = gFFI.getByName("temporary_password"); - final verificationMethod = gFFI.getByName("option", "verification-method"); + final verificationMethod = gFFI.getOption("verification-method"); + final temporaryPasswordLength = gFFI.getOption("temporary-password-length"); + final oldPwdText = _serverPasswd.text; if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; + } + if (verificationMethod == kUsePermanentPassword) { + _serverPasswd.text = '-'; + } + if (oldPwdText != _serverPasswd.text) { update = true; } - if (_verificationMethod != verificationMethod) { _verificationMethod = verificationMethod; update = true; } + if (_temporaryPasswordLength != temporaryPasswordLength) { + _temporaryPasswordLength = temporaryPasswordLength; + update = true; + } if (update) { notifyListeners(); } @@ -272,7 +311,7 @@ class ServerModel with ChangeNotifier { Future setPermanentPassword(String newPW) async { parent.target?.setByName("permanent_password", newPW); await Future.delayed(Duration(milliseconds: 500)); - final pw = parent.target?.getByName("permanent_password", newPW); + final pw = parent.target?.getByName("permanent_password"); if (newPW == pw) { return true; } else { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9f40b69d5..413e3cde3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -24,8 +24,7 @@ use crate::ui_interface::{ get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option, - set_options, set_peer_option, set_socks, store_fav, temporary_password, test_if_valid_server, - using_public_server, + set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -613,7 +612,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } "option" => { if let Ok(arg) = arg.to_str() { - res = Config::get_option(arg); + res = ui_interface::get_option(arg.to_owned()); } } // "image_quality" => { @@ -642,7 +641,10 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co res = ui_interface::get_id(); } "temporary_password" => { - res = password_security::temporary_password(); + res = ui_interface::temporary_password(); + } + "permanent_password" => { + res = ui_interface::permanent_password(); } "connect_statue" => { res = ONLINE @@ -829,7 +831,7 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { if let Ok(m) = serde_json::from_str::>(value) { if let Some(name) = m.get("name") { if let Some(value) = m.get("value") { - Config::set_option(name.to_owned(), value.to_owned()); + ui_interface::set_option(name.to_owned(), value.to_owned()); if name == "custom-rendezvous-server" { #[cfg(target_os = "android")] crate::rendezvous_mediator::RendezvousMediator::restart(); @@ -1049,6 +1051,12 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { connection_manager::close_conn(id); }; } + "temporary_password" => { + ui_interface::update_temporary_password(); + } + "permanent_password" => { + ui_interface::set_permanent_password(value.to_owned()); + } _ => { log::error!("Unknown name of set_by_name: {}", name); } diff --git a/src/ui.rs b/src/ui.rs index f51b3e7c9..284c3c55d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -32,9 +32,9 @@ use crate::ui_interface::{ is_login_wayland, is_ok_change_id, is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, new_remote, open_url, peer_has_password, permanent_password, post_request, recent_sessions_updated, remove_peer, run_without_install, set_local_option, - set_option, set_options, set_peer_option, set_remote_id, set_share_rdp, set_socks, - show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, - update_temporary_password, using_public_server, + set_option, set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, + set_socks, show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, + update_me, update_temporary_password, using_public_server, }; mod cm; @@ -205,7 +205,7 @@ impl UI { } fn set_permanent_password(&self, password: String) { - allow_err!(ipc::set_permanent_password(password)); + set_permanent_password(password); } fn get_remote_id(&mut self) -> String { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5b4850271..7e08f9855 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -5,11 +5,13 @@ use std::{ time::SystemTime, }; +#[cfg(any(target_os = "android", target_os = "ios"))] +use hbb_common::password_security; use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, - log, password_security, + log, protobuf::Message as _, rendezvous_proto::*, sleep, @@ -129,7 +131,10 @@ pub fn get_license() -> String { } pub fn get_option(key: String) -> String { - get_option_(&key) + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_option(arg); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_option_(&key); } fn get_option_(key: &str) -> String { @@ -243,20 +248,25 @@ pub fn set_options(m: HashMap) { } pub fn set_option(key: String, value: String) { - let mut options = OPTIONS.lock().unwrap(); - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_option(name.to_owned(), value.to_owned()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + ipc::set_options(options.clone()).ok(); } - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); } pub fn install_path() -> String { @@ -358,16 +368,32 @@ pub fn get_connect_status() -> Status { res } +pub fn temporary_password() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return password_security::temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return TEMPORARY_PASSWD.lock().unwrap().clone(); +} + pub fn update_temporary_password() { + #[cfg(any(target_os = "android", target_os = "ios"))] + password_security::update_temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] allow_err!(ipc::update_temporary_password()); } pub fn permanent_password() -> String { - ipc::get_permanent_password() + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_permanent_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_permanent_password(); } -pub fn temporary_password() -> String { - password_security::temporary_password() +pub fn set_permanent_password(password: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_permanent_password(&password); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(ipc::set_permanent_password(password)); } pub fn get_peer(id: String) -> PeerConfig { @@ -680,6 +706,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { if name == "id" { id = value; + } else if name == "temporary-password" { + *TEMPORARY_PASSWD.lock().unwrap() = value; } } Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { @@ -699,6 +727,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver