From 43a9e287c34bf905461105097305e9d664d92ead Mon Sep 17 00:00:00 2001 From: Maxwell <136101+mxswd@users.noreply.github.com> Date: Sun, 12 Mar 2023 10:21:09 +1000 Subject: [PATCH 1/3] Rounded out the rest of the SmartCardStore API (#450) * Rounded out the rest of the SmartCardStore API. * Comments and shuffling around * Expose verify as public api * Verification * Tweak verify signature * Cleanup and tests --------- Co-authored-by: Max Goedjen --- .../SecretKit/Erasers/AnySecretStore.swift | 6 + .../SecretKit/OpenSSH/OpenSSHKeyWriter.swift | 7 + .../Sources/SecretKit/Types/Secret.swift | 12 ++ .../Sources/SecretKit/Types/SecretStore.swift | 20 +++ .../SecureEnclaveStore.swift | 42 +++++- .../SmartCardSecretKit/SmartCardStore.swift | 141 +++++++++++++++++- .../SecretAgentKitTests/AgentTests.swift | 13 +- .../Tests/SecretAgentKitTests/StubStore.swift | 36 +++++ .../Preview Content/PreviewStore.swift | 4 + Sources/Secretive/Views/EmptyStoreView.swift | 2 +- 10 files changed, 266 insertions(+), 17 deletions(-) diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift index 0cb6c40..f70000b 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift @@ -10,6 +10,7 @@ public class AnySecretStore: SecretStore { private let _name: () -> String private let _secrets: () -> [AnySecret] private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data + private let _verify: (Data, Data, AnySecret) throws -> Bool private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext? private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void private let _reloadSecrets: () -> Void @@ -23,6 +24,7 @@ public class AnySecretStore: SecretStore { _id = { secretStore.id } _secrets = { secretStore.secrets.map { AnySecret($0) } } _sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } + _verify = { try secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) } _existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) } _persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) } _reloadSecrets = { secretStore.reloadSecrets() } @@ -51,6 +53,10 @@ public class AnySecretStore: SecretStore { try _sign(data, secret, provenance) } + public func verify(signature: Data, for data: Data, with secret: AnySecret) throws -> Bool { + try _verify(signature, data, secret) + } + public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? { _existingPersistedAuthenticationContext(secret) } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift index 223b935..da8c4b1 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift @@ -64,6 +64,10 @@ extension OpenSSHKeyWriter { switch algorithm { case .ellipticCurve: return "ecdsa-sha2-nistp" + String(describing: length) + case .rsa: + // All RSA keys use the same 512 bit hash function, per + // https://security.stackexchange.com/questions/255074/why-are-rsa-sha2-512-and-rsa-sha2-256-supported-but-not-reported-by-ssh-q-key + return "rsa-sha2-512" } } @@ -76,6 +80,9 @@ extension OpenSSHKeyWriter { switch algorithm { case .ellipticCurve: return "nistp" + String(describing: length) + case .rsa: + // All RSA keys use the same 512 bit hash function + return "rsa-sha2-512" } } diff --git a/Sources/Packages/Sources/SecretKit/Types/Secret.swift b/Sources/Packages/Sources/SecretKit/Types/Secret.swift index 6fc57a1..8f9656c 100644 --- a/Sources/Packages/Sources/SecretKit/Types/Secret.swift +++ b/Sources/Packages/Sources/SecretKit/Types/Secret.swift @@ -20,6 +20,7 @@ public protocol Secret: Identifiable, Hashable { public enum Algorithm: Hashable { case ellipticCurve + case rsa /// Initializes the Algorithm with a secAttr representation of an algorithm. /// - Parameter secAttr: the secAttr, represented as an NSNumber. @@ -28,8 +29,19 @@ public enum Algorithm: Hashable { switch secAttrString { case kSecAttrKeyTypeEC: self = .ellipticCurve + case kSecAttrKeyTypeRSA: + self = .rsa default: fatalError() } } + + public var secAttrKeyType: CFString { + switch self { + case .ellipticCurve: + return kSecAttrKeyTypeEC + case .rsa: + return kSecAttrKeyTypeRSA + } + } } diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift index f13fec7..954fe21 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -23,6 +23,14 @@ public protocol SecretStore: ObservableObject, Identifiable { /// - Returns: The signed data. func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data + /// Verifies that a signature is valid over a specified payload. + /// - Parameters: + /// - signature: The signature over the data. + /// - data: The data to verify the signature of. + /// - secret: The secret whose signature to verify. + /// - Returns: Whether the signature was verified. + func verify(signature: Data, for data: Data, with secret: SecretType) throws -> Bool + /// Checks to see if there is currently a valid persisted authentication for a given secret. /// - Parameters: /// - secret: The ``Secret`` to check if there is a persisted authentication for. @@ -71,3 +79,15 @@ extension NSNotification.Name { public static let secretStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.reloaded") } + +public typealias SecurityError = Unmanaged + +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) + } + +} diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 8c4784a..4cd32f9 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -101,7 +101,7 @@ extension SecureEnclave { reloadSecretsInternal() } - public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data { + public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { let context: LAContext if let existing = persistedAuthenticationContexts[secret], existing.valid { context = existing.context @@ -138,6 +138,41 @@ extension SecureEnclave { return signature as Data } + public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { + let context = LAContext() + context.localizedReason = "verify a signature using secret \"\(secret.name)\"" + context.localizedCancelTitle = "Deny" + let attributes = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecAttrApplicationLabel: secret.id as CFData, + kSecAttrKeyType: Constants.keyType, + kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, + kSecAttrApplicationTag: Constants.keyTag, + kSecUseAuthenticationContext: context, + kSecReturnRef: true + ]) + var verifyError: SecurityError? + 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 + let verified = SecKeyVerifySignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, signature as CFData, &verifyError) + if !verified, let verifyError { + if verifyError.takeUnretainedValue() ~= .verifyError { + return false + } else { + throw SigningError(error: verifyError) + } + } + return verified + } + public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? { guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } return persisted @@ -282,11 +317,6 @@ extension SecureEnclave { } -extension SecureEnclave { - - public typealias SecurityError = Unmanaged - -} extension SecureEnclave { diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index 28026ce..156bc99 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -45,7 +45,7 @@ extension SmartCard { fatalError("Keys must be deleted on the smart card.") } - public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data { + public func sign(data: Data, with secret: Secret, 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)\"" @@ -74,6 +74,10 @@ extension SmartCard { signatureAlgorithm = .ecdsaSignatureMessageX962SHA256 case (.ellipticCurve, 384): signatureAlgorithm = .ecdsaSignatureMessageX962SHA384 + case (.rsa, 1024): + signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512 + case (.rsa, 2048): + signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512 default: fatalError() } @@ -82,6 +86,41 @@ extension SmartCard { } return signature as Data } + public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { + let attributes = KeychainDictionary([ + kSecAttrKeyType: secret.algorithm.secAttrKeyType, + kSecAttrKeySizeInBits: secret.keySize, + kSecAttrKeyClass: kSecAttrKeyClassPublic + ]) + var verifyError: SecurityError? + let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError) + guard let untypedSafe = untyped else { + throw KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + let signatureAlgorithm: SecKeyAlgorithm + 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 verifyError.takeUnretainedValue() ~= .verifyError { + return false + } else { + throw SigningError(error: verifyError) + } + } + return verified + } public func existingPersistedAuthenticationContext(secret: SmartCard.Secret) -> PersistedAuthenticationContext? { nil @@ -140,7 +179,6 @@ extension SmartCard.Store { let attributes = KeychainDictionary([ kSecClass: kSecClassKey, kSecAttrTokenID: tokenID, - kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC kSecReturnRef: true, kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true @@ -164,6 +202,99 @@ extension SmartCard.Store { } + +// MARK: Smart Card specific encryption/decryption/verification +extension SmartCard.Store { + + /// Encrypts a payload with a specified key. + /// - Parameters: + /// - data: The payload to encrypt. + /// - secret: The secret to encrypt with. + /// - Returns: The encrypted data. + /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged. + public func encrypt(data: Data, with secret: SecretType) throws -> Data { + let context = LAContext() + context.localizedReason = "encrypt data using secret \"\(secret.name)\"" + context.localizedCancelTitle = "Deny" + let attributes = KeychainDictionary([ + kSecAttrKeyType: secret.algorithm.secAttrKeyType, + kSecAttrKeySizeInBits: secret.keySize, + kSecAttrKeyClass: kSecAttrKeyClassPublic, + kSecUseAuthenticationContext: context + ]) + var encryptError: SecurityError? + let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError) + guard let untypedSafe = untyped else { + throw SmartCard.KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + let signatureAlgorithm: SecKeyAlgorithm + switch (secret.algorithm, secret.keySize) { + 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 + } + + /// Decrypts a payload with a specified key. + /// - Parameters: + /// - data: The payload to decrypt. + /// - secret: The secret to decrypt with. + /// - Returns: The decrypted data. + /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged. + public func decrypt(data: Data, with secret: SecretType) throws -> Data { + guard let tokenID = tokenID else { fatalError() } + let context = LAContext() + context.localizedReason = "decrypt data using secret \"\(secret.name)\"" + context.localizedCancelTitle = "Deny" + let attributes = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecAttrApplicationLabel: secret.id as CFData, + kSecAttrTokenID: tokenID, + kSecUseAuthenticationContext: context, + kSecReturnRef: true + ]) + var untyped: CFTypeRef? + let status = SecItemCopyMatching(attributes, &untyped) + if status != errSecSuccess { + throw SmartCard.KeychainError(statusCode: status) + } + guard let untypedSafe = untyped else { + throw SmartCard.KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + var encryptError: SecurityError? + let signatureAlgorithm: SecKeyAlgorithm + switch (secret.algorithm, secret.keySize) { + case (.ellipticCurve, 256): + signatureAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM + case (.ellipticCurve, 384): + signatureAlgorithm = .eciesEncryptionStandardX963SHA384AESGCM + case (.rsa, 1024), (.rsa, 2048): + signatureAlgorithm = .rsaEncryptionOAEPSHA512AESGCM + default: + fatalError() + } + guard let signature = SecKeyCreateDecryptedData(key, signatureAlgorithm, data as CFData, &encryptError) else { + throw SmartCard.SigningError(error: encryptError) + } + return signature as Data + } + +} + extension TKTokenWatcher { /// All available tokens, excluding the Secure Enclave. @@ -188,9 +319,3 @@ extension SmartCard { } } - -extension SmartCard { - - public typealias SecurityError = Unmanaged - -} diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift index 2c3e29e..398da9f 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift @@ -61,8 +61,17 @@ class AgentTests: XCTestCase { var rs = r rs.append(s) let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs) - let valid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign) - XCTAssertTrue(valid) + let referenceValid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign) + let store = list.stores.first! + let derVerifies = try! store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret)) + let invalidRandomSignature = try? store.verify(signature: "invalid".data(using: .utf8)!, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret)) + let invalidRandomData = try? store.verify(signature: signature.derRepresentation, for: "invalid".data(using: .utf8)!, with: AnySecret(Constants.Secrets.ecdsa256Secret)) + let invalidWrongKey = try? store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa384Secret)) + XCTAssertTrue(referenceValid) + XCTAssertTrue(derVerifies) + XCTAssert(invalidRandomSignature == false) + XCTAssert(invalidRandomData == false) + XCTAssert(invalidWrongKey == false) } // MARK: Witness protocol diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index cc6cb3b..acbda1d 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -70,6 +70,42 @@ extension Stub { return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data } + public func verify(signature: Data, for data: Data, with secret: Stub.Secret) throws -> Bool { + let attributes = KeychainDictionary([ + kSecAttrKeyType: secret.algorithm.secAttrKeyType, + kSecAttrKeySizeInBits: secret.keySize, + kSecAttrKeyClass: kSecAttrKeyClassPublic + ]) + var verifyError: Unmanaged? + let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError) + guard let untypedSafe = untyped else { + throw NSError(domain: "test", code: 0, userInfo: nil) + } + let key = untypedSafe as! SecKey + let signatureAlgorithm: SecKeyAlgorithm + 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 verifyError.takeUnretainedValue() ~= .verifyError { + return false + } else { + throw NSError(domain: "test", code: 0, userInfo: nil) + } + } + return verified + } + public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? { nil } diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index 839273e..8006d6c 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -40,6 +40,10 @@ extension Preview { return data } + func verify(data: Data, signature: Data, with secret: Preview.Secret) throws -> Bool { + true + } + func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? { nil } diff --git a/Sources/Secretive/Views/EmptyStoreView.swift b/Sources/Secretive/Views/EmptyStoreView.swift index 689b00b..5bd48c1 100644 --- a/Sources/Secretive/Views/EmptyStoreView.swift +++ b/Sources/Secretive/Views/EmptyStoreView.swift @@ -34,7 +34,7 @@ struct EmptyStoreImmutableView: View { VStack { Text("No Secrets").bold() Text("Use your Smart Card's management tool to create a secret.") - Text("Secretive only supports Elliptic Curve keys.") + Text("Secretive supports EC256, EC384, RSA1024, and RSA2048 keys.") }.frame(maxWidth: .infinity, maxHeight: .infinity) } From 93e79470b7d2b5b9b9000976065cf1fb017d0618 Mon Sep 17 00:00:00 2001 From: Maxwell <136101+mxswd@users.noreply.github.com> Date: Sun, 12 Mar 2023 11:10:43 +1000 Subject: [PATCH 2/3] Fixed arg labels (#455) --- Sources/Secretive/Preview Content/PreviewStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index 8006d6c..9480c88 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -40,7 +40,7 @@ extension Preview { return data } - func verify(data: Data, signature: Data, with secret: Preview.Secret) throws -> Bool { + func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool { true } From be58ddd324dc4e9b8248215ba54679bf0a8c35c7 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 11 Mar 2023 17:58:39 -0800 Subject: [PATCH 3/3] Factor out some common keychain functionality (#456) * Factor out some common keychain functionality * Remove redundant * Remove redundant --- .../Documentation.docc/Documentation.md | 6 ++ .../SecretKit/KeychainDictionary.swift | 5 - .../Sources/SecretKit/KeychainTypes.swift | 71 ++++++++++++++ .../Sources/SecretKit/Types/SecretStore.swift | 12 --- .../SecureEnclaveStore.swift | 19 +--- .../Documentation.docc/SmartCard.md | 6 -- .../SmartCardSecretKit/SmartCardStore.swift | 94 +++++-------------- .../Tests/SecretAgentKitTests/StubStore.swift | 26 +---- 8 files changed, 101 insertions(+), 138 deletions(-) delete mode 100644 Sources/Packages/Sources/SecretKit/KeychainDictionary.swift create mode 100644 Sources/Packages/Sources/SecretKit/KeychainTypes.swift diff --git a/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md b/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md index 3c608d2..a7fed06 100644 --- a/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md +++ b/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md @@ -32,3 +32,9 @@ SecretKit is a collection of protocols describing secrets and stores. ### Authentication Persistence - ``PersistedAuthenticationContext`` + +### Errors + +- ``KeychainError`` +- ``SigningError`` +- ``SecurityError`` diff --git a/Sources/Packages/Sources/SecretKit/KeychainDictionary.swift b/Sources/Packages/Sources/SecretKit/KeychainDictionary.swift deleted file mode 100644 index 460af92..0000000 --- a/Sources/Packages/Sources/SecretKit/KeychainDictionary.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary { - dictionary as CFDictionary -} diff --git a/Sources/Packages/Sources/SecretKit/KeychainTypes.swift b/Sources/Packages/Sources/SecretKit/KeychainTypes.swift new file mode 100644 index 0000000..cfea466 --- /dev/null +++ b/Sources/Packages/Sources/SecretKit/KeychainTypes.swift @@ -0,0 +1,71 @@ +import Foundation + +public typealias SecurityError = Unmanaged + +/// 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() + } + + } + +} diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift index 954fe21..f780201 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -79,15 +79,3 @@ extension NSNotification.Name { public static let secretStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.reloaded") } - -public typealias SecurityError = Unmanaged - -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) - } - -} diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 4cd32f9..5a853c4 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -295,29 +295,12 @@ extension SecureEnclave.Store { ]) let status = SecItemAdd(attributes, nil) 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 { enum Constants { diff --git a/Sources/Packages/Sources/SmartCardSecretKit/Documentation.docc/SmartCard.md b/Sources/Packages/Sources/SmartCardSecretKit/Documentation.docc/SmartCard.md index 79fd58e..6a386f5 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/Documentation.docc/SmartCard.md +++ b/Sources/Packages/Sources/SmartCardSecretKit/Documentation.docc/SmartCard.md @@ -6,9 +6,3 @@ - ``Secret`` - ``Store`` - -### Errors - -- ``KeychainError`` -- ``SigningError`` -- ``SecurityError`` diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index 156bc99..b6fe2fc 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -68,24 +68,12 @@ extension SmartCard { } 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 - case (.rsa, 1024): - signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512 - case (.rsa, 2048): - signatureAlgorithm = .rsaSignatureMessagePKCS1v15SHA512 - default: - fatalError() - } - guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else { + guard let signature = SecKeyCreateSignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, &signError) else { throw SigningError(error: signError) } return signature as Data } + public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { let attributes = KeychainDictionary([ kSecAttrKeyType: secret.algorithm.secAttrKeyType, @@ -98,20 +86,7 @@ extension SmartCard { throw KeychainError(statusCode: errSecSuccess) } let key = untypedSafe as! SecKey - let signatureAlgorithm: SecKeyAlgorithm - 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) + let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, signature as CFData, &verifyError) if !verified, let verifyError { if verifyError.takeUnretainedValue() ~= .verifyError { return false @@ -122,11 +97,11 @@ extension SmartCard { return verified } - public func existingPersistedAuthenticationContext(secret: SmartCard.Secret) -> PersistedAuthenticationContext? { + public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? { nil } - public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws { + public func persistAuthentication(secret: Secret, forDuration: TimeInterval) throws { } /// Reloads all secrets from the store. @@ -186,7 +161,7 @@ extension SmartCard.Store { var untyped: CFTypeRef? SecItemCopyMatching(attributes, &untyped) 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 tokenID = $0[kSecAttrApplicationLabel] as! Data let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber) @@ -225,24 +200,11 @@ extension SmartCard.Store { var encryptError: SecurityError? let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError) guard let untypedSafe = untyped else { - throw SmartCard.KeychainError(statusCode: errSecSuccess) + throw KeychainError(statusCode: errSecSuccess) } let key = untypedSafe as! SecKey - let signatureAlgorithm: SecKeyAlgorithm - switch (secret.algorithm, secret.keySize) { - 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) + guard let signature = SecKeyCreateEncryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else { + throw SigningError(error: encryptError) } return signature as Data } @@ -269,28 +231,30 @@ extension SmartCard.Store { var untyped: CFTypeRef? let status = SecItemCopyMatching(attributes, &untyped) if status != errSecSuccess { - throw SmartCard.KeychainError(statusCode: status) + throw KeychainError(statusCode: status) } guard let untypedSafe = untyped else { - throw SmartCard.KeychainError(statusCode: errSecSuccess) + throw KeychainError(statusCode: errSecSuccess) } let key = untypedSafe as! SecKey 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) { case (.ellipticCurve, 256): - signatureAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM + return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM case (.ellipticCurve, 384): - signatureAlgorithm = .eciesEncryptionStandardX963SHA384AESGCM + return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM case (.rsa, 1024), (.rsa, 2048): - signatureAlgorithm = .rsaEncryptionOAEPSHA512AESGCM + return .rsaEncryptionOAEPSHA512AESGCM default: 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? - } - -} diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index acbda1d..f155706 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -58,16 +58,7 @@ extension Stub { kSecAttrKeyClass: kSecAttrKeyClassPrivate ]) , nil)! - let signatureAlgorithm: SecKeyAlgorithm - switch secret.keySize { - case 256: - signatureAlgorithm = .ecdsaSignatureMessageX962SHA256 - case 384: - signatureAlgorithm = .ecdsaSignatureMessageX962SHA384 - default: - fatalError() - } - return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data + return SecKeyCreateSignature(privateKey, signatureAlgorithm(for: secret), data as CFData, nil)! as Data } 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) } let key = untypedSafe as! SecKey - let signatureAlgorithm: SecKeyAlgorithm - 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) + let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret), data as CFData, signature as CFData, &verifyError) if let verifyError { if verifyError.takeUnretainedValue() ~= .verifyError { return false