新增 windows hello 登录

This commit is contained in:
xkeyC 2023-11-06 00:29:25 +08:00
parent e5958bb8d2
commit 1f68ad8ded
8 changed files with 230 additions and 8 deletions

View File

@ -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();
}
});
}

View 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.');
}
}

View File

@ -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)),
)
],
),
),
@ -32,8 +38,8 @@ class LoginDialog extends BaseUI<LoginDialogModel> {
Text("请输入RSI账户 [${model.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"),
const SizedBox(height: 12),
TextFormBox(
// controller: model.emailCtrl,
),
// controller: model.emailCtrl,
),
const SizedBox(height: 6),
Text(
"*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。",

View File

@ -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);
}
}

View File

@ -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\",\"\")");
}
}
}

View File

@ -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:

View File

@ -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(

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window
flutter_acrylic
local_auth_windows
screen_retriever
url_launcher_windows
window_manager