Compare commits

..

49 Commits

Author SHA1 Message Date
4dcc9b113d Agent Tests (#74) 2020-03-23 23:22:22 -07:00
1f7ebcfe75 MIT notice on brief 2020-03-23 21:19:40 -07:00
b090a96975 Add option to ignore non-critical updates (#73) 2020-03-22 15:26:29 -07:00
594d855f19 Move status badges. 2020-03-21 23:40:43 -07:00
89f0798576 Add status badges 2020-03-21 23:40:10 -07:00
0d74f6f561 Really fix signing for brief 2020-03-21 23:36:52 -07:00
efeb7f09a3 Fix brief signing 2020-03-21 23:33:51 -07:00
104199cf53 Add test 2020-03-21 19:37:08 -07:00
f214da98a4 Remove test asset. 2020-03-21 19:37:03 -07:00
95cf88d3ca Add type eraser tests. 2020-03-21 19:36:11 -07:00
04835fa437 Fix weird spacing 2020-03-21 19:28:49 -07:00
31dfba8265 Add tests for reader. 2020-03-21 19:28:08 -07:00
df8eedebd0 Adding notifications for updater (#70) 2020-03-21 18:43:26 -07:00
0a9ecb039e Minimum frames (#69) 2020-03-21 18:10:37 -07:00
2e9a3eb90d Merge branch 'master' of github.com:maxgoedjen/secretive 2020-03-21 18:09:44 -07:00
df599fbe62 Update readme screenshot 2020-03-21 17:56:29 -07:00
7c60e9b04b Improve subtitle 2020-03-21 17:55:31 -07:00
f7f182ec77 Merge branch 'master' of github.com:maxgoedjen/secretive 2020-03-21 17:52:56 -07:00
a0052e8395 Richer notifications (#68) 2020-03-21 17:52:51 -07:00
10d3fa150c Better copy 2020-03-20 21:23:15 -07:00
c5abca4099 Indent. 2020-03-20 21:14:51 -07:00
f674d47507 Merge branch 'master' of github.com:maxgoedjen/secretive 2020-03-20 21:08:48 -07:00
1e3ece600d Fixes #51. 2020-03-20 21:07:58 -07:00
7481498c5b Basic no stores view (#66) 2020-03-20 20:20:20 -07:00
a6fbcdbe55 MIT licensing notices 2020-03-19 21:36:25 -07:00
36e655e527 Better links 2020-03-19 21:33:57 -07:00
dd3acf5ae1 Remove redundant import 2020-03-19 21:28:27 -07:00
2257e1e190 Fix build number 2020-03-19 21:21:28 -07:00
fe16e4d9c4 Build and About page (#64)
* Adding credits txt

* Switch up build number
2020-03-19 21:16:14 -07:00
1caf65c68d Header cleanup 2020-03-18 20:04:48 -07:00
85eb4983bc Split witness call (fixes #62) 2020-03-18 20:04:24 -07:00
5a2d3ecc2e Fix dumb mistake. 2020-03-17 23:28:51 -07:00
32f0ed88f4 Validate code signature as well. 2020-03-17 22:59:03 -07:00
d35c58509b Hacky fix for #16 2020-03-17 20:41:37 -07:00
f810b185f0 Fix debug config for running 2020-03-17 20:34:32 -07:00
5ef1fe996b Add path to request trace (#61) 2020-03-17 01:23:49 -07:00
2b5fdf541d Request Attribution (#59) 2020-03-17 00:56:55 -07:00
4b66e874a7 Cleanup of agent (#58)
* Extract key selection.

* Moving agent and socket stuff to SecretAgentKit

* Cleanup of agent
2020-03-16 23:39:34 -07:00
aa52da2c04 OpenSSH writer tests (#57) 2020-03-16 23:08:48 -07:00
bd683b16f2 Fix #53 (#54) 2020-03-15 18:30:45 -07:00
017bdb85a4 Test infrastructure (#52)
* Adding tests target

* Adding test plan

* Fixing deps

* Add test workflow

* Add config

* More

* Change workflow

* Add test step to release

* Signing changes

* Signing changes

* Remove 11.4 specific test

* More signing
2020-03-15 17:04:20 -07:00
99c8562cf5 Set Up -> Setup 2020-03-15 16:14:02 -07:00
12b920b0af Fix window close behavior (fixes #48) 2020-03-15 16:03:20 -07:00
bfb4f80b8c Self 2020-03-15 15:57:11 -07:00
d661b9002b Run status check on setup dismiss (fixes #49) 2020-03-15 15:56:21 -07:00
9daae8957a Add notice if agent isn't running (#47)
* Add agent checker
* Check on foreground
2020-03-15 14:36:07 -07:00
bde9085d31 Reorg 2020-03-15 14:13:28 -07:00
84521cf8d6 Fixes #44 2020-03-15 14:03:57 -07:00
4a6e8c0b11 Add run ID 2020-03-15 01:17:45 -07:00
59 changed files with 1802 additions and 298 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -5,6 +5,20 @@ on:
tags:
- '*'
jobs:
test:
runs-on: macOS-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v1
- name: Setup Signing
env:
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
run: ./.github/scripts/signing.sh
- name: Test
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
build:
runs-on: macOS-latest
timeout-minutes: 10
@ -18,7 +32,7 @@ jobs:
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: ''
body: "Build: https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}"
draft: true
prerelease: false
- name: Setup Signing
@ -31,9 +45,12 @@ jobs:
- name: Update Build Number
env:
TAG_NAME: ${{ github.ref }}
RUN_ID: ${{ github.run_id }}
run: |
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
sed -i '' -e "s/CI_VERSION = 0.0.0/CI_VERSION = $CLEAN_TAG/g" Config/Config.xcconfig
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Config/Config.xcconfig
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Config/Config.xcconfig
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Secretive/Credits.rtf
- name: Build
run: xcrun xcodebuild -project Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs

18
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Test
on: push
jobs:
test:
runs-on: macOS-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v1
- name: Setup Signing
env:
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
run: ./.github/scripts/signing.sh
- name: Test
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive

19
Brief/Brief.h Normal file
View 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
View 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>

View File

@ -1,17 +1,17 @@
import Foundation
import Combine
protocol UpdaterProtocol: ObservableObject {
public protocol UpdaterProtocol: ObservableObject {
var update: Release? { get }
}
class Updater: ObservableObject, UpdaterProtocol {
public class Updater: ObservableObject, UpdaterProtocol {
@Published var update: Release?
@Published public var update: Release?
init() {
public init() {
checkForUpdates()
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
self.checkForUpdates()
@ -19,7 +19,7 @@ class Updater: ObservableObject, UpdaterProtocol {
timer.tolerance = 60*60
}
func checkForUpdates() {
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 }
@ -27,7 +27,20 @@ class Updater: ObservableObject, UpdaterProtocol {
}.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(from: release.name)
let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
for (latest, current) in zip(latestVersion, currentVersion) {
@ -48,6 +61,14 @@ class Updater: ObservableObject, UpdaterProtocol {
return split
}
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")!
}
}
extension Updater {
@ -58,16 +79,24 @@ extension Updater {
}
struct Release: Codable {
let name: String
let html_url: URL
let body: String
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 {
var critical: Bool {
public var critical: Bool {
return body.contains(Constants.securityContent)
}

View File

@ -1 +1,2 @@
CI_VERSION = 0.0.0
CI_VERSION = GITHUB_CI_VERSION
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER

View File

@ -0,0 +1,41 @@
{
"configurations" : [
{
"id" : "5896AE5A-6D5A-48D3-837B-668B646A3273",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:Secretive.xcodeproj",
"identifier" : "50617DAF23FCE4AB0099B055",
"name" : "SecretKitTests"
}
},
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:Secretive.xcodeproj",
"identifier" : "5099A073240242BA0062B6F2",
"name" : "SecretAgentKitTests"
}
},
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:Secretive.xcodeproj",
"identifier" : "50617D9323FCE48E0099B055",
"name" : "SecretiveTests"
}
}
],
"version" : 1
}

View File

@ -1,9 +1,11 @@
# Secretive
# Secretive ![Test](https://github.com/maxgoedjen/secretive/workflows/Test/badge.svg) ![Release](https://github.com/maxgoedjen/secretive/workflows/Release/badge.svg)
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

View File

@ -1,6 +1,9 @@
import Cocoa
import SecretKit
import OSLog
import Combine
import SecretKit
import SecretAgentKit
import Brief
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@ -11,25 +14,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store())
return list
}()
let updater = Updater()
let notifier = Notifier()
lazy var agent: Agent = {
Agent(storeList: storeList, notifier: notifier)
Agent(storeList: storeList, witness: notifier)
}()
lazy var socketController: SocketController = {
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:))
}
}

View File

@ -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>

View File

@ -1,22 +1,123 @@
import Foundation
import SecretKit
import UserNotifications
import AppKit
import SecretKit
import SecretAgentKit
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.updateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
}
func prompt() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.requestAuthorization(options: .alert) { _, _ in
}
}
func notify<SecretType: Secret>(accessTo secret: SecretType) {
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."
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 {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
}
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
notify(accessTo: secret, by: provenance)
}
}
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)
}
}

View File

@ -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>

View File

@ -2,45 +2,53 @@ import Foundation
import CryptoKit
import OSLog
import SecretKit
import SecretAgentKit
import AppKit
class Agent {
public class Agent {
fileprivate let storeList: SecretStoreList
fileprivate let notifier: Notifier
fileprivate let witness: SigningWitness?
fileprivate let writer = OpenSSHKeyWriter()
fileprivate let requestTracer = SigningRequestTracer()
public init(storeList: SecretStoreList, notifier: Notifier) {
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
os_log(.debug, "Agent is running")
self.storeList = storeList
self.notifier = notifier
self.witness = witness
}
}
extension Agent {
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))
response.append(try sign(data: data, provenance: provenance))
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentSignResponse.debugDescription)
}
} catch {
@ -49,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)
@ -74,26 +82,21 @@ extension Agent {
return countData + keyData
}
func sign(data: Data) throws -> Data {
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
let reader = OpenSSHReader(data: data)
let writer = OpenSSHKeyWriter()
let hash = try reader.readNextChunk()
let matching = storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
let allMatching = store.secrets.filter { secret in
hash == writer.data(secret: secret)
}
if let matching = allMatching.first {
return (store, matching)
}
return nil
}
guard let (store, secret) = matching.first else {
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 dataToSign = try reader.readNextChunk()
if let witness = witness {
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, by: provenance)
}
let dataToSign = reader.readNextChunk()
let derSignature = try store.sign(data: dataToSign, with: secret)
// TODO: Move this
notifier.notify(accessTo: secret)
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
// Convert from DER formatted rep to raw (r||s)
@ -105,7 +108,7 @@ extension Agent {
case (.ellipticCurve, 384):
rawRepresentation = try CryptoKit.P384.Signing.ECDSASignature(derRepresentation: derSignature).rawRepresentation
default:
fatalError()
throw AgentError.unsupportedKeyType
}
@ -123,6 +126,10 @@ extension Agent {
sub.append(writer.lengthAndData(of: signatureChunk))
signedData.append(writer.lengthAndData(of: sub))
if let witness = witness {
try witness.witness(accessTo: secret, by: provenance)
}
os_log(.debug, "Agent signed request")
return signedData
@ -130,12 +137,29 @@ extension Agent {
}
extension Agent {
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
let allMatching = store.secrets.filter { secret in
hash == writer.data(secret: secret)
}
if let matching = allMatching.first {
return (store, matching)
}
return nil
}.first
}
}
extension Agent {
enum AgentError: Error {
case unhandledType
case noMatchingKey
case unsupportedKeyType
}
}

View 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)
}
}

