mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-09-19 14:48:01 +02:00
Merge branch 'main' into auth_splitout
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"],
|
||||
|
||||
@@ -67,6 +67,7 @@ import SecretKit
|
||||
public func update(certificate: Certificate) throws {
|
||||
let updateQuery = KeychainDictionary([
|
||||
kSecClass: Constants.keyClass,
|
||||
kSecAttrService: Constants.keyTag,
|
||||
kSecAttrAccount: certificate.id,
|
||||
])
|
||||
|
||||
@@ -134,7 +135,7 @@ extension CertificateStore {
|
||||
|
||||
enum Constants {
|
||||
static let keyClass = kSecClassGenericPassword as String
|
||||
static let keyTag = Data("com.maxgoedjen.certificatestore.opensshcertificate".utf8)
|
||||
static let keyTag = "com.maxgoedjen.certificatestore.opensshcertificate"
|
||||
static let notificationToken = UUID().uuidString
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ extension SecureEnclave {
|
||||
let attributes = try JSONEncoder().encode(attributes)
|
||||
let updatedAttributes = KeychainDictionary([
|
||||
kSecAttrLabel: name,
|
||||
kSecAttrService: Constants.keyTag,
|
||||
kSecAttrGeneric: attributes,
|
||||
])
|
||||
|
||||
@@ -268,7 +269,7 @@ extension SecureEnclave.Store {
|
||||
|
||||
enum Constants {
|
||||
static let keyClass = kSecClassGenericPassword as String
|
||||
static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
||||
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key"
|
||||
static let notificationToken = UUID().uuidString
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 */; };
|
||||
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503647472F870B7800977A23 /* BatchedRequestsView.swift */; };
|
||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.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>"; };
|
||||
503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = "<group>"; };
|
||||
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.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 = {
|
||||
@@ -915,6 +921,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,6 +4,7 @@ import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import Brief
|
||||
import CertificateKit
|
||||
import SettingsKit
|
||||
|
||||
@main
|
||||
struct Secretive: App {
|
||||
@@ -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,7 +106,8 @@ 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 {
|
||||
|
||||
@@ -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