diff --git a/.github/readme/app.png b/.github/readme/app.png index ce8a000..1999502 100644 Binary files a/.github/readme/app.png and b/.github/readme/app.png differ diff --git a/.github/readme/apple_watch_auth_mac.png b/.github/readme/apple_watch_auth_mac.png new file mode 100644 index 0000000..6bd1e06 Binary files /dev/null and b/.github/readme/apple_watch_auth_mac.png differ diff --git a/.github/readme/apple_watch_auth_watch.png b/.github/readme/apple_watch_auth_watch.png new file mode 100644 index 0000000..06ad7d1 Binary files /dev/null and b/.github/readme/apple_watch_auth_watch.png differ diff --git a/.github/readme/apple_watch_system_prefs.png b/.github/readme/apple_watch_system_prefs.png new file mode 100644 index 0000000..eb77010 Binary files /dev/null and b/.github/readme/apple_watch_system_prefs.png differ diff --git a/.github/readme/notification.png b/.github/readme/notification.png index 40ec014..55c97f2 100644 Binary files a/.github/readme/notification.png and b/.github/readme/notification.png differ diff --git a/.github/readme/touchid.png b/.github/readme/touchid.png index b943978..e651a27 100644 Binary files a/.github/readme/touchid.png and b/.github/readme/touchid.png differ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4522a2e..471d75c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_11.4.app + run: sudo xcrun xcode-select -s /Applications/Xcode_12_beta.app - name: Test run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive build: @@ -44,6 +44,8 @@ jobs: HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }} AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }} run: ./.github/scripts/signing.sh + - name: Set Environment + run: sudo xcrun xcode-select -s /Applications/Xcode_12_beta.app - name: Update Build Number env: TAG_NAME: ${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1cd8b8..f078ca6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,6 @@ jobs: steps: - uses: actions/checkout@v1 - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_11.4.app + run: sudo xcrun xcode-select -s /Applications/Xcode_12_beta.app - name: Test run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive diff --git a/.gitignore b/.gitignore index 06da090..45d21c2 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ iOSInjectionProject/ # Build script products Archive.xcarchive +.DS_Store diff --git a/Brief/Updater.swift b/Brief/Updater.swift index f9f67c6..a4a524f 100644 --- a/Brief/Updater.swift +++ b/Brief/Updater.swift @@ -11,8 +11,11 @@ public class Updater: ObservableObject, UpdaterProtocol { @Published public var update: Release? - public init() { - checkForUpdates() + public init(checkOnLaunch: Bool) { + 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: 60*60*24, repeats: true) { _ in self.checkForUpdates() } @@ -41,6 +44,7 @@ extension Updater { func evaluate(release: Release) { guard !userIgnored(release: release) else { return } + guard !release.prerelease else { return } let latestVersion = semVer(from: release.name) let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String) for (latest, current) in zip(latestVersion, currentVersion) { @@ -82,22 +86,31 @@ extension Updater { public struct Release: Codable { public let name: String + public let prerelease: Bool public let html_url: URL public let body: String - public init(name: String, html_url: URL, body: String) { + public init(name: String, prerelease: Bool, html_url: URL, body: String) { self.name = name + self.prerelease = prerelease self.html_url = html_url self.body = body } } +extension Release: Identifiable { + + public var id: String { + html_url.absoluteString + } + +} extension Release { public var critical: Bool { - return body.contains(Constants.securityContent) + body.contains(Constants.securityContent) } } diff --git a/Config/Secretive.xctestplan b/Config/Secretive.xctestplan index 6e10639..25eec62 100644 --- a/Config/Secretive.xctestplan +++ b/Config/Secretive.xctestplan @@ -29,6 +29,7 @@ } }, { + "enabled" : false, "parallelizable" : true, "target" : { "containerPath" : "container:Secretive.xcodeproj", diff --git a/FAQ.md b/FAQ.md index d39df75..04bb227 100644 --- a/FAQ.md +++ b/FAQ.md @@ -20,12 +20,13 @@ Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [ 1) Make sure you have enabled "Use your Apple Watch to unlock apps and your Mac" in System Preferences --> Security & Privacy: -![System Preferences Setting](assets/apple_watch_system_prefs.png) +![System Preferences Setting](.github/readme/apple_watch_system_prefs.png) 2) Ensure that unlocking your Mac with Apple Watch is working (lock and unlock at least once) 3) Now you should get prompted on the watch when your key is accessed. Double click the side button to approve: -![Apple Watch Prompt](assets/apple_watch_auth.png) +![Apple Watch Prompt](.github/readme/apple_watch_auth_mac.png) +![Apple Watch Prompt](.github/readme/apple_watch_auth_watch.png) ### Why should I trust you? diff --git a/SecretAgent/AppDelegate.swift b/SecretAgent/AppDelegate.swift index b8dbd10..af21c2e 100644 --- a/SecretAgent/AppDelegate.swift +++ b/SecretAgent/AppDelegate.swift @@ -8,18 +8,18 @@ import Brief @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - let storeList: SecretStoreList = { + private let storeList: SecretStoreList = { let list = SecretStoreList() list.add(store: SecureEnclave.Store()) list.add(store: SmartCard.Store()) return list }() - let updater = Updater() - let notifier = Notifier() - lazy var agent: Agent = { + private let updater = Updater(checkOnLaunch: false) + private let notifier = Notifier() + private lazy var agent: Agent = { Agent(storeList: storeList, witness: notifier) }() - lazy var socketController: SocketController = { + private lazy var socketController: SocketController = { let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String return SocketController(path: path) }() diff --git a/SecretAgent/Notifier.swift b/SecretAgent/Notifier.swift index 94a526e..cf580f7 100644 --- a/SecretAgent/Notifier.swift +++ b/SecretAgent/Notifier.swift @@ -13,15 +13,14 @@ class Notifier { let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: "Update", options: []) let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: []) let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: []) - let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: []) + let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory]) UNUserNotificationCenter.current().delegate = notificationDelegate } func prompt() { let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.requestAuthorization(options: .alert) { _, _ in - } + notificationCenter.requestAuthorization(options: .alert) { _, _ in } } func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) { @@ -117,7 +116,7 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler(.alert) + completionHandler([.list, .banner]) } } diff --git a/SecretAgentKit/SocketController.swift b/SecretAgentKit/SocketController.swift index 7b6a14b..f477bb9 100644 --- a/SecretAgentKit/SocketController.swift +++ b/SecretAgentKit/SocketController.swift @@ -33,7 +33,7 @@ public class SocketController { addr.sun_family = sa_family_t(AF_UNIX) var len: Int = 0 - _ = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in + withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in path.withCString { cstring in len = strlen(cstring) strncpy(pointer, cstring, len) @@ -42,7 +42,7 @@ public class SocketController { addr.sun_len = UInt8(len+2) var data: Data! - _ = withUnsafePointer(to: &addr) { pointer in + withUnsafePointer(to: &addr) { pointer in data = Data(bytes: pointer, count: MemoryLayout.size) } diff --git a/Secretive.xcodeproj/project.pbxproj b/Secretive.xcodeproj/project.pbxproj index 12347fc..bc24f35 100644 --- a/Secretive.xcodeproj/project.pbxproj +++ b/Secretive.xcodeproj/project.pbxproj @@ -8,15 +8,16 @@ /* Begin PBXBuildFile section */ 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; + 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; + 50153E22250DECA300525160 /* SecretListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListView.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; - 50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* AppDelegate.swift */; }; + 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; }; 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; - 50617D8D23FCE48E0099B055 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8B23FCE48E0099B055 /* Main.storyboard */; }; 50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; }; 50617DB123FCE4AB0099B055 /* SecretKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50617DA823FCE4AB0099B055 /* SecretKit.framework */; }; 50617DBA23FCE4AB0099B055 /* SecretKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 50617DAA23FCE4AB0099B055 /* SecretKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -28,6 +29,9 @@ 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; }; 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; + 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; + 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; + 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -41,7 +45,7 @@ 506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; }; 506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; }; 506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 50731669241E00C20023809E /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731668241E00C20023809E /* NoticeView.swift */; }; + 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; 507CE4ED2420A3C70029F750 /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79F24026B9900D209EA /* Agent.swift */; }; 507CE4EE2420A3CA0029F750 /* SocketController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79D24026B9900D209EA /* SocketController.swift */; }; 507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4EF2420A4C50029F750 /* SigningWitness.swift */; }; @@ -57,6 +61,7 @@ 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */; }; 508A5911241EF09C0069DC07 /* SecretAgentKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5099A06C240242BA0062B6F2 /* SecretAgentKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 508A5913241EF0B20069DC07 /* SecretKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50617DA823FCE4AB0099B055 /* SecretKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */; }; 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; }; 5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02823FE35240062B6F2 /* SmartCardStore.swift */; }; @@ -77,7 +82,6 @@ 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A2240789E600AF2719 /* OpenSSHReader.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; - 50C385A9240B636500AF2719 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A8240B636500AF2719 /* SetupView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -206,16 +210,17 @@ /* Begin PBXFileReference section */ 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; + 50153E21250DECA300525160 /* SecretListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListView.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = ""; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = ""; }; 50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = ""; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 50617D8223FCE48E0099B055 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50617D8623FCE48E0099B055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 50617D8C23FCE48E0099B055 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = ""; }; 50617D9423FCE48E0099B055 /* SecretiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecretiveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -232,6 +237,9 @@ 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = ""; }; 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = ""; }; 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = ""; }; + 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; + 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = ""; }; + 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = ""; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = ""; }; 506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -243,7 +251,7 @@ 5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = ""; }; 506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = ""; }; 506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = ""; }; - 50731668241E00C20023809E /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = ""; }; + 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = ""; }; 507CE4EF2420A4C50029F750 /* SigningWitness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningWitness.swift; sourceTree = ""; }; 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestProvenance.swift; sourceTree = ""; }; 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestTracer.swift; sourceTree = ""; }; @@ -257,6 +265,7 @@ 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusChecker.swift; sourceTree = ""; }; 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAgentStatusChecker.swift; sourceTree = ""; }; 508A590F241EEF6D0069DC07 /* Secretive.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Secretive.xctestplan; sourceTree = ""; }; + 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDirectoryController.swift; sourceTree = ""; }; 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = ""; }; 5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = ""; }; 5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = ""; }; @@ -281,7 +290,6 @@ 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = ""; }; 50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; - 50C385A8240B636500AF2719 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -394,11 +402,10 @@ 50617D8123FCE48E0099B055 /* Secretive */ = { isa = PBXGroup; children = ( - 50617D8223FCE48E0099B055 /* AppDelegate.swift */, + 50617D8223FCE48E0099B055 /* App.swift */, 508A58B0241ED1C40069DC07 /* Views */, 508A58B1241ED1EA0069DC07 /* Controllers */, 50617D8623FCE48E0099B055 /* Assets.xcassets */, - 50617D8B23FCE48E0099B055 /* Main.storyboard */, 50617D8E23FCE48E0099B055 /* Info.plist */, 50617D8F23FCE48E0099B055 /* Secretive.entitlements */, 506772C62424784600034DED /* Credits.rtf */, @@ -501,13 +508,16 @@ isa = PBXGroup; children = ( 50617D8423FCE48E0099B055 /* ContentView.swift */, - 50731668241E00C20023809E /* NoticeView.swift */, + 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, + 50153E21250DECA300525160 /* SecretListView.swift */, 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, 50B8550C24138C4F009958AC /* DeleteSecretView.swift */, 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, 506772C82425BB8500034DED /* NoStoresView.swift */, - 50C385A8240B636500AF2719 /* SetupView.swift */, + 50153E1F250AFCB200525160 /* UpdateView.swift */, + 5066A6C12516F303004B5A36 /* SetupView.swift */, + 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, ); path = Views; sourceTree = ""; @@ -516,8 +526,10 @@ isa = PBXGroup; children = ( 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, + 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */, + 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */, ); path = Controllers; sourceTree = ""; @@ -854,7 +866,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 50617D8D23FCE48E0099B055 /* Main.storyboard in Resources */, 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */, 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */, 506772C72424784600034DED /* Credits.rtf in Resources */, @@ -920,19 +931,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 50C385A9240B636500AF2719 /* SetupView.swift in Sources */, + 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, + 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, + 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, + 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, + 50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, + 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, - 50731669241E00C20023809E /* NoticeView.swift in Sources */, - 50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */, + 50617D8323FCE48E0099B055 /* App.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */, + 50153E22250DECA300525160 /* SecretListView.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */, ); @@ -1070,14 +1086,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 50617D8B23FCE48E0099B055 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 50617D8C23FCE48E0099B055 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 50A3B79524026B7600D209EA /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -1140,7 +1148,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1195,7 +1203,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -1223,7 +1231,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1251,7 +1258,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1274,7 +1280,6 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1296,7 +1301,6 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1324,6 +1328,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1354,6 +1359,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1376,6 +1382,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1395,6 +1402,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1421,6 +1429,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Brief; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1450,6 +1459,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Brief; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1480,6 +1490,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Brief; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1540,7 +1551,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1566,7 +1577,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1588,7 +1598,6 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1611,7 +1620,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1638,6 +1646,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1661,6 +1670,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1687,6 +1697,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1710,6 +1721,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1737,6 +1749,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1767,6 +1780,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1789,6 +1803,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1808,6 +1823,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1830,7 +1846,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1855,7 +1870,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Secretive/App.swift b/Secretive/App.swift new file mode 100644 index 0000000..3ade857 --- /dev/null +++ b/Secretive/App.swift @@ -0,0 +1,64 @@ +import Cocoa +import SwiftUI +import SecretKit +import Brief + +@main +struct Secretive: App { + + private let storeList: SecretStoreList = { + let list = SecretStoreList() + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) + return list + }() + private let agentStatusChecker = AgentStatusChecker() + private let justUpdatedChecker = JustUpdatedChecker() + + @AppStorage("defaultsHasRunSetup") var hasRunSetup = false + @State private var showingSetup = false + @State private var showingCreation = false + + @SceneBuilder var body: some Scene { + WindowGroup { + ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) + .environmentObject(storeList) + .environmentObject(Updater(checkOnLaunch: hasRunSetup)) + .environmentObject(agentStatusChecker) + .onAppear { + if !hasRunSetup { + showingSetup = true + } else if agentStatusChecker.running && justUpdatedChecker.justUpdated { + // Relaunch the agent, since it'll be running from earlier update still + _ = LaunchAgentController().install() + } + } + } + .commands { + CommandGroup(after: CommandGroupPlacement.newItem) { + Button("New Secret") { + showingCreation = true + } + .keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift])) + } + CommandGroup(replacing: .help) { + Button("Help") { + NSWorkspace.shared.open(Constants.helpURL) + } + } + CommandGroup(after: .help) { + Button("Setup Secretive") { + showingSetup = true + } + } + SidebarCommands() + } + } + +} + + +private enum Constants { + static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")! +} + diff --git a/Secretive/AppDelegate.swift b/Secretive/AppDelegate.swift deleted file mode 100644 index c9e116d..0000000 --- a/Secretive/AppDelegate.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Cocoa -import SwiftUI -import SecretKit -import Brief - -@NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate { - - var window: NSWindow! - @IBOutlet var newMenuItem: NSMenuItem! - @IBOutlet var toolbar: NSToolbar! - let storeList: SecretStoreList = { - let list = SecretStoreList() - list.add(store: SecureEnclave.Store()) - list.add(store: SmartCard.Store()) - return list - }() - let updater = Updater() - let agentStatusChecker = AgentStatusChecker() - let justUpdatedChecker = JustUpdatedChecker() - - func applicationDidFinishLaunching(_ aNotification: Notification) { - let contentView = ContentView(storeList: storeList, updater: updater, agentStatusChecker: agentStatusChecker, runSetupBlock: { self.runSetup(sender: nil) }) - // Create the window and set the content view. - window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, defer: false) - window.center() - window.setFrameAutosaveName("Main Window") - window.contentView = NSHostingView(rootView: contentView) - window.makeKeyAndOrderFront(nil) - window.titleVisibility = .hidden - window.toolbar = toolbar - window.isReleasedWhenClosed = false - if storeList.modifiableStore?.isAvailable ?? false { - let plus = NSTitlebarAccessoryViewController() - plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:))) - plus.layoutAttribute = .right - window.addTitlebarAccessoryViewController(plus) - newMenuItem.isEnabled = true - } - runSetupIfNeeded() - relaunchAgentIfNeeded() - } - - func applicationDidBecomeActive(_ notification: Notification) { - agentStatusChecker.check() - } - - func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - guard !flag else { return false } - window.makeKeyAndOrderFront(self) - return true - } - - @IBAction func add(sender: AnyObject?) { - var addWindow: NSWindow! - let addView = CreateSecretView(store: storeList.modifiableStore!) { - self.window.endSheet(addWindow) - } - addWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, defer: false) - addWindow.contentView = NSHostingView(rootView: addView) - window.beginSheet(addWindow, completionHandler: nil) - } - - @IBAction func runSetup(sender: AnyObject?) { - let setupWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 0, height: 0), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, defer: false) - let setupView = SetupView() { success in - self.window.endSheet(setupWindow) - self.agentStatusChecker.check() - } - setupWindow.contentView = NSHostingView(rootView: setupView) - window.beginSheet(setupWindow, completionHandler: nil) - } - -} - -extension AppDelegate { - - func runSetupIfNeeded() { - if !UserDefaults.standard.bool(forKey: Constants.defaultsHasRunSetup) { - UserDefaults.standard.set(true, forKey: Constants.defaultsHasRunSetup) - runSetup(sender: nil) - } - } - - func relaunchAgentIfNeeded() { - if agentStatusChecker.running && justUpdatedChecker.justUpdated { - LaunchAgentController().relaunch() - } - } - -} - -extension AppDelegate { - - enum Constants { - static let defaultsHasRunSetup = "defaultsHasRunSetup" - } - -} diff --git a/Secretive/Base.lproj/Main.storyboard b/Secretive/Base.lproj/Main.storyboard deleted file mode 100644 index b7df350..0000000 --- a/Secretive/Base.lproj/Main.storyboard +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Secretive/Controllers/ApplicationDirectoryController.swift b/Secretive/Controllers/ApplicationDirectoryController.swift new file mode 100644 index 0000000..1b41227 --- /dev/null +++ b/Secretive/Controllers/ApplicationDirectoryController.swift @@ -0,0 +1,21 @@ +import Foundation + +struct ApplicationDirectoryController { +} + +extension ApplicationDirectoryController { + + var isInApplicationsDirectory: Bool { + let bundlePath = Bundle.main.bundlePath + for directory in NSSearchPathForDirectoriesInDomains(.applicationDirectory, .allDomainsMask, true) { + if bundlePath.hasPrefix(directory) { + return true + } + } + if bundlePath.contains("/Library/Developer/Xcode") { + return true + } + return false + } + +} diff --git a/Secretive/Controllers/LaunchAgentController.swift b/Secretive/Controllers/LaunchAgentController.swift index 8077da5..10b9150 100644 --- a/Secretive/Controllers/LaunchAgentController.swift +++ b/Secretive/Controllers/LaunchAgentController.swift @@ -4,12 +4,8 @@ import ServiceManagement struct LaunchAgentController { func install() -> Bool { - setEnabled(true) - } - - func relaunch() { _ = setEnabled(false) - _ = setEnabled(true) + return setEnabled(true) } private func setEnabled(_ enabled: Bool) -> Bool { diff --git a/Secretive/Controllers/ShellConfigurationController.swift b/Secretive/Controllers/ShellConfigurationController.swift new file mode 100644 index 0000000..4162a60 --- /dev/null +++ b/Secretive/Controllers/ShellConfigurationController.swift @@ -0,0 +1,58 @@ +import Foundation +import Cocoa + +struct ShellConfigurationController { + + let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String + + var shellInstructions: [ShellConfigInstruction] { + [ + ShellConfigInstruction(shell: "zsh", + shellConfigDirectory: "~/", + shellConfigFilename: ".zshrc", + text: "export SSH_AUTH_SOCK=\(socketPath)"), + ShellConfigInstruction(shell: "bash", + shellConfigDirectory: "~/", + shellConfigFilename: ".bashrc", + text: "export SSH_AUTH_SOCK=\(socketPath)"), + ShellConfigInstruction(shell: "fish", + shellConfigDirectory: "~/.config/fish", + shellConfigFilename: "config.fish", + text: "set -x SSH_AUTH_SOCK=\(socketPath)"), + ] + + } + + + func addToShell(shellInstructions: ShellConfigInstruction) -> Bool { + let openPanel = NSOpenPanel() + // This is sync, so no need to strongly retain + let delegate = Delegate(name: shellInstructions.shellConfigFilename) + openPanel.delegate = delegate + openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically." + openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)" + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.showsHiddenFiles = true + openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory) + openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename + openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText] + openPanel.runModal() + guard let fileURL = openPanel.urls.first else { return false } + let handle: FileHandle + do { + handle = try FileHandle(forUpdating: fileURL) + guard let existing = try handle.readToEnd(), + let existingString = String(data: existing, encoding: .utf8) else { return false } + guard !existingString.contains(shellInstructions.text) else { + return true + } + try handle.seekToEnd() + } catch { + return false + } + handle.write("\n# Secretive Config\n\(shellInstructions.text)\n".data(using: .utf8)!) + return true + } + +} diff --git a/Secretive/Info.plist b/Secretive/Info.plist index 5ea2305..beb76e8 100644 --- a/Secretive/Info.plist +++ b/Secretive/Info.plist @@ -24,8 +24,6 @@ $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_NAME) is MIT Licensed. - NSMainStoryboardFile - Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination diff --git a/Secretive/Preview Content/PreviewUpdater.swift b/Secretive/Preview Content/PreviewUpdater.swift index 2adb988..5ca16f0 100644 --- a/Secretive/Preview Content/PreviewUpdater.swift +++ b/Secretive/Preview Content/PreviewUpdater.swift @@ -11,9 +11,9 @@ class PreviewUpdater: UpdaterProtocol { case .none: self.update = nil case .advisory: - self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Some regular update") + self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update") case .critical: - self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update") + self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") } } diff --git a/Secretive/Secretive.entitlements b/Secretive/Secretive.entitlements index 8776520..c1bb5e0 100644 --- a/Secretive/Secretive.entitlements +++ b/Secretive/Secretive.entitlements @@ -4,10 +4,12 @@ com.apple.security.app-sandbox - com.apple.security.smartcard + com.apple.security.files.user-selected.read-write com.apple.security.network.client + com.apple.security.smartcard + keychain-access-groups $(AppIdentifierPrefix)com.maxgoedjen.Secretive diff --git a/Secretive/Views/ContentView.swift b/Secretive/Views/ContentView.swift index aa23966..eec1628 100644 --- a/Secretive/Views/ContentView.swift +++ b/Secretive/Views/ContentView.swift @@ -3,146 +3,189 @@ import SecretKit import Brief struct ContentView: View { - - @ObservedObject var storeList: SecretStoreList - @ObservedObject var updater: UpdaterType - @ObservedObject var agentStatusChecker: AgentStatusCheckerType - var runSetupBlock: (() -> Void)? - @State private var active: AnySecret.ID? - @State private var showingDeletion = false - @State private var deletingSecret: AnySecret? - + @Binding var showingCreation: Bool + @Binding var runningSetup: Bool + @Binding var hasRunSetup: Bool + + @EnvironmentObject private var storeList: SecretStoreList + @EnvironmentObject private var updater: UpdaterType + @EnvironmentObject private var agentStatusChecker: AgentStatusCheckerType + + @State private var selectedUpdate: Release? + @State private var showingAppPathNotice = false + var body: some View { VStack { - if updater.update != nil { - updateNotice() - } - if !agentStatusChecker.running { - agentNotice() - } if storeList.anyAvailable { - NavigationView { - List(selection: $active) { - ForEach(storeList.stores) { store in - if store.isAvailable { - Section(header: Text(store.name)) { - if store.secrets.isEmpty { - if store is AnySecretStoreModifiable { - NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) { - Text("No Secrets") - } - } else { - NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) { - Text("No Secrets") - } - } - } else { - ForEach(store.secrets) { secret in - NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { - Text(secret.name) - }.contextMenu { - if store is AnySecretStoreModifiable { - Button(action: { self.delete(secret: secret) }) { - Text("Delete") - } - } - } - } - } - } - } - } - }.onAppear { - self.active = self.nextDefaultSecret - } - .listStyle(SidebarListStyle()) - .frame(minWidth: 100, idealWidth: 240) - } - .navigationViewStyle(DoubleColumnNavigationViewStyle()) - .sheet(isPresented: $showingDeletion) { - if self.storeList.modifiableStore != nil { - DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) { deleted in - self.showingDeletion = false - if deleted { - self.active = self.nextDefaultSecret - } - } - } - } + StoreListView(showingCreation: $showingCreation) } else { NoStoresView() } - }.frame(minWidth: 640, minHeight: 320) + } + .sheet(isPresented: $showingCreation) { + if let modifiable = storeList.modifiableStore { + CreateSecretView(store: modifiable, showing: $showingCreation) + } + } + .frame(minWidth: 640, minHeight: 320) + .toolbar { + updateNotice + setupNotice + appPathNotice + newItem + } } - func updateNotice() -> some View { - guard let update = updater.update else { return AnyView(Spacer()) } - let severity: NoticeView.Severity +} + +extension ContentView { + + var updateNotice: ToolbarItem { + guard let update = updater.update else { + return ToolbarItem { AnyView(EmptyView()) } + } + let color: Color let text: String if update.critical { - severity = .critical text = "Critical Security Update Required" + color = .red } else { - severity = .advisory text = "Update Available" + color = .orange } - return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update") { - NSWorkspace.shared.open(update.html_url) - }) - } - - func agentNotice() -> some View { - NoticeView(text: "Secret Agent isn't running. Run setup again to fix.", severity: .advisory, actionTitle: "Run Setup") { - self.runSetupBlock?() + return ToolbarItem { + AnyView( + Button(action: { + selectedUpdate = update + }, label: { + Text(text) + .font(.headline) + .foregroundColor(.white) + }) + .background(color) + .cornerRadius(5) + .popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in + UpdateDetailView(update: update) + } + ) } } - func delete(secret: SecretType) { - deletingSecret = AnySecret(secret) - self.showingDeletion = true + var newItem: ToolbarItem { + guard storeList.modifiableStore?.isAvailable ?? false else { + return ToolbarItem { AnyView(EmptyView()) } + } + return ToolbarItem { + AnyView( + Button(action: { + showingCreation = true + }, label: { + Image(systemName: "plus") + }) + ) + } } - var nextDefaultSecret: AnyHashable? { - let fallback: AnyHashable - if self.storeList.modifiableStore?.isAvailable ?? false { - fallback = Constants.emptyStoreModifiableTag - } else { - fallback = Constants.emptyStoreTag + var setupNotice: ToolbarItem { + return ToolbarItem { + AnyView( + Group { + if runningSetup || !hasRunSetup || !agentStatusChecker.running { + Button(action: { + runningSetup = true + }, label: { + Group { + if hasRunSetup && !agentStatusChecker.running { + Text("Secret Agent Is Not Running") + } else { + Text("Setup Secretive") + } + } + .font(.headline) + .foregroundColor(.white) + }) + .background(Color.orange) + .cornerRadius(5) + } else { + EmptyView() + } + } + .sheet(isPresented: $runningSetup) { + SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) + } + ) } - return self.storeList.stores.compactMap(\.secrets.first).first?.id ?? fallback } - + + var appPathNotice: ToolbarItem { + let controller = ApplicationDirectoryController() + guard !controller.isInApplicationsDirectory else { + return ToolbarItem { AnyView(EmptyView()) } + } + return ToolbarItem { + AnyView( + Button(action: { + showingAppPathNotice = true + }, label: { + Group { + Text("Secretive Is Not in Applications Folder") + } + .font(.headline) + .foregroundColor(.white) + }) + .background(Color.orange) + .cornerRadius(5) + .popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + VStack { + Image(systemName: "exclamationmark.triangle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 64) + Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.") + .frame(maxWidth: 300) + } + .padding() + } + ) + } + } + } -private enum Constants { - static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" - static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag" -} - - #if DEBUG struct ContentView_Previews: PreviewProvider { + + private static let storeList: SecretStoreList = { + let list = SecretStoreList() + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) + return list + }() + private static let agentStatusChecker = AgentStatusChecker() + private static let justUpdatedChecker = JustUpdatedChecker() + + @State var hasRunSetup = false + @State private var showingSetup = false + @State private var showingCreation = false + static var previews: some View { Group { - ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], - modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), - updater: PreviewUpdater(), - agentStatusChecker: PreviewAgentStatusChecker()) - ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(), - agentStatusChecker: PreviewAgentStatusChecker()) - ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater(), - agentStatusChecker: PreviewAgentStatusChecker()) - ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(), - agentStatusChecker: PreviewAgentStatusChecker()) - ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory), - agentStatusChecker: PreviewAgentStatusChecker()) - ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical), - agentStatusChecker: PreviewAgentStatusChecker()) - ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical), - agentStatusChecker: PreviewAgentStatusChecker(running: false)) + // Empty on modifiable and nonmodifiable + ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) + .environmentObject(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) + .environmentObject(PreviewUpdater()) + .environmentObject(agentStatusChecker) + + // 5 items on modifiable and nonmodifiable + ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) + .environmentObject(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) + .environmentObject(PreviewUpdater()) + .environmentObject(agentStatusChecker) } + .environmentObject(agentStatusChecker) + } } diff --git a/Secretive/Views/CopyableView.swift b/Secretive/Views/CopyableView.swift new file mode 100644 index 0000000..5ddd8fc --- /dev/null +++ b/Secretive/Views/CopyableView.swift @@ -0,0 +1,135 @@ +import SwiftUI + +struct CopyableView: View { + + var title: String + var image: Image + var text: String + + @State private var interactionState: InteractionState = .normal + + var body: some View { + VStack(alignment: .leading) { + HStack { + image + .renderingMode(.template) + .imageScale(.large) + .foregroundColor(primaryTextColor) + Text(title) + .font(.headline) + .foregroundColor(primaryTextColor) + Spacer() + if interactionState != .normal { + Text(hoverText) + .bold() + .textCase(.uppercase) + .foregroundColor(secondaryTextColor) + .transition(.opacity) + } + + } + .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + Divider() + Text(text) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(primaryTextColor) + .padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20)) + .multilineTextAlignment(.leading) + .font(.system(.body, design: .monospaced)) + } + .background(backgroundColor) + .frame(minWidth: 150, maxWidth: .infinity) + .cornerRadius(10) + .onHover { hovering in + withAnimation { + interactionState = hovering ? .hovering : .normal + } + } + .onDrag { + NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String) + } + .onTapGesture { + copy() + withAnimation { + interactionState = .clicking + } + } + .gesture( + TapGesture() + .onEnded { + withAnimation { + interactionState = .normal + } + } + ) + } + + var hoverText: String { + switch interactionState { + case .hovering: + return "Click to Copy" + case .clicking: + return "Copied" + case .normal: + fatalError() + } + } + + var backgroundColor: Color { + let color: NSColor + switch interactionState { + case .normal: + color = .windowBackgroundColor + case .hovering: + color = .unemphasizedSelectedContentBackgroundColor + case .clicking: + color = .selectedContentBackgroundColor + } + return Color(color) + } + + var primaryTextColor: Color { + let color: NSColor + switch interactionState { + case .normal, .hovering: + color = .textColor + case .clicking: + color = .white + } + return Color(color) + } + + var secondaryTextColor: Color { + let color: NSColor + switch interactionState { + case .normal, .hovering: + color = .secondaryLabelColor + case .clicking: + color = .white + } + return Color(color) + } + + func copy() { + NSPasteboard.general.declareTypes([.string], owner: nil) + NSPasteboard.general.setString(text, forType: .string) + } + + private enum InteractionState { + case normal, hovering, clicking + } + +} + +#if DEBUG + +struct CopyableView_Previews: PreviewProvider { + static var previews: some View { + Group { + CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Hello world.") + CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") + } + } +} + +#endif diff --git a/Secretive/Views/CreateSecretView.swift b/Secretive/Views/CreateSecretView.swift index f8d0160..68d512f 100644 --- a/Secretive/Views/CreateSecretView.swift +++ b/Secretive/Views/CreateSecretView.swift @@ -1,15 +1,14 @@ import SwiftUI import SecretKit -struct CreateSecretView: View { +struct CreateSecretView: View { - @ObservedObject var store: AnySecretStoreModifiable - - @State var name = "" - @State var requiresAuthentication = true - - var dismissalBlock: () -> () + @ObservedObject var store: StoreType + @Binding var showing: Bool + @State private var name = "" + @State private var requiresAuthentication = true + var body: some View { VStack { HStack { @@ -33,22 +32,22 @@ struct CreateSecretView: View { Spacer() } } - .onExitCommand(perform: dismissalBlock) } HStack { Spacer() - Button(action: dismissalBlock) { - Text("Cancel") + Button("Cancel") { + showing = false } - Button(action: save) { - Text("Create") - }.disabled(name.isEmpty) + .keyboardShortcut(.cancelAction) + Button("Create", action: save) + .disabled(name.isEmpty) + .keyboardShortcut(.defaultAction) } }.padding() } func save() { try! store.create(name: name, requiresAuthentication: requiresAuthentication) - dismissalBlock() + showing = false } } diff --git a/Secretive/Views/DeleteSecretView.swift b/Secretive/Views/DeleteSecretView.swift index d0618ed..7810c3e 100644 --- a/Secretive/Views/DeleteSecretView.swift +++ b/Secretive/Views/DeleteSecretView.swift @@ -2,20 +2,13 @@ import SwiftUI import SecretKit struct DeleteSecretView: View { - - let secret: StoreType.SecretType + @ObservedObject var store: StoreType - - @State var confirm = "" - - private var dismissalBlock: (Bool) -> () - - init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping (Bool) -> ()) { - self.secret = secret - self.store = store - self.dismissalBlock = dismissalBlock - } - + let secret: StoreType.SecretType + var dismissalBlock: (Bool) -> () + + @State private var confirm = "" + var body: some View { VStack { HStack { @@ -38,24 +31,27 @@ struct DeleteSecretView: View { } } .onExitCommand { - self.dismissalBlock(false) + dismissalBlock(false) } } HStack { Spacer() - Button(action: delete) { - Text("Delete") - }.disabled(confirm != secret.name) - Button(action: { self.dismissalBlock(false) }) { - Text("Don't Delete") + Button("Delete", action: delete) + .disabled(confirm != secret.name) + .keyboardShortcut(.delete) + Button("Don't Delete") { + dismissalBlock(false) } + .keyboardShortcut(.cancelAction) } - }.padding() + } + .padding() .frame(minWidth: 400) } func delete() { try! store.delete(secret: secret) - self.dismissalBlock(true) + dismissalBlock(true) } + } diff --git a/Secretive/Views/EmptyStoreView.swift b/Secretive/Views/EmptyStoreView.swift index 0f8a6f4..db85890 100644 --- a/Secretive/Views/EmptyStoreView.swift +++ b/Secretive/Views/EmptyStoreView.swift @@ -1,6 +1,34 @@ import SwiftUI +import SecretKit struct EmptyStoreView: View { + + @ObservedObject var store: AnySecretStore + @Binding var activeSecret: AnySecret.ID? + + var body: some View { + if store is AnySecretStoreModifiable { + NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: $activeSecret) { + Text("No Secrets") + } + } else { + NavigationLink(destination: EmptyStoreImmutableView(), tag: Constants.emptyStoreTag, selection: $activeSecret) { + Text("No Secrets") + } + } + } +} + +extension EmptyStoreView { + + enum Constants { + static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" + static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag" + } + +} + +struct EmptyStoreImmutableView: View { var body: some View { VStack { @@ -48,7 +76,7 @@ struct EmptyStoreModifiableView: View { struct EmptyStoreModifiableView_Previews: PreviewProvider { static var previews: some View { Group { - EmptyStoreView() + EmptyStoreImmutableView() EmptyStoreModifiableView() } } diff --git a/Secretive/Views/NoStoresView.swift b/Secretive/Views/NoStoresView.swift index 95aaa47..496656f 100644 --- a/Secretive/Views/NoStoresView.swift +++ b/Secretive/Views/NoStoresView.swift @@ -1,29 +1,23 @@ -// -// NoStoresView.swift -// Secretive -// -// Created by Max Goedjen on 3/20/20. -// Copyright © 2020 Max Goedjen. All rights reserved. -// - import SwiftUI struct NoStoresView: View { + var body: some View { VStack { Text("No Secure Storage Available").bold() Text("Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted.") - Button(action: { - NSWorkspace.shared.open(URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!) - }) { - Text("If you're looking to add one to your Mac, the YubiKey 5 Series are great.") - } + Link("If you're looking to add one to your Mac, the YubiKey 5 Series are great.", destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!) }.padding() } + } +#if DEBUG + struct NoStoresView_Previews: PreviewProvider { static var previews: some View { NoStoresView() } } + +#endif diff --git a/Secretive/Views/NoticeView.swift b/Secretive/Views/NoticeView.swift deleted file mode 100644 index dceba20..0000000 --- a/Secretive/Views/NoticeView.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Foundation -import SwiftUI - -struct NoticeView: View { - - let text: String - let severity: Severity - let actionTitle: String? - let action: (() -> Void)? - - var body: some View { - HStack { - Text(text).bold() - Spacer() - if action != nil { - Button(action: action!) { - Text(actionTitle!) - } - } - }.padding().background(color) - } - - var color: Color { - switch severity { - case .advisory: - return Color.orange - case .critical: - return Color.red - } - } - -} - -extension NoticeView { - - enum Severity { - case advisory, critical - } - -} - -#if DEBUG - -struct NoticeView_Previews: PreviewProvider { - static var previews: some View { - Group { - NoticeView(text: "Agent Not Running", severity: .advisory, actionTitle: "Run Setup") { - print("OK") - } - NoticeView(text: "Critical Security Update Required", severity: .critical, actionTitle: "Update") { - print("OK") - } - } - } -} - -#endif diff --git a/Secretive/Views/SecretDetailView.swift b/Secretive/Views/SecretDetailView.swift index 01d42ed..e262ea7 100644 --- a/Secretive/Views/SecretDetailView.swift +++ b/Secretive/Views/SecretDetailView.swift @@ -2,58 +2,34 @@ import SwiftUI import SecretKit struct SecretDetailView: View { - + @State var secret: SecretType - let keyWriter = OpenSSHKeyWriter() + private let keyWriter = OpenSSHKeyWriter() + var body: some View { Form { Section { - GroupBox(label: Text("Fingerprint")) { - HStack { - Text(keyWriter.openSSHFingerprint(secret: secret)) - Spacer() - } - .frame(minWidth: 150, maxWidth: .infinity) - .padding() - }.onDrag { - return NSItemProvider(item: NSData(data: self.keyWriter.openSSHFingerprint(secret: self.secret).data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String) - } - Spacer().frame(height: 10) - GroupBox(label: Text("Public Key")) { - VStack { - Text(keyWriter.openSSHString(secret: secret)) - .multilineTextAlignment(.leading) - .frame(minWidth: 150, maxWidth: .infinity) - HStack { - Spacer() - Button(action: copy) { - Text("Copy") - } - } - } - .padding() - } - .onDrag { - return NSItemProvider(item: NSData(data: self.keyString.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String) - } + CopyableView(title: "Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHFingerprint(secret: secret)) + Spacer() + .frame(height: 20) + CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyWriter.openSSHString(secret: secret)) Spacer() } - }.padding() - .frame(minHeight: 150, maxHeight: .infinity) - + } + .padding() + .frame(minHeight: 200, maxHeight: .infinity) } - + var keyString: String { keyWriter.openSSHString(secret: secret) } - + func copy() { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(keyString, forType: .string) } - - + } #if DEBUG diff --git a/Secretive/Views/SecretListView.swift b/Secretive/Views/SecretListView.swift new file mode 100644 index 0000000..34268a4 --- /dev/null +++ b/Secretive/Views/SecretListView.swift @@ -0,0 +1,40 @@ +import SwiftUI +import SecretKit + +struct SecretListView: View { + + @ObservedObject var store: AnySecretStore + @Binding var activeSecret: AnySecret.ID? + @Binding var deletingSecret: AnySecret? + + var deletedSecret: (AnySecret) -> Void + + var body: some View { + ForEach(store.secrets) { secret in + NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) { + Text(secret.name) + }.contextMenu { + if store is AnySecretStoreModifiable { + Button(action: { delete(secret: secret) }) { + Text("Delete") + } + } + } + .sheet(item: $deletingSecret) { secret in + if let modifiable = store as? AnySecretStoreModifiable { + DeleteSecretView(store: modifiable, secret: secret) { deleted in + deletingSecret = nil + if deleted { + deletedSecret(AnySecret(secret)) + } + } + } + } + } + } + + func delete(secret: SecretType) { + deletingSecret = AnySecret(secret) + } + +} diff --git a/Secretive/Views/SetupView.swift b/Secretive/Views/SetupView.swift index b475d77..8c9433d 100644 --- a/Secretive/Views/SetupView.swift +++ b/Secretive/Views/SetupView.swift @@ -1,145 +1,295 @@ -import Foundation import SwiftUI struct SetupView: View { - - var completion: ((Bool) -> Void)? - + + @State var stepIndex = 0 + @Binding var visible: Bool + @Binding var setupComplete: Bool + var body: some View { - Form { - SetupStepView(text: "Secretive needs to install a helper app to sign requests when the main app isn't running. This app is called \"SecretAgent\" and you might see it in Activity Manager from time to time.", - index: 1, - nestedView: nil, - actionText: "Install") { - self.installLaunchAgent() - } - SetupStepView(text: "Add this line to your shell config (.bashrc or .zshrc) telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.", - index: 2, - nestedView: SetupStepCommandView(text: Constants.socketPrompt), - actionText: "Added") { - self.markAsDone() - } - HStack { - Spacer() - Button(action: { self.completion?(true) }) { - Text("Finish") + GeometryReader { proxy in + VStack { + StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width) + GeometryReader { _ in + HStack(spacing: 0) { + SecretAgentSetupView(buttonAction: advance) + .frame(width: proxy.size.width) + SSHAgentSetupView(buttonAction: advance) + .frame(width: proxy.size.width) + UpdaterExplainerView { + visible = false + setupComplete = true + } + .frame(width: proxy.size.width) + } + .offset(x: -proxy.size.width * CGFloat(stepIndex), y: 0) } - .padding() } - }.frame(minWidth: 640, minHeight: 400) + } + .frame(idealWidth: 500, idealHeight: 500) } - + + + func advance() { + withAnimation(.spring()) { + stepIndex += 1 + } + } + } -struct SetupStepView: View { - - let text: String - let index: Int - let nestedView: NestedViewType? - @State var completed = false - let actionText: String - let action: (() -> Bool) - +struct StepView: View { + + let numberOfSteps: Int + let currentStep: Int + + // Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7 + let width: CGFloat + var body: some View { - Section { + ZStack(alignment: .leading) { + Rectangle() + .foregroundColor(.blue) + .frame(height: 5) + Rectangle() + .foregroundColor(.green) + .frame(width: max(0, ((width - (Constants.padding * 2)) / CGFloat(numberOfSteps - 1)) * CGFloat(currentStep) - (Constants.circleWidth / 2)), height: 5) + .animation(.spring()) HStack { - ZStack { - if completed { - Circle().foregroundColor(.green) - .frame(width: 30, height: 30) - Text("✓") - .foregroundColor(.white) - .bold() - } else { - Circle().foregroundColor(.blue) - .frame(width: 30, height: 30) - Text(String(describing: index)) - .foregroundColor(.white) - .bold() + ForEach(0.. index { + Circle() + .foregroundColor(.green) + .frame(width: Constants.circleWidth, height: Constants.circleWidth) + Text("✓") + .foregroundColor(.white) + .bold() + } else { + Circle() + .foregroundColor(.blue) + .frame(width: Constants.circleWidth, height: Constants.circleWidth) + if currentStep == index { + Circle() + .strokeBorder(Color.white, lineWidth: 3) + .frame(width: Constants.circleWidth, height: Constants.circleWidth) + } + Text(String(describing: index + 1)) + .foregroundColor(.white) + .bold() + } + } + if index < numberOfSteps - 1 { + Spacer(minLength: 30) } } - .padding() - VStack { - Text(text) - .opacity(completed ? 0.5 : 1) - .lineLimit(nil) - if nestedView != nil { - nestedView!.padding() - } - } - .padding() - Button(action: { - self.completed = self.action() - }) { - Text(actionText) - }.disabled(completed) - .padding() } - } + }.padding(Constants.padding) } + } -struct SetupStepCommandView: View { - - let text: String - - var body: some View { - VStack(alignment: .leading) { - Text(text) - .lineLimit(nil) - .font(.system(.caption, design: .monospaced)) - .multilineTextAlignment(.leading) - .frame(minHeight: 50) - HStack { - Spacer() - Button(action: copy) { - Text("Copy") - } - } - } - .padding() - .background(Color(white: 0, opacity: 0.10)) - .cornerRadius(10) - .onDrag { - return NSItemProvider(item: NSData(data: self.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String) - - } - } - - func copy() { - NSPasteboard.general.declareTypes([.string], owner: nil) - NSPasteboard.general.setString(text, forType: .string) - } - -} +extension StepView { -extension SetupView { - - func installLaunchAgent() -> Bool { - LaunchAgentController().install() - } - - func markAsDone() -> Bool { - return true - } - -} - -extension SetupView { - enum Constants { - static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String - static let socketPrompt = "export SSH_AUTH_SOCK=\(socketPath)" + + static let padding: CGFloat = 15 + static let circleWidth: CGFloat = 30 + } - + +} + +struct SetupStepView : View where Content : View { + + let title: String + let image: Image + let bodyText: String + let buttonTitle: String + let buttonAction: () -> Void + let content: Content + + init(title: String, image: Image, bodyText: String, buttonTitle: String, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) { + self.title = title + self.image = image + self.bodyText = bodyText + self.buttonTitle = buttonTitle + self.buttonAction = buttonAction + self.content = content() + } + + var body: some View { + VStack { + Text(title) + .font(.title) + Spacer() + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 64) + Spacer() + Text(bodyText) + .multilineTextAlignment(.center) + Spacer() + content + Spacer() + Button(buttonTitle) { + buttonAction() + } + }.padding() + } + +} + +struct SecretAgentSetupView: View { + + let buttonAction: () -> Void + + var body: some View { + SetupStepView(title: "Setup Secret Agent", + image: Image(nsImage: NSApp.applicationIconImage), + bodyText: "Secretive needs to set up a helper app to work properly. It will sign requests from SSH clients in the background, so you don't need to keep the main Secretive app open.", + buttonTitle: "Install", + buttonAction: install) { + (Text("This helper app is called ") + Text("Secret Agent").bold().underline() + Text(" and you may see it in Activity Manager from time to time.")) + .multilineTextAlignment(.center) + } + } + + func install() { + _ = LaunchAgentController().install() + buttonAction() + } + +} + +struct SSHAgentSetupView: View { + + let buttonAction: () -> Void + + private static let controller = ShellConfigurationController() + @State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first! + + var body: some View { + SetupStepView(title: "Configure your SSH Agent", + image: Image(systemName: "terminal"), + bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file.", + buttonTitle: "I Added it Manually", + buttonAction: buttonAction) { + Picker(selection: $selectedShellInstruction, label: EmptyView()) { + ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in + Text(instruction.shell) + .tag(instruction) + .padding() + } + }.pickerStyle(SegmentedPickerStyle()) + CopyableView(title: "Add to \(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) + Button("Add it For Me") { + let controller = ShellConfigurationController() + if controller.addToShell(shellInstructions: selectedShellInstruction) { + buttonAction() + } + } + } + } + +} + +class Delegate: NSObject, NSOpenSavePanelDelegate { + + private let name: String + + init(name: String) { + self.name = name + } + + func panel(_ sender: Any, shouldEnable url: URL) -> Bool { + return url.lastPathComponent == name + } + +} + +struct UpdaterExplainerView: View { + + let buttonAction: () -> Void + + var body: some View { + SetupStepView(title: "Updates", + image: Image(systemName: "dot.radiowaves.left.and.right"), + bodyText: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.", + buttonTitle: "Okay", + buttonAction: buttonAction) { + Link("Read more about this here.", destination: SetupView.Constants.updaterFAQURL) + } + } + +} + +extension SetupView { + + enum Constants { + static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! + } + +} + +struct ShellConfigInstruction: Identifiable, Hashable { + + var shell: String + var shellConfigDirectory: String + var shellConfigFilename: String + var text: String + + var id: String { + shell + } + + var shellConfigPath: String { + return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename) + } + } #if DEBUG struct SetupView_Previews: PreviewProvider { + static var previews: some View { - SetupView() + Group { + SetupView(visible: .constant(true), setupComplete: .constant(false)) + } } + +} + +struct SecretAgentSetupView_Previews: PreviewProvider { + + static var previews: some View { + Group { + SecretAgentSetupView(buttonAction: {}) + } + } + +} + +struct SSHAgentSetupView_Previews: PreviewProvider { + + static var previews: some View { + Group { + SSHAgentSetupView(buttonAction: {}) + } + } + +} + +struct UpdaterExplainerView_Previews: PreviewProvider { + + static var previews: some View { + Group { + UpdaterExplainerView(buttonAction: {}) + } + } + } #endif diff --git a/Secretive/Views/StoreListView.swift b/Secretive/Views/StoreListView.swift new file mode 100644 index 0000000..7730930 --- /dev/null +++ b/Secretive/Views/StoreListView.swift @@ -0,0 +1,53 @@ +import SwiftUI +import SecretKit + +struct StoreListView: View { + + @Binding var showingCreation: Bool + + @State private var activeSecret: AnySecret.ID? + @State private var deletingSecret: AnySecret? + + @EnvironmentObject private var storeList: SecretStoreList + + var body: some View { + NavigationView { + List(selection: $activeSecret) { + ForEach(storeList.stores) { store in + if store.isAvailable { + Section(header: Text(store.name)) { + if store.secrets.isEmpty { + EmptyStoreView(store: store, activeSecret: $activeSecret) + } else { + SecretListView(store: store, activeSecret: $activeSecret, deletingSecret: $deletingSecret, deletedSecret: { _ in + activeSecret = nextDefaultSecret + }) + } + } + } + } + } + .listStyle(SidebarListStyle()) + .onAppear { + activeSecret = nextDefaultSecret + } + .frame(minWidth: 100, idealWidth: 240) + } + + } + +} + +extension StoreListView { + + var nextDefaultSecret: AnyHashable? { + let fallback: AnyHashable + if storeList.modifiableStore?.isAvailable ?? false { + fallback = EmptyStoreView.Constants.emptyStoreModifiableTag + } else { + fallback = EmptyStoreView.Constants.emptyStoreTag + } + return storeList.stores.compactMap(\.secrets.first).first?.id ?? fallback + } + +} diff --git a/Secretive/Views/UpdateView.swift b/Secretive/Views/UpdateView.swift new file mode 100644 index 0000000..afe620e --- /dev/null +++ b/Secretive/Views/UpdateView.swift @@ -0,0 +1,61 @@ +import SwiftUI +import Brief + +struct UpdateDetailView: View { + + @EnvironmentObject var updater: UpdaterType + + let update: Release + + var body: some View { + VStack { + Text("Secretive \(update.name)").font(.title) + GroupBox(label: Text("Release Notes")) { + ScrollView { + attributedBody + } + } + HStack { + if !update.critical { + Button("Ignore") { + updater.ignore(release: update) + } + Spacer() + } + Button("Update") { + NSWorkspace.shared.open(update.html_url) + } + .keyboardShortcut(.defaultAction) + } + + } + .padding() + .frame(maxWidth: 500) + } + + var attributedBody: Text { + var text = Text("") + for line in update.body.split(whereSeparator: \.isNewline) { + let attributed: Text + let split = line.split(separator: " ") + let unprefixed = split.dropFirst().joined() + if let prefix = split.first { + switch prefix { + case "#": + attributed = Text(unprefixed).font(.title) + Text("\n") + case "##": + attributed = Text(unprefixed).font(.title2) + Text("\n") + case "###": + attributed = Text(unprefixed).font(.title3) + Text("\n") + default: + attributed = Text(line) + Text("\n\n") + } + } else { + attributed = Text(line) + Text("\n\n") + } + text = text + attributed + } + return text + } + +} diff --git a/assets/apple_watch_auth.png b/assets/apple_watch_auth.png deleted file mode 100644 index a1c8bbf..0000000 Binary files a/assets/apple_watch_auth.png and /dev/null differ diff --git a/assets/apple_watch_system_prefs.png b/assets/apple_watch_system_prefs.png deleted file mode 100644 index 00584a3..0000000 Binary files a/assets/apple_watch_system_prefs.png and /dev/null differ