mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 01:33:37 +00:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
12b920b0af | |||
bfb4f80b8c | |||
d661b9002b | |||
9daae8957a | |||
bde9085d31 | |||
84521cf8d6 | |||
4a6e8c0b11 | |||
ae06e8e892 | |||
57e6b6779f | |||
6136fc2e69 | |||
2c052e2657 | |||
0918e570e4 | |||
024c25bcd9 | |||
154dac3e50 | |||
d778760cc1 |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@ -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
76
CODE_OF_CONDUCT.md
Normal 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
19
CONTRIBUTING.md
Normal 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
1
Config/Config.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
CI_VERSION = 0.0.0
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
|
@ -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
|
33
Secretive/Controllers/AgentStatusChecker.swift
Normal file
33
Secretive/Controllers/AgentStatusChecker.swift
Normal 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"
|
||||
}
|
||||
|
||||
}
|
82
Secretive/Controllers/Updater.swift
Normal file
82
Secretive/Controllers/Updater.swift
Normal 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"
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
12
Secretive/Preview Content/PreviewAgentStatusChecker.swift
Normal file
12
Secretive/Preview Content/PreviewAgentStatusChecker.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
let running: Bool
|
||||
|
||||
init(running: Bool = true) {
|
||||
self.running = running
|
||||
}
|
||||
|
||||
}
|
27
Secretive/Preview Content/PreviewUpdater.swift
Normal file
27
Secretive/Preview Content/PreviewUpdater.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
144
Secretive/Views/ContentView.swift
Normal file
144
Secretive/Views/ContentView.swift
Normal 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
|
@ -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)
|
||||
}
|
||||
}
|
57
Secretive/Views/NoticeView.swift
Normal file
57
Secretive/Views/NoticeView.swift
Normal 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
|
Reference in New Issue
Block a user