mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 09:43:37 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d6bbd2a04 | |||
35aea0729d | |||
c5bd4c2189 | |||
8114acf50a | |||
cd965b9ec6 | |||
f30d1f802f | |||
af479408a4 | |||
42b034270a | |||
dca340ad40 | |||
8dfabd35aa |
BIN
.github/readme/app.png
vendored
BIN
.github/readme/app.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 456 KiB |
7
.github/scripts/signing.sh
vendored
7
.github/scripts/signing.sh
vendored
@ -10,10 +10,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k ci ci.keyc
|
||||
|
||||
# Import Profiles
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
echo $HOST_PROFILE_DATA | base64 -d -o Host.provisionprofile
|
||||
HOST_UUID=`grep UUID -A1 -a Host.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
||||
cp Host.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$HOST_UUID.provisionprofile
|
||||
echo $AGENT_PROFILE_DATA | base64 -d -o Agent.provisionprofile
|
||||
AGENT_UUID=`grep UUID -A1 -a Agent.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
||||
cp Agent.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$AGENT_UUID.provisionprofile
|
||||
cp Agent.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$AGENT_UUID.provisionprofile
|
||||
|
||||
# Create directories for ASC key
|
||||
mkdir ~/.private_keys
|
||||
echo -n "$APPLE_API_KEY_DATA" > ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -16,9 +16,11 @@ jobs:
|
||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: ./.github/scripts/signing.sh
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.3.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
|
||||
- name: Test
|
||||
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||
build:
|
||||
@ -32,9 +34,11 @@ jobs:
|
||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: ./.github/scripts/signing.sh
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.2.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
|
||||
- name: Update Build Number
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
@ -52,9 +56,9 @@ jobs:
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --username $APPLE_USERNAME --password $APPLE_PASSWORD --file Secretive.zip
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --apiKey $APPLE_API_KEY_ID --apiIssuer $APPLE_API_ISSUER --file Secretive.zip
|
||||
- name: Document SHAs
|
||||
run: |
|
||||
shasum -a 512 Secretive.zip
|
||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -8,6 +8,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.3.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
|
||||
- name: Test
|
||||
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||
|
@ -14,6 +14,14 @@ Secretive is designed to be easily auditable by people who are considering using
|
||||
|
||||
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
## Credits
|
||||
|
||||
If you make a material contribution to the app, please add yourself to the end of the [credits](https://github.com/maxgoedjen/secretive/blob/main/Secretive/Credits.rtf).
|
||||
|
||||
## Collaborator Status
|
||||
|
||||
I will not grant collaborator access to any contributors for this repository. This is basically just because collaborators [can accesss the secrets Secretive uses for the signing credentials stored in the repository](https://docs.github.com/en/actions/reference/encrypted-secrets#accessing-your-secrets).
|
||||
|
||||
## 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).
|
||||
|
BIN
Icon.sketch
BIN
Icon.sketch
Binary file not shown.
7
SecretKit/Common/BundleIDs.swift
Normal file
7
SecretKit/Common/BundleIDs.swift
Normal file
@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
extension Bundle {
|
||||
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!}
|
||||
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
|
||||
}
|
@ -49,10 +49,12 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||
|
||||
private let _create: (String, Bool) throws -> Void
|
||||
private let _delete: (AnySecret) throws -> Void
|
||||
private let _update: (AnySecret, String) throws -> Void
|
||||
|
||||
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
|
||||
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
|
||||
_update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
|
||||
super.init(secretStore)
|
||||
}
|
||||
|
||||
@ -64,4 +66,7 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||
try _delete(secret)
|
||||
}
|
||||
|
||||
public func update(secret: AnySecret, name: String) throws {
|
||||
try _update(secret, name)
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ public protocol SecretStoreModifiable: SecretStore {
|
||||
|
||||
func create(name: String, requiresAuthentication: Bool) throws
|
||||
func delete(secret: SecretType) throws
|
||||
func update(secret: SecretType, name: String) throws
|
||||
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ extension SecureEnclave {
|
||||
let deleteAttributes = [
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrApplicationLabel: secret.id as CFData
|
||||
] as CFDictionary
|
||||
] as CFDictionary
|
||||
let status = SecItemDelete(deleteAttributes)
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError(statusCode: status)
|
||||
@ -76,6 +76,23 @@ extension SecureEnclave {
|
||||
reloadSecrets()
|
||||
}
|
||||
|
||||
public func update(secret: Secret, name: String) throws {
|
||||
let updateQuery = [
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrApplicationLabel: secret.id as CFData
|
||||
] as CFDictionary
|
||||
|
||||
let updatedAttributes = [
|
||||
kSecAttrLabel: name,
|
||||
] as CFDictionary
|
||||
|
||||
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError(statusCode: status)
|
||||
}
|
||||
reloadSecrets()
|
||||
}
|
||||
|
||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
||||
let context = LAContext()
|
||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
||||
|
@ -7,9 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */; };
|
||||
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 */; };
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
|
||||
50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; };
|
||||
@ -87,6 +88,7 @@
|
||||
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 */; };
|
||||
FA0B34672599619E0013AB3A /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0B34662599619E0013AB3A /* BundleIDs.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -221,9 +223,10 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameSecretView.swift; sourceTree = "<group>"; };
|
||||
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||
50153E21250DECA300525160 /* SecretListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListView.swift; sourceTree = "<group>"; };
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||
50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = "<group>"; };
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
|
||||
@ -308,6 +311,7 @@
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
FA0B34662599619E0013AB3A /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -539,10 +543,11 @@
|
||||
children = (
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */,
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||
@ -591,6 +596,7 @@
|
||||
5068389F2415EA4F00F55094 /* Erasers */,
|
||||
506838A42415EA6800F55094 /* OpenSSH */,
|
||||
5068389D241471CD00F55094 /* SecretStoreList.swift */,
|
||||
FA0B34662599619E0013AB3A /* BundleIDs.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
@ -1002,6 +1008,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
@ -1019,7 +1026,7 @@
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
|
||||
);
|
||||
@ -1037,6 +1044,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FA0B34672599619E0013AB3A /* BundleIDs.swift in Sources */,
|
||||
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */,
|
||||
50617DC723FCE4EA0099B055 /* SecretStore.swift in Sources */,
|
||||
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import AppKit
|
||||
import SecretKit
|
||||
|
||||
protocol AgentStatusCheckerProtocol: ObservableObject {
|
||||
var running: Bool { get }
|
||||
@ -20,7 +21,7 @@ class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
|
||||
|
||||
// All processes, including ones from older versions, etc
|
||||
var secretAgentProcesses: [NSRunningApplication] {
|
||||
NSRunningApplication.runningApplications(withBundleIdentifier: Constants.secretAgentAppID)
|
||||
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
|
||||
}
|
||||
|
||||
// The process corresponding to this instance of Secretive
|
||||
@ -37,10 +38,4 @@ class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
|
||||
|
||||
}
|
||||
|
||||
extension AgentStatusChecker {
|
||||
|
||||
enum Constants {
|
||||
static let secretAgentAppID = "com.maxgoedjen.Secretive.SecretAgent"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ import Foundation
|
||||
import ServiceManagement
|
||||
import AppKit
|
||||
import OSLog
|
||||
import SecretKit
|
||||
|
||||
struct LaunchAgentController {
|
||||
|
||||
|
||||
func install(completion: (() -> Void)? = nil) {
|
||||
Logger().debug("Installing agent")
|
||||
_ = setEnabled(false)
|
||||
@ -21,8 +22,12 @@ struct LaunchAgentController {
|
||||
func forceLaunch(completion: ((Bool) -> Void)?) {
|
||||
Logger().debug("Agent is not running, attempting to force launch")
|
||||
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: NSWorkspace.OpenConfiguration()) { app, error in
|
||||
completion?(error == nil)
|
||||
let config = NSWorkspace.OpenConfiguration()
|
||||
config.activates = false
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: config) { app, error in
|
||||
DispatchQueue.main.async {
|
||||
completion?(error == nil)
|
||||
}
|
||||
if let error = error {
|
||||
Logger().error("Error force launching \(error.localizedDescription)")
|
||||
} else {
|
||||
@ -32,7 +37,7 @@ struct LaunchAgentController {
|
||||
}
|
||||
|
||||
private func setEnabled(_ enabled: Bool) -> Bool {
|
||||
SMLoginItemSetEnabled("com.maxgoedjen.Secretive.SecretAgent" as CFString, enabled)
|
||||
SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import SecretKit
|
||||
|
||||
struct ShellConfigurationController {
|
||||
|
||||
let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String
|
||||
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
||||
|
||||
var shellInstructions: [ShellConfigInstruction] {
|
||||
[
|
||||
ShellConfigInstruction(shell: "global",
|
||||
shellConfigDirectory: "~/.ssh/",
|
||||
shellConfigFilename: "config",
|
||||
text: "Host *\n\tIdentityAgent \(socketPath)"),
|
||||
ShellConfigInstruction(shell: "zsh",
|
||||
shellConfigDirectory: "~/",
|
||||
shellConfigFilename: ".zshrc",
|
||||
|
@ -1,4 +1,4 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2511
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2580
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
@ -12,6 +12,19 @@
|
||||
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
|
||||
\
|
||||
Special Thanks To:\
|
||||
\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive/graphs/contributors"}}{\fldrslt Contributors}}:\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/0xflotus"}}{\fldrslt 0xflotus}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/aaron-trout"}}{\fldrslt Aaron Trout}}\
|
||||
\pard\pardeftab720\partightenfactor0
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/EppO"}}{\fldrslt \cf0 Florent Monbillard}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/vladimyr"}}{\fldrslt Dario Vladovi\uc0\u263 }}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/lavalleeale"}}{\fldrslt Alex Lavallee}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/joshheyse"}}{\fldrslt Josh}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/diesal11"}}{\fldrslt Dylan Lundy}}\
|
||||
\
|
||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||
\cf0 Testers:\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
|
||||
@ -20,5 +33,4 @@ Special Thanks To:\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/marksands"}}{\fldrslt Mark Sands}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/mergesort"}}{\fldrslt Joe Fabisevich}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/phillco"}}{\fldrslt Phil Cohen}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/zacwest"}}{\fldrslt Zac West}}}
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}
|
@ -42,7 +42,6 @@ extension Preview {
|
||||
}
|
||||
|
||||
class StoreModifiable: Store, SecretStoreModifiable {
|
||||
|
||||
override var name: String { "Modifiable Preview Store" }
|
||||
|
||||
func create(name: String, requiresAuthentication: Bool) throws {
|
||||
@ -50,8 +49,10 @@ extension Preview {
|
||||
|
||||
func delete(secret: Preview.Secret) throws {
|
||||
}
|
||||
}
|
||||
|
||||
func update(secret: Preview.Secret, name: String) throws {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Preview {
|
||||
|
@ -2,10 +2,10 @@ import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
|
||||
|
||||
@ObservedObject var store: StoreType
|
||||
@Binding var showing: Bool
|
||||
|
||||
|
||||
@State private var name = ""
|
||||
@State private var requiresAuthentication = true
|
||||
|
||||
@ -26,8 +26,12 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
TextField("Shhhhh", text: $name).focusable()
|
||||
}
|
||||
HStack {
|
||||
Toggle(isOn: $requiresAuthentication) {
|
||||
Text("Requires Authentication (Biometrics or Password)")
|
||||
VStack(spacing: 20) {
|
||||
Picker("", selection: $requiresAuthentication) {
|
||||
Text("Requires Authentication (Biometrics or Password) before each use").tag(true)
|
||||
Text("Authentication not required when Mac is unlocked").tag(false)
|
||||
}
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@ -45,7 +49,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
|
||||
|
||||
func save() {
|
||||
try! store.create(name: name, requiresAuthentication: requiresAuthentication)
|
||||
showing = false
|
||||
|
@ -30,9 +30,6 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
TextField(secret.name, text: $confirm)
|
||||
}
|
||||
}
|
||||
.onExitCommand {
|
||||
dismissalBlock(false)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
@ -47,6 +44,9 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
}
|
||||
.padding()
|
||||
.frame(minWidth: 400)
|
||||
.onExitCommand {
|
||||
dismissalBlock(false)
|
||||
}
|
||||
}
|
||||
|
||||
func delete() {
|
||||
|
50
Secretive/Views/RenameSecretView.swift
Normal file
50
Secretive/Views/RenameSecretView.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
|
||||
@ObservedObject var store: StoreType
|
||||
let secret: StoreType.SecretType
|
||||
var dismissalBlock: (_ renamed: Bool) -> ()
|
||||
|
||||
@State private var newName = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 64, height: 64)
|
||||
.padding()
|
||||
VStack {
|
||||
HStack {
|
||||
Text("Type your new name for \"\(secret.name)\" below.")
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
TextField(secret.name, text: $newName).focusable()
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Rename", action: rename)
|
||||
.disabled(newName.count == 0)
|
||||
.keyboardShortcut(.return)
|
||||
Button("Cancel") {
|
||||
dismissalBlock(false)
|
||||
}.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(minWidth: 400)
|
||||
.onExitCommand {
|
||||
dismissalBlock(false)
|
||||
}
|
||||
}
|
||||
|
||||
func rename() {
|
||||
try? store.update(secret: secret, name: newName)
|
||||
dismissalBlock(true)
|
||||
}
|
||||
}
|
@ -25,9 +25,9 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
}
|
||||
|
||||
var dashedKeyName: String {
|
||||
secret.name.replacingOccurrences(of: " ", with: "-")
|
||||
secret.name.replacingOccurrences(of: " ", with: "-")
|
||||
}
|
||||
|
||||
|
||||
var dashedHostName: String {
|
||||
["secretive", Host.current().localizedName, "local"]
|
||||
.compactMap { $0 }
|
||||
|
54
Secretive/Views/SecretListItemView.swift
Normal file
54
Secretive/Views/SecretListItemView.swift
Normal file
@ -0,0 +1,54 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
struct SecretListItemView: View {
|
||||
|
||||
@ObservedObject var store: AnySecretStore
|
||||
var secret: AnySecret
|
||||
@Binding var activeSecret: AnySecret.ID?
|
||||
|
||||
@State var isDeleting: Bool = false
|
||||
@State var isRenaming: Bool = false
|
||||
|
||||
var deletedSecret: (AnySecret) -> Void
|
||||
var renamedSecret: (AnySecret) -> Void
|
||||
|
||||
var body: some View {
|
||||
let showingPopupWrapped = Binding(
|
||||
get: { isDeleting || isRenaming },
|
||||
set: { if $0 == false { isDeleting = false; isRenaming = false } }
|
||||
)
|
||||
|
||||
return NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) {
|
||||
Text(secret.name)
|
||||
}.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { isRenaming = true }) {
|
||||
Text("Rename")
|
||||
}
|
||||
Button(action: { isDeleting = true }) {
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover(isPresented: showingPopupWrapped) {
|
||||
if let modifiable = store as? AnySecretStoreModifiable {
|
||||
if isDeleting {
|
||||
DeleteSecretView(store: modifiable, secret: secret) { deleted in
|
||||
isDeleting = false
|
||||
if deleted {
|
||||
deletedSecret(secret)
|
||||
}
|
||||
}
|
||||
} else if isRenaming {
|
||||
RenameSecretView(store: modifiable, secret: secret) { renamed in
|
||||
isRenaming = false
|
||||
if renamed {
|
||||
renamedSecret(secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover(isPresented: .constant(deletingSecret == secret)) {
|
||||
if let modifiable = store as? AnySecretStoreModifiable {
|
||||
DeleteSecretView(store: modifiable, secret: secret) { deleted in
|
||||
deletingSecret = nil
|
||||
if deleted {
|
||||
deletedSecret(AnySecret(secret))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func delete<SecretType: Secret>(secret: SecretType) {
|
||||
deletingSecret = AnySecret(secret)
|
||||
}
|
||||
|
||||
}
|
@ -6,10 +6,17 @@ struct StoreListView: View {
|
||||
@Binding var showingCreation: Bool
|
||||
|
||||
@State private var activeSecret: AnySecret.ID?
|
||||
@State private var deletingSecret: AnySecret?
|
||||
|
||||
@EnvironmentObject private var storeList: SecretStoreList
|
||||
|
||||
private func secretDeleted(secret: AnySecret) {
|
||||
activeSecret = nextDefaultSecret
|
||||
}
|
||||
|
||||
private func secretRenamed(secret: AnySecret) {
|
||||
activeSecret = nextDefaultSecret
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(selection: $activeSecret) {
|
||||
@ -19,9 +26,15 @@ struct StoreListView: View {
|
||||
if store.secrets.isEmpty {
|
||||
EmptyStoreView(store: store, activeSecret: $activeSecret)
|
||||
} else {
|
||||
SecretListView(store: store, activeSecret: $activeSecret, deletingSecret: $deletingSecret, deletedSecret: { _ in
|
||||
activeSecret = nextDefaultSecret
|
||||
})
|
||||
ForEach(store.secrets) { secret in
|
||||
SecretListItemView(
|
||||
store: store,
|
||||
secret: secret,
|
||||
activeSecret: $activeSecret,
|
||||
deletedSecret: self.secretDeleted,
|
||||
renamedSecret: self.secretRenamed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,9 +46,7 @@ struct StoreListView: View {
|
||||
}
|
||||
.frame(minWidth: 100, idealWidth: 240)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StoreListView {
|
||||
|
Reference in New Issue
Block a user