secretive/SecretKit/SecureEnclave/SecureEnclaveStore.swift

248 lines
9.1 KiB
Swift
Raw Normal View History

2020-02-19 04:52:00 +00:00
import Foundation
import Security
2020-03-07 23:42:40 +00:00
import CryptoTokenKit
import LocalAuthentication
2020-02-19 04:52:00 +00:00
extension SecureEnclave {
2021-11-07 02:18:58 +00:00
public class Store: SecretStoreModifiable, SecretStoreAuthenticationPersistable {
2020-02-19 04:52:00 +00:00
2020-03-07 23:42:40 +00:00
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.
2020-03-11 08:53:20 +00:00
TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
2020-03-07 23:42:40 +00:00
}
2020-03-09 03:03:40 +00:00
public let id = UUID()
2020-03-04 07:14:38 +00:00
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
2020-05-16 06:19:00 +00:00
@Published public private(set) var secrets: [Secret] = []
2021-11-07 02:18:58 +00:00
private var pendingAuthenticationContext: PersistentAuthenticationContext? = nil
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
2020-02-19 04:52:00 +00:00
public init() {
2020-03-04 07:14:38 +00:00
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecrets(notify: false)
}
2020-02-19 04:52:00 +00:00
loadSecrets()
}
2020-03-04 07:14:38 +00:00
// MARK: Public API
public func create(name: String, requiresAuthentication: Bool) throws {
var accessError: SecurityError?
let flags: SecAccessControlCreateFlags
if requiresAuthentication {
flags = [.privateKeyUsage, .userPresence]
} else {
flags = .privateKeyUsage
}
let access =
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags,
&accessError) as Any
if let error = accessError {
throw error.takeRetainedValue() as Error
}
let attributes = [
kSecAttrLabel: name,
kSecAttrKeyType: Constants.keyType,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
kSecAttrApplicationTag: Constants.keyTag,
kSecPrivateKeyAttrs: [
kSecAttrIsPermanent: true,
kSecAttrAccessControl: access
]
] as CFDictionary
var privateKey: SecKey? = nil
var publicKey: SecKey? = nil
let status = SecKeyGeneratePair(attributes, &publicKey, &privateKey)
guard privateKey != nil, let pk = publicKey else {
throw KeychainError(statusCode: status)
}
try savePublicKey(pk, name: name)
reloadSecrets()
}
public func delete(secret: Secret) throws {
let deleteAttributes = [
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData
] as CFDictionary
2020-03-04 07:14:38 +00:00
let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadSecrets()
2020-02-19 04:52:00 +00:00
}
public func update(secret: Secret, name: String) throws {
let updateQuery = [
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData
] as CFDictionary
let updatedAttributes = [
kSecAttrLabel: name,
] as CFDictionary
let status = SecItemUpdate(updateQuery, updatedAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadSecrets()
}
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
2021-11-06 23:49:38 +00:00
let context: LAContext
2021-11-07 02:18:58 +00:00
if let existing = persistedAuthenticationContexts[secret], existing.valid {
context = existing.context
2021-11-06 23:49:38 +00:00
} else {
let newContext = LAContext()
newContext.localizedCancelTitle = "Deny"
2021-11-07 02:18:58 +00:00
pendingAuthenticationContext = PersistentAuthenticationContext(secret: secret, context: newContext, expiration: Date(timeIntervalSinceNow: Constants.durationOneMinute))
2021-11-06 23:49:38 +00:00
context = newContext
}
2021-11-07 02:18:58 +00:00
context.localizedReason = "saign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
2020-03-04 07:14:38 +00:00
let attributes = [
kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrKeyType: Constants.keyType,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
kSecAttrApplicationTag: Constants.keyTag,
kSecUseAuthenticationContext: context,
2020-03-04 07:14:38 +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
}
2021-11-07 02:18:58 +00:00
public func persistAuthentication(secret: Secret, forDuration: TimeInterval) throws {
guard secret == pendingAuthenticationContext?.secret else { throw AuthenticationPersistenceError() }
persistedAuthenticationContexts[secret] = pendingAuthenticationContext
pendingAuthenticationContext = nil
}
2020-03-04 07:14:38 +00:00
}
}
extension SecureEnclave.Store {
2020-05-16 06:19:00 +00:00
private func reloadSecrets(notify: Bool = true) {
2020-03-04 07:14:38 +00:00
secrets.removeAll()
loadSecrets()
if notify {
DistributedNotificationCenter.default().post(name: .secretStoreUpdated, object: nil)
}
}
2020-05-16 06:19:00 +00:00
private func loadSecrets() {
2020-03-04 07:14:38 +00:00
let attributes = [
kSecClass: kSecClassKey,
kSecAttrKeyType: SecureEnclave.Constants.keyType,
kSecAttrApplicationTag: SecureEnclave.Constants.keyTag,
kSecAttrKeyClass: kSecAttrKeyClassPublic,
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: [SecureEnclave.Secret] = typed.map {
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
let id = $0[kSecAttrApplicationLabel] as! Data
let publicKeyRef = $0[kSecValueRef] as! SecKey
let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data
2021-11-07 02:18:58 +00:00
// TODO: FIX
return SecureEnclave.Secret(id: id, name: name, publicKey: publicKey, requiresAuthentication: false)
2020-03-04 07:14:38 +00:00
}
secrets.append(contentsOf: wrapped)
}
2020-05-16 06:19:00 +00:00
private func savePublicKey(_ publicKey: SecKey, name: String) throws {
2020-03-04 07:14:38 +00:00
let attributes = [
kSecClass: kSecClassKey,
kSecAttrKeyType: SecureEnclave.Constants.keyType,
kSecAttrKeyClass: kSecAttrKeyClassPublic,
kSecAttrApplicationTag: SecureEnclave.Constants.keyTag,
kSecValueRef: publicKey,
kSecAttrIsPermanent: true,
kSecReturnData: true,
kSecAttrLabel: name
] as CFDictionary
let status = SecItemAdd(attributes, nil)
if status != errSecSuccess {
throw SecureEnclave.KeychainError(statusCode: status)
}
}
2021-11-06 23:49:38 +00:00
2020-03-04 07:14:38 +00:00
}
extension SecureEnclave {
public struct KeychainError: Error {
public let statusCode: OSStatus
}
public struct SigningError: Error {
public let error: SecurityError?
}
2021-11-07 02:18:58 +00:00
public struct AuthenticationPersistenceError: Error {
}
2020-03-04 07:14:38 +00:00
}
extension SecureEnclave {
public typealias SecurityError = Unmanaged<CFError>
}
extension SecureEnclave {
enum Constants {
2020-05-16 06:19:00 +00:00
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
static let keyType = kSecAttrKeyTypeECSECPrimeRandom
2021-11-07 02:18:58 +00:00
static let durationOneMinute: TimeInterval = 60
static let durationFiveMinutes = durationOneMinute * 5
static let durationSixtyMinutes = durationOneMinute * 60
}
}
extension SecureEnclave {
private struct PersistentAuthenticationContext {
let secret: Secret
let context: LAContext
// TODO: monotonic time instead of Date() to prevent people setting the clock back.
let expiration: Date
var valid: Bool {
Date() < expiration
}
2020-02-19 04:52:00 +00:00
}
}