Compare commits

..

11 Commits

Author SHA1 Message Date
df4661a718 . 2022-01-02 22:52:53 -08:00
d80d2ca656 XPC scaffolding 2022-01-02 22:15:18 -08:00
98bf285ad2 . 2022-01-02 16:21:22 -08:00
e99a42bdbb . 2022-01-02 16:21:11 -08:00
7ba5e85aaf . 2022-01-02 16:20:12 -08:00
d902130c01 . 2022-01-02 16:15:22 -08:00
ef4bf74ac5 . 2022-01-02 16:15:09 -08:00
64225af0c1 . 2022-01-02 16:07:33 -08:00
d365da11cd Merge 2022-01-02 15:27:55 -08:00
0bab89fac4 WIP 2021-11-16 23:04:51 -08:00
a3b5ccbc3d WIP 2021-11-16 23:04:46 -08:00
28 changed files with 269 additions and 149 deletions

2
FAQ.md
View File

@ -38,7 +38,7 @@ Awesome! Just bear in mind that because an app only has access to the keychain i
### What's this network request to GitHub? ### What's this network request to GitHub?
Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Sources/Packages/Sources/Brief/Updater.swift). Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Brief/Updater.swift).
### I have a security issue ### I have a security issue

View File

@ -24,6 +24,9 @@ let package = Package(
.library( .library(
name: "SecretAgentKitHeaders", name: "SecretAgentKitHeaders",
targets: ["SecretAgentKitHeaders"]), targets: ["SecretAgentKitHeaders"]),
.library(
name: "SecretAgentKitProtocol",
targets: ["SecretAgentKitProtocol"]),
.library( .library(
name: "Brief", name: "Brief",
targets: ["Brief"]), targets: ["Brief"]),
@ -32,8 +35,7 @@ let package = Package(
], ],
targets: [ targets: [
.target( .target(
name: "SecretKit", name: "SecretKit"
dependencies: []
), ),
.testTarget( .testTarget(
name: "SecretKitTests", name: "SecretKitTests",
@ -49,18 +51,20 @@ let package = Package(
), ),
.target( .target(
name: "SecretAgentKit", name: "SecretAgentKit",
dependencies: ["SecretKit", "SecretAgentKitHeaders"] dependencies: ["SecretKit", "SecretAgentKitHeaders", "SecretAgentKitProtocol"]
), ),
.systemLibrary( .systemLibrary(
name: "SecretAgentKitHeaders" name: "SecretAgentKitHeaders"
), ),
.target(
name: "SecretAgentKitProtocol"
),
.testTarget( .testTarget(
name: "SecretAgentKitTests", name: "SecretAgentKitTests",
dependencies: ["SecretAgentKit"]) dependencies: ["SecretAgentKit"])
, ,
.target( .target(
name: "Brief", name: "Brief"
dependencies: []
), ),
.testTarget( .testTarget(
name: "BriefTests", name: "BriefTests",

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// A release is a representation of a downloadable update. /// A release is a representation of a downloadable update.
public struct Release: Codable, Sendable { public struct Release: Codable {
/// The user-facing name of the release. Typically "Secretive 1.2.3" /// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String public let name: String
@ -30,9 +30,6 @@ public struct Release: Codable, Sendable {
} }
// TODO: REMOVE WHEN(?) URL GAINS NATIVE CONFORMANCE
extension URL: @unchecked Sendable {}
extension Release: Identifiable { extension Release: Identifiable {
public var id: String { public var id: String {

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// A representation of a Semantic Version. /// A representation of a Semantic Version.
public struct SemVer: Sendable { public struct SemVer {
/// The SemVer broken into an array of integers. /// The SemVer broken into an array of integers.
let versionNumbers: [Int] let versionNumbers: [Int]

View File

@ -2,59 +2,53 @@ import Foundation
import Combine import Combine
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. /// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
public actor Updater: ObservableObject, UpdaterProtocol { public class Updater: ObservableObject, UpdaterProtocol {
@MainActor @Published public var update: Release? @Published public var update: Release?
public let testBuild: Bool public let testBuild: Bool
/// The current OS version. /// The current OS version.
private let osVersion: SemVer private let osVersion: SemVer
/// The current version of the app that is running. /// The current version of the app that is running.
private let currentVersion: SemVer private let currentVersion: SemVer
/// The timer responsible for checking for updates regularly.
private var timer: Timer? = nil
/// Initializes an Updater. /// Initializes an Updater.
/// - Parameters: /// - Parameters:
/// - checkOnLaunch: A boolean describing whether the Updater should check for available updates on launch.
/// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
/// - osVersion: The current OS version. /// - osVersion: The current OS version.
/// - currentVersion: The current version of the app that is running. /// - currentVersion: The current version of the app that is running.
public init(osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) { public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
self.osVersion = osVersion self.osVersion = osVersion
self.currentVersion = currentVersion self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0") testBuild = currentVersion == SemVer("0.0.0")
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
checkForUpdates()
} }
let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
/// Begins checking for updates with the specified frequency. self.checkForUpdates()
/// - Parameter checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
public func beginChecking(checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
Task {
await self.checkForUpdates()
} }
} timer.tolerance = 60*60
timer?.tolerance = 60*60
}
/// Ends checking for updates.
public func stopChecking() {
timer?.invalidate()
timer = nil
} }
/// Manually trigger an update check. /// Manually trigger an update check.
public func checkForUpdates() async { public func checkForUpdates() {
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL), URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
let releases = try? JSONDecoder().decode([Release].self, from: data) else { return } guard let data = data else { return }
await evaluate(releases: releases) guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
self.evaluate(releases: releases)
}.resume()
} }
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release. /// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
/// - Parameter release: The release to ignore. /// - Parameter release: The release to ignore.
public func ignore(release: Release) async { public func ignore(release: Release) {
guard !release.critical else { return } guard !release.critical else { return }
defaults.set(true, forKey: release.name) defaults.set(true, forKey: release.name)
await setUpdate(update: update) DispatchQueue.main.async {
self.update = nil
}
} }
} }
@ -63,7 +57,7 @@ extension Updater {
/// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run. /// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run.
/// - Parameter releases: An array of ``Release`` objects. /// - Parameter releases: An array of ``Release`` objects.
func evaluate(releases: [Release]) async { func evaluate(releases: [Release]) {
guard let release = releases guard let release = releases
.sorted() .sorted()
.reversed() .reversed()
@ -73,12 +67,10 @@ extension Updater {
guard !release.prerelease else { return } guard !release.prerelease else { return }
let latestVersion = SemVer(release.name) let latestVersion = SemVer(release.name)
if latestVersion > currentVersion { if latestVersion > currentVersion {
await setUpdate(update: update) DispatchQueue.main.async {
self.update = release
} }
} }
@MainActor private func setUpdate(update: Release?) {
self.update = update
} }
/// Checks whether the user has ignored a release. /// Checks whether the user has ignored a release.
@ -103,21 +95,3 @@ extension Updater {
} }
} }
@available(macOS, deprecated: 12)
extension URLSession {
// Backport for macOS 11
func data(from url: URL) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response else {
continuation.resume(throwing: error ?? NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil))
return
}
continuation.resume(returning: (data, response))
}
}
}
}

View File

@ -4,9 +4,9 @@ import Foundation
public protocol UpdaterProtocol: ObservableObject { public protocol UpdaterProtocol: ObservableObject {
/// The latest update /// The latest update
@MainActor var update: Release? { get } var update: Release? { get }
/// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build) /// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build)
@MainActor var testBuild: Bool { get } var testBuild: Bool { get }
} }

View File

@ -6,8 +6,6 @@ public class SocketController {
/// The active FileHandle. /// The active FileHandle.
private var fileHandle: FileHandle? private var fileHandle: FileHandle?
/// The active SocketPort.
private var port: SocketPort?
/// A handler that will be notified when a new read/write handle is available. /// A handler that will be notified when a new read/write handle is available.
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)? public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
@ -21,7 +19,6 @@ public class SocketController {
let exists = FileManager.default.fileExists(atPath: path) let exists = FileManager.default.fileExists(atPath: path)
assert(!exists) assert(!exists)
Logger().debug("Socket controller path is clear") Logger().debug("Socket controller path is clear")
port = socketPort(at: path)
configureSocket(at: path) configureSocket(at: path)
Logger().debug("Socket listening at \(path)") Logger().debug("Socket listening at \(path)")
} }
@ -29,7 +26,7 @@ public class SocketController {
/// Configures the socket and a corresponding FileHandle. /// Configures the socket and a corresponding FileHandle.
/// - Parameter path: The path to use as a socket. /// - Parameter path: The path to use as a socket.
func configureSocket(at path: String) { func configureSocket(at path: String) {
guard let port = port else { return } let port = socketPort(at: path)
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)

View File

@ -0,0 +1,12 @@
import Foundation
@objc public protocol AgentProtocol {
func updatedStore(withID: UUID) async throws
}
public struct AgentProtocolStoreNotFoundError: Error {
public init() {
}
}

View File

@ -58,11 +58,13 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
private let _create: (String, Bool) throws -> Void private let _create: (String, Bool) throws -> Void
private let _delete: (AnySecret) throws -> Void private let _delete: (AnySecret) throws -> Void
private let _update: (AnySecret, String) throws -> Void private let _update: (AnySecret, String) throws -> Void
private let _reload: () throws -> Void
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) } _create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } _delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
_update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) } _update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
_reload = { try secretStore.reload() }
super.init(secretStore) super.init(secretStore)
} }
@ -78,4 +80,8 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
try _update(secret, name) try _update(secret, name)
} }
public func reload() throws {
try _reload()
}
} }