View File

@ -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>

View File

@ -12,7 +12,7 @@ extension SSHAgent {
switch self {
case .requestIdentities:
return "RequestIdentities"
default:
case .signRequest:
return "SignRequest"
}
}

View File

@ -1,12 +1,14 @@
//
// 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>
// Forward declarations
// from libproc.h
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
// from SecTask.h
OSStatus SecCodeCreateWithPID(int32_t, SecCSFlags, SecCodeRef *);
//! Project version number for SecretAgentKit.
FOUNDATION_EXPORT double SecretAgentKitVersionNumber;
@ -14,6 +16,4 @@ FOUNDATION_EXPORT double SecretAgentKitVersionNumber;
//! Project version string for SecretAgentKit.
FOUNDATION_EXPORT const unsigned char SecretAgentKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SecretAgentKit/PublicHeader.h>

View File

@ -0,0 +1,45 @@
import Foundation
import AppKit
public struct SigningRequestProvenance: Equatable {
public var chain: [Process]
public init(root: Process) {
self.chain = [root]
}
}
extension SigningRequestProvenance {
public var origin: Process {
chain.last!
}
public var intact: Bool {
return chain.reduce(true) { $0 && $1.validSignature }
}
}
extension SigningRequestProvenance {
public struct Process: Equatable {
public let pid: Int32
public let name: String
public let path: String
public let validSignature: Bool
let parentPID: Int32?
init(pid: Int32, name: String, path: String, validSignature: Bool, parentPID: Int32?) {
self.pid = pid
self.name = name
self.path = path
self.validSignature = validSignature
self.parentPID = parentPID
}
}
}

