mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
trust this device to skip 2fa (#9012)
* trust this device to skip 2fa Signed-off-by: 21pages <sunboeasy@gmail.com> * Update connection.rs --------- Signed-off-by: 21pages <sunboeasy@gmail.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
@@ -1831,6 +1831,7 @@ void changeBot({Function()? callback}) async {
|
||||
void change2fa({Function()? callback}) async {
|
||||
if (bind.mainHasValid2FaSync()) {
|
||||
await bind.mainSetOption(key: "2fa", value: "");
|
||||
await bind.mainClearTrustedDevices();
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
@@ -1898,6 +1899,7 @@ void enter2FaDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
final RxBool submitReady = false.obs;
|
||||
final RxBool trustThisDevice = false.obs;
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -1907,7 +1909,7 @@ void enter2FaDialog(
|
||||
}
|
||||
|
||||
submit() {
|
||||
gFFI.send2FA(sessionId, controller.text.trim());
|
||||
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
@@ -1921,9 +1923,27 @@ void enter2FaDialog(
|
||||
onChanged: () => submitReady.value = codeField.isReady,
|
||||
);
|
||||
|
||||
final trustField = Obx(() => CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(translate("Trust this device")),
|
||||
value: trustThisDevice.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
trustThisDevice.value = value;
|
||||
},
|
||||
));
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('enter-2fa-title')),
|
||||
content: codeField,
|
||||
content: Column(
|
||||
children: [
|
||||
codeField,
|
||||
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
|
||||
trustField,
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton('Cancel',
|
||||
onPressed: cancel,
|
||||
@@ -2313,3 +2333,157 @@ void checkUnlockPinDialog(String correctPin, Function() passCallback) {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void confrimDeleteTrustedDevicesDialog(
|
||||
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
|
||||
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
|
||||
() async {
|
||||
if (selectedDevices.isEmpty) return;
|
||||
if (selectedDevices.length == trustedDevices.length) {
|
||||
await bind.mainClearTrustedDevices();
|
||||
trustedDevices.clear();
|
||||
selectedDevices.clear();
|
||||
} else {
|
||||
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
|
||||
await bind.mainRemoveTrustedDevices(json: json);
|
||||
trustedDevices.removeWhere((element) {
|
||||
return selectedDevices.contains(element.hwid);
|
||||
});
|
||||
selectedDevices.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void manageTrustedDeviceDialog() async {
|
||||
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
|
||||
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Manage trusted devices")),
|
||||
content: trustedDevicesTable(trustedDevices, selectedDevices),
|
||||
actions: [
|
||||
Obx(() => dialogButton(translate("Delete"),
|
||||
onPressed: selectedDevices.isEmpty
|
||||
? null
|
||||
: () {
|
||||
confrimDeleteTrustedDevicesDialog(
|
||||
trustedDevices,
|
||||
selectedDevices,
|
||||
);
|
||||
},
|
||||
isOutline: false)
|
||||
.marginOnly(top: 12)),
|
||||
dialogButton(translate("Close"), onPressed: close, isOutline: true)
|
||||
.marginOnly(top: 12),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class TrustedDevice {
|
||||
late final Uint8List hwid;
|
||||
late final int time;
|
||||
late final String id;
|
||||
late final String name;
|
||||
late final String platform;
|
||||
|
||||
TrustedDevice.fromJson(Map<String, dynamic> json) {
|
||||
final hwidList = json['hwid'] as List<dynamic>;
|
||||
hwid = Uint8List.fromList(hwidList.cast<int>());
|
||||
time = json['time'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
platform = json['platform'];
|
||||
}
|
||||
|
||||
String daysRemaining() {
|
||||
final expiry = time + 90 * 24 * 60 * 60 * 1000;
|
||||
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
|
||||
if (remaining < 0) {
|
||||
return '0';
|
||||
}
|
||||
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
|
||||
}
|
||||
|
||||
static Future<List<TrustedDevice>> get() async {
|
||||
final List<TrustedDevice> devices = List.empty(growable: true);
|
||||
try {
|
||||
final devicesJson = await bind.mainGetTrustedDevices();
|
||||
if (devicesJson.isNotEmpty) {
|
||||
final devicesList = json.decode(devicesJson);
|
||||
if (devicesList is List) {
|
||||
for (var device in devicesList) {
|
||||
devices.add(TrustedDevice.fromJson(device));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
devices.sort((a, b) => b.time.compareTo(a.time));
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
Widget trustedDevicesTable(
|
||||
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
|
||||
RxBool selectAll = false.obs;
|
||||
setSelectAll() {
|
||||
if (selectedDevices.isNotEmpty &&
|
||||
selectedDevices.length == devices.length) {
|
||||
selectAll.value = true;
|
||||
} else {
|
||||
selectAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
devices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
selectedDevices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
return FittedBox(
|
||||
child: Obx(() => DataTable(
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Checkbox(
|
||||
value: selectAll.value,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedDevices.clear();
|
||||
selectedDevices.addAll(devices.map((e) => e.hwid));
|
||||
} else {
|
||||
selectedDevices.clear();
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataColumn(label: Text(translate('Platform'))),
|
||||
DataColumn(label: Text(translate('ID'))),
|
||||
DataColumn(label: Text(translate('Username'))),
|
||||
DataColumn(label: Text(translate('Days remaining'))),
|
||||
],
|
||||
rows: devices.map((device) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Checkbox(
|
||||
value: selectedDevices.contains(device.hwid),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
selectedDevices.remove(device.hwid);
|
||||
selectedDevices.add(device.hwid);
|
||||
} else {
|
||||
selectedDevices.remove(device.hwid);
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataCell(Text(device.platform)),
|
||||
DataCell(Text(device.id)),
|
||||
DataCell(Text(device.name)),
|
||||
DataCell(Text(device.daysRemaining())),
|
||||
]);
|
||||
}).toList(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||
const String kOptionStopService = "stop-service";
|
||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
|
||||
// buildin opitons
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
|
||||
@@ -783,8 +783,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
onChangedBot(!hasBot.value);
|
||||
},
|
||||
).marginOnly(left: _kCheckBoxLeftMargin + 30);
|
||||
|
||||
final trust = Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate("enable-trusted-devices-tip"),
|
||||
child: _OptionCheckBox(context, "Enable trusted devices",
|
||||
kOptionEnableTrustedDevices,
|
||||
enabled: !locked, update: (v) {
|
||||
setState(() {});
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
|
||||
ElevatedButton(
|
||||
onPressed: locked
|
||||
? null
|
||||
: () {
|
||||
manageTrustedDeviceDialog();
|
||||
},
|
||||
child: Text(translate('Manage trusted devices')))
|
||||
],
|
||||
).marginOnly(left: 30);
|
||||
|
||||
return Column(
|
||||
children: [tfa, bot],
|
||||
children: [tfa, bot, trust],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
@@ -87,6 +88,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _hideServer = false;
|
||||
var _hideProxy = false;
|
||||
var _hideNetwork = false;
|
||||
var _enableTrustedDevices = false;
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
@@ -113,6 +115,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
_hideNetwork =
|
||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -243,18 +246,57 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
));
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
final enable2fa = bind.mainHasValid2FaSync();
|
||||
final List<AbstractSettingsTile> tfaTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('enable-2fa-title')),
|
||||
initialValue: bind.mainHasValid2FaSync(),
|
||||
onToggle: (_) async {
|
||||
initialValue: enable2fa,
|
||||
onToggle: (v) async {
|
||||
update() async {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
change2fa(callback: update);
|
||||
if (v == false) {
|
||||
CommonConfirmDialog(
|
||||
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||
change2fa(callback: update);
|
||||
});
|
||||
} else {
|
||||
change2fa(callback: update);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (enable2fa)
|
||||
SettingsTile.switchTile(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate('Enable trusted devices')),
|
||||
Text(translate('enable-trusted-devices-tip'),
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
initialValue: _enableTrustedDevices,
|
||||
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
|
||||
? null
|
||||
: (v) async {
|
||||
mainSetBoolOption(kOptionEnableTrustedDevices, v);
|
||||
setState(() {
|
||||
_enableTrustedDevices = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (enable2fa && _enableTrustedDevices)
|
||||
SettingsTile(
|
||||
title: Text(translate('Manage trusted devices')),
|
||||
trailing: Icon(Icons.arrow_forward_ios),
|
||||
onPressed: (context) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return _ManageTrustedDevices();
|
||||
}));
|
||||
})
|
||||
];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN discovery')),
|
||||
initialValue: _denyLANDiscovery,
|
||||
@@ -642,6 +684,11 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isAndroid &&
|
||||
!disabledSettings &&
|
||||
!outgoingOnly &&
|
||||
!hideSecuritySettings)
|
||||
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
|
||||
if (isAndroid &&
|
||||
!disabledSettings &&
|
||||
!outgoingOnly &&
|
||||
@@ -963,6 +1010,51 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ManageTrustedDevices extends StatefulWidget {
|
||||
const _ManageTrustedDevices();
|
||||
|
||||
@override
|
||||
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
|
||||
}
|
||||
|
||||
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
|
||||
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
|
||||
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(translate('Manage trusted devices')),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
Obx(() => IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: selectedDevices.isEmpty
|
||||
? null
|
||||
: () {
|
||||
confrimDeleteTrustedDevicesDialog(
|
||||
trustedDevices, selectedDevices);
|
||||
}))
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: TrustedDevice.get(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
}
|
||||
final devices = snapshot.data as List<TrustedDevice>;
|
||||
trustedDevices = devices.obs;
|
||||
return trustedDevicesTable(trustedDevices, selectedDevices);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioEntry {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@@ -2611,8 +2611,9 @@ class FFI {
|
||||
remember: remember);
|
||||
}
|
||||
|
||||
void send2FA(SessionID sessionId, String code) {
|
||||
bind.sessionSend2Fa(sessionId: sessionId, code: code);
|
||||
void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
|
||||
bind.sessionSend2Fa(
|
||||
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
|
||||
}
|
||||
|
||||
/// Close the remote session.
|
||||
|
||||
@@ -142,7 +142,10 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<void> sessionSend2Fa(
|
||||
{required UuidValue sessionId, required String code, dynamic hint}) {
|
||||
{required UuidValue sessionId,
|
||||
required String code,
|
||||
required bool trustThisDevice,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', ['send_2fa', code]));
|
||||
}
|
||||
|
||||
@@ -1630,5 +1633,22 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool sessionGetEnableTrustedDevices(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<String> mainGetTrustedDevices({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> mainRemoveTrustedDevices({required String json, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> mainClearTrustedDevices({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user