View File

@ -5,11 +5,10 @@ import OSLog
public class PublicKeyFileStoreController { public class PublicKeyFileStoreController {
private let logger = Logger() private let logger = Logger()
private let directory: String private let directory = NSHomeDirectory().appending("/PublicKeys")
/// Initializes a PublicKeyFileStoreController. /// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: String) { public init() {
directory = homeDirectory.appending("/PublicKeys")
} }
/// Writes out the keys specified to disk. /// Writes out the keys specified to disk.

View File

@ -52,6 +52,9 @@ public protocol SecretStoreModifiable: SecretStore {
/// - name: The new name for the Secret. /// - name: The new name for the Secret.
func update(secret: SecretType, name: String) throws func update(secret: SecretType, name: String) throws
/// Reloads the secrets from the backing store.
func reload() throws
} }
extension NSNotification.Name { extension NSNotification.Name {

View File

@ -23,9 +23,6 @@ extension SecureEnclave {
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecrets(notify: false)
}
loadSecrets() loadSecrets()
} }
@ -68,7 +65,7 @@ extension SecureEnclave {
throw KeychainError(statusCode: nil) throw KeychainError(statusCode: nil)
} }
try savePublicKey(publicKey, name: name) try savePublicKey(publicKey, name: name)
reloadSecrets() reload()
} }
public func delete(secret: Secret) throws { public func delete(secret: Secret) throws {
@ -80,7 +77,7 @@ extension SecureEnclave {
if status != errSecSuccess { if status != errSecSuccess {
throw KeychainError(statusCode: status) throw KeychainError(statusCode: status)
} }
reloadSecrets() reload()
} }
public func update(secret: Secret, name: String) throws { public func update(secret: Secret, name: String) throws {
@ -97,7 +94,12 @@ extension SecureEnclave {
if status != errSecSuccess { if status != errSecSuccess {
throw KeychainError(statusCode: status) throw KeychainError(statusCode: status)
} }
reloadSecrets() reload()
}
public func reload() {
secrets.removeAll()
loadSecrets()
} }
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData { public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
@ -170,16 +172,6 @@ extension SecureEnclave {
extension SecureEnclave.Store { extension SecureEnclave.Store {
/// Reloads all secrets from the store.
/// - Parameter notify: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well.
private func reloadSecrets(notify: Bool = true) {
secrets.removeAll()
loadSecrets()
if notify {
DistributedNotificationCenter.default().post(name: .secretStoreUpdated, object: nil)
}
}
/// Loads all secrets from the store. /// Loads all secrets from the store.
private func loadSecrets() { private func loadSecrets() {
let attributes = [ let attributes = [

View File

@ -7,8 +7,11 @@ import SmartCardSecretKit
import SecretAgentKit import SecretAgentKit
import Brief import Brief
import SecretKit
import SecretAgentKitProtocol
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate, AgentProtocol {
private let storeList: SecretStoreList = { private let storeList: SecretStoreList = {
let list = SecretStoreList() let list = SecretStoreList()
@ -16,9 +19,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store()) list.add(store: SmartCard.Store())
return list return list
}() }()
private let updater = Updater() private let updater = Updater(checkOnLaunch: false)
private let notifier = Notifier() private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let publicKeyFileStoreController = PublicKeyFileStoreController()
private lazy var agent: Agent = { private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier) Agent(storeList: storeList, witness: notifier)
}() }()
@ -27,22 +30,57 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return SocketController(path: path) return SocketController(path: path)
}() }()
private var updateSink: AnyCancellable? private var updateSink: AnyCancellable?
private let logger = Logger()
var delegate: ServiceDelegate? = nil
let listener = NSXPCListener(machServiceName: Bundle.main.bundleIdentifier!)
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger().debug("SecretAgent finished launching") logger.debug("SecretAgent finished launching")
DispatchQueue.main.async { DispatchQueue.main.async {
self.socketController.handler = self.agent.handle(reader:writer:) self.socketController.handler = self.agent.handle(reader:writer:)
} }
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [self] _ in
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
}
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true) try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
notifier.prompt() notifier.prompt()
// updateSink = updater.$update.sink { update in updateSink = updater.$update.sink { update in
// guard let update = update else { return } guard let update = update else { return }
// self.notifier.notify(update: update, ignore: self.updater.ignore(release:)) self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
// } }
// TODO: REMOVE
notifier.notify(update: Release(name: "Test", prerelease: false, html_url: URL(string: "https://example.com")!, body: ""), ignore: nil)
connect()
}
func connect() {
delegate = ServiceDelegate(exportedObject: self)
listener.delegate = delegate
listener.resume()
}
func updatedStore(withID id: UUID) async throws {
// TODO: REMOVE
notifier.notify(update: Release(name: "UPDATESTORE", prerelease: false, html_url: URL(string: "https://example.com")!, body: ""), ignore: nil)
logger.debug("Reloading keys for store with id: \(id)")
guard let store = storeList.modifiableStore, store.id == id else { throw AgentProtocolStoreNotFoundError() }
try store.reload()
try publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
} }
} }
// TODO: MOVE
class ServiceDelegate: NSObject, NSXPCListenerDelegate {
let exported: AgentProtocol
init(exportedObject: AgentProtocol) {
self.exported = exportedObject
}
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: AgentProtocol.self)
newConnection.exportedObject = exported
newConnection.resume()
return true
}
}

