Compare commits

..

15 Commits

Author SHA1 Message Date
12b920b0af Fix window close behavior (fixes #48) 2020-03-15 16:03:20 -07:00
bfb4f80b8c Self 2020-03-15 15:57:11 -07:00
d661b9002b Run status check on setup dismiss (fixes #49) 2020-03-15 15:56:21 -07:00
9daae8957a Add notice if agent isn't running (#47)
* Add agent checker
* Check on foreground
2020-03-15 14:36:07 -07:00
bde9085d31 Reorg 2020-03-15 14:13:28 -07:00
84521cf8d6 Fixes #44 2020-03-15 14:03:57 -07:00
4a6e8c0b11 Add run ID 2020-03-15 01:17:45 -07:00
ae06e8e892 Fix updater ref 2020-03-15 01:11:16 -07:00
57e6b6779f Fix sed. 2020-03-15 01:08:41 -07:00
6136fc2e69 Updater (#43) 2020-03-15 01:01:40 -07:00
2c052e2657 Update CONTRIBUTING.md 2020-03-14 21:51:50 -07:00
0918e570e4 Add security contact email 2020-03-14 21:48:34 -07:00
024c25bcd9 Create CONTRIBUTING.md 2020-03-14 21:44:11 -07:00
154dac3e50 Add CoC 2020-03-14 21:31:45 -07:00
d778760cc1 Update README.md 2020-03-14 19:58:48 -07:00
24 changed files with 553 additions and 120 deletions

View File

@ -18,16 +18,22 @@ jobs:
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: ''
body: "Build: https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}"
draft: true
prerelease: false
- name: Set up signing
- name: Setup Signing
env:
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
run: ./.github/scripts/signing.sh
- name: Update Build Number
env:
TAG_NAME: ${{ github.ref }}
run: |
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
sed -i '' -e "s/CI_VERSION = 0.0.0/CI_VERSION = $CLEAN_TAG/g" Config/Config.xcconfig
- name: Build
run: xcrun xcodebuild -project Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at max.goedjen@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

19
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,19 @@
# Contributing to Secretive
Thanks for your interest in contributing to Secretive! Before you contribute, there are a few things I'd like to lay out.
## Security
Security is obviously paramount for a project like Secretive. As such, any contributions that compromise the security or auditabilty of the project will be rejected.
### Dependencies
Secretive is desigend to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
## Code of Conduct
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
## Secretive is Opinionated
I'm releasing Secretive as open source so that other people can use it and audit it, feeling comfortable in knowing that the source is available so they can see what it's doing. I have a pretty strong idea of what I'd like this project to look like, and I may respectfully decline contributions that don't line up with that vision. If you'd like to propose a change before implementing, please feel free to [Open an Issue with the proposed tag](https://github.com/maxgoedjen/secretive/issues/new?labels=proposed).

1
Config/Config.xcconfig Normal file
View File

@ -0,0 +1 @@
CI_VERSION = 0.0.0

View File

@ -38,7 +38,7 @@ For non-command-line based apps, like GUI Git clients, you may need to go throug
### Security Considerations
For the moment, you must build Secretive from source. For an app like this, it's critical that you trust that the app you're running is the app whose source you've checked out. To this end, Secretive has no third party dependecies, and is designed to be easy for you to audit for exploits.
Builds are produced by GitHub Actions with an auditable build and release generation process. Each build has a "Document SHAs" step, which will output SHA checksums for the build produced by the GitHub Action, so you can verify that the source code for a given build corresponds to any given release.
### A Note Around Code Signing and Keychains
@ -47,3 +47,7 @@ While Secretive uses the Secure Enclave for key storage, it still relies on Keyc
### Backups and Transfers to New Machines
Beacuse secrets in the Secure Enclave are not exportable, they are not able to be backed up, and you will not be able to transfer them to a new machine. If you get a new Mac, just create a new set of secrets specific to that Mac.
## Security
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<string>$(CI_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(CI_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(CI_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>

View File

@ -30,6 +30,11 @@
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, ); }; };
50731666241DF8660023809E /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731665241DF8660023809E /* Updater.swift */; };
50731669241E00C20023809E /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731668241E00C20023809E /* NoticeView.swift */; };
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.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 */; };
@ -179,6 +184,12 @@
5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = "<group>"; };
506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = "<group>"; };
506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = "<group>"; };
50731665241DF8660023809E /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
50731668241E00C20023809E /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.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>"; };
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusChecker.swift; sourceTree = "<group>"; };
508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAgentStatusChecker.swift; sourceTree = "<group>"; };
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; };
@ -274,6 +285,7 @@
50A3B78B24026B7500D209EA /* SecretAgent */,
5099A06D240242BA0062B6F2 /* SecretAgentKit */,
5099A07A240242BA0062B6F2 /* SecretAgentKitTests */,
508A58AF241E144C0069DC07 /* Config */,
50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */,
);
@ -297,12 +309,8 @@
isa = PBXGroup;
children = (
50617D8223FCE48E0099B055 /* AppDelegate.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
50C385A8240B636500AF2719 /* SetupView.swift */,
508A58B0241ED1C40069DC07 /* Views */,
508A58B1241ED1EA0069DC07 /* Controllers */,
50617D8623FCE48E0099B055 /* Assets.xcassets */,
50617D8B23FCE48E0099B055 /* Main.storyboard */,
50617D8E23FCE48E0099B055 /* Info.plist */,
@ -317,6 +325,8 @@
children = (
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */,
50617DD123FCEFA90099B055 /* PreviewStore.swift */,
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */,
508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */,
);
path = "Preview Content";
sourceTree = "<group>";
@ -381,6 +391,37 @@
path = OpenSSH;
sourceTree = "<group>";
};
508A58AF241E144C0069DC07 /* Config */ = {
isa = PBXGroup;
children = (
508A58AB241E121B0069DC07 /* Config.xcconfig */,
);
path = Config;
sourceTree = "<group>";
};
508A58B0241ED1C40069DC07 /* Views */ = {
isa = PBXGroup;
children = (
50617D8423FCE48E0099B055 /* ContentView.swift */,
50731668241E00C20023809E /* NoticeView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
50C385A8240B636500AF2719 /* SetupView.swift */,
);
path = Views;
sourceTree = "<group>";
};
508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup;
children = (
50731665241DF8660023809E /* Updater.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
5099A02523FE34DE0062B6F2 /* SmartCard */ = {
isa = PBXGroup;
children = (
@ -733,11 +774,16 @@
50C385A9240B636500AF2719 /* SetupView.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50731666241DF8660023809E /* Updater.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50731669241E00C20023809E /* NoticeView.swift in Sources */,
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -871,6 +917,7 @@
/* Begin XCBuildConfiguration section */
50617D9B23FCE48E0099B055 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -931,6 +978,7 @@
};
50617D9C23FCE48E0099B055 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -991,6 +1039,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_HARDENED_RUNTIME = YES;
@ -1001,6 +1050,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1017,6 +1067,7 @@
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_HARDENED_RUNTIME = YES;
@ -1027,6 +1078,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Host";
@ -1293,7 +1345,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.1.0;
MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@ -1318,7 +1370,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.1.0;
MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";

View File

@ -13,10 +13,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store())
return list
}()
let updater = Updater()
let agentStatusChecker = AgentStatusChecker()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(storeList: storeList)
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),
@ -28,6 +29,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
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:)))
@ -37,6 +39,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
runSetupIfNeeded()
}
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!) {
@ -57,6 +69,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
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)

