Factor out some common keychain functionality (#456)

* Factor out some common keychain functionality

* Remove redundant

* Remove redundant
This commit is contained in:
Max Goedjen 2023-03-11 17:58:39 -08:00 committed by GitHub
parent 93e79470b7
commit be58ddd324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 138 deletions

View File

@ -32,3 +32,9 @@ SecretKit is a collection of protocols describing secrets and stores.
### Authentication Persistence ### Authentication Persistence
- ``PersistedAuthenticationContext`` - ``PersistedAuthenticationContext``
### Errors
- ``KeychainError``
- ``SigningError``
- ``SecurityError``

View File

@ -1,5 +0,0 @@
import Foundation
public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
dictionary as CFDictionary
}

View File

@ -0,0 +1,71 @@
import Foundation
public typealias SecurityError = Unmanaged<CFError>
/// Wraps a Swift dictionary in a CFDictionary.
/// - Parameter dictionary: The Swift dictionary to wrap.
/// - Returns: A CFDictionary containing the keys and values.
public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
dictionary as CFDictionary
}
public extension CFError {
/// The CFError returned when a verification operation fails.
static let verifyError = CFErrorCreate(nil, NSOSStatusErrorDomain as CFErrorDomain, CFIndex(errSecVerifyFailed), nil)!
/// Equality operation that only considers domain and code.
static func ~=(lhs: CFError, rhs: CFError) -> Bool {
CFErrorGetDomain(lhs) == CFErrorGetDomain(rhs) && CFErrorGetCode(lhs) == CFErrorGetCode(rhs)
}
}
/// A wrapper around an error code reported by a Keychain API.
public struct KeychainError: Error {
/// The status code involved, if one was reported.
public let statusCode: OSStatus?
/// Initializes a KeychainError with an optional error code.
/// - Parameter statusCode: The status code returned by the keychain operation, if one is applicable.
public init(statusCode: OSStatus?) {
self.statusCode = statusCode
}
}
/// A signing-related error.
public struct SigningError: Error {
/// The underlying error reported by the API, if one was returned.
public let error: SecurityError?
/// Initializes a SigningError with an optional SecurityError.
/// - Parameter statusCode: The SecurityError, if one is applicable.
public init(error: SecurityError?) {
self.error = error
}
}
public extension SecretStore {
/// Returns the appropriate keychian signature algorithm to use for a given secret.
/// - Parameters:
/// - secret: The secret which will be used for signing.
/// - allowRSA: Whether or not RSA key types should be permited.
/// - Returns: The appropriate algorithm.
func signatureAlgorithm(for secret: SecretType, allowRSA: Bool = false) -> SecKeyAlgorithm {
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
return .ecdsaSignatureMessageX962SHA256
case (.ellipticCurve, 384):
return .ecdsaSignatureMessageX962SHA384
case (.rsa, 1024), (.rsa, 2048):
guard allowRSA else { fatalError() }
return .rsaSignatureMessagePKCS1v15SHA512
default:
fatalError()
}
}
}

View File

@ -79,15 +79,3 @@ extension NSNotification.Name {
public static let secretStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.reloaded") public static let secretStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.reloaded")
} }
public typealias SecurityError = Unmanaged<CFError>
extension CFError {
public static let verifyError = CFErrorCreate(nil, NSOSStatusErrorDomain as CFErrorDomain, CFIndex(errSecVerifyFailed), nil)!
static public func ~=(lhs: CFError, rhs: CFError) -> Bool {
CFErrorGetDomain(lhs) == CFErrorGetDomain(rhs) && CFErrorGetCode(lhs) == CFErrorGetCode(rhs)
}
}

View File

@ -295,29 +295,12 @@ extension SecureEnclave.Store {
]) ])
let status = SecItemAdd(attributes, nil) let status = SecItemAdd(attributes, nil)
if status != errSecSuccess { if status != errSecSuccess {
throw SecureEnclave.KeychainError(statusCode: status) throw KeychainError(statusCode: status)
} }
} }
} }
extension SecureEnclave {
/// A wrapper around an error code reported by a Keychain API.
public struct KeychainError: Error {
/// The status code involved, if one was reported.
public let statusCode: OSStatus?
}
/// A signing-related error.
public struct SigningError: Error {
/// The underlying error reported by the API, if one was returned.
public let error: SecurityError?
}
}
extension SecureEnclave { extension SecureEnclave {
enum Constants { enum Constants {

View File

@ -6,9 +6,3 @@
- ``Secret`` - ``Secret``
- ``Store`` - ``Store``
### Errors
- ``KeychainError``
- ``SigningError``
- ``SecurityError``

View File

@ -68,24 +68,12 @@ extension SmartCard {
} }
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
var signError: SecurityError? var signError: SecurityError?
let signatureAlgorithm: SecKeyAlgorithm guard let signature = SecKeyCreateSignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, &signError) else {
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
case (.ellipticCurve, 384):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
case (.rsa, 1024):
signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512
case (.rsa, 2048):
signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512
default:
fatalError()
}
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
throw SigningError(error: signError) throw SigningError(error: signError)
} }
return signature as Data return signature as Data
} }
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecAttrKeyType: secret.algorithm.secAttrKeyType, kSecAttrKeyType: secret.algorithm.secAttrKeyType,
@ -98,20 +86,7 @@ extension SmartCard {
throw KeychainError(statusCode: errSecSuccess) throw KeychainError(statusCode: errSecSuccess)
} }
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
let signatureAlgorithm: SecKeyAlgorithm let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, signature as CFData, &verifyError)
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
case (.ellipticCurve, 384):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
case (.rsa, 1024):
signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512
case (.rsa, 2048):
signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512
default:
fatalError()
}
let verified = SecKeyVerifySignature(key, signatureAlgorithm, data as CFData, signature as CFData, &verifyError)
if !verified, let verifyError { if !verified, let verifyError {
if verifyError.takeUnretainedValue() ~= .verifyError { if verifyError.takeUnretainedValue() ~= .verifyError {
return false return false
@ -122,11 +97,11 @@ extension SmartCard {
return verified return verified
} }
public func existingPersistedAuthenticationContext(secret: SmartCard.Secret) -> PersistedAuthenticationContext? { public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
nil nil
} }
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws { public func persistAuthentication(secret: Secret, forDuration: TimeInterval) throws {
} }
/// Reloads all secrets from the store. /// Reloads all secrets from the store.
@ -186,7 +161,7 @@ extension SmartCard.Store {
var untyped: CFTypeRef? var untyped: CFTypeRef?
SecItemCopyMatching(attributes, &untyped) SecItemCopyMatching(attributes, &untyped)
guard let typed = untyped as? [[CFString: Any]] else { return } guard let typed = untyped as? [[CFString: Any]] else { return }
let wrapped: [SmartCard.Secret] = typed.map { let wrapped = typed.map {
let name = $0[kSecAttrLabel] as? String ?? "Unnamed" let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
let tokenID = $0[kSecAttrApplicationLabel] as! Data let tokenID = $0[kSecAttrApplicationLabel] as! Data
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber) let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
@ -225,24 +200,11 @@ extension SmartCard.Store {
var encryptError: SecurityError? var encryptError: SecurityError?
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError) let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError)
guard let untypedSafe = untyped else { guard let untypedSafe = untyped else {
throw SmartCard.KeychainError(statusCode: errSecSuccess) throw KeychainError(statusCode: errSecSuccess)
} }
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
let signatureAlgorithm: SecKeyAlgorithm guard let signature = SecKeyCreateEncryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
switch (secret.algorithm, secret.keySize) { throw SigningError(error: encryptError)
case (.ellipticCurve, 256):
signatureAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
case (.ellipticCurve, 384):
signatureAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
case (.rsa, 1024):
signatureAlgorithm = .rsaEncryptionOAEPSHA512AESGCM
case (.rsa, 2048):
signatureAlgorithm = .rsaEncryptionOAEPSHA512AESGCM
default:
fatalError()
}
guard let signature = SecKeyCreateEncryptedData(key, signatureAlgorithm, data as CFData, &encryptError) else {
throw SmartCard.SigningError(error: encryptError)
} }
return signature as Data return signature as Data
} }
@ -269,28 +231,30 @@ extension SmartCard.Store {
var untyped: CFTypeRef? var untyped: CFTypeRef?
let status = SecItemCopyMatching(attributes, &untyped) let status = SecItemCopyMatching(attributes, &untyped)
if status != errSecSuccess { if status != errSecSuccess {
throw SmartCard.KeychainError(statusCode: status) throw KeychainError(statusCode: status)
} }
guard let untypedSafe = untyped else { guard let untypedSafe = untyped else {
throw SmartCard.KeychainError(statusCode: errSecSuccess) throw KeychainError(statusCode: errSecSuccess)
} }
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
var encryptError: SecurityError? var encryptError: SecurityError?
let signatureAlgorithm: SecKeyAlgorithm guard let signature = SecKeyCreateDecryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
throw SigningError(error: encryptError)
}
return signature as Data
}
private func encryptionAlgorithm(for secret: SecretType) -> SecKeyAlgorithm {
switch (secret.algorithm, secret.keySize) { switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256): case (.ellipticCurve, 256):
signatureAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
case (.ellipticCurve, 384): case (.ellipticCurve, 384):
signatureAlgorithm = .eciesEncryptionStandardX963SHA384AESGCM return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
case (.rsa, 1024), (.rsa, 2048): case (.rsa, 1024), (.rsa, 2048):
signatureAlgorithm = .rsaEncryptionOAEPSHA512AESGCM return .rsaEncryptionOAEPSHA512AESGCM
default: default:
fatalError() fatalError()
} }
guard let signature = SecKeyCreateDecryptedData(key, signatureAlgorithm, data as CFData, &encryptError) else {
throw SmartCard.SigningError(error: encryptError)
}
return signature as Data
} }
} }
@ -303,19 +267,3 @@ extension TKTokenWatcher {
} }
} }
extension SmartCard {
/// A wrapper around an error code reported by a Keychain API.
public struct KeychainError: Error {
/// The status code involved.
public let statusCode: OSStatus
}
/// A signing-related error.
public struct SigningError: Error {
/// The underlying error reported by the API, if one was returned.
public let error: SecurityError?
}
}

