2020-03-04 07:14:38 +00:00
|
|
|
import Foundation
|
|
|
|
import Security
|
|
|
|
import CryptoTokenKit
|
|
|
|
|
2020-03-06 06:47:13 +00:00
|
|
|
// TODO: Might need to split this up into "sub-stores?"
|
|
|
|
// ie, each token has its own Store.
|
2020-03-04 07:14:38 +00:00
|
|
|
extension SmartCard {
|
|
|
|
|
|
|
|
public class Store: SecretStore {
|
|
|
|
|
|
|
|
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
2020-03-07 23:42:40 +00:00
|
|
|
@Published public var isAvailable: Bool = false
|
2020-03-09 03:03:40 +00:00
|
|
|
public let id = UUID()
|
2020-03-04 07:14:38 +00:00
|
|
|
public let name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
|
|
|
@Published public fileprivate(set) var secrets: [Secret] = []
|
|
|
|
fileprivate let watcher = TKTokenWatcher()
|
2020-03-09 03:03:40 +00:00
|
|
|
fileprivate var tokenID: String?
|
2020-03-04 07:14:38 +00:00
|
|
|
|
|
|
|
public init() {
|
2020-03-09 03:03:40 +00:00
|
|
|
tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
|
2020-03-06 08:52:44 +00:00
|
|
|
watcher.setInsertionHandler { string in
|
2020-03-09 03:03:40 +00:00
|
|
|
guard self.tokenID == nil else { return }
|
2020-03-06 06:47:13 +00:00
|
|
|
guard !string.contains("setoken") else { return }
|
2020-03-09 03:03:40 +00:00
|
|
|
self.tokenID = string
|
2020-03-07 23:20:59 +00:00
|
|
|
self.reloadSecrets()
|
2020-03-07 23:42:40 +00:00
|
|
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
2020-03-07 23:20:59 +00:00
|
|
|
}
|
2020-03-09 03:03:40 +00:00
|
|
|
if let tokenID = tokenID {
|
2020-03-07 23:42:40 +00:00
|
|
|
self.isAvailable = true
|
2020-03-09 03:03:40 +00:00
|
|
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
2020-03-04 07:14:38 +00:00
|
|
|
}
|
2020-03-06 08:52:44 +00:00
|
|
|
loadSecrets()
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Public API
|
|
|
|
|
|
|
|
public func create(name: String) throws {
|
|
|
|
fatalError("Keys must be created on the smart card.")
|
2020-03-04 07:14:38 +00:00
|
|
|
}
|
|
|
|
|
2020-03-06 08:52:44 +00:00
|
|
|
public func delete(secret: Secret) throws {
|
|
|
|
fatalError("Keys must be deleted on the smart card.")
|
2020-03-04 07:14:38 +00:00
|
|
|
}
|
|
|
|
|
2020-03-06 08:52:44 +00:00
|
|
|
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
2020-03-09 03:03:40 +00:00
|
|
|
guard let tokenID = tokenID else { fatalError() }
|
2020-03-06 08:52:44 +00:00
|
|
|
let attributes = [
|
|
|
|
kSecClass: kSecClassKey,
|
|
|
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
|
|
|
kSecAttrApplicationLabel: secret.id as CFData,
|
2020-03-09 03:03:40 +00:00
|
|
|
kSecAttrTokenID: tokenID,
|
2020-03-06 08:52:44 +00:00
|
|
|
kSecReturnRef: true
|
|
|
|
] as CFDictionary
|
|
|
|
var untyped: CFTypeRef?
|
|
|
|
let status = SecItemCopyMatching(attributes, &untyped)
|
|
|
|
if status != errSecSuccess {
|
|
|
|
throw KeychainError(statusCode: status)
|
|
|
|
}
|
|
|
|
guard let untypedSafe = untyped else {
|
|
|
|
throw KeychainError(statusCode: errSecSuccess)
|
|
|
|
}
|
|
|
|
let key = untypedSafe as! SecKey
|
|
|
|
var signError: SecurityError?
|
|
|
|
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
|
|
|
|
throw SigningError(error: signError)
|
|
|
|
}
|
|
|
|
return signature as Data
|
2020-03-04 07:14:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-03-06 08:52:44 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension SmartCard.Store {
|
|
|
|
|
2020-03-07 23:42:40 +00:00
|
|
|
fileprivate func smartcardRemoved(for tokenID: String? = nil) {
|
2020-03-09 03:03:40 +00:00
|
|
|
self.tokenID = nil
|
2020-03-07 23:42:40 +00:00
|
|
|
reloadSecrets()
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func reloadSecrets() {
|
2020-03-07 23:20:59 +00:00
|
|
|
DispatchQueue.main.async {
|
2020-03-09 03:03:40 +00:00
|
|
|
self.isAvailable = self.tokenID != nil
|
2020-03-07 23:20:59 +00:00
|
|
|
self.secrets.removeAll()
|
|
|
|
self.loadSecrets()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-06 08:52:44 +00:00
|
|
|
fileprivate func loadSecrets() {
|
2020-03-09 03:03:40 +00:00
|
|
|
guard let tokenID = tokenID else { return }
|
2020-03-06 08:52:44 +00:00
|
|
|
let attributes = [
|
|
|
|
kSecClass: kSecClassKey,
|
2020-03-09 04:16:43 +00:00
|
|
|
kSecAttrKeyType: kSecAttrKeyTypeEC,
|
|
|
|
kSecAttrKeySizeInBits: 256,
|
2020-03-09 03:03:40 +00:00
|
|
|
kSecAttrTokenID: tokenID,
|
2020-03-06 08:52:44 +00:00
|
|
|
kSecReturnRef: true,
|
|
|
|
kSecMatchLimit: kSecMatchLimitAll,
|
|
|
|
kSecReturnAttributes: true
|
|
|
|
] as CFDictionary
|
|
|
|
var untyped: CFTypeRef?
|
2020-03-06 09:05:20 +00:00
|
|
|
SecItemCopyMatching(attributes, &untyped)
|
2020-03-06 08:52:44 +00:00
|
|
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
|
|
|
let wrapped: [SmartCard.Secret] = typed.map {
|
|
|
|
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
2020-03-09 03:03:40 +00:00
|
|
|
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
2020-03-06 08:52:44 +00:00
|
|
|
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
|
|
|
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
|
|
|
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
|
|
|
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
2020-03-09 03:03:40 +00:00
|
|
|
return SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey)
|
2020-03-06 08:52:44 +00:00
|
|
|
}
|
|
|
|
secrets.append(contentsOf: wrapped)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension SmartCard {
|
|
|
|
|
|
|
|
public struct KeychainError: Error {
|
|
|
|
public let statusCode: OSStatus
|
|
|
|
}
|
|
|
|
|
|
|
|
public struct SigningError: Error {
|
|
|
|
public let error: SecurityError?
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension SmartCard {
|
|
|
|
|
|
|
|
public typealias SecurityError = Unmanaged<CFError>
|
|
|
|
|
2020-03-04 07:14:38 +00:00
|
|
|
}
|