View File

@ -0,0 +1,39 @@
import Foundation
import AppKit
import Security
struct SigningRequestTracer {
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 {
provenance.chain.append(process(from: provenance.origin.parentPID!))
}
return provenance
}
func pidAndNameInfo(from pid: Int32) -> kinfo_proc {
var len = MemoryLayout<kinfo_proc>.size
let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: 1)
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
sysctl(&name, UInt32(name.count), infoPointer, &len, nil, 0)
return infoPointer.load(as: kinfo_proc.self)
}
func process(from pid: Int32) -> SigningRequestProvenance.Process {
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
let procName = String(cString: &pidAndNameInfo.kp_proc.p_comm.0)
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
_ = proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
let path = String(cString: pathPointer)
var secCode: Unmanaged<SecCode>!
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
SecCodeCreateWithPID(pid, SecCSFlags(), &secCode)
let valid = SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess
return SigningRequestProvenance.Process(pid: pid, name: procName, path: path, validSignature: valid, parentPID: ppid)
}
}

View File

@ -0,0 +1,9 @@
import Foundation
import SecretKit
public protocol SigningWitness {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
}

View File

@ -1,13 +1,13 @@
import Foundation
import OSLog
class SocketController {
public class SocketController {
fileprivate var fileHandle: FileHandle?
fileprivate var port: SocketPort?
var handler: ((FileHandle) -> Void)?
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
init(path: String) {
public init(path: String) {
os_log(.debug, "Socket controller setting up at %@", path)
if let _ = try? FileManager.default.removeItem(atPath: path) {
os_log(.debug, "Socket controller removed existing socket")
@ -52,7 +52,7 @@ 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 @@ 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)
}
}

View File

@ -0,0 +1,162 @@
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)
let r = rsData.readNextChunk()
let s = rsData.readNextChunk()
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==")!)
}
}
}

