This commit is contained in:
2025-07-25 23:24:13 +08:00
parent e34bcd80dd
commit 4f366c12a3
84 changed files with 12608 additions and 2 deletions

View File

@@ -0,0 +1,356 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXFileReference section */
7195F3972D9A86CC00FA3526 /* hw-sign-apple.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "hw-sign-apple.app"; sourceTree = BUILT_PRODUCTS_DIR; };
7195F3AF2D9A87C700FA3526 /* hw-sign-apple.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = "hw-sign-apple.xcodeproj"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7195F3992D9A86CC00FA3526 /* hw-sign-apple */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "hw-sign-apple";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
7195F3942D9A86CC00FA3526 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
7195F38E2D9A86CC00FA3526 = {
isa = PBXGroup;
children = (
7195F3992D9A86CC00FA3526 /* hw-sign-apple */,
7195F3982D9A86CC00FA3526 /* Products */,
);
sourceTree = "<group>";
};
7195F3982D9A86CC00FA3526 /* Products */ = {
isa = PBXGroup;
children = (
7195F3972D9A86CC00FA3526 /* hw-sign-apple.app */,
);
name = Products;
sourceTree = "<group>";
};
7195F3B02D9A87C700FA3526 /* Products */ = {
isa = PBXGroup;
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
7195F3962D9A86CC00FA3526 /* hw-sign-apple */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7195F3A62D9A86CD00FA3526 /* Build configuration list for PBXNativeTarget "hw-sign-apple" */;
buildPhases = (
7195F3932D9A86CC00FA3526 /* Sources */,
7195F3942D9A86CC00FA3526 /* Frameworks */,
7195F3952D9A86CC00FA3526 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
7195F3992D9A86CC00FA3526 /* hw-sign-apple */,
);
name = "hw-sign-apple";
packageProductDependencies = (
);
productName = "hw-sign-apple";
productReference = 7195F3972D9A86CC00FA3526 /* hw-sign-apple.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7195F38F2D9A86CC00FA3526 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
7195F3962D9A86CC00FA3526 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = 7195F3922D9A86CC00FA3526 /* Build configuration list for PBXProject "hw-sign-apple" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 7195F38E2D9A86CC00FA3526;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 7195F3982D9A86CC00FA3526 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 7195F3B02D9A87C700FA3526 /* Products */;
ProjectRef = 7195F3AF2D9A87C700FA3526 /* hw-sign-apple.xcodeproj */;
},
);
projectRoot = "";
targets = (
7195F3962D9A86CC00FA3526 /* hw-sign-apple */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
7195F3952D9A86CC00FA3526 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
7195F3932D9A86CC00FA3526 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
7195F3A42D9A86CD00FA3526 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
7195F3A52D9A86CD00FA3526 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
7195F3A72D9A86CD00FA3526 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "hw-sign-apple/hw_sign_apple.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"hw-sign-apple/Preview Content\"";
DEVELOPMENT_TEAM = XV53H7ABC6;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fan.ovo.hwsign;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = Debug;
};
7195F3A82D9A86CD00FA3526 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "hw-sign-apple/hw_sign_apple.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"hw-sign-apple/Preview Content\"";
DEVELOPMENT_TEAM = XV53H7ABC6;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fan.ovo.hwsign;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7195F3922D9A86CC00FA3526 /* Build configuration list for PBXProject "hw-sign-apple" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7195F3A42D9A86CD00FA3526 /* Debug */,
7195F3A52D9A86CD00FA3526 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7195F3A62D9A86CD00FA3526 /* Build configuration list for PBXNativeTarget "hw-sign-apple" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7195F3A72D9A86CD00FA3526 /* Debug */,
7195F3A82D9A86CD00FA3526 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7195F38F2D9A86CC00FA3526 /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,60 @@
import SwiftUI
@main
struct HWSignApp: App {
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
@StateObject private var themeManager = ThemeManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(themeManager)
#if os(macOS)
.frame(minWidth: 400, minHeight: 400)
#endif
.preferredColorScheme(themeManager.isDarkMode ? .dark : .light)
}
#if os(macOS)
.windowStyle(HiddenTitleBarWindowStyle())
#endif
}
}
#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
// Setup macOS specific behavior if needed
let appearance =
UserDefaults.standard.bool(forKey: "isDarkMode")
? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
NSApp.appearance = appearance
}
}
#endif
class ThemeManager: ObservableObject {
@Published var isDarkMode: Bool {
didSet {
UserDefaults.standard.set(isDarkMode, forKey: "isDarkMode")
#if os(macOS)
NSApp.appearance = NSAppearance(named: isDarkMode ? .darkAqua : .aqua)
#endif
}
}
init() {
// Use saved preference or default to system setting
if UserDefaults.standard.object(forKey: "isDarkMode") != nil {
self.isDarkMode = UserDefaults.standard.bool(forKey: "isDarkMode")
} else {
#if os(iOS)
self.isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
#else
self.isDarkMode = false
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,85 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,200 @@
import Combine
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = AuthViewModel()
@EnvironmentObject private var themeManager: ThemeManager
var body: some View {
VStack(spacing: 20) {
Text("Hardware Secure Authentication")
.font(.largeTitle)
.fontWeight(.bold)
.padding()
.multilineTextAlignment(.center)
if !viewModel.isAuthenticated {
// Login/Registration Form
loginForm
} else {
// Authenticated View
authenticatedView
}
// Message display
Text(viewModel.message)
.foregroundColor(
viewModel.message.contains("successful") || viewModel.message.contains("verified")
? .green : .red
)
.padding()
.frame(minHeight: 60)
Spacer()
// Dark Mode Toggle
Toggle("Dark Mode", isOn: $themeManager.isDarkMode)
.padding(.horizontal)
}
.padding()
.disabled(viewModel.isLoading)
}
private var loginForm: some View {
Group {
TextField("Username", text: $viewModel.username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disableAutocorrection(true)
.padding(.horizontal)
SecureField("Password", text: $viewModel.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
HStack(spacing: 20) {
Button("Register") {
viewModel.handleRegister()
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
Button("Login") {
viewModel.handleLogin()
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
}
}
}
private var authenticatedView: some View {
Group {
Text("You are authenticated with hardware security!")
.font(.headline)
.foregroundColor(.green)
.padding()
Button("Check Authentication") {
viewModel.checkAuthentication()
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
Button("Logout") {
viewModel.handleLogout()
}
.buttonStyle(.bordered)
.foregroundColor(.red)
.padding(.top)
.disabled(viewModel.isLoading)
}
}
}
class AuthViewModel: ObservableObject {
@Published var username = ""
@Published var password = ""
@Published var message = ""
@Published var isAuthenticated = false
@Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
private let authService = AuthService.shared
init() {
// Check if user is already authenticated
isAuthenticated = KeyManager.shared.getAuthToken() != nil
}
func handleRegister() {
guard !username.isEmpty, !password.isEmpty else {
message = "Username and password required"
return
}
isLoading = true
message = "Registering..."
authService.register(username: username, password: password)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
guard let self = self else { return }
self.isLoading = false
if case let .failure(error) = completion {
self.message = "Registration failed: \(error.localizedDescription)"
}
},
receiveValue: { [weak self] response in
guard let self = self else { return }
self.message = response
}
)
.store(in: &cancellables)
}
func handleLogin() {
guard !username.isEmpty, !password.isEmpty else {
message = "Username and password required"
return
}
isLoading = true
message = "Logging in..."
authService.login(username: username, password: password)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
guard let self = self else { return }
self.isLoading = false
if case let .failure(error) = completion {
self.message = "Login failed: \(error.localizedDescription)"
}
},
receiveValue: { [weak self] response in
guard let self = self else { return }
self.isAuthenticated = true
self.message = response
}
)
.store(in: &cancellables)
}
func checkAuthentication() {
isLoading = true
message = "Checking authentication..."
authService.checkAuthentication()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
guard let self = self else { return }
self.isLoading = false
if case let .failure(error) = completion {
self.message = "Authentication check failed: \(error.localizedDescription)"
}
},
receiveValue: { [weak self] response in
guard let self = self else { return }
self.message = response
}
)
.store(in: &cancellables)
}
func handleLogout() {
authService.logout()
isAuthenticated = false
message = "Logged out successfully"
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ThemeManager())
}
}
#endif

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,283 @@
import Combine
import Foundation
import Security
class AuthService {
static let shared = AuthService()
private let baseURL = URL(string: "https://dbcs-api.ovo.fan")!
// private let baseURL = URL(string: "http://127.0.0.1:28280")!
private let keyManager = KeyManager.shared
private var cancellables = Set<AnyCancellable>()
private init() {}
// MARK: - Authentication Flow
func register(username: String, password: String) -> AnyPublisher<String, Error> {
let body = ["username": username, "password": password]
return makeRequest("register", method: "POST", body: body, responseType: EmptyResponse.self)
.map { _ in "Registration successful!" }
.eraseToAnyPublisher()
}
func login(username: String, password: String) -> AnyPublisher<String, Error> {
return Future { [weak self] promise in
guard let self = self else {
promise(
.failure(
NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Service unavailable"])))
return
}
do {
// Create new hardware key for this session
let hwKey = try self.keyManager.createKey(.hardware, forceNew: true)
guard let hwPubKey = self.keyManager.getPublicKey(for: hwKey) else {
throw NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Failed to get public key"])
}
let hwPubKeyData = try self.keyManager.exportPublicKey(hwPubKey)
let hwPubKeyBase64 = hwPubKeyData.base64EncodedString()
// Make login request with hardware key
let body = ["username": username, "password": password]
var request = try self.createRequest("login", method: "POST", body: body)
request.setValue(hwPubKeyBase64, forHTTPHeaderField: "x-rpc-sec-bound-token-hw-pub")
request.setValue("ecdsa", forHTTPHeaderField: "x-rpc-sec-bound-token-hw-pub-type") // Always use ECDSA
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Invalid response type"])
}
if !(200...299).contains(httpResponse.statusCode) {
throw NSError(
domain: "AuthService", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: "Server error: \(httpResponse.statusCode)"])
}
return data
}
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
promise(.failure(error))
}
},
receiveValue: { response in
self.keyManager.storeAuthToken(response.token)
promise(.success("Login successful!"))
}
)
.store(in: &self.cancellables)
} catch {
promise(.failure(error))
}
}.eraseToAnyPublisher()
}
func checkAuthentication() -> AnyPublisher<String, Error> {
return authenticatedRequest("authenticated", method: "GET", responseType: AuthResponse.self)
.map { _ in "Authentication verified with hardware security!" }
.eraseToAnyPublisher()
}
func logout() {
try? keyManager.deleteKey(.hardware)
try? keyManager.deleteKey(.acceleration)
keyManager.deleteAuthToken()
keyManager.deleteAccelKeyId()
}
// MARK: - Request Helpers
private func makeRequest<T: Codable>(
_ path: String, method: String, body: [String: Any]? = nil, responseType: T.Type
) -> AnyPublisher<T, Error> {
return Future { [weak self] promise in
guard let self = self else {
promise(
.failure(
NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Service unavailable"])))
return
}
do {
let request = try self.createRequest(path, method: method, body: body)
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Invalid response type"])
}
if !(200...299).contains(httpResponse.statusCode) {
throw NSError(
domain: "AuthService", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: "Server error: \(httpResponse.statusCode)"])
}
return data
}
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
promise(.failure(error))
}
},
receiveValue: { response in
promise(.success(response))
}
)
.store(in: &self.cancellables)
} catch {
promise(.failure(error))
}
}.eraseToAnyPublisher()
}
private func authenticatedRequest<T: Codable>(
_ path: String, method: String, responseType: T.Type, body: [String: Any]? = nil
) -> AnyPublisher<T, Error> {
return Future { [weak self] promise in
guard let self = self else {
promise(
.failure(
NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Service unavailable"])))
return
}
do {
let timestamp = String(Int(Date().timeIntervalSince1970))
let accelKeyId = self.keyManager.getAccelKeyId()
var request = try self.createRequest(path, method: method, body: body)
request.setValue(
"Bearer \(self.keyManager.getAuthToken() ?? "")", forHTTPHeaderField: "Authorization")
request.setValue(timestamp, forHTTPHeaderField: "x-rpc-sec-bound-token-data")
if let accelKeyId = accelKeyId {
// Use existing acceleration key
let accelKey = try self.keyManager.loadKey(.acceleration)
let signature = try self.keyManager.sign(
data: timestamp.data(using: .utf8)!, with: accelKey)
request.setValue(
signature.base64EncodedString(), forHTTPHeaderField: "x-rpc-sec-bound-token-data-sig")
request.setValue(accelKeyId, forHTTPHeaderField: "x-rpc-sec-bound-token-accel-pub-id")
} else {
// Create new acceleration key
let accelKey = try self.keyManager.createKey(.acceleration)
guard let accelPubKey = self.keyManager.getPublicKey(for: accelKey) else {
throw NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Failed to get acceleration public key"])
}
let accelPubKeyData = try self.keyManager.exportPublicKey(accelPubKey)
let accelPubKeyBase64 = accelPubKeyData.base64EncodedString()
// Sign acceleration key with hardware key
let hwKey = try self.keyManager.loadKey(.hardware)
let accelKeySig = try self.keyManager.sign(
data: accelPubKeyBase64.data(using: .utf8)!, with: hwKey)
let signature = try self.keyManager.sign(
data: timestamp.data(using: .utf8)!, with: accelKey)
request.setValue(accelPubKeyBase64, forHTTPHeaderField: "x-rpc-sec-bound-token-accel-pub")
request.setValue("ecdsa", forHTTPHeaderField: "x-rpc-sec-bound-token-accel-pub-type") // Always use ECDSA
request.setValue(
accelKeySig.base64EncodedString(), forHTTPHeaderField: "x-rpc-sec-bound-token-accel-pub-sig")
request.setValue(
signature.base64EncodedString(), forHTTPHeaderField: "x-rpc-sec-bound-token-data-sig")
}
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw NSError(
domain: "AuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Invalid response type"])
}
if let accelKeyId = httpResponse.value(
forHTTPHeaderField: "x-rpc-sec-bound-token-accel-pub-id")
{
self.keyManager.storeAccelKeyId(accelKeyId)
}
if !(200...299).contains(httpResponse.statusCode) {
throw NSError(
domain: "AuthService", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: "Server error: \(httpResponse.statusCode)"])
}
return data
}
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
promise(.failure(error))
}
},
receiveValue: { response in
promise(.success(response))
}
)
.store(in: &self.cancellables)
} catch {
promise(.failure(error))
}
}.eraseToAnyPublisher()
}
private func createRequest(_ path: String, method: String, body: [String: Any]? = nil) throws
-> URLRequest
{
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let body = body {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
}
return request
}
}
// MARK: - Response Models
struct LoginResponse: Codable {
let token: String
}
struct AuthResponse: Codable {
let authenticated: Bool
}
struct EmptyResponse: Codable {
// Empty response structure for endpoints that don't return meaningful data
}