View File

@ -1,97 +0,0 @@
import SwiftUI
import SecretKit
struct ContentView: View {
@ObservedObject var storeList: SecretStoreList
@State fileprivate var active: AnySecret.ID?
@State fileprivate var showingDeletion = false
@State fileprivate var deletingSecret: AnySecret?
var body: some View {
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 {
let fallback: AnyHashable
if self.storeList.modifiableStore?.isAvailable ?? false {
fallback = Constants.emptyStoreModifiableTag
} else {
fallback = Constants.emptyStoreTag
}
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
}
.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!) {
self.showingDeletion = false
}
}
}
}
func delete<SecretType: Secret>(secret: SecretType) {
deletingSecret = AnySecret(secret)
self.showingDeletion = true
}
}
extension ContentView {
enum Constants {
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]))
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]))
}
}
}
#endif

View File

@ -0,0 +1,33 @@
import Foundation
import Combine
import AppKit
protocol AgentStatusCheckerProtocol: ObservableObject {
var running: Bool { get }
}
class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
@Published var running: Bool = false
init() {
check()
}
func check() {
running = secretAgentProcess != nil
}
var secretAgentProcess: NSRunningApplication? {
NSRunningApplication.runningApplications(withBundleIdentifier: Constants.secretAgentAppID).first
}
}
extension AgentStatusChecker {
enum Constants {
static let secretAgentAppID = "com.maxgoedjen.Secretive.SecretAgent"
}
}

