2fa for unattended access

This commit is contained in:
rustdesk 2024-01-19 15:35:58 +08:00
parent 80857c22c9
commit 44e6b7dbb0
55 changed files with 673 additions and 60 deletions

37
Cargo.lock generated
View File

@ -518,6 +518,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base32"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.5" version = "0.21.5"
@ -1135,6 +1141,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "constant_time_eq"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -5427,6 +5439,7 @@ dependencies = [
"system_shutdown", "system_shutdown",
"tao", "tao",
"tauri-winrt-notification", "tauri-winrt-notification",
"totp-rs",
"tray-icon", "tray-icon",
"url", "url",
"users 0.11.0", "users 0.11.0",
@ -6503,6 +6516,22 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "totp-rs"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3504f96adf86d28e7eb16fa236a7951ec72c15ee100d1b5318e225944bc8cb"
dependencies = [
"base32",
"constant_time_eq 0.2.6",
"hmac",
"rand 0.8.5",
"sha1",
"sha2",
"url",
"urlencoding",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@ -6701,6 +6730,12 @@ dependencies = [
"serde 1.0.190", "serde 1.0.190",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "users" name = "users"
version = "0.10.0" version = "0.10.0"
@ -7726,7 +7761,7 @@ dependencies = [
"aes", "aes",
"byteorder", "byteorder",
"bzip2", "bzip2",
"constant_time_eq", "constant_time_eq 0.1.5",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"flate2", "flate2",

View File

@ -84,6 +84,7 @@ libloading = "0.8"
fon = "0.6" fon = "0.6"
zip = "0.6" zip = "0.6"
shutdown_hooks = "0.1" shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
cpal = "0.15" cpal = "0.15"

View File

@ -7,6 +7,7 @@ import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
@ -1501,3 +1502,124 @@ void renameDialog(
); );
}); });
} }
void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: "");
callback?.call();
return;
}
var new2fa = (await bind.mainGenerate2Fa());
final secretRegex = RegExp(r'secret=([^&]+)');
final secret = secretRegex.firstMatch(new2fa)?.group(1);
var msg = ''.obs;
final RxString code = "".obs;
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("enable-2fa-title")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(translate("enable-2fa-desc"),
style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
SizedBox(
width: 160,
height: 160,
child: QrImageView(
backgroundColor: Colors.white,
data: new2fa,
version: QrVersions.auto,
size: 160,
gapless: false,
)).marginOnly(bottom: 6),
SelectableText(secret ?? '', style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
Row(children: [
Expanded(
child: Obx(() => TextField(
decoration: InputDecoration(
errorText:
msg.value.isEmpty ? null : translate(msg.value),
hintText: translate("Verification code")),
onChanged: (value) {
code.value = value;
msg.value = '';
},
autofocus: true)))
]),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
Obx(() => dialogButton(
"OK",
onPressed: code.value.trim().length == 6
? () async {
if (await bind.mainVerify2Fa(code: code.value.trim())) {
callback?.call();
close();
} else {
msg.value = translate('wrong-2fa-code');
}
}
: null,
)),
],
onCancel: close,
);
});
}
void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async {
final RxString code = "".obs;
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
cancel() {
close();
closeConnection();
}
submit() {
if (code.value.trim().length != 6) {
return;
}
gFFI.send2FA(sessionId, code.value.trim());
close();
dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection);
}
return CustomAlertDialog(
title: Text(translate("enter-2fa-title")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: translate("Verification code")),
onChanged: (value) {
code.value = value;
},
autofocus: true))
]),
],
),
onSubmit: submit,
onCancel: cancel,
actions: [
dialogButton(
'Cancel',
onPressed: cancel,
isOutline: true,
),
Obx(() => dialogButton(
'OK',
onPressed: code.value.trim().length == 6 ? submit : null,
)),
]);
});
}

View File

