mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-09-09 09:59: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(
|
||||
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"],
|
||||
|
||||
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 */; };
|
||||
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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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