mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 09:43:37 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
067f1526b0 | |||
f43dea0d0d | |||
db8833fa25 | |||
1409e9ac31 | |||
adabe801d3 | |||
19f9494492 | |||
c50d2feaf9 | |||
03d3cc9177 | |||
141cc03b60 | |||
07559bd7ef |
49
.github/workflows/nightly.yml
vendored
Normal file
49
.github/workflows/nightly.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
name: Nightly
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 8 * * *"
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-11.0
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- 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 }}
|
||||||
|
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_13.2.1.app
|
||||||
|
- name: Update Build Number
|
||||||
|
env:
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0/g" Sources/Config/Config.xcconfig
|
||||||
|
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||||
|
- name: Build
|
||||||
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
|
- name: Create ZIPs
|
||||||
|
run: |
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
||||||
|
- name: Notarize
|
||||||
|
env:
|
||||||
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
|
- name: Document SHAs
|
||||||
|
run: |
|
||||||
|
shasum -a 512 Secretive.zip
|
||||||
|
shasum -a 512 Archive.zip
|
||||||
|
- name: Upload App to Artifacts
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: Secretive.zip
|
||||||
|
path: Secretive.zip
|
@ -51,6 +51,30 @@ Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
|
|||||||
|
|
||||||
Log out and log in again before launching Cyberduck.
|
Log out and log in again before launching Cyberduck.
|
||||||
|
|
||||||
|
## GitKraken
|
||||||
|
|
||||||
|
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
|
||||||
|
|
||||||
|
```
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>link-ssh-auth-sock</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/bin/sh</string>
|
||||||
|
<string>-c</string>
|
||||||
|
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
```
|
||||||
|
|
||||||
|
Log out and log in again before launching Gitkraken. Then enable "Use local SSH agent in GitKraken Preferences (Located under Preferences -> SSH)
|
||||||
|
|
||||||
# The app I use isn't listed here!
|
# The app I use isn't listed here!
|
||||||
|
|
||||||
|
4
FAQ.md
4
FAQ.md
@ -40,6 +40,10 @@ Awesome! Just bear in mind that because an app only has access to the keychain i
|
|||||||
|
|
||||||
Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Sources/Packages/Sources/Brief/Updater.swift).
|
Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Sources/Packages/Sources/Brief/Updater.swift).
|
||||||
|
|
||||||
|
### How do I uninstall Secretive?
|
||||||
|
|
||||||
|
Drag Secretive.app to the trash and remove `~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent`. `SecretAgent` may continue running until you quit it or reboot.
|
||||||
|
|
||||||
### I have a security issue
|
### I have a security issue
|
||||||
|
|
||||||
Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP.
|
Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP.
|
||||||
|
@ -30,20 +30,23 @@ extension Agent {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - reader: A ``FileHandleReader`` to read the content of the request.
|
/// - reader: A ``FileHandleReader`` to read the content of the request.
|
||||||
/// - writer: A ``FileHandleWriter`` to write the response to.
|
/// - writer: A ``FileHandleWriter`` to write the response to.
|
||||||
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
/// - Return value:
|
||||||
|
/// - Boolean if data could be read
|
||||||
|
public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
|
||||||
Logger().debug("Agent handling new data")
|
Logger().debug("Agent handling new data")
|
||||||
let data = Data(reader.availableData)
|
let data = Data(reader.availableData)
|
||||||
guard data.count > 4 else { return }
|
guard data.count > 4 else { return false}
|
||||||
let requestTypeInt = data[4]
|
let requestTypeInt = data[4]
|
||||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||||
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
|
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
|
||||||
let subData = Data(data[5...])
|
let subData = Data(data[5...])
|
||||||
let response = handle(requestType: requestType, data: subData, reader: reader)
|
let response = handle(requestType: requestType, data: subData, reader: reader)
|
||||||
writer.write(response)
|
writer.write(response)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
|
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
|
||||||
|
@ -9,7 +9,9 @@ public class SocketController {
|
|||||||
/// The active SocketPort.
|
/// The active SocketPort.
|
||||||
private var port: SocketPort?
|
private var port: SocketPort?
|
||||||
/// A handler that will be notified when a new read/write handle is available.
|
/// A handler that will be notified when a new read/write handle is available.
|
||||||
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
/// False if no data could be read
|
||||||
|
public var handler: ((FileHandleReader, FileHandleWriter) -> Bool)?
|
||||||
|
|
||||||
|
|
||||||
/// Initializes a socket controller with a specified path.
|
/// Initializes a socket controller with a specified path.
|
||||||
/// - Parameter path: The path to use as a socket.
|
/// - Parameter path: The path to use as a socket.
|
||||||
@ -65,7 +67,7 @@ public class SocketController {
|
|||||||
@objc func handleConnectionAccept(notification: Notification) {
|
@objc func handleConnectionAccept(notification: Notification) {
|
||||||
Logger().debug("Socket controller accepted connection")
|
Logger().debug("Socket controller accepted connection")
|
||||||
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
||||||
handler?(new, new)
|
_ = handler?(new, new)
|
||||||
new.waitForDataInBackgroundAndNotify()
|
new.waitForDataInBackgroundAndNotify()
|
||||||
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
}
|
}
|
||||||
@ -76,7 +78,12 @@ public class SocketController {
|
|||||||
Logger().debug("Socket controller has new data available")
|
Logger().debug("Socket controller has new data available")
|
||||||
guard let new = notification.object as? FileHandle else { return }
|
guard let new = notification.object as? FileHandle else { return }
|
||||||
Logger().debug("Socket controller received new file handle")
|
Logger().debug("Socket controller received new file handle")
|
||||||
handler?(new, new)
|
if((handler?(new, new)) == true) {
|
||||||
|
Logger().debug("Socket controller handled data, wait for more data")
|
||||||
|
new.waitForDataInBackgroundAndNotify()
|
||||||
|
} else {
|
||||||
|
Logger().debug("Socket controller called with empty data, socked closed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ public class PublicKeyFileStoreController {
|
|||||||
|
|
||||||
private let logger = Logger()
|
private let logger = Logger()
|
||||||
private let directory: String
|
private let directory: String
|
||||||
|
private let keyWriter = OpenSSHKeyWriter()
|
||||||
|
|
||||||
/// Initializes a PublicKeyFileStoreController.
|
/// Initializes a PublicKeyFileStoreController.
|
||||||
public init(homeDirectory: String) {
|
public init(homeDirectory: String) {
|
||||||
@ -21,7 +22,6 @@ public class PublicKeyFileStoreController {
|
|||||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory))
|
try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory))
|
||||||
}
|
}
|
||||||
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
|
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
|
||||||
let keyWriter = OpenSSHKeyWriter()
|
|
||||||
for secret in secrets {
|
for secret in secrets {
|
||||||
let path = path(for: secret)
|
let path = path(for: secret)
|
||||||
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
|
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
|
||||||
@ -35,7 +35,8 @@ public class PublicKeyFileStoreController {
|
|||||||
/// - Returns: The path to the Secret's public key.
|
/// - Returns: The path to the Secret's public key.
|
||||||
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
|
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
|
||||||
public func path<SecretType: Secret>(for secret: SecretType) -> String {
|
public func path<SecretType: Secret>(for secret: SecretType) -> String {
|
||||||
directory.appending("/").appending("\(secret.name.replacingOccurrences(of: " ", with: "-")).pub")
|
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||||
|
return directory.appending("/").appending("\(minimalHex).pub")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,9 @@ public protocol SecretStoreModifiable: SecretStore {
|
|||||||
|
|
||||||
extension NSNotification.Name {
|
extension NSNotification.Name {
|
||||||
|
|
||||||
|
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)
|
||||||
public static let secretStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.updated")
|
public static let secretStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.updated")
|
||||||
|
// Internal notification that keys were reloaded from the backing store.
|
||||||
|
public static let secretStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.reloaded")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ extension SecureEnclave {
|
|||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
public init() {
|
public init() {
|
||||||
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
||||||
self.reloadSecrets(notify: false)
|
self.reloadSecrets(notifyAgent: false)
|
||||||
}
|
}
|
||||||
loadSecrets()
|
loadSecrets()
|
||||||
}
|
}
|
||||||
@ -171,12 +171,13 @@ extension SecureEnclave {
|
|||||||
extension SecureEnclave.Store {
|
extension SecureEnclave.Store {
|
||||||
|
|
||||||
/// Reloads all secrets from the store.
|
/// Reloads all secrets from the store.
|
||||||
/// - Parameter notify: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well.
|
/// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well.
|
||||||
private func reloadSecrets(notify: Bool = true) {
|
private func reloadSecrets(notifyAgent: Bool = true) {
|
||||||
secrets.removeAll()
|
secrets.removeAll()
|
||||||
loadSecrets()
|
loadSecrets()
|
||||||
if notify {
|
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
||||||
DistributedNotificationCenter.default().post(name: .secretStoreUpdated, object: nil)
|
if notifyAgent {
|
||||||
|
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.socketController.handler = self.agent.handle(reader:writer:)
|
self.socketController.handler = self.agent.handle(reader:writer:)
|
||||||
}
|
}
|
||||||
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [self] _ in
|
NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in
|
||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
|
||||||
}
|
}
|
||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
|
||||||
|
@ -32,6 +32,9 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
|||||||
appPathNotice
|
appPathNotice
|
||||||
newItem
|
newItem
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $runningSetup) {
|
||||||
|
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -65,11 +68,11 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.background(color)
|
.background(color)
|
||||||
.cornerRadius(5)
|
.cornerRadius(5)
|
||||||
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
|
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
|
||||||
UpdateDetailView(update: update)
|
UpdateDetailView(update: update)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,11 +88,11 @@ extension ContentView {
|
|||||||
}, label: {
|
}, label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
})
|
})
|
||||||
.popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
.popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
||||||
if let modifiable = storeList.modifiableStore {
|
if let modifiable = storeList.modifiableStore {
|
||||||
CreateSecretView(store: modifiable, showing: $showingCreation)
|
CreateSecretView(store: modifiable, showing: $showingCreation)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -113,15 +116,12 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.background(Color.orange)
|
.background(Color.orange)
|
||||||
.cornerRadius(5)
|
.cornerRadius(5)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $runningSetup) {
|
|
||||||
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,19 +142,19 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.background(Color.orange)
|
.background(Color.orange)
|
||||||
.cornerRadius(5)
|
.cornerRadius(5)
|
||||||
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
||||||
VStack {
|
VStack {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
Image(systemName: "exclamationmark.triangle")
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 64)
|
.frame(width: 64)
|
||||||
Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.")
|
Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.")
|
||||||
.frame(maxWidth: 300)
|
.frame(maxWidth: 300)
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user