mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 01:33:37 +00:00
Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
cd0a1b0a68 | |||
8bbf489146 | |||
30148ee3a4 | |||
c306801149 | |||
9c60a0b7dd | |||
5b38ef00c1 | |||
ccbf92785d | |||
f1e8e43f62 | |||
7b1563f167 | |||
cca070dbe4 | |||
31e51e45c1 | |||
4bfb3a0012 | |||
a0ed880386 | |||
92b9648e04 | |||
4a2a342670 | |||
1d147d8e34 | |||
4dcc9b113d | |||
1f7ebcfe75 | |||
b090a96975 | |||
594d855f19 | |||
89f0798576 | |||
0d74f6f561 | |||
efeb7f09a3 | |||
104199cf53 | |||
f214da98a4 | |||
95cf88d3ca | |||
04835fa437 | |||
31dfba8265 | |||
df8eedebd0 | |||
0a9ecb039e | |||
2e9a3eb90d | |||
df599fbe62 | |||
7c60e9b04b | |||
f7f182ec77 | |||
a0052e8395 | |||
10d3fa150c | |||
c5abca4099 | |||
f674d47507 | |||
1e3ece600d | |||
7481498c5b | |||
a6fbcdbe55 | |||
36e655e527 | |||
dd3acf5ae1 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: maxgoedjen
|
BIN
.github/readme/notification.png
vendored
BIN
.github/readme/notification.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.6 MiB |
19
Brief/Brief.h
Normal file
19
Brief/Brief.h
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Brief.h
|
||||
// Brief
|
||||
//
|
||||
// Created by Max Goedjen on 3/21/20.
|
||||
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Brief.
|
||||
FOUNDATION_EXPORT double BriefVersionNumber;
|
||||
|
||||
//! Project version string for Brief.
|
||||
FOUNDATION_EXPORT const unsigned char BriefVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Brief/PublicHeader.h>
|
||||
|
||||
|
24
Brief/Info.plist
Normal file
24
Brief/Info.plist
Normal file
@ -0,0 +1,24 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||
</dict>
|
||||
</plist>
|
140
Brief/Updater.swift
Normal file
140
Brief/Updater.swift
Normal file
@ -0,0 +1,140 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public protocol UpdaterProtocol: ObservableObject {
|
||||
|
||||
var update: Release? { get }
|
||||
func ignore(release: Release)
|
||||
|
||||
}
|
||||
|
||||
public class Updater: ObservableObject, UpdaterProtocol {
|
||||
|
||||
@Published public var update: Release?
|
||||
|
||||
public init() {
|
||||
checkForUpdates()
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
|
||||
self.checkForUpdates()
|
||||
}
|
||||
timer.tolerance = 60*60
|
||||
}
|
||||
|
||||
public 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()
|
||||
}
|
||||
|
||||
public func ignore(release: Release) {
|
||||
guard !release.critical else { return }
|
||||
defaults.set(true, forKey: release.name)
|
||||
DispatchQueue.main.async {
|
||||
self.update = nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Updater {
|
||||
|
||||
func evaluate(release: Release) {
|
||||
guard !userIgnored(release: release) else { return }
|
||||
let latestVersion = SemVer(release.name)
|
||||
let currentVersion = SemVer(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
|
||||
if latestVersion > currentVersion {
|
||||
DispatchQueue.main.async {
|
||||
self.update = release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userIgnored(release: Release) -> Bool {
|
||||
guard !release.critical else { return false }
|
||||
return defaults.bool(forKey: release.name)
|
||||
}
|
||||
|
||||
var defaults: UserDefaults {
|
||||
UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")!
|
||||
}
|
||||
}
|
||||
|
||||
struct SemVer {
|
||||
|
||||
let versionNumbers: [Int]
|
||||
|
||||
init(_ version: String) {
|
||||
// Betas have the format 1.2.3_beta1
|
||||
let strippedBeta = version.split(separator: "_").first!
|
||||
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
|
||||
while split.count < 3 {
|
||||
split.append(0)
|
||||
}
|
||||
versionNumbers = split
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SemVer: Comparable {
|
||||
|
||||
static func < (lhs: SemVer, rhs: SemVer) -> Bool {
|
||||
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
|
||||
if latest < current {
|
||||
return true
|
||||
} else if latest > current {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension Updater {
|
||||
|
||||
enum Constants {
|
||||
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct Release: Codable {
|
||||
|
||||
public let name: String
|
||||
public let html_url: URL
|
||||
public let body: String
|
||||
|
||||
public init(name: String, html_url: URL, body: String) {
|
||||
self.name = name
|
||||
self.html_url = html_url
|
||||
self.body = body
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Release: Identifiable {
|
||||
|
||||
public var id: String {
|
||||
html_url.absoluteString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Release {
|
||||
|
||||
public var critical: Bool {
|
||||
body.contains(Constants.securityContent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Release {
|
||||
|
||||
enum Constants {
|
||||
static let securityContent = "Critical Security Update"
|
||||
}
|
||||
|
||||
}
|
37
FAQ.md
Normal file
37
FAQ.md
Normal file
@ -0,0 +1,37 @@
|
||||
# FAQ
|
||||
|
||||
### How do I import my current SSH keys, or export my Secretive Keys?
|
||||
|
||||
The secure enclave doesn't allow import or export of private keys. For any new computer, you should just create a new set of keys. If you're using a smart card, you _might_ be able to export your private key from the vendor's software.
|
||||
|
||||
### Secretive doesn't work with my git client
|
||||
|
||||
Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of clients is provided here:
|
||||
|
||||
Tower - [Instructions](https://www.git-tower.com/help/mac/integration/environment)
|
||||
|
||||
GitHub Desktop: Should just work, no configuration needed
|
||||
|
||||
### Secretive isn't working for me
|
||||
|
||||
Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [new GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) with a description of your issue.
|
||||
|
||||
### Why should I trust you?
|
||||
|
||||
You shouldn't, for a piece of software like this. Secretive, by design, has an auditable build process. Each build has a fully auditable build log, showing the source it was built from and a SHA of the build product. You can check the SHA of the zip you download against the SHA output in the build log (which is linked in the About window).
|
||||
|
||||
### I want to build Secretive from source
|
||||
|
||||
Awesome! Just bear in mind that because an app only has access to the keychain items that it created, if you have secrets that you created with the prebuilt version of Secretive, you'll be unable to access them using your own custom build (since you'll have changed the bundled ID).
|
||||
|
||||
### 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.
|
||||
|
||||
### I have a non-security related bug
|
||||
|
||||
Please file a [GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) for it. I will not provide email support with the exception of the critical security issues mentioned above.
|
||||
|
||||
### I want to contribute to Secretive
|
||||
|
||||
Sweet! Please check out the [contributing guidelines](CONTRIBUTING.md) and go from there.
|
18
README.md
18
README.md
@ -1,9 +1,11 @@
|
||||
# Secretive
|
||||
# Secretive  
|
||||
|
||||
|
||||
Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.
|
||||
|
||||
<img src="/.github/readme/app.png" alt="Screenshot of Secretive" width="600">
|
||||
|
||||
|
||||
## Why?
|
||||
|
||||
### Safer Storage
|
||||
@ -12,7 +14,7 @@ The most common setup for SSH keys is just keeping them on disk, guarded by prop
|
||||
|
||||
### Access Control
|
||||
|
||||
If your Mac has a Secure Enclave, it also has support for strong biometric access controls like Touch ID. You can configure your key so that they require Touch ID (or Watch) authentication before they're accessed.
|
||||
If your Mac has a Secure Enclave, it also has support for strong access controls like Touch ID, or authentication with Apple Watch. You can configure your key so that they require Touch ID (or Watch) authentication before they're accessed.
|
||||
|
||||
<img src="/.github/readme/touchid.png" alt="Screenshot of Secretive authenticating with Touch ID">
|
||||
|
||||
@ -28,15 +30,11 @@ For Macs without Secure Enclaves, you can configure a Smart Card (such as a Yubi
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setup for Third Party Apps
|
||||
### FAQ
|
||||
|
||||
When you first launch Secretive, you'll be prompted to set up your command line environment. You can redisplay this prompt at any time by going to `Menu > Help -> Set Up Helper App`.
|
||||
For non-command-line based apps, like GUI Git clients, you may need to go through app-specific setup.
|
||||
There's a [FAQ here](FAQ.md).
|
||||
|
||||
[Tower](https://www.git-tower.com/help/mac/integration/environment)
|
||||
|
||||
|
||||
### Security Considerations
|
||||
### Auditable Build Process
|
||||
|
||||
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.
|
||||
|
||||
@ -50,4 +48,4 @@ Beacuse secrets in the Secure Enclave are not exportable, they are not able to b
|
||||
|
||||
## Security
|
||||
|
||||
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com
|
||||
If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY."
|
||||
|
@ -1,7 +1,9 @@
|
||||
import Cocoa
|
||||
import OSLog
|
||||
import Combine
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
import OSLog
|
||||
import Brief
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@ -12,6 +14,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
list.add(store: SmartCard.Store())
|
||||
return list
|
||||
}()
|
||||
let updater = Updater()
|
||||
let notifier = Notifier()
|
||||
lazy var agent: Agent = {
|
||||
Agent(storeList: storeList, witness: notifier)
|
||||
@ -20,17 +23,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||
return SocketController(path: path)
|
||||
}()
|
||||
fileprivate var updateSink: AnyCancellable?
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
os_log(.debug, "SecretAgent finished launching")
|
||||
DispatchQueue.main.async {
|
||||
self.socketController.handler = self.agent.handle(fileHandle:)
|
||||
self.socketController.handler = self.agent.handle(reader:writer:)
|
||||
}
|
||||
notifier.prompt()
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
// Insert code here to tear down your application
|
||||
updateSink = updater.$update.sink { update in
|
||||
guard let update = update else { return }
|
||||
self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Max Goedjen. All rights reserved.</string>
|
||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
@ -1,10 +1,23 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import AppKit
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
import UserNotifications
|
||||
import Brief
|
||||
|
||||
class Notifier {
|
||||
|
||||
fileprivate let notificationDelegate = NotificationDelegate()
|
||||
|
||||
init() {
|
||||
let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: "Update", options: [])
|
||||
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: [])
|
||||
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
|
||||
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
|
||||
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
|
||||
UNUserNotificationCenter.current().delegate = notificationDelegate
|
||||
}
|
||||
|
||||
func prompt() {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.requestAuthorization(options: .alert) { _, _ in
|
||||
@ -14,12 +27,49 @@ class Notifier {
|
||||
func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.title = "Signed Request"
|
||||
notificationContent.body = "\(secret.name) was used to sign a request from \(provenance.origin.name)."
|
||||
notificationContent.title = "Signed Request from \(provenance.origin.name)"
|
||||
notificationContent.subtitle = "Using secret \"\(secret.name)\""
|
||||
if let iconURL = iconURL(for: provenance), let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
||||
notificationContent.attachments = [attachment]
|
||||
}
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||
notificationCenter.add(request, withCompletionHandler: nil)
|
||||
}
|
||||
|
||||
func notify(update: Release, ignore: ((Release) -> Void)?) {
|
||||
notificationDelegate.release = update
|
||||
notificationDelegate.ignore = ignore
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
if update.critical {
|
||||
notificationContent.title = "Critical Security Update - \(update.name)"
|
||||
} else {
|
||||
notificationContent.title = "Update Available - \(update.name)"
|
||||
}
|
||||
notificationContent.subtitle = "Click to Update"
|
||||
notificationContent.body = update.body
|
||||
notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||
notificationCenter.add(request, withCompletionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Notifier {
|
||||
|
||||
func iconURL(for provenance: SigningRequestProvenance) -> URL? {
|
||||
do {
|
||||
if let app = NSRunningApplication(processIdentifier: provenance.origin.pid), let icon = app.icon?.tiffRepresentation {
|
||||
let temporaryURL = URL(fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent("\(UUID().uuidString).png"))
|
||||
let bitmap = NSBitmapImageRep(data: icon)
|
||||
try bitmap?.representation(using: .png, properties: [:])?.write(to: temporaryURL)
|
||||
return temporaryURL
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Notifier: SigningWitness {
|
||||
@ -32,3 +82,42 @@ extension Notifier: SigningWitness {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Notifier {
|
||||
|
||||
enum Constants {
|
||||
static let updateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update"
|
||||
static let criticalUpdateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
|
||||
static let updateActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
|
||||
static let ignoreActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.ignoreaction"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
|
||||
fileprivate var release: Release?
|
||||
fileprivate var ignore: ((Release) -> Void)?
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
guard let update = release else { return }
|
||||
switch response.actionIdentifier {
|
||||
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
||||
NSWorkspace.shared.open(update.html_url)
|
||||
case Notifier.Constants.ignoreActionIdentitifier:
|
||||
ignore?(update)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler(.alert)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.smartcard</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import CryptoKit
|
||||
import OSLog
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
import AppKit
|
||||
|
||||
public class Agent {
|
||||
@ -22,28 +21,34 @@ public class Agent {
|
||||
|
||||
extension Agent {
|
||||
|
||||
public func handle(fileHandle: FileHandle) {
|
||||
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
||||
os_log(.debug, "Agent handling new data")
|
||||
let data = fileHandle.availableData
|
||||
let data = reader.availableData
|
||||
guard !data.isEmpty else { return }
|
||||
let requestTypeInt = data[4]
|
||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else { return }
|
||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
||||
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription)
|
||||
return
|
||||
}
|
||||
os_log(.debug, "Agent handling request of type %@", requestType.debugDescription)
|
||||
let subData = Data(data[5...])
|
||||
handle(requestType: requestType, data: subData, fileHandle: fileHandle)
|
||||
let response = handle(requestType: requestType, data: subData, reader: reader)
|
||||
writer.write(response)
|
||||
}
|
||||
|
||||
func handle(requestType: SSHAgent.RequestType, data: Data, fileHandle: FileHandle) {
|
||||
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
|
||||
var response = Data()
|
||||
do {
|
||||
switch requestType {
|
||||
case .requestIdentities:
|
||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||
response.append(try identities())
|
||||
response.append(identities())
|
||||
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)
|
||||
case .signRequest:
|
||||
let provenance = requestTracer.provenance(from: reader)
|
||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||
response.append(try sign(data: data, from: fileHandle.fileDescriptor))
|
||||
response.append(try sign(data: data, provenance: provenance))
|
||||
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentSignResponse.debugDescription)
|
||||
}
|
||||
} catch {
|
||||
@ -52,14 +57,14 @@ extension Agent {
|
||||
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription)
|
||||
}
|
||||
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
||||
fileHandle.write(full)
|
||||
return full
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Agent {
|
||||
|
||||
func identities() throws -> Data {
|
||||
func identities() -> Data {
|
||||
// TODO: RESTORE ONCE XCODE 11.4 IS GM
|
||||
let secrets = storeList.stores.flatMap { $0.secrets }
|
||||
// let secrets = storeList.stores.flatMap(\.secrets)
|
||||
@ -77,20 +82,19 @@ extension Agent {
|
||||
return countData + keyData
|
||||
}
|
||||
|
||||
func sign(data: Data, from pid: Int32) throws -> Data {
|
||||
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
let hash = try reader.readNextChunk()
|
||||
let hash = reader.readNextChunk()
|
||||
guard let (store, secret) = secret(matching: hash) else {
|
||||
os_log(.debug, "Agent did not have a key matching %@", hash as NSData)
|
||||
throw AgentError.noMatchingKey
|
||||
}
|
||||
|
||||
let provenance = requestTracer.provenance(from: pid)
|
||||
if let witness = witness {
|
||||
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, by: provenance)
|
||||
}
|
||||
|
||||
let dataToSign = try reader.readNextChunk()
|
||||
let dataToSign = reader.readNextChunk()
|
||||
let derSignature = try store.sign(data: dataToSign, with: secret)
|
||||
|
||||
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||
@ -104,13 +108,22 @@ extension Agent {
|
||||
case (.ellipticCurve, 384):
|
||||
rawRepresentation = try CryptoKit.P384.Signing.ECDSASignature(derRepresentation: derSignature).rawRepresentation
|
||||
default:
|
||||
fatalError()
|
||||
throw AgentError.unsupportedKeyType
|
||||
}
|
||||
|
||||
|
||||
let rawLength = rawRepresentation.count/2
|
||||
let r = rawRepresentation[0..<rawLength]
|
||||
let s = rawRepresentation[rawLength...]
|
||||
// Check if we need to pad with 0x00 to prevent certain
|
||||
// ssh servers from thinking r or s is negative
|
||||
let paddingRange: ClosedRange<UInt8> = 0x80...0xFF
|
||||
var r = Data(rawRepresentation[0..<rawLength])
|
||||
if paddingRange ~= r.first! {
|
||||
r.insert(0x00, at: 0)
|
||||
}
|
||||
var s = Data(rawRepresentation[rawLength...])
|
||||
if paddingRange ~= s.first! {
|
||||
s.insert(0x00, at: 0)
|
||||
}
|
||||
|
||||
var signatureChunk = Data()
|
||||
signatureChunk.append(writer.lengthAndData(of: r))
|
||||
@ -155,6 +168,7 @@ extension Agent {
|
||||
enum AgentError: Error {
|
||||
case unhandledType
|
||||
case noMatchingKey
|
||||
case unsupportedKeyType
|
||||
}
|
||||
|
||||
}
|
||||
|
26
SecretAgentKit/FileHandleProtocols.swift
Normal file
26
SecretAgentKit/FileHandleProtocols.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
public protocol FileHandleReader {
|
||||
|
||||
var availableData: Data { get }
|
||||
var fileDescriptor: Int32 { get }
|
||||
var pidOfConnectedProcess: Int32 { get }
|
||||
|
||||
}
|
||||
|
||||
public protocol FileHandleWriter {
|
||||
|
||||
func write(_ data: Data)
|
||||
|
||||
}
|
||||
|
||||
extension FileHandle: FileHandleReader, FileHandleWriter {
|
||||
|
||||
public var pidOfConnectedProcess: Int32 {
|
||||
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
|
||||
var len = socklen_t(MemoryLayout<Int32>.size)
|
||||
getsockopt(fileDescriptor, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
|
||||
return pidPointer.load(as: Int32.self)
|
||||
}
|
||||
|
||||
}
|
@ -19,6 +19,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Max Goedjen. All rights reserved.</string>
|
||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -12,7 +12,7 @@ extension SSHAgent {
|
||||
switch self {
|
||||
case .requestIdentities:
|
||||
return "RequestIdentities"
|
||||
default:
|
||||
case .signRequest:
|
||||
return "SignRequest"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,3 @@
|
||||
//
|
||||
// SecretAgentKit.h
|
||||
// SecretAgentKit
|
||||
//
|
||||
// Created by Max Goedjen on 2/22/20.
|
||||
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
public struct SigningRequestProvenance {
|
||||
public struct SigningRequestProvenance: Equatable {
|
||||
|
||||
public var chain: [Process]
|
||||
public init(root: Process) {
|
||||
@ -24,7 +24,7 @@ extension SigningRequestProvenance {
|
||||
|
||||
extension SigningRequestProvenance {
|
||||
|
||||
public struct Process {
|
||||
public struct Process: Equatable {
|
||||
|
||||
public let pid: Int32
|
||||
public let name: String
|
||||
|
@ -4,12 +4,8 @@ import Security
|
||||
|
||||
struct SigningRequestTracer {
|
||||
|
||||
func provenance(from pid: Int32) -> SigningRequestProvenance {
|
||||
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
|
||||
var len = socklen_t(MemoryLayout<Int32>.size)
|
||||
getsockopt(pid, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
|
||||
let pid = pidPointer.load(as: Int32.self)
|
||||
let firstInfo = process(from: pid)
|
||||
func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance {
|
||||
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
|
||||
|
||||
var provenance = SigningRequestProvenance(root: firstInfo)
|
||||
while NSRunningApplication(processIdentifier: provenance.origin.pid) == nil && provenance.origin.parentPID != nil {
|
||||
|
@ -5,7 +5,7 @@ public class SocketController {
|
||||
|
||||
fileprivate var fileHandle: FileHandle?
|
||||
fileprivate var port: SocketPort?
|
||||
public var handler: ((FileHandle) -> Void)?
|
||||
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
||||
|
||||
public init(path: String) {
|
||||
os_log(.debug, "Socket controller setting up at %@", path)
|
||||
@ -52,7 +52,7 @@ public class SocketController {
|
||||
@objc func handleConnectionAccept(notification: Notification) {
|
||||
os_log(.debug, "Socket controller accepted connection")
|
||||
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
||||
handler?(new)
|
||||
handler?(new, new)
|
||||
new.waitForDataInBackgroundAndNotify()
|
||||
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||
}
|
||||
@ -61,7 +61,7 @@ public class SocketController {
|
||||
os_log(.debug, "Socket controller has new data available")
|
||||
guard let new = notification.object as? FileHandle else { return }
|
||||
os_log(.debug, "Socket controller received new file handle")
|
||||
handler?(new)
|
||||
handler?(new, new)
|
||||
}
|
||||
|
||||
}
|
||||
|
169
SecretAgentKitTests/AgentTests.swift
Normal file
169
SecretAgentKitTests/AgentTests.swift
Normal file
@ -0,0 +1,169 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import CryptoKit
|
||||
@testable import SecretKit
|
||||
@testable import SecretAgentKit
|
||||
|
||||
class AgentTests: XCTestCase {
|
||||
|
||||
let stubWriter = StubFileHandleWriter()
|
||||
|
||||
// MARK: Identity Listing
|
||||
|
||||
func testEmptyStores() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities)
|
||||
let agent = Agent(storeList: SecretStoreList())
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesEmpty)
|
||||
}
|
||||
|
||||
func testIdentitiesList() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesMultiple)
|
||||
}
|
||||
|
||||
// MARK: Signatures
|
||||
|
||||
func testNoMatchingIdentities() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignatureWithNoneMatching)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
// XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
func testSignature() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...])
|
||||
_ = requestReader.readNextChunk()
|
||||
let dataToSign = requestReader.readNextChunk()
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
let outer = OpenSSHReader(data: stubWriter.data[5...])
|
||||
let payload = outer.readNextChunk()
|
||||
let inner = OpenSSHReader(data: payload)
|
||||
_ = inner.readNextChunk()
|
||||
let signedData = inner.readNextChunk()
|
||||
let rsData = OpenSSHReader(data: signedData)
|
||||
var r = rsData.readNextChunk()
|
||||
var s = rsData.readNextChunk()
|
||||
// This is fine IRL, but it freaks out CryptoKit
|
||||
if r[0] == 0 {
|
||||
r.removeFirst()
|
||||
}
|
||||
if s[0] == 0 {
|
||||
s.removeFirst()
|
||||
}
|
||||
var rs = r
|
||||
rs.append(s)
|
||||
let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||
let valid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
|
||||
XCTAssertTrue(valid)
|
||||
}
|
||||
|
||||
// MARK: Witness protocol
|
||||
|
||||
func testWitnessObjectionStopsRequest() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||
let witness = StubWitness(speakNow: { _,_ in
|
||||
return true
|
||||
}, witness: { _, _ in })
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
func testWitnessSignature() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||
var witnessed = false
|
||||
let witness = StubWitness(speakNow: { _, trace in
|
||||
return false
|
||||
}, witness: { _, trace in
|
||||
witnessed = true
|
||||
})
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertTrue(witnessed)
|
||||
}
|
||||
|
||||
func testRequestTracing() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||
var speakNowTrace: SigningRequestProvenance! = nil
|
||||
var witnessTrace: SigningRequestProvenance! = nil
|
||||
let witness = StubWitness(speakNow: { _, trace in
|
||||
speakNowTrace = trace
|
||||
return false
|
||||
}, witness: { _, trace in
|
||||
witnessTrace = trace
|
||||
})
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertEqual(witnessTrace, speakNowTrace)
|
||||
XCTAssertEqual(witnessTrace.origin.name, "Finder")
|
||||
XCTAssertEqual(witnessTrace.origin.validSignature, true)
|
||||
XCTAssertEqual(witnessTrace.origin.parentPID, 1)
|
||||
}
|
||||
|
||||
// MARK: Exception Handling
|
||||
|
||||
func testSignatureException() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let store = list.stores.first?.base as! Stub.Store
|
||||
store.shouldThrow = true
|
||||
let agent = Agent(storeList: list)
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
// MARK: Unsupported
|
||||
|
||||
func testUnhandledAdd() {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.addIdentity)
|
||||
let agent = Agent(storeList: SecretStoreList())
|
||||
agent.handle(reader: stubReader, writer: stubWriter)
|
||||
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AgentTests {
|
||||
|
||||
func storeList(with secrets: [Stub.Secret]) -> SecretStoreList {
|
||||
let store = Stub.Store()
|
||||
store.secrets.append(contentsOf: secrets)
|
||||
let storeList = SecretStoreList()
|
||||
storeList.add(store: store)
|
||||
return storeList
|
||||
}
|
||||
|
||||
enum Constants {
|
||||
|
||||
enum Requests {
|
||||
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")!
|
||||
static let addIdentity = Data(base64Encoded: "AAAAARE=")!
|
||||
static let requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
|
||||
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
|
||||
}
|
||||
|
||||
enum Responses {
|
||||
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")!
|
||||
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABKwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0")!
|
||||
static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
|
||||
}
|
||||
|
||||
enum Secrets {
|
||||
static let ecdsa256Secret = Stub.Secret(keySize: 256, publicKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08=")!, privateKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy09nw780wy/TSfUmzj15iJkV234AaCLNl+H8qFL6qK8VIg==")!)
|
||||
static let ecdsa384Secret = Stub.Secret(keySize: 384, publicKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDA==")!, privateKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDHNapAOzrt9E+9QC4/KYoXS7Uw4pmdAz53uIj02tttiq3c0ZyIQ7XoscWWRqRrz8Kw==")!)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// SecretAgentKitTests.swift
|
||||
// SecretAgentKitTests
|
||||
//
|
||||
// Created by Max Goedjen on 2/22/20.
|
||||
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SecretAgentKit
|
||||
|
||||
class SecretAgentKitTests: XCTestCase {
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
}
|
14
SecretAgentKitTests/StubFileHandleReader.swift
Normal file
14
SecretAgentKitTests/StubFileHandleReader.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import SecretAgentKit
|
||||
import AppKit
|
||||
|
||||
struct StubFileHandleReader: FileHandleReader {
|
||||
|
||||
let availableData: Data
|
||||
var fileDescriptor: Int32 {
|
||||
NSWorkspace.shared.runningApplications.filter({ $0.localizedName == "Finder" }).first!.processIdentifier
|
||||
}
|
||||
var pidOfConnectedProcess: Int32 {
|
||||
fileDescriptor
|
||||
}
|
||||
|
||||
}
|
11
SecretAgentKitTests/StubFileHandleWriter.swift
Normal file
11
SecretAgentKitTests/StubFileHandleWriter.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import SecretAgentKit
|
||||
|
||||
class StubFileHandleWriter: FileHandleWriter {
|
||||
|
||||
var data = Data()
|
||||
|
||||
func write(_ data: Data) {
|
||||
self.data.append(data)
|
||||
}
|
||||
|
||||
}
|
113
SecretAgentKitTests/StubStore.swift
Normal file
113
SecretAgentKitTests/StubStore.swift
Normal file
@ -0,0 +1,113 @@
|
||||
import SecretKit
|
||||
import CryptoKit
|
||||
|
||||
struct Stub {}
|
||||
|
||||
extension Stub {
|
||||
|
||||
public class Store: SecretStore {
|
||||
|
||||
public let isAvailable = true
|
||||
public let id = UUID()
|
||||
public let name = "Stub"
|
||||
public var secrets: [Secret] = []
|
||||
public var shouldThrow = false
|
||||
|
||||
public init() {
|
||||
// try! create(size: 256)
|
||||
// try! create(size: 384)
|
||||
}
|
||||
|
||||
public func create(size: Int) throws {
|
||||
let flags: SecAccessControlCreateFlags = []
|
||||
let access =
|
||||
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||
flags,
|
||||
nil) as Any
|
||||
|
||||
let attributes = [
|
||||
kSecAttrLabel: name,
|
||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeySizeInBits: size,
|
||||
kSecPrivateKeyAttrs: [
|
||||
kSecAttrIsPermanent: true,
|
||||
kSecAttrAccessControl: access
|
||||
]
|
||||
] as CFDictionary
|
||||
|
||||
var privateKey: SecKey! = nil
|
||||
var publicKey: SecKey! = nil
|
||||
SecKeyGeneratePair(attributes, &publicKey, &privateKey)
|
||||
let publicAttributes = SecKeyCopyAttributes(publicKey) as! [CFString: Any]
|
||||
let privateAttributes = SecKeyCopyAttributes(privateKey) as! [CFString: Any]
|
||||
let publicData = (publicAttributes[kSecValueData] as! Data)
|
||||
let privateData = (privateAttributes[kSecValueData] as! Data)
|
||||
let secret = Secret(keySize: size, publicKey: publicData, privateKey: privateData)
|
||||
print(secret)
|
||||
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
|
||||
}
|
||||
|
||||
public func sign(data: Data, with secret: Secret) throws -> Data {
|
||||
guard !shouldThrow else {
|
||||
throw NSError()
|
||||
}
|
||||
let privateKey = SecKeyCreateWithData(secret.privateKey as CFData, [
|
||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeySizeInBits: secret.keySize,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate
|
||||
] as CFDictionary
|
||||
, nil)!
|
||||
let signatureAlgorithm: SecKeyAlgorithm
|
||||
switch secret.keySize {
|
||||
case 256:
|
||||
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
|
||||
case 384:
|
||||
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Stub {
|
||||
|
||||
struct Secret: SecretKit.Secret, CustomDebugStringConvertible {
|
||||
|
||||
let id = UUID().uuidString.data(using: .utf8)!
|
||||
let name = UUID().uuidString
|
||||
let algorithm = Algorithm.ellipticCurve
|
||||
|
||||
let keySize: Int
|
||||
let publicKey: Data
|
||||
let privateKey: Data
|
||||
|
||||
init(keySize: Int, publicKey: Data, privateKey: Data) {
|
||||
self.keySize = keySize
|
||||
self.publicKey = publicKey
|
||||
self.privateKey = privateKey
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Key Size \(keySize)
|
||||
Private: \(privateKey.base64EncodedString())
|
||||
Public: \(publicKey.base64EncodedString())
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension Stub.Store {
|
||||
|
||||
struct StubError: Error {
|
||||
}
|
||||
|
||||
}
|
32
SecretAgentKitTests/StubWitness.swift
Normal file
32
SecretAgentKitTests/StubWitness.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
|
||||
struct StubWitness {
|
||||
|
||||
let speakNow: (AnySecret, SigningRequestProvenance) -> Bool
|
||||
let witness: (AnySecret, SigningRequestProvenance) -> ()
|
||||
|
||||
}
|
||||
|
||||
extension StubWitness: SigningWitness {
|
||||
|
||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||
let objection = speakNow(secret, provenance)
|
||||
if objection {
|
||||
throw TheresMyChance()
|
||||
}
|
||||
}
|
||||
|
||||
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||
witness(secret, provenance)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StubWitness {
|
||||
|
||||
struct TheresMyChance: Error {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -8,7 +8,7 @@ public class OpenSSHReader {
|
||||
remaining = Data(data)
|
||||
}
|
||||
|
||||
public func readNextChunk() throws -> Data {
|
||||
public func readNextChunk() -> Data {
|
||||
let lengthRange = 0..<(UInt32.bitWidth/8)
|
||||
let lengthChunk = remaining[lengthRange]
|
||||
remaining.removeSubrange(lengthRange)
|
||||
|
@ -20,6 +20,10 @@ public class SecretStoreList: ObservableObject {
|
||||
addInternal(store: modifiable)
|
||||
}
|
||||
|
||||
public var anyAvailable: Bool {
|
||||
stores.reduce(false, { $0 || $1.isAvailable })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SecretStoreList {
|
||||
|
@ -7,7 +7,7 @@ public protocol Secret: Identifiable, Hashable {
|
||||
|
||||
}
|
||||
|
||||
public enum Algorithm {
|
||||
public enum Algorithm: Hashable {
|
||||
case ellipticCurve
|
||||
public init(secAttr: NSNumber) {
|
||||
let secAttrString = secAttr.stringValue as CFString
|
@ -19,6 +19,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Max Goedjen. All rights reserved.</string>
|
||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -1,11 +1,3 @@
|
||||
//
|
||||
// SecretKit.h
|
||||
// SecretKit
|
||||
//
|
||||
// Created by Max Goedjen on 2/18/20.
|
||||
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for SecretKit.
|
||||
|
17
SecretKitTests/AnySecretTests.swift
Normal file
17
SecretKitTests/AnySecretTests.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import SecretKit
|
||||
|
||||
class AnySecretTests: XCTestCase {
|
||||
|
||||
func testEraser() {
|
||||
let secret = SmartCard.Secret(id: UUID().uuidString.data(using: .utf8)!, name: "Name", algorithm: .ellipticCurve, keySize: 256, publicKey: UUID().uuidString.data(using: .utf8)!)
|
||||
let erased = AnySecret(secret)
|
||||
XCTAssert(erased.id == secret.id as AnyHashable)
|
||||
XCTAssert(erased.name == secret.name)
|
||||
XCTAssert(erased.algorithm == secret.algorithm)
|
||||
XCTAssert(erased.keySize == secret.keySize)
|
||||
XCTAssert(erased.publicKey == secret.publicKey)
|
||||
}
|
||||
|
||||
}
|
25
SecretKitTests/OpenSSHReaderTests.swift
Normal file
25
SecretKitTests/OpenSSHReaderTests.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import SecretKit
|
||||
|
||||
class OpenSSHReaderTests: XCTestCase {
|
||||
|
||||
func testSignatureRequest() {
|
||||
let reader = OpenSSHReader(data: Constants.signatureRequest)
|
||||
let hash = reader.readNextChunk()
|
||||
XCTAssert(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ=="))
|
||||
let dataToSign = reader.readNextChunk()
|
||||
XCTAssert(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU="))
|
||||
let empty = reader.readNextChunk()
|
||||
XCTAssert(empty.isEmpty)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OpenSSHReaderTests {
|
||||
|
||||
enum Constants {
|
||||
static let signatureRequest = Data(base64Encoded: "AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QUAAADvAAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QUAAAAA")!
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,8 @@
|
||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||
50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; };
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
||||
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* AppDelegate.swift */; };
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
|
||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
||||
@ -27,17 +29,29 @@
|
||||
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
|
||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
||||
506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
506773022426F3F400034DED /* Brief.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 506772FB2426F3F400034DED /* Brief.framework */; };
|
||||
506773032426F3F400034DED /* Brief.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 506772FB2426F3F400034DED /* Brief.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
506773092426F3FD00034DED /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506773082426F3FD00034DED /* Updater.swift */; };
|
||||
5067730C2426F40E00034DED /* Brief.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 506772FB2426F3F400034DED /* Brief.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
5067730E242701BA00034DED /* OpenSSHReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067730D242701BA00034DED /* OpenSSHReaderTests.swift */; };
|
||||
506773102427057600034DED /* AnySecretTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067730F2427057600034DED /* AnySecretTests.swift */; };
|
||||
5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5068389D241471CD00F55094 /* SecretStoreList.swift */; };
|
||||
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 */; };
|
||||
507CE4ED2420A3C70029F750 /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79F24026B9900D209EA /* Agent.swift */; };
|
||||
507CE4EE2420A3CA0029F750 /* SocketController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79D24026B9900D209EA /* SocketController.swift */; };
|
||||
507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4EF2420A4C50029F750 /* SigningWitness.swift */; };
|
||||
507CE4F42420A8C10029F750 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
|
||||
507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */; };
|
||||
507EE34224281E12003C4FE3 /* FileHandleProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */; };
|
||||
507EE34624281F89003C4FE3 /* StubFileHandleReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34524281F89003C4FE3 /* StubFileHandleReader.swift */; };
|
||||
507EE34824281FB8003C4FE3 /* StubFileHandleWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34724281FB8003C4FE3 /* StubFileHandleWriter.swift */; };
|
||||
507EE34A2428263B003C4FE3 /* StubStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE3492428263B003C4FE3 /* StubStore.swift */; };
|
||||
507EE34E2428784F003C4FE3 /* StubWitness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34D2428784F003C4FE3 /* StubWitness.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 */; };
|
||||
@ -49,7 +63,7 @@
|
||||
5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02A23FE352C0062B6F2 /* SmartCardSecret.swift */; };
|
||||
5099A02E23FE56E10062B6F2 /* OpenSSHKeyWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02D23FE56E10062B6F2 /* OpenSSHKeyWriter.swift */; };
|
||||
5099A075240242BA0062B6F2 /* SecretAgentKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5099A06C240242BA0062B6F2 /* SecretAgentKit.framework */; };
|
||||
5099A07C240242BA0062B6F2 /* SecretAgentKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A07B240242BA0062B6F2 /* SecretAgentKitTests.swift */; };
|
||||
5099A07C240242BA0062B6F2 /* AgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A07B240242BA0062B6F2 /* AgentTests.swift */; };
|
||||
5099A07E240242BA0062B6F2 /* SecretAgentKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5099A06E240242BA0062B6F2 /* SecretAgentKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
5099A08A240242C20062B6F2 /* SSHAgentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */; };
|
||||
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79024026B7600D209EA /* Assets.xcassets */; };
|
||||
@ -102,6 +116,20 @@
|
||||
remoteGlobalIDString = 50617DA723FCE4AB0099B055;
|
||||
remoteInfo = SecretKit;
|
||||
};
|
||||
506773002426F3F400034DED /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 506772FA2426F3F400034DED;
|
||||
remoteInfo = Brief;
|
||||
};
|
||||
5067730A2426F40A00034DED /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 506772FA2426F3F400034DED;
|
||||
remoteInfo = Brief;
|
||||
};
|
||||
507CE4F12420A6B50029F750 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
|
||||
@ -126,6 +154,7 @@
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
50617DBE23FCE4AB0099B055 /* SecretKit.framework in Embed Frameworks */,
|
||||
506773032426F3F400034DED /* Brief.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -156,6 +185,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
5067730C2426F40E00034DED /* Brief.framework in Embed Frameworks */,
|
||||
50A5C18D240E4B4B00E2996C /* SecretAgentKit.framework in Embed Frameworks */,
|
||||
50A5C190240E4B4C00E2996C /* SecretKit.framework in Embed Frameworks */,
|
||||
);
|
||||
@ -178,6 +208,8 @@
|
||||
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.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>"; };
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
|
||||
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
50617D8223FCE48E0099B055 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
@ -201,14 +233,25 @@
|
||||
50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
|
||||
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; };
|
||||
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
||||
506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
506772FD2426F3F400034DED /* Brief.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Brief.h; sourceTree = "<group>"; };
|
||||
506772FE2426F3F400034DED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
506773082426F3FD00034DED /* Updater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
|
||||
5067730D242701BA00034DED /* OpenSSHReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHReaderTests.swift; sourceTree = "<group>"; };
|
||||
5067730F2427057600034DED /* AnySecretTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretTests.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
507CE4EF2420A4C50029F750 /* SigningWitness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningWitness.swift; sourceTree = "<group>"; };
|
||||
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestProvenance.swift; sourceTree = "<group>"; };
|
||||
507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestTracer.swift; sourceTree = "<group>"; };
|
||||
507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHandleProtocols.swift; sourceTree = "<group>"; };
|
||||
507EE34524281F89003C4FE3 /* StubFileHandleReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubFileHandleReader.swift; sourceTree = "<group>"; };
|
||||
507EE34724281FB8003C4FE3 /* StubFileHandleWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubFileHandleWriter.swift; sourceTree = "<group>"; };
|
||||
507EE3492428263B003C4FE3 /* StubStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubStore.swift; sourceTree = "<group>"; };
|
||||
507EE34D2428784F003C4FE3 /* StubWitness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubWitness.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>"; };
|
||||
@ -223,7 +266,7 @@
|
||||
5099A06E240242BA0062B6F2 /* SecretAgentKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SecretAgentKit.h; sourceTree = "<group>"; };
|
||||
5099A06F240242BA0062B6F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5099A074240242BA0062B6F2 /* SecretAgentKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecretAgentKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5099A07B240242BA0062B6F2 /* SecretAgentKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretAgentKitTests.swift; sourceTree = "<group>"; };
|
||||
5099A07B240242BA0062B6F2 /* AgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentTests.swift; sourceTree = "<group>"; };
|
||||
5099A07D240242BA0062B6F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentProtocol.swift; sourceTree = "<group>"; };
|
||||
50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -247,6 +290,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50617DBD23FCE4AB0099B055 /* SecretKit.framework in Frameworks */,
|
||||
506773022426F3F400034DED /* Brief.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -272,6 +316,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
506772F82426F3F400034DED /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5099A069240242BA0062B6F2 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -299,6 +350,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
504BA92D243171F20064740E /* Types */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50617DCA23FCECA10099B055 /* Secret.swift */,
|
||||
50617DC623FCE4EA0099B055 /* SecretStore.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
50617D7623FCE48D0099B055 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -310,6 +370,7 @@
|
||||
5099A06D240242BA0062B6F2 /* SecretAgentKit */,
|
||||
5099A07A240242BA0062B6F2 /* SecretAgentKitTests */,
|
||||
508A58AF241E144C0069DC07 /* Config */,
|
||||
506772FC2426F3F400034DED /* Brief */,
|
||||
50617D8023FCE48E0099B055 /* Products */,
|
||||
5099A08B240243730062B6F2 /* Frameworks */,
|
||||
);
|
||||
@ -325,6 +386,7 @@
|
||||
5099A06C240242BA0062B6F2 /* SecretAgentKit.framework */,
|
||||
5099A074240242BA0062B6F2 /* SecretAgentKitTests.xctest */,
|
||||
50A3B78A24026B7500D209EA /* SecretAgent.app */,
|
||||
506772FB2426F3F400034DED /* Brief.framework */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -369,8 +431,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50617DAA23FCE4AB0099B055 /* SecretKit.h */,
|
||||
50617DCA23FCECA10099B055 /* Secret.swift */,
|
||||
50617DC623FCE4EA0099B055 /* SecretStore.swift */,
|
||||
5099A02C23FE56D70062B6F2 /* Common */,
|
||||
50617DCC23FCECEE0099B055 /* SecureEnclave */,
|
||||
5099A02523FE34DE0062B6F2 /* SmartCard */,
|
||||
@ -383,6 +443,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */,
|
||||
5067730D242701BA00034DED /* OpenSSHReaderTests.swift */,
|
||||
5067730F2427057600034DED /* AnySecretTests.swift */,
|
||||
50617DB923FCE4AB0099B055 /* Info.plist */,
|
||||
);
|
||||
path = SecretKitTests;
|
||||
@ -398,6 +460,16 @@
|
||||
path = SecureEnclave;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
506772FC2426F3F400034DED /* Brief */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
506773082426F3FD00034DED /* Updater.swift */,
|
||||
506772FD2426F3F400034DED /* Brief.h */,
|
||||
506772FE2426F3F400034DED /* Info.plist */,
|
||||
);
|
||||
path = Brief;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5068389F2415EA4F00F55094 /* Erasers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -434,6 +506,7 @@
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||
50C385A8240B636500AF2719 /* SetupView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
@ -442,8 +515,9 @@
|
||||
508A58B1241ED1EA0069DC07 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50731665241DF8660023809E /* Updater.swift */,
|
||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
@ -461,6 +535,7 @@
|
||||
5099A02C23FE56D70062B6F2 /* Common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504BA92D243171F20064740E /* Types */,
|
||||
5068389F2415EA4F00F55094 /* Erasers */,
|
||||
506838A42415EA6800F55094 /* OpenSSH */,
|
||||
5068389D241471CD00F55094 /* SecretStoreList.swift */,
|
||||
@ -478,6 +553,7 @@
|
||||
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
|
||||
507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */,
|
||||
50A3B79F24026B9900D209EA /* Agent.swift */,
|
||||
507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */,
|
||||
5099A06F240242BA0062B6F2 /* Info.plist */,
|
||||
);
|
||||
path = SecretAgentKit;
|
||||
@ -486,7 +562,11 @@
|
||||
5099A07A240242BA0062B6F2 /* SecretAgentKitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5099A07B240242BA0062B6F2 /* SecretAgentKitTests.swift */,
|
||||
5099A07B240242BA0062B6F2 /* AgentTests.swift */,
|
||||
507EE34524281F89003C4FE3 /* StubFileHandleReader.swift */,
|
||||
507EE34724281FB8003C4FE3 /* StubFileHandleWriter.swift */,
|
||||
507EE34D2428784F003C4FE3 /* StubWitness.swift */,
|
||||
507EE3492428263B003C4FE3 /* StubStore.swift */,
|
||||
5099A07D240242BA0062B6F2 /* Info.plist */,
|
||||
);
|
||||
path = SecretAgentKitTests;
|
||||
@ -532,6 +612,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
506772F62426F3F400034DED /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
506772FF2426F3F400034DED /* Brief.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5099A067240242BA0062B6F2 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -557,6 +645,7 @@
|
||||
);
|
||||
dependencies = (
|
||||
50617DBC23FCE4AB0099B055 /* PBXTargetDependency */,
|
||||
506773012426F3F400034DED /* PBXTargetDependency */,
|
||||
);
|
||||
name = Secretive;
|
||||
productName = Secretive;
|
||||
@ -618,6 +707,24 @@
|
||||
productReference = 50617DB023FCE4AB0099B055 /* SecretKitTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
506772FA2426F3F400034DED /* Brief */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 506773042426F3F400034DED /* Build configuration list for PBXNativeTarget "Brief" */;
|
||||
buildPhases = (
|
||||
506772F62426F3F400034DED /* Headers */,
|
||||
506772F72426F3F400034DED /* Sources */,
|
||||
506772F82426F3F400034DED /* Frameworks */,
|
||||
506772F92426F3F400034DED /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Brief;
|
||||
productName = Brief;
|
||||
productReference = 506772FB2426F3F400034DED /* Brief.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
5099A06B240242BA0062B6F2 /* SecretAgentKit */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 5099A083240242BA0062B6F2 /* Build configuration list for PBXNativeTarget "SecretAgentKit" */;
|
||||
@ -670,6 +777,7 @@
|
||||
dependencies = (
|
||||
5018F5492402736A002EB505 /* PBXTargetDependency */,
|
||||
5018F54B2402736A002EB505 /* PBXTargetDependency */,
|
||||
5067730B2426F40A00034DED /* PBXTargetDependency */,
|
||||
);
|
||||
name = SecretAgent;
|
||||
productName = SecretAgent;
|
||||
@ -700,6 +808,10 @@
|
||||
50617DAF23FCE4AB0099B055 = {
|
||||
CreatedOnToolsVersion = 11.3;
|
||||
};
|
||||
506772FA2426F3F400034DED = {
|
||||
CreatedOnToolsVersion = 11.4;
|
||||
LastSwiftMigration = 1140;
|
||||
};
|
||||
5099A06B240242BA0062B6F2 = {
|
||||
CreatedOnToolsVersion = 11.4;
|
||||
LastSwiftMigration = 1140;
|
||||
@ -732,6 +844,7 @@
|
||||
50617DAF23FCE4AB0099B055 /* SecretKitTests */,
|
||||
5099A06B240242BA0062B6F2 /* SecretAgentKit */,
|
||||
5099A073240242BA0062B6F2 /* SecretAgentKitTests */,
|
||||
506772FA2426F3F400034DED /* Brief */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -769,6 +882,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
506772F92426F3F400034DED /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5099A06A240242BA0062B6F2 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -802,15 +922,17 @@
|
||||
files = (
|
||||
50C385A9240B636500AF2719 /* SetupView.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.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 */,
|
||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
|
||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
50731669241E00C20023809E /* NoticeView.swift in Sources */,
|
||||
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */,
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
|
||||
);
|
||||
@ -849,6 +971,16 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */,
|
||||
506773102427057600034DED /* AnySecretTests.swift in Sources */,
|
||||
5067730E242701BA00034DED /* OpenSSHReaderTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
506772F72426F3F400034DED /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
506773092426F3FD00034DED /* Updater.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -856,6 +988,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
507EE34224281E12003C4FE3 /* FileHandleProtocols.swift in Sources */,
|
||||
507CE4EE2420A3CA0029F750 /* SocketController.swift in Sources */,
|
||||
5099A08A240242C20062B6F2 /* SSHAgentProtocol.swift in Sources */,
|
||||
507CE4ED2420A3C70029F750 /* Agent.swift in Sources */,
|
||||
@ -869,7 +1002,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5099A07C240242BA0062B6F2 /* SecretAgentKitTests.swift in Sources */,
|
||||
507EE34E2428784F003C4FE3 /* StubWitness.swift in Sources */,
|
||||
507EE34624281F89003C4FE3 /* StubFileHandleReader.swift in Sources */,
|
||||
507EE34A2428263B003C4FE3 /* StubStore.swift in Sources */,
|
||||
5099A07C240242BA0062B6F2 /* AgentTests.swift in Sources */,
|
||||
507EE34824281FB8003C4FE3 /* StubFileHandleWriter.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -910,6 +1047,16 @@
|
||||
target = 50617DA723FCE4AB0099B055 /* SecretKit */;
|
||||
targetProxy = 50617DBB23FCE4AB0099B055 /* PBXContainerItemProxy */;
|
||||
};
|
||||
506773012426F3F400034DED /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 506772FA2426F3F400034DED /* Brief */;
|
||||
targetProxy = 506773002426F3F400034DED /* PBXContainerItemProxy */;
|
||||
};
|
||||
5067730B2426F40A00034DED /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 506772FA2426F3F400034DED /* Brief */;
|
||||
targetProxy = 5067730A2426F40A00034DED /* PBXContainerItemProxy */;
|
||||
};
|
||||
507CE4F22420A6B50029F750 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 50617DA723FCE4AB0099B055 /* SecretKit */;
|
||||
@ -1254,6 +1401,95 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
506773052426F3F400034DED /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Brief/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Brief;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
506773062426F3F400034DED /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Brief/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Brief;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
506773072426F3F400034DED /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Brief/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Brief;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
508A5914241EF1A00069DC07 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
|
||||
@ -1427,8 +1663,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
INFOPLIST_FILE = SecretKitTests/Info.plist;
|
||||
@ -1439,6 +1675,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretKitTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Test;
|
||||
@ -1477,7 +1714,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
INFOPLIST_FILE = SecretAgentKitTests/Info.plist;
|
||||
@ -1488,6 +1726,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretAgentKitTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Test;
|
||||
@ -1555,6 +1794,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
INFOPLIST_FILE = SecretAgentKitTests/Info.plist;
|
||||
@ -1573,6 +1813,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
INFOPLIST_FILE = SecretAgentKitTests/Info.plist;
|
||||
@ -1690,6 +1931,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
506773042426F3F400034DED /* Build configuration list for PBXNativeTarget "Brief" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
506773052426F3F400034DED /* Debug */,
|
||||
506773062426F3F400034DED /* Test */,
|
||||
506773072426F3F400034DED /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
5099A083240242BA0062B6F2 /* Build configuration list for PBXNativeTarget "SecretAgentKit" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
import Brief
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@ -16,6 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}()
|
||||
let updater = Updater()
|
||||
let agentStatusChecker = AgentStatusChecker()
|
||||
let justUpdatedChecker = JustUpdatedChecker()
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
let contentView = ContentView(storeList: storeList, updater: updater, agentStatusChecker: agentStatusChecker, runSetupBlock: { self.runSetup(sender: nil) })
|
||||
@ -39,6 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
newMenuItem.isEnabled = true
|
||||
}
|
||||
runSetupIfNeeded()
|
||||
relaunchAgentIfNeeded()
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
@ -66,7 +69,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
@IBAction func runSetup(sender: AnyObject?) {
|
||||
let setupWindow = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
|
||||
contentRect: NSRect(x: 0, y: 0, width: 0, height: 0),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered, defer: false)
|
||||
let setupView = SetupView() { success in
|
||||
@ -88,6 +91,12 @@ extension AppDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func relaunchAgentIfNeeded() {
|
||||
if agentStatusChecker.running && justUpdatedChecker.justUpdated {
|
||||
LaunchAgentController().relaunch()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
|
36
Secretive/Controllers/JustUpdatedChecker.swift
Normal file
36
Secretive/Controllers/JustUpdatedChecker.swift
Normal file
@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import AppKit
|
||||
|
||||
protocol JustUpdatedCheckerProtocol: ObservableObject {
|
||||
var justUpdated: Bool { get }
|
||||
}
|
||||
|
||||
class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
|
||||
|
||||
@Published var justUpdated: Bool = false
|
||||
|
||||
init() {
|
||||
check()
|
||||
}
|
||||
|
||||
func check() {
|
||||
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None"
|
||||
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
|
||||
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
|
||||
if lastBuild != currentBuild {
|
||||
justUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension JustUpdatedChecker {
|
||||
|
||||
enum Constants {
|
||||
static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild"
|
||||
}
|
||||
|
||||
}
|
19
Secretive/Controllers/LaunchAgentController.swift
Normal file
19
Secretive/Controllers/LaunchAgentController.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
import ServiceManagement
|
||||
|
||||
struct LaunchAgentController {
|
||||
|
||||
func install() -> Bool {
|
||||
setEnabled(true)
|
||||
}
|
||||
|
||||
func relaunch() {
|
||||
_ = setEnabled(false)
|
||||
_ = setEnabled(true)
|
||||
}
|
||||
|
||||
private func setEnabled(_ enabled: Bool) -> Bool {
|
||||
SMLoginItemSetEnabled("com.maxgoedjen.Secretive.SecretAgent" as CFString, enabled)
|
||||
}
|
||||
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
@ -5,19 +5,19 @@
|
||||
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6119\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive"}}{\fldrslt
|
||||
\f0\fs24 \cf0 https://github.com/maxgoedjen/secretive}}
|
||||
\f0\fs24 \cf0 GitHub Repository}}
|
||||
\f0\fs24 \
|
||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||
\cf0 \
|
||||
GITHUB_BUILD_URL\
|
||||
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
|
||||
\
|
||||
Special Thanks To:\
|
||||
https://github.com/bdash\
|
||||
https://github.com/danielctull\
|
||||
https://github.com/davedelong\
|
||||
https://github.com/esttorhe\
|
||||
https://github.com/joeblau\
|
||||
https://github.com/marksands\
|
||||
https://github.com/mergesort\
|
||||
https://github.com/phillco\
|
||||
https://github.com/zackdotcomputer}
|
||||
{\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}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/esttorhe"}}{\fldrslt Esteban Torres}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/joeblau"}}{\fldrslt Joe Blau}}\
|
||||
{\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}}}
|
@ -23,7 +23,7 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Max Goedjen. All rights reserved.</string>
|
||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import Brief
|
||||
|
||||
class PreviewUpdater: UpdaterProtocol {
|
||||
|
||||
@ -15,7 +16,9 @@ class PreviewUpdater: UpdaterProtocol {
|
||||
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func ignore(release: Release) {
|
||||
}
|
||||
}
|
||||
|
||||
extension PreviewUpdater {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
import Brief
|
||||
|
||||
struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View {
|
||||
|
||||
@ -20,6 +21,7 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
||||
if !agentStatusChecker.running {
|
||||
agentNotice()
|
||||
}
|
||||
if storeList.anyAvailable {
|
||||
NavigationView {
|
||||
List(selection: $active) {
|
||||
ForEach(storeList.stores) { store in
|
||||
@ -68,7 +70,10 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NoStoresView()
|
||||
}
|
||||
}.frame(minWidth: 640, minHeight: 320)
|
||||
}
|
||||
|
||||
func updateNotice() -> some View {
|
||||
@ -82,9 +87,13 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
||||
severity = .advisory
|
||||
text = "Update Available"
|
||||
}
|
||||
return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update") {
|
||||
NSWorkspace.shared.open(update.html_url)
|
||||
})
|
||||
let action = {
|
||||
_ = NSWorkspace.shared.open(update.html_url)
|
||||
}
|
||||
let ignoreAction = {
|
||||
updater.ignore(release: update)
|
||||
}
|
||||
return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update", action: action, secondaryActionTitle: "Ignore", secondaryAction: ignoreAction))
|
||||
}
|
||||
|
||||
func agentNotice() -> some View {
|
||||
|
29
Secretive/Views/NoStoresView.swift
Normal file
29
Secretive/Views/NoStoresView.swift
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// NoStoresView.swift
|
||||
// Secretive
|
||||
//
|
||||
// Created by Max Goedjen on 3/20/20.
|
||||
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NoStoresView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("No Secure Storage Available").bold()
|
||||
Text("Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted.")
|
||||
Button(action: {
|
||||
NSWorkspace.shared.open(URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
|
||||
}) {
|
||||
Text("If you're looking to add one to your Mac, the YubiKey 5 Series are great.")
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct NoStoresView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoStoresView()
|
||||
}
|
||||
}
|
@ -7,12 +7,28 @@ struct NoticeView: View {
|
||||
let severity: Severity
|
||||
let actionTitle: String?
|
||||
let action: (() -> Void)?
|
||||
let secondaryActionTitle: String?
|
||||
let secondaryAction: (() -> Void)?
|
||||
|
||||
public init(text: String, severity: NoticeView.Severity, actionTitle: String?, action: (() -> Void)?, secondaryActionTitle: String? = nil, secondaryAction: (() -> Void)? = nil) {
|
||||
self.text = text
|
||||
self.severity = severity
|
||||
self.actionTitle = actionTitle
|
||||
self.action = action
|
||||
self.secondaryActionTitle = secondaryActionTitle
|
||||
self.secondaryAction = secondaryAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(text).bold()
|
||||
Spacer()
|
||||
if action != nil {
|
||||
if secondaryAction != nil {
|
||||
Button(action: secondaryAction!) {
|
||||
Text(secondaryActionTitle!)
|
||||
}
|
||||
}
|
||||
Button(action: action!) {
|
||||
Text(actionTitle!)
|
||||
}
|
||||
|
@ -1,20 +1,19 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import ServiceManagement
|
||||
|
||||
struct SetupView: View {
|
||||
|
||||
|
||||
var completion: ((Bool) -> Void)?
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
SetupStepView<Spacer>(text: "Secretive needs to install a helper app to sign requests when the main app isn't running. This app is called \"SecretAgent\" and you might see it in Activity Manager from time to time.",
|
||||
index: 1,
|
||||
nestedView: nil,
|
||||
actionText: "Install") {
|
||||
index: 1,
|
||||
nestedView: nil,
|
||||
actionText: "Install") {
|
||||
self.installLaunchAgent()
|
||||
}
|
||||
SetupStepView(text: "You need to add a line to your shell config (.bashrc or .zshrc) telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.",
|
||||
SetupStepView(text: "Add this line to your shell config (.bashrc or .zshrc) telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.",
|
||||
index: 2,
|
||||
nestedView: SetupStepCommandView(text: Constants.socketPrompt),
|
||||
actionText: "Added") {
|
||||
@ -22,25 +21,25 @@ struct SetupView: View {
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: { self.completion?(true) }) {
|
||||
Text("Finish")
|
||||
}
|
||||
Button(action: { self.completion?(true) }) {
|
||||
Text("Finish")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}.frame(minWidth: 640, minHeight: 400)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct SetupStepView<NestedViewType: View>: View {
|
||||
|
||||
|
||||
let text: String
|
||||
let index: Int
|
||||
let nestedView: NestedViewType?
|
||||
@State var completed = false
|
||||
let actionText: String
|
||||
let action: (() -> Bool)
|
||||
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
HStack {
|
||||
@ -64,10 +63,8 @@ struct SetupStepView<NestedViewType: View>: View {
|
||||
Text(text)
|
||||
.opacity(completed ? 0.5 : 1)
|
||||
.lineLimit(nil)
|
||||
.frame(idealHeight: 0, maxHeight: .infinity)
|
||||
if nestedView != nil {
|
||||
Spacer()
|
||||
nestedView!
|
||||
nestedView!.padding()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@ -83,43 +80,58 @@ struct SetupStepView<NestedViewType: View>: View {
|
||||
}
|
||||
|
||||
struct SetupStepCommandView: View {
|
||||
|
||||
|
||||
let text: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.lineLimit(nil)
|
||||
.frame(idealHeight: 0, maxHeight: .infinity)
|
||||
.padding()
|
||||
.background(Color(white: 0, opacity: 0.10))
|
||||
.cornerRadius(10)
|
||||
.onDrag {
|
||||
return NSItemProvider(item: NSData(data: self.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
|
||||
VStack(alignment: .leading) {
|
||||
Text(text)
|
||||
.lineLimit(nil)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: copy) {
|
||||
Text("Copy")
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(white: 0, opacity: 0.10))
|
||||
.cornerRadius(10)
|
||||
.onDrag {
|
||||
return NSItemProvider(item: NSData(data: self.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func copy() {
|
||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SetupView {
|
||||
|
||||
|
||||
func installLaunchAgent() -> Bool {
|
||||
SMLoginItemSetEnabled("com.maxgoedjen.Secretive.SecretAgent" as CFString, true)
|
||||
LaunchAgentController().install()
|
||||
}
|
||||
|
||||
|
||||
func markAsDone() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SetupView {
|
||||
|
||||
|
||||
enum Constants {
|
||||
static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String
|
||||
static let socketPrompt = "export SSH_AUTH_SOCK=\(socketPath)"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
@ -1,11 +1,3 @@
|
||||
//
|
||||
// SecretiveTests.swift
|
||||
// SecretiveTests
|
||||
//
|
||||
// Created by Max Goedjen on 2/18/20.
|
||||
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Secretive
|
||||
|
||||
|
Reference in New Issue
Block a user