View File

@ -1,34 +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 {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
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.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

View 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
}
}

View File

@ -0,0 +1,11 @@
import SecretAgentKit
class StubFileHandleWriter: FileHandleWriter {
var data = Data()
func write(_ data: Data) {
self.data.append(data)
}
}

View 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 {
}
}

View 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 {
}
}

View File

@ -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)

View File

@ -20,6 +20,10 @@ public class SecretStoreList: ObservableObject {
addInternal(store: modifiable)
}
public var anyAvailable: Bool {
stores.reduce(false, { $0 || $1.isAvailable })
}
}
extension SecretStoreList {

View File

@ -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>

View File

@ -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

View File

@ -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.

View File

@ -11,16 +11,17 @@ extension SmartCard {
// TODO: Read actual smart card name, eg "YubiKey 5c"
@Published public var isAvailable: Bool = false
public let id = UUID()
public let name = NSLocalizedString("Smart Card", comment: "Smart Card")
public fileprivate(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
@Published public fileprivate(set) var secrets: [Secret] = []
fileprivate let watcher = TKTokenWatcher()
fileprivate var tokenID: String?
public init() {
tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
tokenID = watcher.nonSecureEnclaveTokens.first
watcher.setInsertionHandler { string in
guard self.tokenID == nil else { return }
guard !string.contains("setoken") else { return }
self.tokenID = string
self.reloadSecrets()
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
@ -97,6 +98,14 @@ extension SmartCard.Store {
fileprivate func loadSecrets() {
guard let tokenID = tokenID else { return }
// Hack to read name if there's only one smart card
let slotNames = TKSmartCardSlotManager().slotNames
if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
name = slotNames.first!
} else {
name = NSLocalizedString("Smart Card", comment: "Smart Card")
}
let attributes = [
kSecClass: kSecClassKey,
kSecAttrTokenID: tokenID,
@ -124,6 +133,14 @@ extension SmartCard.Store {
}
extension TKTokenWatcher {
fileprivate var nonSecureEnclaveTokens: [String] {
tokenIDs.filter { !$0.contains("setoken") }
}
}
extension SmartCard {
public struct KeychainError: Error {

View 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)
}
}

View 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")!
}
}

View File

@ -0,0 +1,45 @@
import Foundation
import XCTest
@testable import SecretKit
class OpenSSHWriterTests: XCTestCase {
let writer = OpenSSHKeyWriter()
func testECDSA256Fingerprint() {
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
}
func testECDSA256PublicKey() {
XCTAssertEqual(writer.openSSHString(secret: Constants.ecdsa256Secret),
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")
}
func testECDSA256Hash() {
XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo="))
}
func testECDSA384Fingerprint() {
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e")
}
func testECDSA384PublicKey() {
XCTAssertEqual(writer.openSSHString(secret: Constants.ecdsa384Secret),
"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")
}
func testECDSA384Hash() {
XCTAssertEqual(writer.data(secret: Constants.ecdsa384Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ=="))
}
}
extension OpenSSHWriterTests {
enum Constants {
static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", algorithm: .ellipticCurve, keySize: 256, publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!)
static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", algorithm: .ellipticCurve, keySize: 384, publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!)
}
}

View File

@ -1,34 +0,0 @@
//
// SecretKitTests.swift
// SecretKitTests
//
// Created by Max Goedjen on 2/18/20.
// Copyright © 2020 Max Goedjen. All rights reserved.
//
import XCTest
@testable import SecretKit
class SecretKitTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -23,11 +23,47 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Config/Secretive.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5099A073240242BA0062B6F2"
BuildableName = "SecretAgentKitTests.xctest"
BlueprintName = "SecretAgentKitTests"
ReferencedContainer = "container:Secretive.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "50617DAF23FCE4AB0099B055"
BuildableName = "SecretKitTests.xctest"
BlueprintName = "SecretKitTests"
ReferencedContainer = "container:Secretive.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "50617D9323FCE48E0099B055"
BuildableName = "SecretiveTests.xctest"
BlueprintName = "SecretiveTests"
ReferencedContainer = "container:Secretive.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -23,11 +23,29 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Config/Secretive.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5099A073240242BA0062B6F2"
BuildableName = "SecretAgentKitTests.xctest"
BlueprintName = "SecretAgentKitTests"
ReferencedContainer = "container:Secretive.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -23,12 +23,16 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<TestPlans>
<TestPlanReference
reference = "container:Config/Secretive.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -23,10 +23,16 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Config/Secretive.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">

View File

@ -1,11 +1,13 @@
import Cocoa
import SwiftUI
import SecretKit
import Brief
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
@IBOutlet var newMenuItem: NSMenuItem!
@IBOutlet var toolbar: NSToolbar!
let storeList: SecretStoreList = {
let list = SecretStoreList()
@ -14,10 +16,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return list
}()
let updater = Updater()
let agentStatusChecker = AgentStatusChecker()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(storeList: storeList, updater: updater)
let contentView = ContentView(storeList: storeList, updater: updater, agentStatusChecker: agentStatusChecker, runSetupBlock: { self.runSetup(sender: nil) })
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
@ -29,15 +31,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.makeKeyAndOrderFront(nil)
window.titleVisibility = .hidden
window.toolbar = toolbar
window.isReleasedWhenClosed = false
if storeList.modifiableStore?.isAvailable ?? false {
let plus = NSTitlebarAccessoryViewController()
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
plus.layoutAttribute = .right
window.addTitlebarAccessoryViewController(plus)
newMenuItem.isEnabled = true
}
runSetupIfNeeded()
}
func applicationDidBecomeActive(_ notification: Notification) {
agentStatusChecker.check()
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
guard !flag else { return false }
window.makeKeyAndOrderFront(self)
return true
}
@IBAction func add(sender: AnyObject?) {
var addWindow: NSWindow!
let addView = CreateSecretView(store: storeList.modifiableStore!) {
@ -53,11 +67,12 @@ 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
self.window.endSheet(setupWindow)
self.agentStatusChecker.check()
}
setupWindow.contentView = NSHostingView(rootView: setupView)
window.beginSheet(setupWindow, completionHandler: nil)

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15702"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16085"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -9,7 +9,7 @@
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<menu key="mainMenu" title="Main Menu" systemMenu="main" autoenablesItems="NO" id="AYu-sK-qS6">
<items>
<menuItem title="Secretive" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
@ -57,9 +57,9 @@
</menuItem>
<menuItem title="File" id="dMs-cI-mzQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" id="bib-Uj-vzu">
<menu key="submenu" title="File" autoenablesItems="NO" id="bib-Uj-vzu">
<items>
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
<menuItem title="New" enabled="NO" keyEquivalent="n" id="Was-JA-tGl">
<connections>
<action selector="addWithSender:" target="Voe-Tx-rLC" id="U1t-YZ-Hn5"/>
</connections>
@ -102,7 +102,7 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="Set Up Helper App" id="04y-R6-7bF">
<menuItem title="Setup Helper App" id="04y-R6-7bF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="runSetupWithSender:" target="Voe-Tx-rLC" id="Fty-2m-eng"/>
@ -125,6 +125,7 @@
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Secretive" customModuleProvider="target">
<connections>
<outlet property="newMenuItem" destination="Was-JA-tGl" id="C8s-uk-gMA"/>
<outlet property="toolbar" destination="bvo-mt-QR4" id="XSF-g2-znt"/>
</connections>
</customObject>

View File

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

23
Secretive/Credits.rtf Normal file
View File

@ -0,0 +1,23 @@
{\rtf1\ansi\ansicpg1252\cocoartf2511
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\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 GitHub Repository}}
\f0\fs24 \
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
\cf0 \
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
\
Special Thanks To:\
{\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}}}

View File

@ -19,11 +19,11 @@
<key>CFBundleShortVersionString</key>
<string>$(CI_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CI_VERSION)</string>
<string>$(CI_BUILD_NUMBER)</string>
<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>

View File

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

View File

@ -1,5 +1,6 @@
import Foundation
import Combine
import Brief
class PreviewUpdater: UpdaterProtocol {

View File

@ -1,10 +1,13 @@
import SwiftUI
import SecretKit
import Brief
struct ContentView<UpdaterType: UpdaterProtocol>: View {
struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View {
@ObservedObject var storeList: SecretStoreList
@ObservedObject var updater: UpdaterType
@ObservedObject var agentStatusChecker: AgentStatusCheckerType
var runSetupBlock: (() -> Void)?
@State fileprivate var active: AnySecret.ID?
@State fileprivate var showingDeletion = false
@ -15,6 +18,10 @@ struct ContentView<UpdaterType: UpdaterProtocol>: View {
if updater.update != nil {
updateNotice()
}
if !agentStatusChecker.running {
agentNotice()
}
if storeList.anyAvailable {
NavigationView {
List(selection: $active) {
ForEach(storeList.stores) { store in
@ -47,13 +54,7 @@ struct ContentView<UpdaterType: UpdaterProtocol>: View {
}
}
}.onAppear {
let fallback: AnyHashable
if self.storeList.modifiableStore?.isAvailable ?? false {
fallback = Constants.emptyStoreModifiableTag
} else {
fallback = Constants.emptyStoreTag
}
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
self.active = self.nextDefaultSecret
}
.listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 240)
@ -61,12 +62,18 @@ struct ContentView<UpdaterType: UpdaterProtocol>: View {
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.sheet(isPresented: $showingDeletion) {
if self.storeList.modifiableStore != nil {
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) { deleted in
self.showingDeletion = false
if deleted {
self.active = self.nextDefaultSecret
}
}
}
}
}
} else {
NoStoresView()
}
}.frame(minWidth: 640, minHeight: 320)
}
func updateNotice() -> some View {
@ -85,10 +92,26 @@ struct ContentView<UpdaterType: UpdaterProtocol>: View {
})
}
func agentNotice() -> some View {
NoticeView(text: "Secret Agent isn't running. Run setup again to fix.", severity: .advisory, actionTitle: "Run Setup") {
self.runSetupBlock?()
}
}
func delete<SecretType: Secret>(secret: SecretType) {
deletingSecret = AnySecret(secret)
self.showingDeletion = true
}
var nextDefaultSecret: AnyHashable? {
let fallback: AnyHashable
if self.storeList.modifiableStore?.isAvailable ?? false {
fallback = Constants.emptyStoreModifiableTag
} else {
fallback = Constants.emptyStoreTag
}
return self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
}
}
@ -103,12 +126,22 @@ fileprivate enum Constants {
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater())
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater())
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory))
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical))
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)],
modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]),
updater: PreviewUpdater(),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater(),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical),
agentStatusChecker: PreviewAgentStatusChecker(running: false))
}
}
}

View File

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

View File

@ -0,0 +1,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()
}
}

View File

@ -3,18 +3,18 @@ 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 +22,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 +64,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 +81,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)
}
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

View File

@ -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
@ -24,11 +16,5 @@ class SecretiveTests: XCTestCase {
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}