View File

@ -58,16 +58,7 @@ extension Stub {
kSecAttrKeyClass: kSecAttrKeyClassPrivate kSecAttrKeyClass: kSecAttrKeyClassPrivate
]) ])
, nil)! , nil)!
let signatureAlgorithm: SecKeyAlgorithm return SecKeyCreateSignature(privateKey, signatureAlgorithm(for: secret), data as CFData, nil)! as Data
switch secret.keySize {
case 256:
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
case 384:
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
default:
fatalError()
}
return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
} }
public func verify(signature: Data, for data: Data, with secret: Stub.Secret) throws -> Bool { public func verify(signature: Data, for data: Data, with secret: Stub.Secret) throws -> Bool {
@ -82,20 +73,7 @@ extension Stub {
throw NSError(domain: "test", code: 0, userInfo: nil) throw NSError(domain: "test", code: 0, userInfo: nil)
} }
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
let signatureAlgorithm: SecKeyAlgorithm let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret), data as CFData, signature as CFData, &verifyError)
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
case (.ellipticCurve, 384):
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
case (.rsa, 1024):
signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512
case (.rsa, 2048):
signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512
default:
fatalError()
}
let verified = SecKeyVerifySignature(key, signatureAlgorithm, data as CFData, signature as CFData, &verifyError)
if let verifyError { if let verifyError {
if verifyError.takeUnretainedValue() ~= .verifyError { if verifyError.takeUnretainedValue() ~= .verifyError {
return false return false