Secure enclave implementation

This commit is contained in:
Max Goedjen
2020-03-03 23:14:38 -08:00
parent de2082f70e
commit 5965859d4a
38 changed files with 2718 additions and 608 deletions

View File

@@ -0,0 +1,46 @@
import Foundation
import CryptoKit
// For the moment, only supports ecdsa-sha2-nistp256 keys
public struct OpenSSHKeyWriter {
public init() {
}
public func data<SecretType: Secret>(secret: SecretType) -> Data {
lengthAndData(of: Constants.curveType.data(using: .utf8)!) +
lengthAndData(of: Constants.curveIdentifier.data(using: .utf8)!) +
lengthAndData(of: secret.publicKey)
}
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
"\(Constants.curveType) \(data(secret: secret).base64EncodedString())"
}
public func openSSHFingerprint<SecretType: Secret>(secret: SecretType) -> String {
Insecure.MD5.hash(data: data(secret: secret))
.compactMap { String($0, radix: 16, uppercase: false) }
.joined(separator: ":")
}
}
extension OpenSSHKeyWriter {
public func lengthAndData(of data: Data) -> Data {
let rawLength = UInt32(data.count)
var endian = rawLength.bigEndian
return Data(bytes: &endian, count: UInt32.bitWidth/8) + data
}
public func readData() {}
}
extension OpenSSHKeyWriter {
public enum Constants {
public static let curveIdentifier = "nistp256"
public static let curveType = "ecdsa-sha2-nistp256"
}
}

View File

@@ -1,5 +1,6 @@
import Foundation
public protocol Secret: Identifiable, Hashable {
var id: String { get }
var name: String { get }
var publicKey: Data { get }
}

View File

@@ -1,9 +1,18 @@
import Foundation
import Combine
public protocol SecretStore: ObservableObject {
associatedtype SecretType: Secret
var name: String { get }
var secrets: [SecretType] { get }
func sign(data: Data, with secret: SecretType) throws -> Data
func delete(secret: SecretType) throws
}
extension NSNotification.Name {
static let secretStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.updated")
}

View File

@@ -1,3 +1 @@
import Foundation
public enum SecureEnclave {}

View File

@@ -5,7 +5,9 @@ extension SecureEnclave {
public struct Secret: SecretKit.Secret {
public let id: String
public let id: Data
public let name: String
public let publicKey: Data
}

View File

@@ -5,17 +5,173 @@ extension SecureEnclave {
public class Store: SecretStore {
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
@Published public fileprivate(set) var secrets: [Secret] = []
public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecrets(notify: false)
}
loadSecrets()
}
fileprivate func loadSecrets() {
let secret = Secret(id: "Test")
secrets.append(secret)
// 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
let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadSecrets()
}
public func sign(data: Data, with secret: SecretType) throws -> Data {
let attributes = [
kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrKeyType: Constants.keyType,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
kSecAttrApplicationTag: Constants.keyTag,
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
}
}
}
extension SecureEnclave.Store {
fileprivate func reloadSecrets(notify: Bool = true) {
secrets.removeAll()
loadSecrets()
if notify {
DistributedNotificationCenter.default().post(name: .secretStoreUpdated, object: nil)
}
}
fileprivate func loadSecrets() {
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
return SecureEnclave.Secret(id: id, name: name, publicKey: publicKey)
}
secrets.append(contentsOf: wrapped)
}
fileprivate func savePublicKey(_ publicKey: SecKey, name: String) throws {
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)
}
}
}
extension SecureEnclave {
public struct KeychainError: Error {
public let statusCode: OSStatus
}
public struct SigningError: Error {
public let error: SecurityError?
}
}
extension SecureEnclave {
public typealias SecurityError = Unmanaged<CFError>
}
extension SecureEnclave {
enum Constants {
fileprivate static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
fileprivate static let keyType = kSecAttrKeyTypeECSECPrimeRandom
}
}

View File

@@ -0,0 +1 @@
public enum SmartCard {}

View File

@@ -0,0 +1,14 @@
import Foundation
import Combine
extension SmartCard {
public struct Secret: SecretKit.Secret {
public let id: Data
public let name: String
public let publicKey: Data
}
}

View File

@@ -0,0 +1,29 @@
import Foundation
import Security
import CryptoTokenKit
extension SmartCard {
public class Store: SecretStore {
// TODO: Read actual smart card name, eg "YubiKey 5c"
public let name = NSLocalizedString("Smart Card", comment: "Smart Card")
@Published public fileprivate(set) var secrets: [Secret] = []
fileprivate let watcher = TKTokenWatcher()
public init() {
watcher.setInsertionHandler { (string) in
print(string)
}
print(watcher.tokenIDs)
}
public func sign(data: Data, with secret: SmartCard.Secret) throws -> Data {
fatalError()
}
public func delete(secret: SmartCard.Secret) throws {
}
}
}