commit
603d021939
|
@ -4,14 +4,14 @@ import OSLog
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
|
|
||||||
class Agent<StoreType: SecretStore> {
|
class Agent {
|
||||||
|
|
||||||
fileprivate let store: StoreType
|
fileprivate let storeList: SecretStoreList
|
||||||
fileprivate let notifier: Notifier
|
fileprivate let notifier: Notifier
|
||||||
|
|
||||||
public init(store: StoreType, notifier: Notifier) {
|
public init(storeList: SecretStoreList, notifier: Notifier) {
|
||||||
os_log(.debug, "Agent is running")
|
os_log(.debug, "Agent is running")
|
||||||
self.store = store
|
self.storeList = storeList
|
||||||
self.notifier = notifier
|
self.notifier = notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,17 +57,18 @@ extension Agent {
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
func identities() throws -> Data {
|
func identities() throws -> Data {
|
||||||
var count = UInt32(store.secrets.count).bigEndian
|
let secrets = storeList.stores.flatMap(\.secrets)
|
||||||
|
var count = UInt32(secrets.count).bigEndian
|
||||||
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
|
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
|
||||||
var keyData = Data()
|
var keyData = Data()
|
||||||
let writer = OpenSSHKeyWriter()
|
let writer = OpenSSHKeyWriter()
|
||||||
for secret in store.secrets {
|
for secret in secrets {
|
||||||
let keyBlob = writer.data(secret: secret)
|
let keyBlob = writer.data(secret: secret)
|
||||||
keyData.append(writer.lengthAndData(of: keyBlob))
|
keyData.append(writer.lengthAndData(of: keyBlob))
|
||||||
let curveData = OpenSSHKeyWriter.Constants.curveType.data(using: .utf8)!
|
let curveData = OpenSSHKeyWriter.Constants.curveType.data(using: .utf8)!
|
||||||
keyData.append(writer.lengthAndData(of: curveData))
|
keyData.append(writer.lengthAndData(of: curveData))
|
||||||
}
|
}
|
||||||
os_log(.debug, "Agent enumerated %@ identities", store.secrets.count as NSNumber)
|
os_log(.debug, "Agent enumerated %@ identities", secrets.count as NSNumber)
|
||||||
return countData + keyData
|
return countData + keyData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +76,16 @@ extension Agent {
|
||||||
let reader = OpenSSHReader(data: data)
|
let reader = OpenSSHReader(data: data)
|
||||||
let writer = OpenSSHKeyWriter()
|
let writer = OpenSSHKeyWriter()
|
||||||
let hash = try reader.readNextChunk()
|
let hash = try reader.readNextChunk()
|
||||||
let matching = store.secrets.filter { secret in
|
let matching = storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
||||||
hash == writer.data(secret: secret)
|
let allMatching = store.secrets.filter { secret in
|
||||||
|
hash == writer.data(secret: secret)
|
||||||
|
}
|
||||||
|
if let matching = allMatching.first {
|
||||||
|
return (store, matching)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
guard let secret = matching.first else {
|
guard let (store, secret) = matching.first else {
|
||||||
throw AgentError.noMatchingKey
|
throw AgentError.noMatchingKey
|
||||||
}
|
}
|
||||||
let dataToSign = try reader.readNextChunk()
|
let dataToSign = try reader.readNextChunk()
|
||||||
|
|
|
@ -5,10 +5,15 @@ import OSLog
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
let store = SecureEnclave.Store()
|
let storeList: SecretStoreList = {
|
||||||
|
let list = SecretStoreList()
|
||||||
|
list.add(store: SecureEnclave.Store())
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
let notifier = Notifier()
|
let notifier = Notifier()
|
||||||
lazy var agent: Agent = {
|
lazy var agent: Agent = {
|
||||||
Agent(store: store, notifier: notifier)
|
Agent(storeList: storeList, notifier: notifier)
|
||||||
}()
|
}()
|
||||||
lazy var socketController: SocketController = {
|
lazy var socketController: SocketController = {
|
||||||
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct AnySecret: Secret {
|
||||||
|
|
||||||
|
let base: Any
|
||||||
|
fileprivate let hashable: AnyHashable
|
||||||
|
fileprivate let _id: () -> AnyHashable
|
||||||
|
fileprivate let _name: () -> String
|
||||||
|
fileprivate let _publicKey: () -> Data
|
||||||
|
|
||||||
|
public init<T>(_ secret: T) where T: Secret {
|
||||||
|
if let secret = secret as? AnySecret {
|
||||||
|
base = secret.base
|
||||||
|
hashable = secret.hashable
|
||||||
|
_id = secret._id
|
||||||
|
_name = secret._name
|
||||||
|
_publicKey = secret._publicKey
|
||||||
|
} else {
|
||||||
|
base = secret as Any
|
||||||
|
self.hashable = secret
|
||||||
|
_id = { secret.id as AnyHashable }
|
||||||
|
_name = { secret.name }
|
||||||
|
_publicKey = { secret.publicKey }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var id: AnyHashable {
|
||||||
|
return _id()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String {
|
||||||
|
return _name()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var publicKey: Data {
|
||||||
|
return _publicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: AnySecret, rhs: AnySecret) -> Bool {
|
||||||
|
lhs.hashable == rhs.hashable
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hashable.hash(into: &hasher)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public class AnySecretStore: SecretStore {
|
||||||
|
|
||||||
|
let base: Any
|
||||||
|
fileprivate let _isAvailable: () -> Bool
|
||||||
|
fileprivate let _id: () -> UUID
|
||||||
|
fileprivate let _name: () -> String
|
||||||
|
fileprivate let _secrets: () -> [AnySecret]
|
||||||
|
fileprivate let _sign: (Data, AnySecret) throws -> Data
|
||||||
|
fileprivate var sink: AnyCancellable?
|
||||||
|
|
||||||
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
||||||
|
base = secretStore
|
||||||
|
_isAvailable = { secretStore.isAvailable }
|
||||||
|
_name = { secretStore.name }
|
||||||
|
_id = { secretStore.id }
|
||||||
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
|
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType) }
|
||||||
|
sink = secretStore.objectWillChange.sink { _ in
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAvailable: Bool {
|
||||||
|
return _isAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var id: UUID {
|
||||||
|
return _id()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String {
|
||||||
|
return _name()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var secrets: [AnySecret] {
|
||||||
|
return _secrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sign(data: Data, with secret: AnySecret) throws -> Data {
|
||||||
|
try _sign(data, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||||
|
|
||||||
|
fileprivate let _create: (String, Bool) throws -> Void
|
||||||
|
fileprivate let _delete: (AnySecret) throws -> Void
|
||||||
|
|
||||||
|
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||||
|
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
|
||||||
|
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
|
super.init(secretStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func create(name: String, requiresAuthentication: Bool) throws {
|
||||||
|
try _create(name, requiresAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func delete(secret: AnySecret) throws {
|
||||||
|
try _delete(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public class SecretStoreList: ObservableObject {
|
||||||
|
|
||||||
|
@Published public var stores: [AnySecretStore] = []
|
||||||
|
@Published public var modifiableStore: AnySecretStoreModifiable?
|
||||||
|
fileprivate var sinks: [AnyCancellable] = []
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
||||||
|
addInternal(store: AnySecretStore(store))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||||
|
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||||
|
modifiableStore = modifiable
|
||||||
|
addInternal(store: modifiable)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecretStoreList {
|
||||||
|
|
||||||
|
fileprivate func addInternal(store: AnySecretStore) {
|
||||||
|
stores.append(store)
|
||||||
|
let sink = store.objectWillChange.sink {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
sinks.append(sink)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,12 +1,21 @@
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
public protocol SecretStore: ObservableObject {
|
public protocol SecretStore: ObservableObject, Identifiable {
|
||||||
|
|
||||||
associatedtype SecretType: Secret
|
associatedtype SecretType: Secret
|
||||||
|
|
||||||
|
var isAvailable: Bool { get }
|
||||||
|
var id: UUID { get }
|
||||||
var name: String { get }
|
var name: String { get }
|
||||||
var secrets: [SecretType] { get }
|
var secrets: [SecretType] { get }
|
||||||
|
|
||||||
func sign(data: Data, with secret: SecretType) throws -> Data
|
func sign(data: Data, with secret: SecretType) throws -> Data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol SecretStoreModifiable: SecretStore {
|
||||||
|
|
||||||
|
func create(name: String, requiresAuthentication: Bool) throws
|
||||||
func delete(secret: SecretType) throws
|
func delete(secret: SecretType) throws
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
|
import CryptoTokenKit
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
public class Store: SecretStore {
|
public class Store: SecretStoreModifiable {
|
||||||
|
|
||||||
|
public var isAvailable: Bool {
|
||||||
|
// For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false
|
||||||
|
// error msg "Received error sending GET UNIQUE DEVICE command"
|
||||||
|
// Verify it with TKTokenWatcher manually.
|
||||||
|
return TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
|
||||||
|
}
|
||||||
|
public let id = UUID()
|
||||||
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
|
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
|
||||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||||
|
|
||||||
|
|
|
@ -9,19 +9,25 @@ extension SmartCard {
|
||||||
public class Store: SecretStore {
|
public class Store: SecretStore {
|
||||||
|
|
||||||
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
// 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 let name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||||
fileprivate let watcher = TKTokenWatcher()
|
fileprivate let watcher = TKTokenWatcher()
|
||||||
fileprivate var id: String?
|
fileprivate var tokenID: String?
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
id = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
|
tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
|
||||||
watcher.setInsertionHandler { string in
|
watcher.setInsertionHandler { string in
|
||||||
guard self.id == nil else { return }
|
guard self.tokenID == nil else { return }
|
||||||
guard !string.contains("setoken") else { return }
|
guard !string.contains("setoken") else { return }
|
||||||
self.id = string
|
self.tokenID = string
|
||||||
self.secrets.removeAll()
|
self.reloadSecrets()
|
||||||
self.loadSecrets()
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
||||||
|
}
|
||||||
|
if let tokenID = tokenID {
|
||||||
|
self.isAvailable = true
|
||||||
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
||||||
}
|
}
|
||||||
loadSecrets()
|
loadSecrets()
|
||||||
}
|
}
|
||||||
|
@ -37,12 +43,12 @@ extension SmartCard {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
||||||
guard let id = id else { fatalError() }
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
let attributes = [
|
let attributes = [
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
kSecAttrApplicationLabel: secret.id as CFData,
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
kSecAttrTokenID: id,
|
kSecAttrTokenID: tokenID,
|
||||||
kSecReturnRef: true
|
kSecReturnRef: true
|
||||||
] as CFDictionary
|
] as CFDictionary
|
||||||
var untyped: CFTypeRef?
|
var untyped: CFTypeRef?
|
||||||
|
@ -67,11 +73,26 @@ extension SmartCard {
|
||||||
|
|
||||||
extension SmartCard.Store {
|
extension SmartCard.Store {
|
||||||
|
|
||||||
|
fileprivate func smartcardRemoved(for tokenID: String? = nil) {
|
||||||
|
self.tokenID = nil
|
||||||
|
reloadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func reloadSecrets() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isAvailable = self.tokenID != nil
|
||||||
|
self.secrets.removeAll()
|
||||||
|
self.loadSecrets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func loadSecrets() {
|
fileprivate func loadSecrets() {
|
||||||
guard let id = id else { return }
|
guard let tokenID = tokenID else { return }
|
||||||
let attributes = [
|
let attributes = [
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrTokenID: id,
|
kSecAttrKeyType: kSecAttrKeyTypeEC,
|
||||||
|
kSecAttrKeySizeInBits: 256,
|
||||||
|
kSecAttrTokenID: tokenID,
|
||||||
kSecReturnRef: true,
|
kSecReturnRef: true,
|
||||||
kSecMatchLimit: kSecMatchLimitAll,
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
kSecReturnAttributes: true
|
kSecReturnAttributes: true
|
||||||
|
@ -81,12 +102,12 @@ extension SmartCard.Store {
|
||||||
guard let typed = untyped as? [[CFString: Any]] else { return }
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
let wrapped: [SmartCard.Secret] = typed.map {
|
let wrapped: [SmartCard.Secret] = typed.map {
|
||||||
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
||||||
let id = $0[kSecAttrApplicationLabel] as! Data
|
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
||||||
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
||||||
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
||||||
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
||||||
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
||||||
return SmartCard.Secret(id: id, name: name, publicKey: publicKey)
|
return SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey)
|
||||||
}
|
}
|
||||||
secrets.append(contentsOf: wrapped)
|
secrets.append(contentsOf: wrapped)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; };
|
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; };
|
||||||
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
|
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.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, ); }; };
|
506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
|
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
|
||||||
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
|
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
|
||||||
|
@ -172,6 +175,9 @@
|
||||||
50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; };
|
50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; };
|
||||||
50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
|
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>"; };
|
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.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>"; };
|
||||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
|
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
|
||||||
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
|
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
|
||||||
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; };
|
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; };
|
||||||
|
@ -193,7 +199,7 @@
|
||||||
50A3B79D24026B9900D209EA /* SocketController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketController.swift; sourceTree = "<group>"; };
|
50A3B79D24026B9900D209EA /* SocketController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketController.swift; sourceTree = "<group>"; };
|
||||||
50A3B79F24026B9900D209EA /* Agent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = "<group>"; };
|
50A3B79F24026B9900D209EA /* Agent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = "<group>"; };
|
||||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
|
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
|
||||||
50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretAgentKit/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; };
|
50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; };
|
||||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
||||||
50C385A8240B636500AF2719 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
50C385A8240B636500AF2719 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
@ -354,6 +360,24 @@
|
||||||
path = SecureEnclave;
|
path = SecureEnclave;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
5068389F2415EA4F00F55094 /* Erasers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
506838A02415EA5600F55094 /* AnySecret.swift */,
|
||||||
|
506838A22415EA5D00F55094 /* AnySecretStore.swift */,
|
||||||
|
);
|
||||||
|
path = Erasers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
506838A42415EA6800F55094 /* OpenSSH */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5099A02D23FE56E10062B6F2 /* OpenSSHKeyWriter.swift */,
|
||||||
|
50C385A2240789E600AF2719 /* OpenSSHReader.swift */,
|
||||||
|
);
|
||||||
|
path = OpenSSH;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
5099A02523FE34DE0062B6F2 /* SmartCard */ = {
|
5099A02523FE34DE0062B6F2 /* SmartCard */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -367,8 +391,9 @@
|
||||||
5099A02C23FE56D70062B6F2 /* Common */ = {
|
5099A02C23FE56D70062B6F2 /* Common */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5099A02D23FE56E10062B6F2 /* OpenSSHKeyWriter.swift */,
|
5068389F2415EA4F00F55094 /* Erasers */,
|
||||||
50C385A2240789E600AF2719 /* OpenSSHReader.swift */,
|
506838A42415EA6800F55094 /* OpenSSH */,
|
||||||
|
5068389D241471CD00F55094 /* SecretStoreList.swift */,
|
||||||
);
|
);
|
||||||
path = Common;
|
path = Common;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -729,8 +754,11 @@
|
||||||
50617DCB23FCECA10099B055 /* Secret.swift in Sources */,
|
50617DCB23FCECA10099B055 /* Secret.swift in Sources */,
|
||||||
5099A02E23FE56E10062B6F2 /* OpenSSHKeyWriter.swift in Sources */,
|
5099A02E23FE56E10062B6F2 /* OpenSSHKeyWriter.swift in Sources */,
|
||||||
50617DC923FCE50E0099B055 /* SecureEnclaveStore.swift in Sources */,
|
50617DC923FCE50E0099B055 /* SecureEnclaveStore.swift in Sources */,
|
||||||
|
506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */,
|
||||||
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */,
|
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */,
|
||||||
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */,
|
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */,
|
||||||
|
5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */,
|
||||||
|
506838A12415EA5600F55094 /* AnySecret.swift in Sources */,
|
||||||
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */,
|
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */,
|
||||||
5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */,
|
5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */,
|
||||||
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */,
|
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */,
|
||||||
|
|
|
@ -7,12 +7,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
var window: NSWindow!
|
var window: NSWindow!
|
||||||
@IBOutlet var toolbar: NSToolbar!
|
@IBOutlet var toolbar: NSToolbar!
|
||||||
let secureEnclave = SecureEnclave.Store()
|
let storeList: SecretStoreList = {
|
||||||
let smartCard = SmartCard.Store()
|
let list = SecretStoreList()
|
||||||
|
list.add(store: SecureEnclave.Store())
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
|
||||||
let contentView = ContentView(store: secureEnclave)
|
let contentView = ContentView(storeList: storeList)
|
||||||
// Create the window and set the content view.
|
// Create the window and set the content view.
|
||||||
window = NSWindow(
|
window = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
|
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
|
||||||
|
@ -24,16 +28,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
window.titleVisibility = .hidden
|
window.titleVisibility = .hidden
|
||||||
window.toolbar = toolbar
|
window.toolbar = toolbar
|
||||||
let plus = NSTitlebarAccessoryViewController()
|
if storeList.modifiableStore?.isAvailable ?? false {
|
||||||
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
|
let plus = NSTitlebarAccessoryViewController()
|
||||||
plus.layoutAttribute = .right
|
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
|
||||||
window.addTitlebarAccessoryViewController(plus)
|
plus.layoutAttribute = .right
|
||||||
|
window.addTitlebarAccessoryViewController(plus)
|
||||||
|
}
|
||||||
runSetupIfNeeded()
|
runSetupIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func add(sender: AnyObject?) {
|
@IBAction func add(sender: AnyObject?) {
|
||||||
var addWindow: NSWindow!
|
var addWindow: NSWindow!
|
||||||
let addView = CreateSecretView(store: secureEnclave) {
|
let addView = CreateSecretView(store: storeList.modifiableStore!) {
|
||||||
self.window.endSheet(addWindow)
|
self.window.endSheet(addWindow)
|
||||||
}
|
}
|
||||||
addWindow = NSWindow(
|
addWindow = NSWindow(
|
||||||
|
|
|
@ -3,45 +3,53 @@ import SecretKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@ObservedObject var store: SecureEnclave.Store
|
@ObservedObject var storeList: SecretStoreList
|
||||||
@State var active: SecureEnclave.Secret.ID?
|
@State var active: AnySecret.ID?
|
||||||
|
|
||||||
@State var showingDeletion = false
|
@State var showingDeletion = false
|
||||||
@State var deletingSecret: SecureEnclave.Secret?
|
@State var deletingSecret: AnySecret?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List(selection: $active) {
|
List(selection: $active) {
|
||||||
Section(header: Text(store.name)) {
|
ForEach(storeList.stores) { store in
|
||||||
ForEach(store.secrets) { secret in
|
if store.isAvailable {
|
||||||
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
|
Section(header: Text(store.name)) {
|
||||||
Text(secret.name)
|
ForEach(store.secrets) { secret in
|
||||||
}.contextMenu {
|
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
|
||||||
Button(action: { self.delete(secret: secret) }) {
|
Text(secret.name)
|
||||||
Text("Delete")
|
}.contextMenu {
|
||||||
|
if store is AnySecretStoreModifiable {
|
||||||
|
Button(action: { self.delete(secret: secret) }) {
|
||||||
|
Text("Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onAppear {
|
}.onAppear {
|
||||||
self.active = self.store.secrets.first?.id
|
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id
|
||||||
}
|
}
|
||||||
.listStyle(SidebarListStyle())
|
.listStyle(SidebarListStyle())
|
||||||
.frame(minWidth: 100, idealWidth: 240)
|
.frame(minWidth: 100, idealWidth: 240)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||||
.sheet(isPresented: $showingDeletion) {
|
.sheet(isPresented: $showingDeletion) {
|
||||||
DeleteSecretView(secret: self.deletingSecret!, store: self.store) {
|
if self.storeList.modifiableStore != nil {
|
||||||
self.showingDeletion = false
|
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
|
||||||
|
self.showingDeletion = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func delete(secret: SecureEnclave.Secret) {
|
func delete<SecretType: Secret>(secret: SecretType) {
|
||||||
deletingSecret = secret
|
deletingSecret = AnySecret(secret)
|
||||||
showingDeletion = true
|
self.showingDeletion = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import SecretKit
|
||||||
|
|
||||||
struct CreateSecretView: View {
|
struct CreateSecretView: View {
|
||||||
|
|
||||||
@ObservedObject var store: SecureEnclave.Store
|
@ObservedObject var store: AnySecretStoreModifiable
|
||||||
|
|
||||||
@State var name = ""
|
@State var name = ""
|
||||||
@State var requiresAuthentication = true
|
@State var requiresAuthentication = true
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
struct DeleteSecretView: View {
|
struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
||||||
|
|
||||||
let secret: SecureEnclave.Secret
|
let secret: StoreType.SecretType
|
||||||
@ObservedObject var store: SecureEnclave.Store
|
@ObservedObject var store: StoreType
|
||||||
|
|
||||||
@State var confirm = ""
|
@State var confirm = ""
|
||||||
|
|
||||||
fileprivate var dismissalBlock: () -> ()
|
fileprivate var dismissalBlock: () -> ()
|
||||||
|
|
||||||
init(secret: SecureEnclave.Secret, store: SecureEnclave.Store, dismissalBlock: @escaping () -> ()) {
|
init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping () -> ()) {
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
self.store = store
|
self.store = store
|
||||||
self.dismissalBlock = dismissalBlock
|
self.dismissalBlock = dismissalBlock
|
||||||
|
|
|
@ -19,6 +19,8 @@ extension Preview {
|
||||||
|
|
||||||
class Store: SecretStore, ObservableObject {
|
class Store: SecretStore, ObservableObject {
|
||||||
|
|
||||||
|
let isAvailable = true
|
||||||
|
let id = UUID()
|
||||||
let name = "Preview Store"
|
let name = "Preview Store"
|
||||||
@Published var secrets: [Secret] = []
|
@Published var secrets: [Secret] = []
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue