Phase 0: scaffold
Two SPM packages (VNCCore, VNCUI) + thin iOS app target wired via xcodegen. Builds for iPhone 17 simulator, unit tests pass. - VNCCore: SessionState, SessionController stub, Transport protocol with DirectTransport (NWConnection), DiscoveryService (Bonjour on _rfb._tcp and _workstation._tcp), SavedConnection @Model, ConnectionStore, KeychainService, ClipboardBridge - VNCUI: ConnectionListView, AddConnectionView, SessionView, FramebufferView/FramebufferUIView (UIKit CALayer), InputMapper, SettingsView; UIKit bits guarded with #if canImport(UIKit) so swift test runs on macOS - App: @main VNCApp, AppStateController state machine, RootView - RoyalVNCKit dependency pinned to main (transitive CryptoSwift constraint blocks tagged releases) - xcodegen Project.yml + README + .gitignore Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
public protocol KeychainServicing: Sendable {
|
||||
func storePassword(_ password: String, account: String) throws
|
||||
func loadPassword(account: String) throws -> String?
|
||||
func deletePassword(account: String) throws
|
||||
}
|
||||
|
||||
public struct KeychainService: KeychainServicing {
|
||||
private let service: String
|
||||
|
||||
public init(service: String = "com.screens.vnc") {
|
||||
self.service = service
|
||||
}
|
||||
|
||||
public func storePassword(_ password: String, account: String) throws {
|
||||
guard let data = password.data(using: .utf8) else { throw KeychainError.encoding }
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
var attributes = query
|
||||
attributes[kSecValueData as String] = data
|
||||
attributes[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
let status = SecItemAdd(attributes as CFDictionary, nil)
|
||||
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
|
||||
}
|
||||
|
||||
public func loadPassword(account: String) throws -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
if status == errSecItemNotFound { return nil }
|
||||
guard status == errSecSuccess, let data = result as? Data else {
|
||||
throw KeychainError.unhandled(status)
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
public func deletePassword(account: String) throws {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account
|
||||
]
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
if status != errSecSuccess && status != errSecItemNotFound {
|
||||
throw KeychainError.unhandled(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum KeychainError: Error, Sendable {
|
||||
case encoding
|
||||
case unhandled(OSStatus)
|
||||
}
|
||||
Reference in New Issue
Block a user