mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-05 09:24:49 +01:00
More progress.
This commit is contained in:
@@ -9,16 +9,26 @@ public protocol Secret: Identifiable, Hashable {
|
||||
|
||||
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 {
|
||||
self.hashable = secret
|
||||
_id = { secret.id as AnyHashable }
|
||||
_name = { secret.name }
|
||||
_publicKey = { secret.publicKey }
|
||||
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 {
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import Combine
|
||||
|
||||
public protocol SecretStore: ObservableObject {
|
||||
public protocol SecretStore: ObservableObject, Identifiable {
|
||||
|
||||
associatedtype SecretType: Secret
|
||||
|
||||
var isAvailable: Bool { get }
|
||||
var id: UUID { get }
|
||||
var name: String { get }
|
||||
var secrets: [SecretType] { get }
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
@@ -20,26 +28,30 @@ extension NSNotification.Name {
|
||||
|
||||
public class AnySecretStore: SecretStore {
|
||||
|
||||
fileprivate let base: Any
|
||||
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 let _delete: (AnySecret) throws -> Void
|
||||
|
||||
public init<T>(_ secretStore: T) where T: SecretStore {
|
||||
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 as! T.SecretType) }
|
||||
_delete = { try secretStore.delete(secret: $0 as! T.SecretType) }
|
||||
_sign = { try secretStore.sign(data: $0, with: $1 as! SecretStoreType.SecretType) }
|
||||
}
|
||||
|
||||
|
||||
public var isAvailable: Bool {
|
||||
return _isAvailable()
|
||||
}
|
||||
|
||||
public var id: UUID {
|
||||
return _id()
|
||||
}
|
||||
|
||||
public var name: String {
|
||||
return _name()
|
||||
}
|
||||
@@ -52,6 +64,23 @@ public class AnySecretStore: SecretStore {
|
||||
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)
|
||||
}
|
||||
|
||||
22
SecretKit/SecretStoreList.swift
Normal file
22
SecretKit/SecretStoreList.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public class SecretStoreList: ObservableObject {
|
||||
|
||||
@Published public var stores: [AnySecretStore] = []
|
||||
@Published public var modifiableStore: AnySecretStoreModifiable?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
||||
stores.append(AnySecretStore(store))
|
||||
}
|
||||
|
||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||
modifiableStore = modifiable
|
||||
stores.append(modifiable)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import CryptoTokenKit
|
||||
|
||||
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
|
||||
@@ -12,6 +12,7 @@ extension SecureEnclave {
|
||||
// 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")
|
||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||
|
||||
|
||||
@@ -10,23 +10,24 @@ 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")
|
||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||
fileprivate let watcher = TKTokenWatcher()
|
||||
fileprivate var id: String?
|
||||
fileprivate var tokenID: String?
|
||||
|
||||
public init() {
|
||||
id = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
|
||||
tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
|
||||
watcher.setInsertionHandler { string in
|
||||
guard self.id == nil else { return }
|
||||
guard self.tokenID == nil else { return }
|
||||
guard !string.contains("setoken") else { return }
|
||||
self.id = string
|
||||
self.tokenID = string
|
||||
self.reloadSecrets()
|
||||
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
||||
}
|
||||
if let id = id {
|
||||
if let tokenID = tokenID {
|
||||
self.isAvailable = true
|
||||
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: id)
|
||||
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
||||
}
|
||||
loadSecrets()
|
||||
}
|
||||
@@ -42,12 +43,12 @@ extension SmartCard {
|
||||
}
|
||||
|
||||
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
||||
guard let id = id else { fatalError() }
|
||||
guard let tokenID = tokenID else { fatalError() }
|
||||
let attributes = [
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||
kSecAttrApplicationLabel: secret.id as CFData,
|
||||
kSecAttrTokenID: id,
|
||||
kSecAttrTokenID: tokenID,
|
||||
kSecReturnRef: true
|
||||
] as CFDictionary
|
||||
var untyped: CFTypeRef?
|
||||
@@ -73,23 +74,23 @@ extension SmartCard {
|
||||
extension SmartCard.Store {
|
||||
|
||||
fileprivate func smartcardRemoved(for tokenID: String? = nil) {
|
||||
id = nil
|
||||
self.tokenID = nil
|
||||
reloadSecrets()
|
||||
}
|
||||
|
||||
fileprivate func reloadSecrets() {
|
||||
DispatchQueue.main.async {
|
||||
self.isAvailable = self.id != nil
|
||||
self.isAvailable = self.tokenID != nil
|
||||
self.secrets.removeAll()
|
||||
self.loadSecrets()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func loadSecrets() {
|
||||
guard let id = id else { return }
|
||||
guard let tokenID = tokenID else { return }
|
||||
let attributes = [
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrTokenID: id,
|
||||
kSecAttrTokenID: tokenID,
|
||||
kSecReturnRef: true,
|
||||
kSecMatchLimit: kSecMatchLimitAll,
|
||||
kSecReturnAttributes: true
|
||||
@@ -99,12 +100,12 @@ extension SmartCard.Store {
|
||||
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||
let wrapped: [SmartCard.Secret] = typed.map {
|
||||
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 publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
||||
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user