View File

@ -0,0 +1,82 @@
import Foundation
import Combine
protocol UpdaterProtocol: ObservableObject {
var update: Release? { get }
}
class Updater: ObservableObject, UpdaterProtocol {
@Published var update: Release?
init() {
checkForUpdates()
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
self.checkForUpdates()
}
timer.tolerance = 60*60
}
func checkForUpdates() {
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
guard let data = data else { return }
guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return }
self.evaluate(release: release)
}.resume()
}
func evaluate(release: Release) {
let latestVersion = semVer(from: release.name)
let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
for (latest, current) in zip(latestVersion, currentVersion) {
if latest > current {
DispatchQueue.main.async {
self.update = release
}
return
}
}
}
func semVer(from stringVersion: String) -> [Int] {
var split = stringVersion.split(separator: ".").compactMap { Int($0) }
while split.count < 3 {
split.append(0)
}
return split
}
}
extension Updater {
enum Constants {
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
}
}
struct Release: Codable {
let name: String
let html_url: URL
let body: String
}
extension Release {
var critical: Bool {
return body.contains(Constants.securityContent)
}
}
extension Release {
enum Constants {
static let securityContent = "Critical Security Update"
}
}

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(CI_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CI_VERSION)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>

View File

@ -0,0 +1,12 @@
import Foundation
import Combine
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
let running: Bool
init(running: Bool = true) {
self.running = running
}
}

View File

@ -0,0 +1,27 @@
import Foundation
import Combine
class PreviewUpdater: UpdaterProtocol {
let update: Release?
init(update: Update = .none) {
switch update {
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")
case .critical:
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
}
}
}
extension PreviewUpdater {
enum Update {
case none, advisory, critical
}
}

View File

@ -6,6 +6,8 @@
<true/>
<key>com.apple.security.smartcard</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>

View File

@ -0,0 +1,144 @@
import SwiftUI
import SecretKit
struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View {
@ObservedObject var storeList: SecretStoreList
@ObservedObject var updater: UpdaterType
@ObservedObject var agentStatusChecker: AgentStatusCheckerType
var runSetupBlock: (() -> Void)?
@State fileprivate var active: AnySecret.ID?
@State fileprivate var showingDeletion = false
@State fileprivate var deletingSecret: AnySecret?
var body: some View {
VStack {
if updater.update != nil {
updateNotice()
}
if !agentStatusChecker.running {
agentNotice()
}
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
}
}
}
}
}
}
func updateNotice() -> some View {
guard let update = updater.update else { return AnyView(Spacer()) }
let severity: NoticeView.Severity
let text: String
if update.critical {
severity = .critical
text = "Critical Security Update Required"
} else {
severity = .advisory
text = "Update Available"
}
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?()
}
}
func delete<SecretType: Secret>(secret: SecretType) {
deletingSecret = AnySecret(secret)
self.showingDeletion = true
}
var nextDefaultSecret: AnyHashable? {
let fallback: AnyHashable
if self.storeList.modifiableStore?.isAvailable ?? false {
fallback = Constants.emptyStoreModifiableTag
} else {
fallback = Constants.emptyStoreTag
}
return self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
}
}
fileprivate enum Constants {
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
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))
}
}
}
#endif

View File

@ -8,9 +8,9 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
@State var confirm = ""
fileprivate var dismissalBlock: () -> ()
fileprivate var dismissalBlock: (Bool) -> ()
init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping () -> ()) {
init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping (Bool) -> ()) {
self.secret = secret
self.store = store
self.dismissalBlock = dismissalBlock
@ -37,14 +37,16 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
TextField(secret.name, text: $confirm)
}
}
.onExitCommand(perform: dismissalBlock)
.onExitCommand {
self.dismissalBlock(false)
}
}
HStack {
Spacer()
Button(action: delete) {
Text("Delete")
}.disabled(confirm != secret.name)
Button(action: dismissalBlock) {
Button(action: { self.dismissalBlock(false) }) {
Text("Don't Delete")
}
}
@ -53,6 +55,6 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
func delete() {
try! store.delete(secret: secret)
dismissalBlock()
self.dismissalBlock(true)
}
}

View File

@ -0,0 +1,57 @@
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