mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-09-11 19:09:03 +02:00
Secure Settings store (#831)
* Add SettingsStore * Use keychain instead of UserDefaults in JustUpdatedChecker.swift * Add ability disable the SSH comment via settings * Make SettingsStore functions non-static, re-implement them as subscripts and make settingsStore an environmentObject Fixes: https://github.com/maxgoedjen/secretive/pull/536#discussion_r1508446459 * Add rawValues for CommentStyle enum * Use SettingsStore for querying the comment style in SecretDetailView Fixes: https://github.com/maxgoedjen/secretive/pull/536#discussion_r1509655340 * Revert "Use keychain instead of UserDefaults in JustUpdatedChecker.swift" This reverts commit ae8a21a1bf7f9e2d04fcb83c4543995db0111325. * Remove copyright info * Use "enum Constants" in SettingsStore * WIP * Cleanup * Cleanup * Cleanup strings * WIP * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup --------- Co-authored-by: Paul Heidekrüger <paul.heidekrueger@tum.de>
This commit is contained in:
@@ -22,6 +22,9 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "CertificateKit",
|
name: "CertificateKit",
|
||||||
targets: ["CertificateKit"]),
|
targets: ["CertificateKit"]),
|
||||||
|
.library(
|
||||||
|
name: "SettingsKit",
|
||||||
|
targets: ["SettingsKit"]),
|
||||||
.library(
|
.library(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
targets: ["SecretAgentKit"]),
|
targets: ["SecretAgentKit"]),
|
||||||
@@ -76,6 +79,12 @@ let package = Package(
|
|||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "SettingsKit",
|
||||||
|
dependencies: [],
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
||||||
|
|||||||
14
Sources/Packages/Sources/SettingsKit/SettingKeys.swift
Normal file
14
Sources/Packages/Sources/SettingsKit/SettingKeys.swift
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
114
Sources/Packages/Sources/SettingsKit/SettingsStore.swift
Normal file
114
Sources/Packages/Sources/SettingsKit/SettingsStore.swift
Normal file
@@ -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<SettingsKeyType: SettingsKey>(_ 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<SettingsKeyType.Value>.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<Value>: Equatable, Codable, Sendable {
|
||||||
|
associatedtype Value: Codable
|
||||||
|
static var defaultValue: Value { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SettingsStore {
|
||||||
|
|
||||||
|
struct SettingValue<Value: Codable>: 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.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 */; };
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.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 = "<group>"; };
|
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
||||||
|
5018D825304F6F1600F1FEE8 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
||||||
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
|
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
|
||||||
@@ -322,6 +325,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
||||||
|
5018D828304F6F4200F1FEE8 /* SettingsKit in Frameworks */,
|
||||||
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */,
|
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */,
|
||||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */,
|
501421622781262300BBAA70 /* Brief in Frameworks */,
|
||||||
@@ -404,6 +408,7 @@
|
|||||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||||
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
|
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
|
||||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||||
|
5018D825304F6F1600F1FEE8 /* SettingsView.swift */,
|
||||||
);
|
);
|
||||||
path = Secrets;
|
path = Secrets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -653,6 +658,7 @@
|
|||||||
505F5EF12FA9635700C45824 /* CertificateKit */,
|
505F5EF12FA9635700C45824 /* CertificateKit */,
|
||||||
50E205832FAB296A00402380 /* SharedXPCServices */,
|
50E205832FAB296A00402380 /* SharedXPCServices */,
|
||||||
50E205892FAC2EB600402380 /* Formatters */,
|
50E205892FAC2EB600402380 /* Formatters */,
|
||||||
|
5018D827304F6F4200F1FEE8 /* SettingsKit */,
|
||||||
);
|
);
|
||||||
productName = Secretive;
|
productName = Secretive;
|
||||||
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
||||||
@@ -772,7 +778,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 2700;
|
LastSwiftUpdateCheck = 2700;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2700;
|
||||||
ORGANIZATIONNAME = "Max Goedjen";
|
ORGANIZATIONNAME = "Max Goedjen";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
5054028C3034B5E3000C3356 = {
|
5054028C3034B5E3000C3356 = {
|
||||||
@@ -916,6 +922,7 @@
|
|||||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
||||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
||||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
||||||
|
5018D826304F6F1600F1FEE8 /* SettingsView.swift in Sources */,
|
||||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||||
@@ -2092,6 +2099,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Brief;
|
productName = Brief;
|
||||||
};
|
};
|
||||||
|
5018D827304F6F4200F1FEE8 /* SettingsKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = SettingsKit;
|
||||||
|
};
|
||||||
505402A63034B7A4000C3356 /* XPCWrappers */ = {
|
505402A63034B7A4000C3356 /* XPCWrappers */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = XPCWrappers;
|
productName = XPCWrappers;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SecureEnclaveSecretKit
|
|||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
|
import SettingsKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Secretive: App {
|
struct Secretive: App {
|
||||||
@@ -16,6 +17,7 @@ struct Secretive: App {
|
|||||||
ContentView()
|
ContentView()
|
||||||
.environment(EnvironmentValues._secretStoreList)
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
.environment(EnvironmentValues._certificateStore)
|
.environment(EnvironmentValues._certificateStore)
|
||||||
|
.environment(EnvironmentValues._settingsStore)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||||
Task {
|
Task {
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
@@ -42,6 +44,9 @@ struct Secretive: App {
|
|||||||
}
|
}
|
||||||
.windowStyle(.hiddenTitleBar)
|
.windowStyle(.hiddenTitleBar)
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
|
Settings {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,8 @@ extension EnvironmentValues {
|
|||||||
return list
|
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()
|
private static let _agentLaunchController = AgentLaunchController()
|
||||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||||
@@ -122,6 +128,10 @@ extension EnvironmentValues {
|
|||||||
@MainActor var certificateStore: CertificateStore {
|
@MainActor var certificateStore: CertificateStore {
|
||||||
EnvironmentValues._certificateStore
|
EnvironmentValues._certificateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor var settingsStore: SettingsStore {
|
||||||
|
EnvironmentValues._settingsStore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FocusedValues {
|
extension FocusedValues {
|
||||||
|
|||||||
14
Sources/Secretive/Views/Secrets/SettingsView.swift
Normal file
14
Sources/Secretive/Views/Secrets/SettingsView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user