View File

@@ -0,0 +1,149 @@
import Foundation
import LocalAuthentication
import Security
class KeyManager {
static let shared = KeyManager()
private let tagPrefix = "fan.ovo.hwsign"
enum KeyType: String {
case hardware = "hardware"
case acceleration = "acceleration"
}
private init() {}
// MARK: - Key Management
func createKey(_ type: KeyType, forceNew: Bool = false) throws -> SecKey {
let tag = "\(tagPrefix).\(type.rawValue)"
// Always attempt to delete an existing key with the same tag to avoid conflicts
try? deleteKey(type)
let flags: SecAccessControlCreateFlags = [.privateKeyUsage]
let access = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags,
nil
)!
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrAccessControl as String: access,
kSecAttrApplicationTag as String: tag.data(using: .utf8)!,
],
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Error
}
return privateKey
}
func loadKey(_ type: KeyType) throws -> SecKey {
let tag = "\(tagPrefix).\(type.rawValue)"
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag.data(using: .utf8)!,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecReturnRef as String: true,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
}
// Fix: Use proper type safety pattern instead of direct force casting
guard let key = item else {
throw NSError(
domain: "KeyManager", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Retrieved key is nil"])
}
return (key as! SecKey) // This cast is safe because SecItemCopyMatching guarantees a SecKey when using kSecReturnRef
}
func deleteKey(_ type: KeyType) throws {
let tag = "\(tagPrefix).\(type.rawValue)"
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag.data(using: .utf8)!,
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
}
}
// MARK: - Signing Operations
func sign(data: Data, with key: SecKey) throws -> Data {
var error: Unmanaged<CFError>?
guard
let signature = SecKeyCreateSignature(
key,
.ecdsaSignatureMessageX962SHA256,
data as CFData,
&error
) as Data?
else {
throw error!.takeRetainedValue() as Error
}
return signature
}
func getPublicKey(for privateKey: SecKey) -> SecKey? {
return SecKeyCopyPublicKey(privateKey)
}
func exportPublicKey(_ key: SecKey) throws -> Data {
// This encodes ec public to x962
var error: Unmanaged<CFError>?
guard let exportedKey = SecKeyCopyExternalRepresentation(key, &error) as Data? else {
throw error!.takeRetainedValue() as Error
}
return exportedKey
}
// MARK: - Token Management
func storeAuthToken(_ token: String) {
UserDefaults.standard.set(token, forKey: "\(tagPrefix).authToken")
}
func getAuthToken() -> String? {
return UserDefaults.standard.string(forKey: "\(tagPrefix).authToken")
}
func deleteAuthToken() {
UserDefaults.standard.removeObject(forKey: "\(tagPrefix).authToken")
}
func storeAccelKeyId(_ keyId: String) {
UserDefaults.standard.set(keyId, forKey: "\(tagPrefix).accelKeyId")
}
func getAccelKeyId() -> String? {
return UserDefaults.standard.string(forKey: "\(tagPrefix).accelKeyId")
}
func deleteAccelKeyId() {
UserDefaults.standard.removeObject(forKey: "\(tagPrefix).accelKeyId")
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.dns</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>