secretive/SecretKit/SmartCard/SmartCardStore.swift

166 lines
5.7 KiB
Swift
Raw Normal View History

2020-03-04 07:14:38 +00:00
import Foundation
import Security
import CryptoTokenKit
import LocalAuthentication
2020-03-04 07:14:38 +00:00
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-05-16 06:19:00 +00:00
public private(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
@Published public private(set) var secrets: [Secret] = []
private let watcher = TKTokenWatcher()
private var tokenID: String?
2020-03-04 07:14:38 +00:00
public init() {
2020-03-18 03:41:37 +00:00
tokenID = watcher.nonSecureEnclaveTokens.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-18 03:41:37 +00:00
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
}
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
2020-03-09 03:03:40 +00:00
guard let tokenID = tokenID else { fatalError() }
let context = LAContext()
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
context.localizedCancelTitle = "Deny"
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,
kSecUseAuthenticationContext: context,
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?
let signatureAlgorithm: SecKeyAlgorithm
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
case (.ellipticCurve, 384):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
default:
fatalError()
}
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
2020-03-06 08:52:44 +00:00
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-05-16 06:19:00 +00:00
private 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()
}
2020-05-16 06:19:00 +00:00
private 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-05-16 06:19:00 +00:00
private func loadSecrets() {
2020-03-09 03:03:40 +00:00
guard let tokenID = tokenID else { return }
2020-03-18 03:41:37 +00:00
// 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")
}
2020-03-06 08:52:44 +00:00
let attributes = [
kSecClass: kSecClassKey,
2020-03-09 03:03:40 +00:00
kSecAttrTokenID: tokenID,
2020-03-10 05:06:51 +00:00
kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC
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
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
let keySize = $0[kSecAttrKeySizeInBits] as! Int
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
return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey)
2020-03-06 08:52:44 +00:00
}
secrets.append(contentsOf: wrapped)
}
}
2020-03-18 03:41:37 +00:00
extension TKTokenWatcher {
fileprivate var nonSecureEnclaveTokens: [String] {
tokenIDs.filter { !$0.contains("setoken") }
}
}
2020-03-06 08:52:44 +00:00
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
}