mirror of
https://github.com/maxgoedjen/secretive.git
synced 2024-11-23 05:57:07 +00:00
166 lines
5.7 KiB
Swift
166 lines
5.7 KiB
Swift
import Foundation
|
|
import Security
|
|
import CryptoTokenKit
|
|
import LocalAuthentication
|
|
|
|
// TODO: Might need to split this up into "sub-stores?"
|
|
// ie, each token has its own Store.
|
|
extension SmartCard {
|
|
|
|
public class Store: SecretStore {
|
|
|
|
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
|
@Published public var isAvailable: Bool = false
|
|
public let id = UUID()
|
|
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?
|
|
|
|
public init() {
|
|
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)
|
|
}
|
|
if let tokenID = tokenID {
|
|
self.isAvailable = true
|
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
|
}
|
|
loadSecrets()
|
|
}
|
|
|
|
// MARK: Public API
|
|
|
|
public func create(name: String) throws {
|
|
fatalError("Keys must be created on the smart card.")
|
|
}
|
|
|
|
public func delete(secret: Secret) throws {
|
|
fatalError("Keys must be deleted on the smart card.")
|
|
}
|
|
|
|
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
|
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"
|
|
let attributes = [
|
|
kSecClass: kSecClassKey,
|
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
|
kSecAttrApplicationLabel: secret.id as CFData,
|
|
kSecAttrTokenID: tokenID,
|
|
kSecUseAuthenticationContext: context,
|
|
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 {
|
|
throw SigningError(error: signError)
|
|
}
|
|
return signature as Data
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
extension SmartCard.Store {
|
|
|
|
private func smartcardRemoved(for tokenID: String? = nil) {
|
|
self.tokenID = nil
|
|
reloadSecrets()
|
|
}
|
|
|
|
private func reloadSecrets() {
|
|
DispatchQueue.main.async {
|
|
self.isAvailable = self.tokenID != nil
|
|
self.secrets.removeAll()
|
|
self.loadSecrets()
|
|
}
|
|
}
|
|
|
|
private 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,
|
|
kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC
|
|
kSecReturnRef: true,
|
|
kSecMatchLimit: kSecMatchLimitAll,
|
|
kSecReturnAttributes: true
|
|
] as CFDictionary
|
|
var untyped: CFTypeRef?
|
|
SecItemCopyMatching(attributes, &untyped)
|
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
|
let wrapped: [SmartCard.Secret] = typed.map {
|
|
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
|
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
|
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
|
|
let keySize = $0[kSecAttrKeySizeInBits] as! Int
|
|
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)
|
|
}
|
|
secrets.append(contentsOf: wrapped)
|
|
}
|
|
|
|
}
|
|
|
|
extension TKTokenWatcher {
|
|
|
|
fileprivate var nonSecureEnclaveTokens: [String] {
|
|
tokenIDs.filter { !$0.contains("setoken") }
|
|
}
|
|
|
|
}
|
|
|
|
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>
|
|
|
|
}
|