Skip to content

Instantly share code, notes, and snippets.

@badlands
Created November 27, 2017 13:54
Show Gist options
  • Save badlands/85e99d71b625230574375756dbedc664 to your computer and use it in GitHub Desktop.
Save badlands/85e99d71b625230574375756dbedc664 to your computer and use it in GitHub Desktop.
/**
* Copyright (c) 2017 Håvard Fossli.
*
* Licensed under the MIT license, as follows:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import Foundation
import Security
import LocalAuthentication
@available(OSX 10.12.1, iOS 9.0, *)
public enum EllipticCurveKeyPair {
public typealias Logger = (String) -> ()
public static var logger: Logger?
public struct Config {
// The label used to identify the public key in keychain
public var publicLabel: String
// The label used to identify the private key on the secure enclave
public var privateLabel: String
// The text presented to the user about why we need his/her fingerprint / device pin
// If you are passing an LAContext to sign or decrypt this value will be rejected
public var operationPrompt: String?
// The access control used to manage the access to the public key
public var publicKeyAccessControl: AccessControl
// The access control used to manage the access to the private key
public var privateKeyAccessControl: AccessControl
// The access group e.g. "BBDV3R8HVV.no.agens.demo"
// Useful for shared keychain items
public var publicKeyAccessGroup: String?
// The access group e.g. "BBDV3R8HVV.no.agens.demo"
// Useful for shared keychain items
public var privateKeyAccessGroup: String?
// Should it be stored on .secureEnclave or in .keychain ?
public var token: Token
public init(publicLabel: String,
privateLabel: String,
operationPrompt: String?,
publicKeyAccessControl: AccessControl,
privateKeyAccessControl: AccessControl,
publicKeyAccessGroup: String? = nil,
privateKeyAccessGroup: String? = nil,
token: Token) {
self.publicLabel = publicLabel
self.privateLabel = privateLabel
self.operationPrompt = operationPrompt
self.publicKeyAccessControl = publicKeyAccessControl
self.privateKeyAccessControl = privateKeyAccessControl
self.publicKeyAccessGroup = publicKeyAccessGroup
self.privateKeyAccessGroup = privateKeyAccessGroup
self.token = token
}
}
// A stateful and opiniated manager for using the secure enclave and keychain
// If the private or public key is not found this manager will naively just recreate a new keypair
// If the device doesn't have a Secure Enclave it will store the private key in keychain just like the public key
//
// If you think this manager is "too smart" in that sense you may use this manager as an example
// and create your own manager
public final class Manager {
private let config: Config
private let helper: Helper
private var cachedPublicKey: PublicKey? = nil
private var cachedPrivateKey: PrivateKey? = nil
public init(config: Config) {
self.config = config
self.helper = Helper(config: config)
}
public func deleteKeyPair() throws {
clearCache()
try helper.delete()
}
public func publicKey() throws -> PublicKey {
do {
if let key = cachedPublicKey {
return key
}
let key = try helper.getPublicKey()
cachedPublicKey = key
return key
} catch EllipticCurveKeyPair.Error.underlying(_, let underlying) where underlying.code == errSecItemNotFound {
let keys = try helper.generateKeyPair()
cachedPublicKey = keys.public
cachedPrivateKey = keys.private
return keys.public
} catch {
throw error
}
}
public func privateKey(context: LAContext? = nil) throws -> PrivateKey {
do {
if cachedPrivateKey?.context !== context {
cachedPrivateKey = nil
}
if let key = cachedPrivateKey {
return key
}
let key = try helper.getPrivateKey(context: context)
cachedPrivateKey = key
return key
} catch EllipticCurveKeyPair.Error.underlying(_, let underlying) where underlying.code == errSecItemNotFound {
if config.publicKeyAccessControl.flags.contains(.privateKeyUsage) == false, (try? helper.getPublicKey()) != nil {
throw Error.probablyAuthenticationError(underlying: underlying)
}
let keys = try helper.generateKeyPair(context: nil)
cachedPublicKey = keys.public
cachedPrivateKey = keys.private
return keys.private
} catch {
throw error
}
}
public func keys(context: LAContext? = nil) throws -> (`public`: PublicKey, `private`: PrivateKey) {
let privateKey = try self.privateKey(context: context)
let publicKey = try self.publicKey()
return (public: publicKey, private: privateKey)
}
public func clearCache() {
cachedPublicKey = nil
cachedPrivateKey = nil
}
@available(iOS 10, *)
public func sign(_ digest: Data, hash: Hash, context: LAContext? = nil) throws -> Data {
return try helper.sign(digest, privateKey: privateKey(context: context), hash: hash)
}
@available(OSX, unavailable)
@available(iOS, deprecated: 10.0, message: "This method and extra complexity will be removed when 9.0 is obsolete.")
public func signUsingSha256(_ digest: Data, context: LAContext? = nil) throws -> Data {
#if os(iOS)
return try helper.signUsingSha256(digest, privateKey: privateKey(context: context))
#else
throw Error.inconcistency(message: "Should be unreachable.")
#endif
}
@available(iOS 10, *)
public func verify(signature: Data, originalDigest: Data, hash: Hash) throws {
try helper.verify(signature: signature, digest: originalDigest, publicKey: publicKey(), hash: hash)
}
@available(OSX, unavailable)
@available(iOS, deprecated: 10.0, message: "This method and extra complexity will be removed when 9.0 is obsolete.")
public func verifyUsingSha256(signature: Data, originalDigest: Data) throws {
#if os(iOS)
try helper.verifyUsingSha256(signature: signature, digest: originalDigest, publicKey: publicKey())
#else
throw Error.inconcistency(message: "Should be unreachable.")
#endif
}
@available(iOS 10.3, *) // API available at 10.0, but bugs made it unusable on versions lower than 10.3
public func encrypt(_ digest: Data, hash: Hash = .sha256) throws -> Data {
return try helper.encrypt(digest, publicKey: publicKey(), hash: hash)
}
@available(iOS 10.3, *) // API available at 10.0, but bugs made it unusable on versions lower than 10.3
public func decrypt(_ encrypted: Data, hash: Hash = .sha256, context: LAContext? = nil) throws -> Data {
return try helper.decrypt(encrypted, privateKey: privateKey(), hash: hash)
}
}
// Helper is a stateless class for querying the secure enclave and keychain
// You may create a small stateful facade around this
// `Manager` is an example of such an opiniated facade
public struct Helper {
// The user visible label in the device's key chain
public let config: Config
public func getPublicKey() throws -> PublicKey {
return try Query.getPublicKey(labeled: config.publicLabel, accessGroup: config.publicKeyAccessGroup)
}
public func getPrivateKey(context: LAContext? = nil) throws -> PrivateKey {
let context = context ?? LAContext()
return try Query.getPrivateKey(labeled: config.privateLabel, accessGroup: config.privateKeyAccessGroup, context: context)
}
public func getKeys(context: LAContext? = nil) throws -> (`public`: PublicKey, `private`: PrivateKey) {
let privateKey = try getPrivateKey(context: context)
let publicKey = try getPublicKey()
return (public: publicKey, private: privateKey)
}
public func generateKeyPair(context: LAContext? = nil) throws -> (`public`: PublicKey, `private`: PrivateKey) {
guard config.privateLabel != config.publicLabel else{
throw Error.inconcistency(message: "Public key and private key can not have same label")
}
let context = context ?? LAContext()
let query = try Query.generateKeyPairQuery(config: config, token: config.token, context: context)
var publicOptional, privateOptional: SecKey?
logger?("SecKeyGeneratePair: \(query)")
let status = SecKeyGeneratePair(query as CFDictionary, &publicOptional, &privateOptional)
guard status == errSecSuccess else {
if status == errSecAuthFailed {
throw Error.osStatus(message: "Could not generate keypair. Security probably doesn't like the access flags you provided. Specifically if this device doesn't have secure enclave and you pass `.privateKeyUsage`. it will produce this error.", osStatus: status)
} else {
throw Error.osStatus(message: "Could not generate keypair.", osStatus: status)
}
}
guard let publicSec = publicOptional, let privateSec = privateOptional else {
throw Error.inconcistency(message: "Created private public key pair successfully, but weren't able to retreive it.")
}
let publicKey = PublicKey(publicSec)
let privateKey = PrivateKey(privateSec, context: context)
print("=== PAIR HAS BEEN GENERATED ===")
print(".. is private key on SE? \(String(describing: try? privateKey.isStoredOnSecureEnclave()))")
try Query.forceSavePublicKey(publicKey, label: config.publicLabel)
return (public: publicKey, private: privateKey)
}
public func delete() throws {
try Query.deletePublicKey(labeled: config.publicLabel, accessGroup: config.publicKeyAccessGroup)
try Query.deletePrivateKey(labeled: config.privateLabel, accessGroup: config.privateKeyAccessGroup)
}
@available(iOS 10.0, *)
public func sign(_ digest: Data, privateKey: PrivateKey, hash: Hash) throws -> Data {
Helper.logToConsoleIfExecutingOnMainThread()
var error : Unmanaged<CFError>?
let result = SecKeyCreateSignature(privateKey.underlying, hash.signatureMessage, digest as CFData, &error)
guard let signature = result else {
throw Error.fromError(error?.takeRetainedValue(), message: "Could not create signature.")
}
return signature as Data
}
@available(OSX, unavailable)
@available(iOS, deprecated: 10.0, message: "This method and extra complexity will be removed when 9.0 is obsolete.")
public func signUsingSha256(_ digest: Data, privateKey: PrivateKey) throws -> Data {
#if os(iOS)
Helper.logToConsoleIfExecutingOnMainThread()
let digestToSign = digest.sha256()
var digestToSignBytes = [UInt8](repeating: 0, count: digestToSign.count)
digestToSign.copyBytes(to: &digestToSignBytes, count: digestToSign.count)
var signatureBytes = [UInt8](repeating: 0, count: 128)
var signatureLength = 128
let signErr = SecKeyRawSign(privateKey.underlying, .PKCS1, &digestToSignBytes, digestToSignBytes.count, &signatureBytes, &signatureLength)
guard signErr == errSecSuccess else {
throw Error.osStatus(message: "Could not create signature.", osStatus: signErr)
}
let signature = Data(bytes: &signatureBytes, count: signatureLength)
return signature
#else
throw Error.inconcistency(message: "Should be unreachable.")
#endif
}
@available(iOS 10.0, *)
public func verify(signature: Data, digest: Data, publicKey: PublicKey, hash: Hash) throws {
var error : Unmanaged<CFError>?
let valid = SecKeyVerifySignature(publicKey.underlying, hash.signatureMessage, digest as CFData, signature as CFData, &error)
if let error = error?.takeRetainedValue() {
throw Error.fromError(error, message: "Could not verify signature.")
}
guard valid == true else {
throw Error.inconcistency(message: "Signature yielded no error, but still marks itself as unsuccessful")
}
}
@available(OSX, unavailable)
@available(iOS, deprecated: 10.0, message: "This method and extra complexity will be removed when 9.0 is obsolete.")
public func verifyUsingSha256(signature: Data, digest: Data, publicKey: PublicKey) throws {
#if os(iOS)
let sha = digest.sha256()
var shaBytes = [UInt8](repeating: 0, count: sha.count)
sha.copyBytes(to: &shaBytes, count: sha.count)
var signatureBytes = [UInt8](repeating: 0, count: signature.count)
signature.copyBytes(to: &signatureBytes, count: signature.count)
let status = SecKeyRawVerify(publicKey.underlying, .PKCS1, &shaBytes, shaBytes.count, &signatureBytes, signatureBytes.count)
guard status == errSecSuccess else {
throw Error.osStatus(message: "Could not verify signature.", osStatus: status)
}
#else
throw Error.inconcistency(message: "Should be unreachable.")
#endif
}
@available(iOS 10.3, *)
public func encrypt(_ digest: Data, publicKey: PublicKey, hash: Hash) throws -> Data {
var error : Unmanaged<CFError>?
let result = SecKeyCreateEncryptedData(publicKey.underlying, hash.encryptionEciesEcdh, digest as CFData, &error)
guard let data = result else {
throw Error.fromError(error?.takeRetainedValue(), message: "Could not encrypt.")
}
return data as Data
}
@available(iOS 10.3, *)
public func decrypt(_ encrypted: Data, privateKey: PrivateKey, hash: Hash) throws -> Data {
Helper.logToConsoleIfExecutingOnMainThread()
var error : Unmanaged<CFError>?
let result = SecKeyCreateDecryptedData(privateKey.underlying, hash.encryptionEciesEcdh, encrypted as CFData, &error)
guard let data = result else {
throw Error.fromError(error?.takeRetainedValue(), message: "Could not decrypt.")
}
return data as Data
}
public static func logToConsoleIfExecutingOnMainThread() {
if Thread.isMainThread {
let _ = LogOnce.shouldNotBeMainThread
}
}
}
private struct LogOnce {
static var shouldNotBeMainThread: Void = {
print("[WARNING] \(EllipticCurveKeyPair.self): Decryption and signing should be done off main thread because LocalAuthentication may need the thread to show UI. This message is logged only once.")
}()
}
private struct Query {
static func getKey(_ query: [String: Any]) throws -> SecKey {
var raw: CFTypeRef?
logger?("SecItemCopyMatching: \(query)")
let status = SecItemCopyMatching(query as CFDictionary, &raw)
guard status == errSecSuccess, let result = raw else {
throw Error.osStatus(message: "Could not get key for query: \(query)", osStatus: status)
}
return result as! SecKey
}
static func publicKeyQuery(labeled: String, accessGroup: String?) -> [String:Any] {
var params: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
kSecAttrLabel as String: labeled,
kSecReturnRef as String: true,
]
if let accessGroup = accessGroup {
params[kSecAttrAccessGroup as String] = accessGroup
}
return params
}
static func privateKeyQuery(labeled: String, accessGroup: String?, context: LAContext?) -> [String: Any] {
var params: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
kSecAttrLabel as String: labeled,
kSecReturnRef as String: true,
]
if let accessGroup = accessGroup {
params[kSecAttrAccessGroup as String] = accessGroup
print(accessGroup )
}
// if let context = context {
// params[kSecUseAuthenticationContext as String] = context
// }
return params
}
static func generateKeyPairQuery(config: Config, token: Token, context: LAContext? = nil) throws -> [String:Any] {
print("--- generateKeyPair ---")
print("kSecAttrAccessControl: \(try! config.privateKeyAccessControl.underlying())")
// private
var privateKeyParams: [String: Any] = [
kSecAttrLabel as String: config.privateLabel,
kSecAttrIsPermanent as String: true,
kSecAttrAccessControl as String: try config.privateKeyAccessControl.underlying(),
//kSecAttrAccessible as String: kSecAttrAccessibleAlways,
// kSecUseAuthenticationUI as String: kSecUseAuthenticationUIAllow,
]
if let privateKeyAccessGroup = config.privateKeyAccessGroup {
privateKeyParams[kSecAttrAccessGroup as String] = privateKeyAccessGroup
}
// if let context = context {
// privateKeyParams[kSecUseAuthenticationContext as String] = context
// }
// public
var publicKeyParams: [String: Any] = [
kSecAttrLabel as String: config.publicLabel,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, // QUA: kSecAttrAccessibleAlways
//kSecAttrAccessControl as String: try config.publicKeyAccessControl.underlying(),
]
if let publicKeyAccessGroup = config.publicKeyAccessGroup {
publicKeyParams[kSecAttrAccessGroup as String] = publicKeyAccessGroup
}
// combined
var params: [String: Any] = [
kSecAttrKeyType as String: Constants.attrKeyTypeEllipticCurve,
kSecPrivateKeyAttrs as String: privateKeyParams,
kSecPublicKeyAttrs as String: publicKeyParams,
kSecAttrKeySizeInBits as String: 256,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock // QUA: kSecAttrAccessibleAlways,
]
if token == .secureEnclave {
params[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
}
return params
}
static func getPublicKey(labeled: String, accessGroup: String?) throws -> PublicKey {
let query = publicKeyQuery(labeled: labeled, accessGroup: accessGroup)
return PublicKey(try getKey(query))
}
static func getPrivateKey(labeled: String, accessGroup: String?, context: LAContext? = nil) throws -> PrivateKey {
let query = privateKeyQuery(labeled: labeled, accessGroup: accessGroup, context: context)
return PrivateKey(try getKey(query), context: context)
}
static func deletePublicKey(labeled: String, accessGroup: String?) throws {
let query = publicKeyQuery(labeled: labeled, accessGroup: accessGroup) as CFDictionary
logger?("SecItemDelete: \(query)")
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw Error.osStatus(message: "Could not delete private key.", osStatus: status)
}
}
static func deletePrivateKey(labeled: String, accessGroup: String?) throws {
let query = privateKeyQuery(labeled: labeled, accessGroup: accessGroup, context: nil) as CFDictionary
logger?("SecItemDelete: \(query)")
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw Error.osStatus(message: "Could not delete private key.", osStatus: status)
}
}
static func forceSavePublicKey(_ publicKey: PublicKey, label: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrLabel as String: label,
kSecValueRef as String: publicKey.underlying,
]
var raw: CFTypeRef?
logger?("SecItemAdd: \(query)")
var status = SecItemAdd(query as CFDictionary, &raw)
if status == errSecDuplicateItem {
logger?("SecItemDelete: \(query)")
status = SecItemDelete(query as CFDictionary)
logger?("SecItemAdd: \(query)")
status = SecItemAdd(query as CFDictionary, &raw)
}
if status == errSecInvalidRecord {
throw Error.osStatus(message: "Could not save public key. It is possible that the access control you have provided is not supported on this OS and/or hardware.", osStatus: status)
} else if status != errSecSuccess {
throw Error.osStatus(message: "Could not save public key", osStatus: status)
}
}
}
public struct Constants {
public static let noCompression: UInt8 = 4
public static let attrKeyTypeEllipticCurve: String = {
if #available(iOS 10.0, *) {
return kSecAttrKeyTypeECSECPrimeRandom as String
} else {
return kSecAttrKeyTypeEC as String
}
}()
}
public final class PublicKeyData {
// As received from Security framework
public let raw: Data
// The open ssl compatible DER format X.509
//
// We take the raw key and prepend an ASN.1 headers to it. The end result is an
// ASN.1 SubjectPublicKeyInfo structure, which is what OpenSSL is looking for.
//
// See the following DevForums post for more details on this.
// https://forums.developer.apple.com/message/84684#84684
//
// End result looks like this
// https://lapo.it/asn1js/#3059301306072A8648CE3D020106082A8648CE3D030107034200041F4E3F6CD8163BCC14505EBEEC9C30971098A7FA9BFD52237A3BCBBC48009162AAAFCFC871AC4579C0A180D5F207316F74088BF01A31F83E9EBDC029A533525B
//
public lazy var DER: Data = {
var x9_62HeaderECHeader = [UInt8]([
/* sequence */ 0x30, 0x59,
/* |-> sequence */ 0x30, 0x13,
/* |---> ecPublicKey */ 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, // http://oid-info.com/get/1.2.840.10045.2.1 (ANSI X9.62 public key type)
/* |---> prime256v1 */ 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, // http://oid-info.com/get/1.2.840.10045.3.1.7 (ANSI X9.62 named elliptic curve)
/* |-> bit headers */ 0x07, 0x03, 0x42, 0x00
])
var result = Data()
result.append(Data(x9_62HeaderECHeader))
result.append(self.raw)
return result
}()
public lazy var PEM: String = {
var lines = String()
lines.append("-----BEGIN PUBLIC KEY-----\n")
lines.append(self.DER.base64EncodedString(options: [.lineLength64Characters, .endLineWithCarriageReturn]))
lines.append("\n-----END PUBLIC KEY-----")
return lines
}()
internal init(_ raw: Data) {
self.raw = raw
}
}
public class Key {
public let underlying: SecKey
internal init(_ underlying: SecKey) {
self.underlying = underlying
}
private var cachedAttributes: [String:Any]? = nil
public func attributes() throws -> [String:Any] {
if let attributes = cachedAttributes {
return attributes
} else {
let attributes = try queryAttributes()
cachedAttributes = attributes
return attributes
}
}
public func label() throws -> String {
guard let attribute = try self.attributes()[kSecAttrLabel as String] as? String else {
throw Error.inconcistency(message: "We've got a private key, but we are missing its label.")
}
return attribute
}
public func accessGroup() throws -> String? {
return try self.attributes()[kSecAttrAccessGroup as String] as? String
}
public func accessControl() throws -> SecAccessControl {
guard let attribute = try self.attributes()[kSecAttrAccessControl as String] else {
throw Error.inconcistency(message: "We've got a private key, but we are missing its access control.")
}
return attribute as! SecAccessControl
}
private func queryAttributes() throws -> [String:Any] {
var matchResult: AnyObject? = nil
let query: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecValueRef as String: underlying,
kSecReturnAttributes as String: true
]
logger?("SecItemCopyMatching: \(query)")
let status = SecItemCopyMatching(query as CFDictionary, &matchResult)
guard status == errSecSuccess else {
throw Error.osStatus(message: "Could not read attributes for key", osStatus: status)
}
guard let attributes = matchResult as? [String:Any] else {
throw Error.inconcistency(message: "Tried reading key attributes something went wrong. Expected dictionary, but received \(String(describing: matchResult)).")
}
return attributes
}
}
public final class PublicKey: Key {
private var cachedData: PublicKeyData? = nil
public func data() throws -> PublicKeyData {
if let data = cachedData {
return data
} else {
let data = try queryData()
cachedData = data
return data
}
}
private func queryData() throws -> PublicKeyData {
let keyRaw: Data
if #available(iOS 10.0, *) {
keyRaw = try export()
} else {
keyRaw = try exportWithOldApi()
}
guard keyRaw.first == Constants.noCompression else {
throw Error.inconcistency(message: "Tried reading public key bytes, but its headers says it is compressed and this library only handles uncompressed keys.")
}
return PublicKeyData(keyRaw)
}
@available(iOS 10.0, *)
private func export() throws -> Data {
var error : Unmanaged<CFError>?
guard let raw = SecKeyCopyExternalRepresentation(underlying, &error) else {
throw EllipticCurveKeyPair.Error.fromError(error?.takeRetainedValue(), message: "Tried reading public key bytes.")
}
return raw as Data
}
private func exportWithOldApi() throws -> Data {
var matchResult: AnyObject? = nil
let query: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecValueRef as String: underlying,
kSecReturnData as String: true
]
logger?("SecItemCopyMatching: \(query)")
let status = SecItemCopyMatching(query as CFDictionary, &matchResult)
guard status == errSecSuccess else {
throw Error.osStatus(message: "Could not generate keypair", osStatus: status)
}
guard let keyRaw = matchResult as? Data else {
throw Error.inconcistency(message: "Tried reading public key bytes. Expected data, but received \(String(describing: matchResult)).")
}
return keyRaw
}
}
public final class PrivateKey: Key {
public private(set) var context: LAContext?
internal init(_ underlying: SecKey, context: LAContext?) {
super.init(underlying)
self.context = context
}
public func isStoredOnSecureEnclave() throws -> Bool {
// DEBUG
print("kSecAttrAccessible: \(String(describing: try self.attributes()[kSecAttrAccessible as String] as? String))")
print("kSecAttrAccessControl: \(String(describing: try self.attributes()[kSecAttrAccessControl as String] as? String))")
//
let attribute = try self.attributes()[kSecAttrTokenID as String] as? String
return attribute == (kSecAttrTokenIDSecureEnclave as String)
}
}
public final class AccessControl {
// E.g. kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
public let protection: CFTypeRef
// E.g. [.userPresence, .privateKeyUsage]
public let flags: SecAccessControlCreateFlags
public init(protection: CFTypeRef, flags: SecAccessControlCreateFlags) {
self.protection = protection
self.flags = flags
}
public func underlying() throws -> SecAccessControl {
var error: Unmanaged<CFError>?
let result = SecAccessControlCreateWithFlags(kCFAllocatorDefault, protection, flags, &error)
guard let accessControl = result else {
throw EllipticCurveKeyPair.Error.fromError(error?.takeRetainedValue(), message: "Tried creating access control object with flags \(flags) and protection \(protection)")
}
return accessControl
}
}
public enum Error: LocalizedError {
case underlying(message: String, error: NSError)
case inconcistency(message: String)
case authentication(error: LAError)
public var errorDescription: String? {
switch self {
case let .underlying(message: message, error: error):
return "\(message) \(error.localizedDescription)"
case let .authentication(error: error):
return "Authentication failed. \(error.localizedDescription)"
case let .inconcistency(message: message):
return "Inconcistency in setup, configuration or keychain. \(message)"
}
}
internal static func osStatus(message: String, osStatus: OSStatus) -> Error {
let error = NSError(domain: NSOSStatusErrorDomain, code: Int(osStatus), userInfo: [
NSLocalizedDescriptionKey: message,
NSLocalizedRecoverySuggestionErrorKey: "See https://www.osstatus.com/search/results?platform=all&framework=all&search=\(osStatus)"
])
return .underlying(message: message, error: error)
}
internal static func probablyAuthenticationError(underlying: NSError) -> Error {
return Error.authentication(error: .init(_nsError: NSError(domain: LAErrorDomain, code: LAError.authenticationFailed.rawValue, userInfo: [
NSLocalizedFailureReasonErrorKey: "Found public key, but couldn't find or access private key. The errSecItemNotFound error is sometimes wrongfully reported when LAContext authentication fails",
NSUnderlyingErrorKey: underlying
])))
}
internal static func fromError(_ error: CFError?, message: String) -> Error {
let any = error as Any
if let authenticationError = any as? LAError {
return .authentication(error: authenticationError)
}
if let error = error,
let domain = CFErrorGetDomain(error) as String? {
let code = Int(CFErrorGetCode(error))
var userInfo = (CFErrorCopyUserInfo(error) as? [String:Any]) ?? [String:Any]()
if userInfo[NSLocalizedRecoverySuggestionErrorKey] == nil {
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "See https://www.osstatus.com/search/results?platform=all&framework=all&search=\(code)"
}
let underlying = NSError(domain: domain, code: code, userInfo: userInfo)
return .underlying(message: message, error: underlying)
}
return .inconcistency(message: "\(message) Unknown error occured.")
}
}
@available(iOS 10.0, *)
public enum Hash: String {
case sha1
case sha224
case sha256
case sha384
case sha512
@available(iOS 10.0, *)
var signatureMessage: SecKeyAlgorithm {
switch self {
case .sha1:
return SecKeyAlgorithm.ecdsaSignatureMessageX962SHA1
case .sha224:
return SecKeyAlgorithm.ecdsaSignatureMessageX962SHA224
case .sha256:
return SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
case .sha384:
return SecKeyAlgorithm.ecdsaSignatureMessageX962SHA384
case .sha512:
return SecKeyAlgorithm.ecdsaSignatureMessageX962SHA512
}
}
@available(iOS 10.0, *)
var encryptionEciesEcdh: SecKeyAlgorithm {
switch self {
case .sha1:
return SecKeyAlgorithm.eciesEncryptionStandardX963SHA1AESGCM
case .sha224:
return SecKeyAlgorithm.eciesEncryptionStandardX963SHA224AESGCM
case .sha256:
return SecKeyAlgorithm.eciesEncryptionStandardX963SHA384AESGCM
case .sha384:
return SecKeyAlgorithm.eciesEncryptionStandardX963SHA256AESGCM
case .sha512:
return SecKeyAlgorithm.eciesEncryptionStandardX963SHA512AESGCM
}
}
}
public enum Token {
case secureEnclave
case keychain
public static var secureEnclaveIfAvailable: Token {
return Device.hasSecureEnclave ? .secureEnclave : .keychain
}
}
public enum Device {
public static var hasTouchID: Bool {
if #available(OSX 10.12.2, *) {
return LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
} else {
return false
}
}
public static var isSimulator: Bool {
return TARGET_OS_SIMULATOR != 0
}
public static var hasSecureEnclave: Bool {
return hasTouchID && !isSimulator
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment