mirror of
https://ghfast.top/https://github.com/StarCitizenToolBox/app.git
synced 2025-03-13 07:31:23 +08:00
新增 windows hello 登录
This commit is contained in:
parent
e5958bb8d2
commit
1f68ad8ded
@ -219,6 +219,21 @@ InitWebLocalization();
|
|||||||
|
|
||||||
/// ----- Login Script ----
|
/// ----- Login Script ----
|
||||||
async function getRSILauncherToken(channelId) {
|
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
|
// check login
|
||||||
let r = await fetch("api/launcher/v3/account/check", {
|
let r = await fetch("api/launcher/v3/account/check", {
|
||||||
method: 'POST', headers: {
|
method: 'POST', headers: {
|
||||||
@ -280,7 +295,25 @@ async function getRSILauncherToken(channelId) {
|
|||||||
'claims': claimsData,
|
'claims': claimsData,
|
||||||
'authToken': TokenData,
|
'authToken': TokenData,
|
||||||
'releaseInfo': releaseDataJson,
|
'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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
78
lib/common/win32/credentials.dart
Normal file
78
lib/common/win32/credentials.dart
Normal file
@ -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<CREDENTIAL>()
|
||||||
|
..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<String, String>? read(String credentialName) {
|
||||||
|
dPrint('Reading $credentialName ...');
|
||||||
|
final credPointer = calloc<Pointer<CREDENTIAL>>();
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
@ -19,12 +19,18 @@ class LoginDialog extends BaseUI<LoginDialogModel> {
|
|||||||
children: [
|
children: [
|
||||||
const Row(),
|
const Row(),
|
||||||
if (model.loginStatus == 0) ...[
|
if (model.loginStatus == 0) ...[
|
||||||
const Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text("登录中..."),
|
const Text("登录中..."),
|
||||||
SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
ProgressRing()
|
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<LoginDialogModel> {
|
|||||||
Text("请输入RSI账户 [${model.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"),
|
Text("请输入RSI账户 [${model.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormBox(
|
TextFormBox(
|
||||||
// controller: model.emailCtrl,
|
// controller: model.emailCtrl,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
"*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。",
|
"*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。",
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:jwt_decode/jwt_decode.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/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/home_ui_model.dart';
|
||||||
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
|
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -27,6 +30,8 @@ class LoginDialogModel extends BaseUIModel {
|
|||||||
|
|
||||||
LoginDialogModel(this.installPath, this.homeUIModel);
|
LoginDialogModel(this.installPath, this.homeUIModel);
|
||||||
|
|
||||||
|
final LocalAuthentication localAuth = LocalAuthentication();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initModel() {
|
void initModel() {
|
||||||
_launchWebLogin();
|
_launchWebLogin();
|
||||||
@ -53,6 +58,34 @@ class LoginDialogModel extends BaseUIModel {
|
|||||||
.replaceAll("\")", "");
|
.replaceAll("\")", "");
|
||||||
Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
|
Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
|
||||||
nickname = payload["nickname"] ?? "";
|
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");
|
final buildInfoFile = File("$installPath\\build_manifest.id");
|
||||||
if (await buildInfoFile.exists()) {
|
if (await buildInfoFile.exists()) {
|
||||||
final buildInfo =
|
final buildInfo =
|
||||||
@ -150,10 +183,11 @@ class LoginDialogModel extends BaseUIModel {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
Future<void> _readyForLaunch() async {
|
Future<void> _readyForLaunch() async {
|
||||||
|
final userBox = await Hive.openBox("rsi_account_data");
|
||||||
loginStatus = 2;
|
loginStatus = 2;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final launchData = {
|
final launchData = {
|
||||||
"username": "",
|
"username": userBox.get("account_email", defaultValue: ""),
|
||||||
"token": webToken,
|
"token": webToken,
|
||||||
"auth_token": authToken,
|
"auth_token": authToken,
|
||||||
"star_network": {
|
"star_network": {
|
||||||
@ -196,4 +230,33 @@ class LoginDialogModel extends BaseUIModel {
|
|||||||
}
|
}
|
||||||
return "LIVE";
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive/hive.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/conf.dart';
|
||||||
|
import 'package:starcitizen_doctor/common/win32/credentials.dart';
|
||||||
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
|
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
|
||||||
|
|
||||||
import '../../../api/api.dart';
|
import '../../../api/api.dart';
|
||||||
@ -148,6 +151,7 @@ class WebViewModel {
|
|||||||
final message = json.decode(messageString);
|
final message = json.decode(messageString);
|
||||||
if (message["action"] == "webview_rsi_login_show_window") {
|
if (message["action"] == "webview_rsi_login_show_window") {
|
||||||
webview.setWebviewWindowVisibility(true);
|
webview.setWebviewWindowVisibility(true);
|
||||||
|
_checkAutoLogin(webview);
|
||||||
} else if (message["action"] == "webview_rsi_login_success") {
|
} else if (message["action"] == "webview_rsi_login_success") {
|
||||||
_loginModeSuccess = true;
|
_loginModeSuccess = true;
|
||||||
loginCallback?.call(message, true);
|
loginCallback?.call(message, true);
|
||||||
@ -247,4 +251,34 @@ class WebViewModel {
|
|||||||
}
|
}
|
||||||
_isClosed = true;
|
_isClosed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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\",\"\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,10 @@ dependencies:
|
|||||||
flutter_rust_bridge: ^1.82.3
|
flutter_rust_bridge: ^1.82.3
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
meta: ^1.9.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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||||
|
#include <local_auth_windows/local_auth_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
||||||
FlutterAcrylicPluginRegisterWithRegistrar(
|
FlutterAcrylicPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
||||||
|
LocalAuthPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_webview_window
|
desktop_webview_window
|
||||||
flutter_acrylic
|
flutter_acrylic
|
||||||
|
local_auth_windows
|
||||||
screen_retriever
|
screen_retriever
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
window_manager
|
window_manager
|
||||||
|
Loading…
x
Reference in New Issue
Block a user