View File

@ -4,6 +4,10 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)com.maxgoedjen.secretive</string>
</array>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.smartcard</key> <key>com.apple.security.smartcard</key>

View File

@ -24,7 +24,7 @@
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50571E0524393D1500F76F6C /* AgentLaunchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* AgentLaunchController.swift */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; }; 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
@ -36,6 +36,8 @@
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; }; 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
50736F652782C05000A723B6 /* AgentCommunicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50736F642782C05000A723B6 /* AgentCommunicationController.swift */; };
50736F672782C31800A723B6 /* SecretAgentKitProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 50736F662782C31800A723B6 /* SecretAgentKitProtocol */; };
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; }; 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; }; 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
@ -111,7 +113,7 @@
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>"; };
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>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; }; 50571E0424393D1500F76F6C /* AgentLaunchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentLaunchController.swift; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -128,6 +130,7 @@
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; }; 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
50736F642782C05000A723B6 /* AgentCommunicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentCommunicationController.swift; sourceTree = "<group>"; };
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = "<group>"; }; 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = "<group>"; };
508A58AB241E121B0069DC07 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; }; 508A58AB241E121B0069DC07 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
@ -155,6 +158,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
50736F672782C31800A723B6 /* SecretAgentKitProtocol in Frameworks */,
501421622781262300BBAA70 /* Brief in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */,
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */, 5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */, 5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */,
@ -283,9 +287,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
50736F642782C05000A723B6 /* AgentCommunicationController.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */, 50571E0424393D1500F76F6C /* AgentLaunchController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */, 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
); );
path = Controllers; path = Controllers;
@ -345,6 +350,7 @@
5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */, 5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */,
5003EF602780081600DF2006 /* SmartCardSecretKit */, 5003EF602780081600DF2006 /* SmartCardSecretKit */,
501421612781262300BBAA70 /* Brief */, 501421612781262300BBAA70 /* Brief */,
50736F662782C31800A723B6 /* SecretAgentKitProtocol */,
); );
productName = Secretive; productName = Secretive;
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
@ -485,8 +491,9 @@
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 50571E0524393D1500F76F6C /* AgentLaunchController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50736F652782C05000A723B6 /* AgentCommunicationController.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,
@ -830,6 +837,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Secretive/Secretive.entitlements;
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -874,6 +882,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
@ -885,7 +894,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = Z72PRUAWF6.com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@ -908,7 +917,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = Z72PRUAWF6.com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@ -932,7 +941,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = Z72PRUAWF6.com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent"; PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -1021,6 +1030,10 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Brief; productName = Brief;
}; };
50736F662782C31800A723B6 /* SecretAgentKitProtocol */ = {
isa = XCSwiftPackageProductDependency;
productName = SecretAgentKitProtocol;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 50617D7723FCE48D0099B055 /* Project object */; rootObject = 50617D7723FCE48D0099B055 /* Project object */;

View File

@ -15,8 +15,9 @@ struct Secretive: App {
return list return list
}() }()
private let agentStatusChecker = AgentStatusChecker() private let agentStatusChecker = AgentStatusChecker()
private let agentLaunchController = AgentLaunchController()
private let agentCommunicationController = AgentCommunicationController()
private let justUpdatedChecker = JustUpdatedChecker() private let justUpdatedChecker = JustUpdatedChecker()
private let updater = Updater()
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false @State private var showingSetup = false
@ -26,25 +27,31 @@ struct Secretive: App {
WindowGroup { WindowGroup {
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
.environmentObject(storeList) .environmentObject(storeList)
.environmentObject(updater) .environmentObject(Updater(checkOnLaunch: hasRunSetup))
.environmentObject(agentStatusChecker) .environmentObject(agentStatusChecker)
.environmentObject(agentCommunicationController)
.onAppear { .onAppear {
if !hasRunSetup { if !hasRunSetup {
showingSetup = true showingSetup = true
} else {
Task { [updater] in
await updater.checkForUpdates()
}
} }
} }
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard hasRunSetup else { return } guard hasRunSetup else { return }
agentStatusChecker.check() agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdated { if justUpdatedChecker.justUpdated || !agentStatusChecker.running {
// Relaunch the agent, since it'll be running from earlier update still // Two conditions in which we reinstall/attempt a force launch:
reinstallAgent() // 1: The app was just updated, and an old version of the agent is alive. Reinstall will deactivate this and activate a new one.
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild { // 2: The agent is not running for some reason. We'll attempt to reinstall it, or relaunch directly if that fails.
forceLaunchAgent() reinstallAgent(uninstallFirst: agentStatusChecker.running) {
if agentStatusChecker.noninstanceSecretAgentProcesses.isEmpty {
agentLaunchController.killNonInstanceAgents(agents: agentStatusChecker.noninstanceSecretAgentProcesses)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
agentCommunicationController.configure()
}
}
} else {
agentCommunicationController.configure()
} }
} }
} }
@ -65,6 +72,13 @@ struct Secretive: App {
showingSetup = true showingSetup = true
} }
} }
CommandGroup(after: .help) {
Button("TEST") {
Task {
try await agentCommunicationController.agent?.updatedStore(withID: storeList.modifiableStore?.id ?? UUID())
}
}
}
SidebarCommands() SidebarCommands()
} }
} }
@ -73,24 +87,21 @@ struct Secretive: App {
extension Secretive { extension Secretive {
private func reinstallAgent() { private func reinstallAgent(uninstallFirst: Bool, completion: @escaping () -> Void) {
justUpdatedChecker.check() justUpdatedChecker.check()
LaunchAgentController().install { agentLaunchController.install(uninstallFirst: uninstallFirst) {
// Wait a second for launchd to kick in (next runloop isn't enough). // Wait a second for launchd to kick in (next runloop isn't enough).
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
agentStatusChecker.check() agentStatusChecker.check()
if !agentStatusChecker.running { if !agentStatusChecker.running {
forceLaunchAgent() agentLaunchController.forceLaunch { _ in
}
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
LaunchAgentController().forceLaunch { _ in
agentStatusChecker.check() agentStatusChecker.check()
completion()
}
} else {
completion()
}
}
} }
} }

View File

@ -0,0 +1,37 @@
import Foundation
import Combine
import AppKit
import OSLog
import SecretKit
import SecretAgentKitProtocol
protocol AgentCommunicationControllerProtocol: ObservableObject {
var agent: AgentProtocol? { get }
}
class AgentCommunicationController: ObservableObject, AgentCommunicationControllerProtocol {
private(set) var agent: AgentProtocol? = nil
private var connection: NSXPCConnection? = nil
private var running = false
init() {
}
func configure() {
guard !running else { return }
connection = NSXPCConnection(machServiceName: Bundle.main.agentBundleID)
connection?.remoteObjectInterface = NSXPCInterface(with: AgentProtocol.self)
connection?.invalidationHandler = {
Logger().warning("XPC connection invalidated")
}
connection?.resume()
agent = connection?.remoteObjectProxyWithErrorHandler({ error in
Logger().error("\(String(describing: error))")
}) as! AgentProtocol
running = true
}
}

View File

@ -4,23 +4,27 @@ import AppKit
import OSLog import OSLog
import SecretKit import SecretKit
struct LaunchAgentController { struct AgentLaunchController {
func install(completion: (() -> Void)? = nil) { func install(uninstallFirst: Bool = true, completion: (() -> Void)? = nil) {
Logger().debug("Installing agent") Logger().debug("Installing agent")
if uninstallFirst {
_ = setEnabled(false) _ = setEnabled(false)
}
// This is definitely a bit of a "seems to work better" thing but: // This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new? // and start new?
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
_ = setEnabled(true) _ = setEnabled(true)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion?() completion?()
} }
}
} }
func forceLaunch(completion: ((Bool) -> Void)?) { func forceLaunch(completion: ((Bool) -> Void)?) {
Logger().debug("Agent is not running, attempting to force launch") Logger().debug("Agent is still not running, attempting to force launch")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app") let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration() let config = NSWorkspace.OpenConfiguration()
config.activates = false config.activates = false
@ -36,6 +40,12 @@ struct LaunchAgentController {
} }
} }
func killNonInstanceAgents(agents: [NSRunningApplication]) {
for agent in agents {
agent.terminate()
}
}
private func setEnabled(_ enabled: Bool) -> Bool { private func setEnabled(_ enabled: Bool) -> Bool {
SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled) SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled)
} }

