diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 9b83feb..0fd392d 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -22,6 +22,9 @@ let package = Package( .library( name: "CertificateKit", targets: ["CertificateKit"]), + .library( + name: "SettingsKit", + targets: ["SettingsKit"]), .library( name: "SecretAgentKit", targets: ["SecretAgentKit"]), @@ -76,6 +79,12 @@ let package = Package( resources: [localization], swiftSettings: swiftSettings, ), + .target( + name: "SettingsKit", + dependencies: [], + resources: [localization], + swiftSettings: swiftSettings, + ), .target( name: "SecretAgentKit", dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"], diff --git a/Sources/Packages/Sources/SettingsKit/SettingKeys.swift b/Sources/Packages/Sources/SettingsKit/SettingKeys.swift new file mode 100644 index 0000000..4663529 --- /dev/null +++ b/Sources/Packages/Sources/SettingsKit/SettingKeys.swift @@ -0,0 +1,14 @@ +import Foundation + +struct RequireDestinationInformationSettingsKey: SettingsStore.SettingsKey { + static let defaultValue: Bool = true +} + +extension SettingsStore { + + public var requireDestinationInformation: Bool { + get { self[RequireDestinationInformationSettingsKey.self] } + set { self[RequireDestinationInformationSettingsKey.self] = newValue } + } + +} diff --git a/Sources/Packages/Sources/SettingsKit/SettingsStore.swift b/Sources/Packages/Sources/SettingsKit/SettingsStore.swift new file mode 100644 index 0000000..2f222dc --- /dev/null +++ b/Sources/Packages/Sources/SettingsKit/SettingsStore.swift @@ -0,0 +1,114 @@ +import Foundation +import Observation +import Security +import OSLog + +// Setting store backed by macOS keychain for stronger guarantees around ownership/other-process-modification than UserDefaults offers. +@Observable @MainActor public final class SettingsStore: Sendable { + + private let logger = Logger(subsystem: "com.maxgoedjen.secretive.settings", category: "SettingsStore") + + public init() { + } + + private var state: [ObjectIdentifier: UUID] = [:] + + subscript(_ key: SettingsKeyType.Type) -> SettingsKeyType.Value { + get { + _ = state[ObjectIdentifier(key)] + let queryAttributes = KeychainDictionary([ + kSecClass: Constants.keyClass, + kSecAttrService: Constants.keyTag, + kSecAttrAccount: String(describing: SettingsKeyType.self), + kSecUseDataProtectionKeychain: true, + kSecReturnData: true, + kSecReturnAttributes: true, + kSecMatchLimit: kSecMatchLimitOne, + ]) + var untyped: CFTypeRef? + unsafe SecItemCopyMatching(queryAttributes, &untyped) + guard let typed = untyped as? [CFString: Any] else { return SettingsKeyType.defaultValue } + let decoder = JSONDecoder() + guard let data = typed[kSecValueData] as? Data else { return SettingsKeyType.defaultValue } + return (try? decoder.decode(SettingValue.self, from: data).value) ?? SettingsKeyType.defaultValue + } + set { + do { + let data = try JSONEncoder().encode(SettingValue(value: newValue)) + let keychainAttributes = KeychainDictionary([ + kSecClass: Constants.keyClass, + kSecAttrService: Constants.keyTag, + kSecAttrAccount: String(describing: SettingsKeyType.self), + kSecUseDataProtectionKeychain: true, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + kSecValueData: data, + ]) + let status = SecItemAdd(keychainAttributes, nil) + switch status { + case errSecSuccess: + break + case errSecDuplicateItem: + let updateQuery = KeychainDictionary([ + kSecClass: Constants.keyClass, + kSecAttrService: Constants.keyTag, + kSecAttrAccount: String(describing: SettingsKeyType.self), + ]) + let updatedAttributes = KeychainDictionary([ + kSecValueData: data, + ]) + let status = SecItemUpdate(updateQuery, updatedAttributes) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + default: + throw KeychainError(statusCode: status) + } + state[ObjectIdentifier(key)] = UUID() + } catch { + logger.error("Error updating key: \(String(describing: SettingsKeyType.self), privacy: .public): \(error.localizedDescription.debugDescription, privacy: .public)") + } + } + } + + +} + +extension SettingsStore { + + public protocol SettingsKey: Equatable, Codable, Sendable { + associatedtype Value: Codable + static var defaultValue: Value { get } + } + +} + +extension SettingsStore { + + struct SettingValue: Codable { + let value: Value + } + +} + + +extension SettingsStore { + + fileprivate struct KeychainError: Error { + let statusCode: OSStatus? + } + +} + +fileprivate func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary { + dictionary as CFDictionary +} + + +extension SettingsStore { + + enum Constants { + static let keyClass = kSecClassGenericPassword as String + static let keyTag = "com.maxgoedjen.settingsStore" + } + +} diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 94630d1..0b54acb 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; }; + 5018D826304F6F1600F1FEE8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018D825304F6F1600F1FEE8 /* SettingsView.swift */; }; + 5018D828304F6F4200F1FEE8 /* SettingsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5018D827304F6F4200F1FEE8 /* SettingsKit */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; @@ -236,6 +238,7 @@ 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = ""; }; + 5018D825304F6F1600F1FEE8 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = ""; }; @@ -322,6 +325,7 @@ buildActionMask = 2147483647; files = ( 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */, + 5018D828304F6F4200F1FEE8 /* SettingsKit in Frameworks */, 50E2058A2FAC2EB600402380 /* Formatters in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */, @@ -404,6 +408,7 @@ 50153E21250DECA300525160 /* SecretListItemView.swift */, 50E204EC2FAA997F00402380 /* CertificateListItemView.swift */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, + 5018D825304F6F1600F1FEE8 /* SettingsView.swift */, ); path = Secrets; sourceTree = ""; @@ -653,6 +658,7 @@ 505F5EF12FA9635700C45824 /* CertificateKit */, 50E205832FAB296A00402380 /* SharedXPCServices */, 50E205892FAC2EB600402380 /* Formatters */, + 5018D827304F6F4200F1FEE8 /* SettingsKit */, ); productName = Secretive; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; @@ -772,7 +778,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 2700; - LastUpgradeCheck = 2640; + LastUpgradeCheck = 2700; ORGANIZATIONNAME = "Max Goedjen"; TargetAttributes = { 5054028C3034B5E3000C3356 = { @@ -916,6 +922,7 @@ 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, + 5018D826304F6F1600F1FEE8 /* SettingsView.swift in Sources */, 50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, @@ -2092,6 +2099,10 @@ isa = XCSwiftPackageProductDependency; productName = Brief; }; + 5018D827304F6F4200F1FEE8 /* SettingsKit */ = { + isa = XCSwiftPackageProductDependency; + productName = SettingsKit; + }; 505402A63034B7A4000C3356 /* XPCWrappers */ = { isa = XCSwiftPackageProductDependency; productName = XPCWrappers; diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 202b380..eea9c3c 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -4,10 +4,11 @@ import SecureEnclaveSecretKit import SmartCardSecretKit import Brief import CertificateKit +import SettingsKit @main struct Secretive: App { - + @Environment(\.agentLaunchController) var agentLaunchController @Environment(\.justUpdatedChecker) var justUpdatedChecker @@ -16,6 +17,7 @@ struct Secretive: App { ContentView() .environment(EnvironmentValues._secretStoreList) .environment(EnvironmentValues._certificateStore) + .environment(EnvironmentValues._settingsStore) .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in Task { @AppStorage("defaultsHasRunSetup") var hasRunSetup = false @@ -42,6 +44,9 @@ struct Secretive: App { } .windowStyle(.hiddenTitleBar) .windowResizability(.contentSize) + Settings { + SettingsView() + } } } @@ -101,8 +106,9 @@ extension EnvironmentValues { return list }() - @MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore() - + @MainActor fileprivate static let _certificateStore = CertificateStore() + @MainActor fileprivate static let _settingsStore = SettingsStore() + private static let _agentLaunchController = AgentLaunchController() @Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController @@ -122,6 +128,10 @@ extension EnvironmentValues { @MainActor var certificateStore: CertificateStore { EnvironmentValues._certificateStore } + + @MainActor var settingsStore: SettingsStore { + EnvironmentValues._settingsStore + } } extension FocusedValues { diff --git a/Sources/Secretive/Views/Secrets/SettingsView.swift b/Sources/Secretive/Views/Secrets/SettingsView.swift new file mode 100644 index 0000000..bc2b418 --- /dev/null +++ b/Sources/Secretive/Views/Secrets/SettingsView.swift @@ -0,0 +1,14 @@ +import SwiftUI +import SettingsKit + +struct SettingsView: View { + + @Environment(\.settingsStore) var settingsStore + + var body: some View { + Form { + } + .padding() + .frame(minWidth: 480, minHeight: 320) + } +}