mirror of
https://ghfast.top/https://github.com/StarCitizenToolBox/app.git
synced 2025-03-13 06:21:25 +08:00
新增 windows hello 登录
This commit is contained in:
parent
e5958bb8d2
commit
1f68ad8ded
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
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: [
|
||||
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)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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<String, dynamic> 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<void> _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);
|
||||
}
|
||||
}
|
||||
|
@ -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<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
|
||||
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:
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <desktop_webview_window/desktop_webview_window_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 <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
@ -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(
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_webview_window
|
||||
flutter_acrylic
|
||||
local_auth_windows
|
||||
screen_retriever
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
|
Loading…
x
Reference in New Issue
Block a user