From 1f68ad8ded6d356a2c78267904bd7ef4231dfe19 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Mon, 6 Nov 2023 00:29:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20windows=20hello=20?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/web_script.js | 35 ++++++++- lib/common/win32/credentials.dart | 78 +++++++++++++++++++ lib/ui/home/login/login_dialog_ui.dart | 18 +++-- lib/ui/home/login/login_dialog_ui_model.dart | 65 +++++++++++++++- lib/ui/home/webview/webview.dart | 34 ++++++++ pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 lib/common/win32/credentials.dart diff --git a/assets/web_script.js b/assets/web_script.js index c355e1f..0a91b16 100644 --- a/assets/web_script.js +++ b/assets/web_script.js @@ -219,6 +219,21 @@ InitWebLocalization(); /// ----- Login Script ---- async function getRSILauncherToken(channelId) { + if (!window.location.href.includes("robertsspaceindustries.com")) return; + + if (window.location.href.startsWith("https://robertsspaceindustries.com/connect")) { + $(function () { + $('#email').on('input', function () { + let inputEmail = $('#email').val() + sessionStorage.setItem('inputEmail', inputEmail); + }); + $('#password').on('input', function () { + let inputPassword = $('#password').val() + sessionStorage.setItem('inputPassword', inputPassword); + }); + }); + } + // check login let r = await fetch("api/launcher/v3/account/check", { method: 'POST', headers: { @@ -280,7 +295,25 @@ async function getRSILauncherToken(channelId) { 'claims': claimsData, 'authToken': TokenData, 'releaseInfo': releaseDataJson, - "avatar": avatarUrl + "avatar": avatarUrl, + "inputEmail": sessionStorage.getItem("inputEmail"), + "inputPassword": sessionStorage.getItem("inputPassword") } }); } + +function RSIAutoLogin(email, pwd) { + if (!window.location.href.includes("robertsspaceindustries.com")) return; + $(function () { + if (email !== "") { + $('#email').val(email) + } + if (pwd !== "") { + $('#password').val(pwd) + } + if (email !== "" && pwd !== "") { + $('.c-form__submit-button-label').click(); + } + }); + +} \ No newline at end of file diff --git a/lib/common/win32/credentials.dart b/lib/common/win32/credentials.dart new file mode 100644 index 0000000..5bd3902 --- /dev/null +++ b/lib/common/win32/credentials.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2020, Dart | Windows. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Reads and writes credentials + +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +import '../utils/base_utils.dart'; + +class Win32Credentials { + static void write( + {required String credentialName, + required String userName, + required String password}) { + final examplePassword = utf8.encode(password) as Uint8List; + final blob = examplePassword.allocatePointer(); + + final credential = calloc() + ..ref.Type = CRED_TYPE_GENERIC + ..ref.TargetName = credentialName.toNativeUtf16() + ..ref.Persist = CRED_PERSIST_LOCAL_MACHINE + ..ref.UserName = userName.toNativeUtf16() + ..ref.CredentialBlob = blob + ..ref.CredentialBlobSize = examplePassword.length; + + final result = CredWrite(credential, 0); + + if (result != TRUE) { + final errorCode = GetLastError(); + dPrint('Error ($result): $errorCode'); + return; + } + dPrint('Success (blob size: ${credential.ref.CredentialBlobSize})'); + + free(blob); + free(credential); + } + + static MapEntry? read(String credentialName) { + dPrint('Reading $credentialName ...'); + final credPointer = calloc>(); + final result = CredRead( + credentialName.toNativeUtf16(), CRED_TYPE_GENERIC, 0, credPointer); + if (result != TRUE) { + final errorCode = GetLastError(); + var errorText = '$errorCode'; + if (errorCode == ERROR_NOT_FOUND) { + errorText += ' Not found.'; + } + dPrint('Error ($result): $errorText'); + return null; + } + final cred = credPointer.value.ref; + final blob = cred.CredentialBlob.asTypedList(cred.CredentialBlobSize); + final password = utf8.decode(blob); + CredFree(credPointer.value); + free(credPointer); + return MapEntry(cred.UserName.toDartString(), password); + } + + static void delete(String credentialName) { + dPrint('Deleting $credentialName'); + final result = + CredDelete(credentialName.toNativeUtf16(), CRED_TYPE_GENERIC, 0); + if (result != TRUE) { + final errorCode = GetLastError(); + dPrint('Error ($result): $errorCode'); + return; + } + dPrint('Successfully deleted credential.'); + } +} diff --git a/lib/ui/home/login/login_dialog_ui.dart b/lib/ui/home/login/login_dialog_ui.dart index b2198d8..9b18d12 100644 --- a/lib/ui/home/login/login_dialog_ui.dart +++ b/lib/ui/home/login/login_dialog_ui.dart @@ -19,12 +19,18 @@ class LoginDialog extends BaseUI { children: [ const Row(), if (model.loginStatus == 0) ...[ - const Center( + Center( child: Column( children: [ - Text("登录中..."), - SizedBox(height: 12), - ProgressRing() + const Text("登录中..."), + const SizedBox(height: 12), + const ProgressRing(), + const SizedBox(height: 24), + Text( + "* 若开启了自动填充,请留意弹出的 Windows Hello 窗口", + style: TextStyle( + fontSize: 13, color: Colors.white.withOpacity(.6)), + ) ], ), ), @@ -32,8 +38,8 @@ class LoginDialog extends BaseUI { Text("请输入RSI账户 [${model.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"), const SizedBox(height: 12), TextFormBox( - // controller: model.emailCtrl, - ), + // controller: model.emailCtrl, + ), const SizedBox(height: 6), Text( "*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。", diff --git a/lib/ui/home/login/login_dialog_ui_model.dart b/lib/ui/home/login/login_dialog_ui_model.dart index 08086dc..a146e8a 100644 --- a/lib/ui/home/login/login_dialog_ui_model.dart +++ b/lib/ui/home/login/login_dialog_ui_model.dart @@ -1,10 +1,13 @@ import 'dart:convert'; import 'dart:io'; +import 'package:cryptography/cryptography.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:hive/hive.dart'; import 'package:jwt_decode/jwt_decode.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:starcitizen_doctor/base/ui_model.dart'; +import 'package:starcitizen_doctor/common/win32/credentials.dart'; import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/webview/webview.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -27,6 +30,8 @@ class LoginDialogModel extends BaseUIModel { LoginDialogModel(this.installPath, this.homeUIModel); + final LocalAuthentication localAuth = LocalAuthentication(); + @override void initModel() { _launchWebLogin(); @@ -53,6 +58,34 @@ class LoginDialogModel extends BaseUIModel { .replaceAll("\")", ""); Map payload = Jwt.parseJwt(authToken!); nickname = payload["nickname"] ?? ""; + + final inputEmail = data["inputEmail"]; + final inputPassword = data["inputPassword"]; + + if (inputEmail != null && inputEmail != "") { + final userBox = await Hive.openBox("rsi_account_data"); + await userBox.put("account_email", inputEmail); + } + + if (await localAuth.isDeviceSupported()) { + if (inputEmail != null && + inputEmail != "" && + inputPassword != null && + inputPassword != "") { + final ok = await showConfirmDialogs( + context!, + "是否开启自动密码填充?", + const Text( + "盒子将使用 PIN 与 Windows 凭据加密保存您的密码,密码只存储在您的设备中。\n\n当下次登录需要输入密码时,您只需授权PIN即可自动填充登录。")); + if (ok == true) { + if (await localAuth.authenticate(localizedReason: "输入PIN以启用加密") == + true) { + await _savePwd(inputEmail, inputPassword); + } + } + } + } + final buildInfoFile = File("$installPath\\build_manifest.id"); if (await buildInfoFile.exists()) { final buildInfo = @@ -150,10 +183,11 @@ class LoginDialogModel extends BaseUIModel { // } Future _readyForLaunch() async { + final userBox = await Hive.openBox("rsi_account_data"); loginStatus = 2; notifyListeners(); final launchData = { - "username": "", + "username": userBox.get("account_email", defaultValue: ""), "token": webToken, "auth_token": authToken, "star_network": { @@ -196,4 +230,33 @@ class LoginDialogModel extends BaseUIModel { } return "LIVE"; } + + _savePwd(String inputEmail, String inputPassword) async { + final algorithm = AesGcm.with256bits(); + final secretKey = await algorithm.newSecretKey(); + final nonce = algorithm.newNonce(); + + final secretBox = await algorithm.encrypt(utf8.encode(inputPassword), + secretKey: secretKey, nonce: nonce); + + await algorithm.decrypt( + SecretBox(secretBox.cipherText, + nonce: secretBox.nonce, mac: secretBox.mac), + secretKey: secretKey); + + final pwdEncrypted = base64.encode(secretBox.cipherText); + + final userBox = await Hive.openBox("rsi_account_data"); + await userBox.put("account_email", inputEmail); + await userBox.put("account_pwd_encrypted", pwdEncrypted); + await userBox.put("nonce", base64.encode(secretBox.nonce)); + await userBox.put("mac", base64.encode(secretBox.mac.bytes)); + + final secretKeyStr = base64.encode((await secretKey.extractBytes())); + + Win32Credentials.write( + credentialName: "SCToolbox_RSI_Account_secret", + userName: inputEmail, + password: secretKeyStr); + } } diff --git a/lib/ui/home/webview/webview.dart b/lib/ui/home/webview/webview.dart index 516ba69..eb44b2a 100644 --- a/lib/ui/home/webview/webview.dart +++ b/lib/ui/home/webview/webview.dart @@ -3,11 +3,14 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cryptography/cryptography.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:starcitizen_doctor/common/conf.dart'; +import 'package:starcitizen_doctor/common/win32/credentials.dart'; import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart'; import '../../../api/api.dart'; @@ -148,6 +151,7 @@ class WebViewModel { final message = json.decode(messageString); if (message["action"] == "webview_rsi_login_show_window") { webview.setWebviewWindowVisibility(true); + _checkAutoLogin(webview); } else if (message["action"] == "webview_rsi_login_success") { _loginModeSuccess = true; loginCallback?.call(message, true); @@ -247,4 +251,34 @@ class WebViewModel { } _isClosed = true; } + + Future _checkAutoLogin(Webview webview) async { + final LocalAuthentication localAuth = LocalAuthentication(); + if (!await localAuth.isDeviceSupported()) return; + + final userBox = await Hive.openBox("rsi_account_data"); + final email = await userBox.get("account_email", defaultValue: ""); + + final pwdE = await userBox.get("account_pwd_encrypted", defaultValue: ""); + final nonceStr = await userBox.get("nonce", defaultValue: ""); + final macStr = await userBox.get("mac", defaultValue: ""); + if (email == "") return; + if (pwdE != "" && nonceStr != "" && macStr != "") { + // decrypt + if (await localAuth.authenticate(localizedReason: "请输入设备PIN以自动登录RSI账户") != + true) return; + final kv = Win32Credentials.read("SCToolbox_RSI_Account_secret"); + if (kv == null || kv.key != email) return; + + final algorithm = AesGcm.with256bits(); + final r = await algorithm.decrypt( + SecretBox(base64.decode(pwdE), + nonce: base64.decode(nonceStr), mac: Mac(base64.decode(macStr))), + secretKey: SecretKey(base64.decode(kv.value))); + final decryptedPwd = utf8.decode(r); + webview.evaluateJavaScript("RSIAutoLogin(\"$email\",\"$decryptedPwd\")"); + } else { + webview.evaluateJavaScript("RSIAutoLogin(\"$email\",\"\")"); + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 8dba17c..2b12da6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,10 @@ dependencies: flutter_rust_bridge: ^1.82.3 freezed_annotation: ^2.4.1 meta: ^1.9.1 + win32: ^5.0.9 + local_auth: ^2.1.7 + cryptography: ^2.7.0 + cryptography_flutter: ^2.3.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e6e4ff9..efa31e7 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FlutterAcrylicPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b44485c..373d104 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window flutter_acrylic + local_auth_windows screen_retriever url_launcher_windows window_manager