View File

@ -30,13 +30,18 @@ class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
let agents = secretAgentProcesses let agents = secretAgentProcesses
for agent in agents { for agent in agents {
guard let url = agent.bundleURL else { continue } guard let url = agent.bundleURL else { continue }
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) { if url.absoluteString.contains(Bundle.main.bundleURL.absoluteString) {
return agent return agent
} }
} }
return nil return nil
} }
// All processes, _NOT_ including one the instance agent.
var noninstanceSecretAgentProcesses: [NSRunningApplication] {
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
.filter({ !($0.bundleURL?.absoluteString.contains(Bundle.main.bundleURL.absoluteString) ?? false) })
}
// Whether Secretive is being run in an Xcode environment. // Whether Secretive is being run in an Xcode environment.
var developmentBuild: Bool { var developmentBuild: Bool {

View File

@ -9,6 +9,7 @@ protocol JustUpdatedCheckerProtocol: ObservableObject {
class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol { class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
@Published var justUpdated: Bool = false @Published var justUpdated: Bool = false
var alreadyRelaunchedForDebug = false
init() { init() {
check() check()
@ -18,7 +19,12 @@ class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None" let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None"
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey) UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
if currentBuild != Constants.debugVersionKey {
justUpdated = lastBuild != currentBuild justUpdated = lastBuild != currentBuild
} else {
justUpdated = !alreadyRelaunchedForDebug
alreadyRelaunchedForDebug = true
}
} }
@ -29,6 +35,7 @@ extension JustUpdatedChecker {
enum Constants { enum Constants {
static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild" static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild"
static let debugVersionKey = "GITHUB_CI_VERSION"
} }
} }

View File

@ -2,6 +2,6 @@ import Foundation
extension Bundle { extension Bundle {
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!} public var agentBundleID: String { "Z72PRUAWF6.com.maxgoedjen.Secretive.SecretAgent" }
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!} public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
} }