@ -10,7 +10,6 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/manager.dart';
@ -567,6 +566,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: Column(children: [ child: Column(children: [
permissions(context), permissions(context),
password(context), password(context),
_Card(title: '2FA', children: [tfa()]),
_Card(title: 'ID', children: [changeId()]), _Card(title: 'ID', children: [changeId()]),
more(context), more(context),
]), ]),
@ -575,6 +575,45 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
)).marginOnly(bottom: _kListViewBottomMargin)); )).marginOnly(bottom: _kListViewBottomMargin));
} }
Widget tfa() {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool has2fa = bind.mainHasValid2FaSync().obs;
update() async {
has2fa.value = bind.mainHasValid2FaSync();
}
onChanged(bool? checked) async {
change2fa(callback: update);
}
return GestureDetector(
child: InkWell(
child: Obx(() => Row(
children: [
Checkbox(
value: has2fa.value,
onChanged: enabled ? onChanged : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('enable-2fa-title'),
style:
TextStyle(color: _disabledTextColor(context, enabled)),
))
],
)),
),
onTap: () {
onChanged(!has2fa.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
}
return tmpWrapper();
}
Widget changeId() { Widget changeId() {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: gFFI.serverModel, value: gFFI.serverModel,

View File

@ -220,6 +220,16 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
Provider.of<FfiModel>(context); Provider.of<FfiModel>(context);
final List<AbstractSettingsTile> enhancementsTiles = []; final List<AbstractSettingsTile> enhancementsTiles = [];
final List<AbstractSettingsTile> shareScreenTiles = [ final List<AbstractSettingsTile> shareScreenTiles = [
SettingsTile.switchTile(
title: Text(translate('enable-2fa-title')),
initialValue: bind.mainHasValid2FaSync(),
onToggle: (_) async {
update() async {
setState(() {});
}
change2fa(callback: update);
},
),
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('Deny LAN discovery')), title: Text(translate('Deny LAN discovery')),
initialValue: _denyLANDiscovery, initialValue: _denyLANDiscovery,

View File

@ -498,6 +498,8 @@ class FfiModel with ChangeNotifier {
final link = evt['link']; final link = evt['link'];
if (type == 're-input-password') { if (type == 're-input-password') {
wrongPasswordDialog(sessionId, dialogManager, type, title, text); wrongPasswordDialog(sessionId, dialogManager, type, title, text);
} else if (type == 'input-2fa') {
enter2FaDialog(sessionId, dialogManager);
} else if (type == 'input-password') { } else if (type == 'input-password') {
enterPasswordDialog(sessionId, dialogManager); enterPasswordDialog(sessionId, dialogManager);
} else if (type == 'session-login' || type == 'session-re-login') { } else if (type == 'session-login' || type == 'session-re-login') {
@ -2303,6 +2305,10 @@ class FFI {
remember: remember); remember: remember);
} }
void send2FA(SessionID sessionId, String code) {
bind.sessionSend2Fa(sessionId: sessionId, code: code);
}
/// Close the remote session. /// Close the remote session.
Future<void> close({bool closeSession = true}) async { Future<void> close({bool closeSession = true}) async {
closed = true; closed = true;

View File

@ -1077,6 +1077,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.0"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
qr_code_scanner: qr_code_scanner:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1085,6 +1093,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:

View File

@ -105,6 +105,7 @@ dependencies:
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9 ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3 pull_down_button: ^0.9.3
device_info_plus: ^9.1.0 device_info_plus: ^9.1.0
qr_flutter: ^4.1.0
dev_dependencies: dev_dependencies:
icons_launcher: ^2.0.4 icons_launcher: ^2.0.4

View File

@ -82,6 +82,10 @@ message LoginRequest {
OSLogin os_login = 12; OSLogin os_login = 12;
} }
message Auth2FA {
string code = 1;
}
message ChatMessage { string text = 1; } message ChatMessage { string text = 1; }
message Features { message Features {
@ -771,5 +775,6 @@ message Message {
VoiceCallResponse voice_call_response = 24; VoiceCallResponse voice_call_response = 24;
PeerInfo peer_info = 25; PeerInfo peer_info = 25;
PointerDeviceEvent pointer_device_event = 26; PointerDeviceEvent pointer_device_event = 26;
Auth2FA auth_2fa = 27;
} }
} }

105
src/auth_2fa.rs Normal file
View File

@ -0,0 +1,105 @@
use hbb_common::{
bail,
config::Config,
get_time,
password_security::{decrypt_vec_or_original, encrypt_vec_or_original},
ResultType,
};
use serde_derive::{Deserialize, Serialize};
use std::sync::Mutex;
use totp_rs::{Algorithm, Secret, TOTP};
lazy_static::lazy_static! {
static ref CURRENT_2FA: Mutex<Option<(TOTPInfo, TOTP)>> = Mutex::new(None);
}
const ISSUER: &str = "RustDesk";
const TAG_LOGIN: &str = "Connection";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TOTPInfo {
pub name: String,
pub secret: Vec<u8>,
pub digits: usize,
pub created_at: i64,
}
impl TOTPInfo {
fn new_totp(&self) -> ResultType<TOTP> {
let totp = TOTP::new(
Algorithm::SHA1,
self.digits,
1,
30,
self.secret.clone(),
Some(format!("{} {}", ISSUER, TAG_LOGIN)),
self.name.clone(),
)?;
Ok(totp)
}
fn gen_totp_info(name: String, digits: usize) -> ResultType<TOTPInfo> {
let secret = Secret::generate_secret();
let totp = TOTPInfo {
secret: secret.to_bytes()?,
name,
digits,
created_at: get_time(),
..Default::default()
};
Ok(totp)
}
pub fn into_string(&self) -> ResultType<String> {
let secret = encrypt_vec_or_original(self.secret.as_slice(), "00", 1024);
let totp_info = TOTPInfo {
secret,
..self.clone()
};
let s = serde_json::to_string(&totp_info)?;
Ok(s)
}
pub fn from_str(data: &str) -> ResultType<TOTP> {
let mut totp_info = serde_json::from_str::<TOTPInfo>(data)?;
let (secret, success, _) = decrypt_vec_or_original(&totp_info.secret, "00");
if success {
totp_info.secret = secret;
return Ok(totp_info.new_totp()?);
} else {
bail!("decrypt_vec_or_original 2fa secret failed")
}
}
}
pub fn generate2fa() -> String {
if let Ok(info) = TOTPInfo::gen_totp_info(crate::ipc::get_id(), 6) {
if let Ok(totp) = info.new_totp() {
let code = totp.get_url();
*CURRENT_2FA.lock().unwrap() = Some((info, totp));
return code;
}
}
"".to_owned()
}
pub fn verify2fa(code: String) -> bool {
if let Some((info, totp)) = CURRENT_2FA.lock().unwrap().as_ref() {
if let Ok(cur) = totp.generate_current() {
let res = code == cur;
if res {
if let Ok(v) = info.into_string() {
crate::ipc::set_option("2fa", &v);
return res;
}
}
}
}
false
}
pub fn get_2fa(raw: Option<String>) -> Option<TOTP> {
TOTPInfo::from_str(&raw.unwrap_or(Config::get_option("2fa")))
.map(|x| Some(x))
.unwrap_or_default()
}

View File

@ -92,6 +92,8 @@ pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG: &str =
"Desktop session not ready, password wrong"; "Desktop session not ready, password wrong";
pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password"; pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password";
pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password"; pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password";
pub const LOGIN_MSG_2FA_WRONG: &str = "Wrong 2FA Code";
pub const REQUIRE_2FA: &'static str = "2FA Required";
pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
pub const LOGIN_MSG_OFFLINE: &str = "Offline"; pub const LOGIN_MSG_OFFLINE: &str = "Offline";
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported"; pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
@ -2561,6 +2563,10 @@ pub fn handle_login_error(
lc.write().unwrap().password = Default::default(); lc.write().unwrap().password = Default::default();
interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); interface.msgbox("re-input-password", err, "Do you want to enter again?", "");
true true
} else if err == LOGIN_MSG_2FA_WRONG || err == REQUIRE_2FA {
lc.write().unwrap().password = Default::default();
interface.msgbox("input-2fa", err, "", "");
true
} else if LOGIN_ERROR_MAP.contains_key(err) { } else if LOGIN_ERROR_MAP.contains_key(err) {
if let Some(msgbox_info) = LOGIN_ERROR_MAP.get(err) { if let Some(msgbox_info) = LOGIN_ERROR_MAP.get(err) {
interface.msgbox( interface.msgbox(

View File

@ -180,6 +180,15 @@ pub fn session_login(
} }
} }
pub fn session_send2fa(
session_id: SessionID,
code: String,
) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send2fa(code);
}
}
pub fn session_close(session_id: SessionID) { pub fn session_close(session_id: SessionID) {
if let Some(session) = sessions::remove_session_by_session_id(&session_id) { if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
session.close_event_stream(session_id); session.close_event_stream(session_id);
@ -2016,6 +2025,23 @@ pub fn main_supported_input_source() -> SyncReturn<String> {
} }
} }
pub fn main_generate2fa() -> String {
crate::auth_2fa::generate2fa()
}
pub fn main_verify2fa(code: String) -> bool {
let res = crate::auth_2fa::verify2fa(code);
if res {
refresh_options();
}
res
}
pub fn main_has_valid_2fa_sync() -> SyncReturn<bool> {
let raw = get_option("2fa");
SyncReturn(crate::auth_2fa::get_2fa(Some(raw)).is_some())
}
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub mod server_side { pub mod server_side {
use hbb_common::{config, log}; use hbb_common::{config, log};

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "双重认证代码"), ("2FA code", "双重认证代码"),
("2fa_tip", "请输入授权 app 中的双重认证代码"), ("2fa_tip", "请输入授权 app 中的双重认证代码"),
("More", "更多"), ("More", "更多"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "2FA kód"), ("2FA code", "2FA kód"),
("2fa_tip", "Zadejte svůj kód 2FA do ověřovací aplikace."), ("2fa_tip", "Zadejte svůj kód 2FA do ověřovací aplikace."),
("More", "Více"), ("More", "Více"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "2FA-Code"), ("2FA code", "2FA-Code"),
("2fa_tip", "Bitte geben Sie Ihren 2FA-Code in der Authentifizierungs-App ein."), ("2fa_tip", "Bitte geben Sie Ihren 2FA-Code in der Authentifizierungs-App ein."),
("More", "Mehr"), ("More", "Mehr"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "κωδικός 2FA"), ("2FA code", "κωδικός 2FA"),
("2fa_tip", "Παρακαλώ εισάγεται τον κωδικό 2FA στην εφαρμογή πιστοποίησης"), ("2fa_tip", "Παρακαλώ εισάγεται τον κωδικό 2FA στην εφαρμογή πιστοποίησης"),
("More", "Περισσότερα"), ("More", "Περισσότερα"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -212,5 +212,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("swap-left-right-mouse", "Swap left-right mouse button"), ("swap-left-right-mouse", "Swap left-right mouse button"),
("2FA code", "2FA code"), ("2FA code", "2FA code"),
("2fa_tip", "Please enter your 2FA code in the authentication app."), ("2fa_tip", "Please enter your 2FA code in the authentication app."),
("enable-2fa-title", "Enable two-factor authentication"),
("enable-2fa-desc", "Please set up your authenticator now. You can use an authenticator app such as Authy, Microsoft or Google Authenticator on your phone or desktop.\n\nScan the QR code with your app and enter the code that your app shows to enable two-factor authentication."),
("wrong-2fa-code", "Can't verify the code. Check that code and local time settings are correct"),
("enter-2fa-title", "Two-factor authentication"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "کد ورود 2 مرحله ای"), ("2FA code", "کد ورود 2 مرحله ای"),
("2fa_tip", "لطفا کد ورود 2 مرحله ای خود را در برنامه احراز هویت وارد کنید"), ("2fa_tip", "لطفا کد ورود 2 مرحله ای خود را در برنامه احراز هویت وارد کنید"),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "Codice 2FA"), ("2FA code", "Codice 2FA"),
("2fa_tip", "Inserisci il codice 2FA nell'app di autenticazione."), ("2fa_tip", "Inserisci il codice 2FA nell'app di autenticazione."),
("More", "Altro"), ("More", "Altro"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "2FA kods"), ("2FA code", "2FA kods"),
("2fa_tip", "Lūdzu, ievadiet savu 2FA kodu autentifikācijas lietotnē."), ("2fa_tip", "Lūdzu, ievadiet savu 2FA kodu autentifikācijas lietotnē."),
("More", "Vairāk"), ("More", "Vairāk"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "2FA-code"), ("2FA code", "2FA-code"),
("2fa_tip", "Geef je 2FA-code op in de verificatie-app."), ("2fa_tip", "Geef je 2FA-code op in de verificatie-app."),
("More", "Meer"), ("More", "Meer"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "Kod 2FA"), ("2FA code", "Kod 2FA"),
("2fa_tip", "Proszę wprowadzić swój kod 2FA w aplikacji do autoryzacji."), ("2fa_tip", "Proszę wprowadzić swój kod 2FA w aplikacji do autoryzacji."),
("More", "Więcej"), ("More", "Więcej"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "Код 2FA"), ("2FA code", "Код 2FA"),
("2fa_tip", "Введите код двухфакторной аутентификации из приложения для аутентификации."), ("2fa_tip", "Введите код двухфакторной аутентификации из приложения для аутентификации."),
("More", "Ещё"), ("More", "Ещё"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "2FA kód"), ("2FA code", "2FA kód"),
("2fa_tip", "Zadajte svoj kód 2FA do aplikácie na overovanie."), ("2fa_tip", "Zadajte svoj kód 2FA do aplikácie na overovanie."),
("More", "Viac"), ("More", "Viac"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", "Код двофакторної автентифікації"), ("2FA code", "Код двофакторної автентифікації"),
("2fa_tip", "Будь ласка, введіть код двофакторної автентифікації в застосунку для автентифікації."), ("2fa_tip", "Будь ласка, введіть код двофакторної автентифікації в застосунку для автентифікації."),
("More", "Більше"), ("More", "Більше"),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -581,5 +581,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code", ""), ("2FA code", ""),
("2fa_tip", ""), ("2fa_tip", ""),
("More", ""), ("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -44,6 +44,7 @@ mod lang;
mod license; mod license;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
mod port_forward; mod port_forward;
mod auth_2fa;
#[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]

View File

@ -67,7 +67,7 @@ use std::collections::HashSet;
pub type Sender = mpsc::UnboundedSender<(Instant, Arc<Message>)>; pub type Sender = mpsc::UnboundedSender<(Instant, Arc<Message>)>;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref LOGIN_FAILURES: Arc::<Mutex<HashMap<String, (i32, i32, i32)>>> = Default::default(); static ref LOGIN_FAILURES: [Arc::<Mutex<HashMap<String, (i32, i32, i32)>>>; 2] = Default::default();
static ref SESSIONS: Arc::<Mutex<HashMap<String, Session>>> = Default::default(); static ref SESSIONS: Arc::<Mutex<HashMap<String, Session>>> = Default::default();
static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default(); static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default();
static ref AUTHED_CONNS: Arc::<Mutex<Vec<(i32, AuthConnType)>>> = Default::default(); static ref AUTHED_CONNS: Arc::<Mutex<Vec<(i32, AuthConnType)>>> = Default::default();
@ -150,6 +150,7 @@ struct Session {
session_id: u64, session_id: u64,
last_recv_time: Arc<Mutex<Instant>>, last_recv_time: Arc<Mutex<Instant>>,
random_password: String, random_password: String,
tfa: bool,
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@ -181,6 +182,7 @@ pub struct Connection {
port_forward_address: String, port_forward_address: String,
tx_to_cm: mpsc::UnboundedSender<ipc::Data>, tx_to_cm: mpsc::UnboundedSender<ipc::Data>,
authorized: bool, authorized: bool,
require_2fa: Option<totp_rs::TOTP>,
keyboard: bool, keyboard: bool,
clipboard: bool, clipboard: bool,
audio: bool, audio: bool,
@ -317,6 +319,7 @@ impl Connection {
tx: Some(tx), tx: Some(tx),
tx_video: Some(tx_video), tx_video: Some(tx_video),
}, },
require_2fa: crate::auth_2fa::get_2fa(None),
display_idx: *display_service::PRIMARY_DISPLAY_IDX, display_idx: *display_service::PRIMARY_DISPLAY_IDX,
stream, stream,
server, server,
@ -434,6 +437,7 @@ impl Connection {
Some(data) = rx_from_cm.recv() => { Some(data) = rx_from_cm.recv() => {
match data { match data {
ipc::Data::Authorize => { ipc::Data::Authorize => {
conn.require_2fa.take();
conn.send_logon_response().await; conn.send_logon_response().await;
if conn.port_forward_socket.is_some() { if conn.port_forward_socket.is_some() {
break; break;
@ -1029,6 +1033,10 @@ impl Connection {
if self.authorized { if self.authorized {
return; return;
} }
if self.require_2fa.is_some() && !self.is_recent_session(true) {
self.send_login_error(crate::client::REQUIRE_2FA).await;
return;
}
let (conn_type, auth_conn_type) = if self.file_transfer.is_some() { let (conn_type, auth_conn_type) = if self.file_transfer.is_some() {
(1, AuthConnType::FileTransfer) (1, AuthConnType::FileTransfer)
} else if self.port_forward_socket.is_some() { } else if self.port_forward_socket.is_some() {
@ -1402,6 +1410,7 @@ impl Connection {
session_id: self.lr.session_id, session_id: self.lr.session_id,
last_recv_time: self.last_recv_time.clone(), last_recv_time: self.last_recv_time.clone(),
random_password: password, random_password: password,
tfa: false,
}, },
); );
return true; return true;
@ -1415,7 +1424,7 @@ impl Connection {
false false
} }
fn is_recent_session(&mut self) -> bool { fn is_recent_session(&mut self, tfa: bool) -> bool {
SESSIONS SESSIONS
.lock() .lock()
.unwrap() .unwrap()
@ -1426,21 +1435,18 @@ impl Connection {
.get(&self.lr.my_id) .get(&self.lr.my_id)
.map(|s| s.to_owned()); .map(|s| s.to_owned());
// last_recv_time is a mutex variable shared with connection, can be updated lively. // last_recv_time is a mutex variable shared with connection, can be updated lively.
if let Some(session) = session { if let Some(mut session) = session {
if session.name == self.lr.my_name if session.name == self.lr.my_name
&& session.session_id == self.lr.session_id && session.session_id == self.lr.session_id
&& !self.lr.password.is_empty() && !self.lr.password.is_empty()
&& self.validate_one_password(session.random_password.clone()) && (tfa && session.tfa
|| !tfa && self.validate_one_password(session.random_password.clone()))
{ {
SESSIONS.lock().unwrap().insert( session.last_recv_time = self.last_recv_time.clone();
self.lr.my_id.clone(), SESSIONS
Session { .lock()
name: self.lr.my_name.clone(), .unwrap()
session_id: self.lr.session_id, .insert(self.lr.my_id.clone(), session);
last_recv_time: self.last_recv_time.clone(),
random_password: session.random_password,
},
);
return true; return true;
} }
} }
@ -1614,7 +1620,7 @@ impl Connection {
{ {
self.send_login_error("Connection not allowed").await; self.send_login_error("Connection not allowed").await;
return false; return false;
} else if self.is_recent_session() { } else if self.is_recent_session(false) {
if err_msg.is_empty() { if err_msg.is_empty() {
#[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(all(target_os = "linux", feature = "linux_headless"))]
#[cfg(not(any(feature = "flatpak", feature = "appimage")))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
@ -1637,47 +1643,12 @@ impl Connection {
.await; .await;
} }
} else { } else {
let mut failure = LOGIN_FAILURES let (failure, res) = self.check_failure(0).await;
.lock() if !res {
.unwrap() return true;
.get(&self.ip)
.map(|x| x.clone())
.unwrap_or((0, 0, 0));
let time = (get_time() / 60_000) as i32;
if failure.2 > 30 {
self.send_login_error("Too many wrong password attempts")
.await;
Self::post_alarm_audit(
AlarmAuditType::ExceedThirtyAttempts,
json!({
"ip":self.ip,
"id":lr.my_id.clone(),
"name": lr.my_name.clone(),
}),
);
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
Self::post_alarm_audit(
AlarmAuditType::SixAttemptsWithinOneMinute,
json!({
"ip":self.ip,
"id":lr.my_id.clone(),
"name": lr.my_name.clone(),
}),
);
} else if !self.validate_password() {
if failure.0 == time {
failure.1 += 1;
failure.2 += 1;
} else {
failure.0 = time;
failure.1 = 1;
failure.2 += 1;
} }
LOGIN_FAILURES if !self.validate_password() {
.lock() self.update_failure(failure, false, 0);
.unwrap()
.insert(self.ip.clone(), failure);
if err_msg.is_empty() { if err_msg.is_empty() {
self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
.await; .await;
@ -1689,9 +1660,7 @@ impl Connection {
.await; .await;
} }
} else { } else {
if failure.0 != 0 { self.update_failure(failure, true, 0);
LOGIN_FAILURES.lock().unwrap().remove(&self.ip);
}
if err_msg.is_empty() { if err_msg.is_empty() {
#[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(all(target_os = "linux", feature = "linux_headless"))]
#[cfg(not(any(feature = "flatpak", feature = "appimage")))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
@ -1706,6 +1675,47 @@ impl Connection {
} }
} }
} }
} else if let Some(message::Union::Auth2fa(tfa)) = msg.union {
let (failure, res) = self.check_failure(1).await;
if !res {
return true;
}
if let Some(totp) = self.require_2fa.as_ref() {
if let Ok(code) = totp.generate_current() {
if tfa.code == code {
self.update_failure(failure, true, 1);
self.require_2fa.take();
self.send_logon_response().await;
let session = SESSIONS
.lock()
.unwrap()
.get(&self.lr.my_id)
.map(|s| s.to_owned());
if let Some(mut session) = session {
session.tfa = true;
SESSIONS
.lock()
.unwrap()
.insert(self.lr.my_id.clone(), session);
} else {
SESSIONS.lock().unwrap().insert(
self.lr.my_id.clone(),
Session {
name: self.lr.my_name.clone(),
session_id: self.lr.session_id,
last_recv_time: self.last_recv_time.clone(),
random_password: "".to_owned(),
tfa: true,
},
);
}
} else {
self.update_failure(failure, false, 1);
self.send_login_error(crate::client::LOGIN_MSG_2FA_WRONG)
.await;
}
}
}
} else if let Some(message::Union::TestDelay(t)) = msg.union { } else if let Some(message::Union::TestDelay(t)) = msg.union {
if t.from_client { if t.from_client {
let mut msg_out = Message::new(); let mut msg_out = Message::new();
@ -2245,6 +2255,63 @@ impl Connection {
true true
} }
fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) {
if remove {
if failure.0 != 0 {
LOGIN_FAILURES[i].lock().unwrap().remove(&self.ip);
}
return;
}
if failure.0 == time {
failure.1 += 1;
failure.2 += 1;
} else {
failure.0 = time;
failure.1 = 1;
failure.2 += 1;
}
LOGIN_FAILURES[i]
.lock()
.unwrap()
.insert(self.ip.clone(), failure);
}
async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) {
let failure = LOGIN_FAILURES[i]
.lock()
.unwrap()
.get(&self.ip)
.map(|x| x.clone())
.unwrap_or((0, 0, 0));
let time = (get_time() / 60_000) as i32;
let res = if failure.2 > 30 {
self.send_login_error("Too many wrong attempts").await;
Self::post_alarm_audit(
AlarmAuditType::ExceedThirtyAttempts,
json!({
"ip": self.ip,
"id": self.lr.my_id.clone(),
"name": self.lr.my_name.clone(),
}),
);
false
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
Self::post_alarm_audit(
AlarmAuditType::SixAttemptsWithinOneMinute,
json!({
"ip": self.ip,
"id": self.lr.my_id.clone(),
"name": self.lr.my_name.clone(),
}),
);
false
} else {
true
};
((failure, time), res)
}
fn refresh_video_display(&self, display: Option<usize>) { fn refresh_video_display(&self, display: Option<usize>) {
video_service::refresh(); video_service::refresh();
self.server.upgrade().map(|s| { self.server.upgrade().map(|s| {

View File

@ -141,6 +141,14 @@ pub fn get_license() -> String {
Default::default() Default::default()
} }
#[inline]
pub fn refresh_options() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
*OPTIONS.lock().unwrap() = Config::get_options();
}
}
#[inline] #[inline]
pub fn get_option<T: AsRef<str>>(key: T) -> String { pub fn get_option<T: AsRef<str>>(key: T) -> String {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]

View File

@ -1082,6 +1082,15 @@ impl<T: InvokeUiSession> Session<T> {
self.send(Data::Login((os_username, os_password, password, remember))); self.send(Data::Login((os_username, os_password, password, remember)));
} }
pub fn send2fa(&self, code: String) {
let mut msg_out = Message::new();
msg_out.set_auth_2fa(Auth2FA {
code,
..Default::default()
});
self.send(Data::Message(msg_out));
}
pub fn new_rdp(&self) { pub fn new_rdp(&self) {
self.send(Data::NewRDP); self.send(Data::NewRDP);
} }