View File

@ -45,6 +45,7 @@ extension Preview {
} }
class StoreModifiable: Store, SecretStoreModifiable { class StoreModifiable: Store, SecretStoreModifiable {
override var name: String { "Modifiable Preview Store" } override var name: String { "Modifiable Preview Store" }
func create(name: String, requiresAuthentication: Bool) throws { func create(name: String, requiresAuthentication: Bool) throws {
@ -55,6 +56,10 @@ extension Preview {
func update(secret: Preview.Secret, name: String) throws { func update(secret: Preview.Secret, name: String) throws {
} }
func reload() throws {
}
} }
} }

View File

@ -4,6 +4,10 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)com.maxgoedjen.secretive</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key> <key>com.apple.security.files.user-selected.read-write</key>
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>

View File

@ -87,7 +87,7 @@ extension ContentView {
}) })
.popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { .popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
if let modifiable = storeList.modifiableStore { if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation) CreateSecretView<AnySecretStoreModifiable, AgentCommunicationController>(store: modifiable, showing: $showingCreation)
} }
} }

View File

@ -1,9 +1,10 @@
import SwiftUI import SwiftUI
import SecretKit import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View { struct CreateSecretView<StoreType: SecretStoreModifiable, AgentCommunicationControllerType: AgentCommunicationControllerProtocol>: View {
@ObservedObject var store: StoreType @ObservedObject var store: StoreType
@EnvironmentObject private var agentCommunicationController: AgentCommunicationControllerType
@Binding var showing: Bool @Binding var showing: Bool
@State private var name = "" @State private var name = ""
@ -52,6 +53,9 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
func save() { func save() {
try! store.create(name: name, requiresAuthentication: requiresAuthentication) try! store.create(name: name, requiresAuthentication: requiresAuthentication)
Task {
try! await agentCommunicationController.agent!.updatedStore(withID: store.id)
}
showing = false showing = false
} }
} }

View File

@ -6,7 +6,7 @@ struct SecretDetailView<SecretType: Secret>: View {
@State var secret: SecretType @State var secret: SecretType
private let keyWriter = OpenSSHKeyWriter() private let keyWriter = OpenSSHKeyWriter()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) private let publicKeyFileStoreController = PublicKeyFileStoreController()
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -18,7 +18,7 @@ struct SecretDetailView<SecretType: Secret>: View {
CopyableView(title: "MD5 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret)) CopyableView(title: "MD5 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString) CopyableView(title: "Public Key Contents", image: Image(systemName: "key"), text: keyString)
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.path(for: secret)) CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.path(for: secret))

View File

@ -22,7 +22,7 @@ struct SetupView: View {
} }
.frame(width: proxy.size.width) .frame(width: proxy.size.width)
} }
.offset(x: -proxy.size.width * Double(stepIndex), y: 0) .offset(x: -proxy.size.width * CGFloat(stepIndex), y: 0)
} }
} }
} }
@ -44,7 +44,7 @@ struct StepView: View {
let currentStep: Int let currentStep: Int
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7 // Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
let width: Double let width: CGFloat
var body: some View { var body: some View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
@ -53,7 +53,7 @@ struct StepView: View {
.frame(height: 5) .frame(height: 5)
Rectangle() Rectangle()
.foregroundColor(.green) .foregroundColor(.green)
.frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5) .frame(width: max(0, ((width - (Constants.padding * 2)) / CGFloat(numberOfSteps - 1)) * CGFloat(currentStep) - (Constants.circleWidth / 2)), height: 5)
HStack { HStack {
ForEach(0..<numberOfSteps) { index in ForEach(0..<numberOfSteps) { index in
ZStack { ZStack {
@ -92,8 +92,8 @@ extension StepView {
enum Constants { enum Constants {
static let padding: Double = 15 static let padding: CGFloat = 15
static let circleWidth: Double = 30 static let circleWidth: CGFloat = 30
} }
@ -156,7 +156,7 @@ struct SecretAgentSetupView: View {
} }
func install() { func install() {
LaunchAgentController().install() AgentLaunchController().install()
buttonAction() buttonAction()
} }

View File

@ -18,9 +18,7 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
HStack { HStack {
if !update.critical { if !update.critical {
Button("Ignore") { Button("Ignore") {
Task { [updater, update] in updater.ignore(release: update)
await updater.ignore(release: update)
}
} }
Spacer() Spacer()
} }