diff --git a/Sources/ShieldSecurity/Errors.swift b/Sources/ShieldSecurity/Errors.swift new file mode 100644 index 000000000..ceef5ae43 --- /dev/null +++ b/Sources/ShieldSecurity/Errors.swift @@ -0,0 +1,114 @@ +// +// Errors.swift +// Shield +// +// Copyright © 2021 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation + +extension CFError { + + var humanReadableDescription: String { + humanReadableDescriptionLines.joined(separator: "\n") + } + + var humanReadableDescriptionLines: [String] { + + guard let userInfo = CFErrorCopyUserInfo(self) as? [CFString: Any] else { + return [CFErrorCopyDescription(self) as String] + } + + var lines: [String] = [] + + lines += [ + "Code: \(CFErrorGetCode(self))", + "Domain: \(CFErrorGetDomain(self)!)", + ] + + if let localizedErrorDesc = userInfo[kCFErrorLocalizedDescriptionKey] { + lines.append("Description: \(localizedErrorDesc)") + } + + if let localizedFailureReasonDesc = userInfo[kCFErrorLocalizedFailureReasonKey] { + lines.append("Failure Reason: \(localizedFailureReasonDesc)") + } + + if let localizedRecoverySuggestion = userInfo[kCFErrorLocalizedRecoverySuggestionKey] { + lines.append("Recovery Suggestion: \(localizedRecoverySuggestion)") + } + + if let underlyingError = userInfo[kCFErrorUnderlyingErrorKey] { + lines += describe(title: "Underlying Error", error: underlyingError) + } + + return lines + } + +} + +extension NSError { + + var humanReadableDescription: String { + humanReadableDescriptionLines.joined(separator: "\n") + } + + var humanReadableDescriptionLines: [String] { + + var lines: [String] = [] + + lines += [ + "Code: \(code)", + "Domain: \(domain)", + "Description: \(localizedDescription)", + ] + + if let localizedFailureReason { + lines.append("Failure Reason: \(localizedFailureReason)") + } + + if let localizedRecoverySuggestion { + lines.append("Recovery Suggestion: \(localizedRecoverySuggestion)") + } + + if let localizedRecoveryOptions { + lines += ["Recovery Options:"] + localizedRecoveryOptions.enumerated().map { idx, option in + " Option \(idx): \(option)" + } + } + + if let underlyingError = userInfo[NSUnderlyingErrorKey] as? NSError { + lines += describe(title: "Underlying Error", error: underlyingError) + } + + if #available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *) { + + if let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? NSError { + lines += ["Underlying Errors:"] + describe(title: "Error 0", error: underlyingErrors) + } + + if let underlyignErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? NSArray { + lines += ["Underlying Errors:"] + underlyignErrors.enumerated().flatMap { idx, error in + return describe(title: "Error \(idx)", error: error) + }.map { " \($0)" } + } + + } + + return lines + } + +} + +private func describe(title: String, error: Any) -> [String] { + if let error = error as? NSError { + return ["\(title):"] + error.humanReadableDescriptionLines.map { " \($0)" } + } + else { + let error = error as! CFError // swiftlint:disable:this force_cast + return ["\(title):"] + error.humanReadableDescriptionLines.map { " \($0)" } + } +} diff --git a/Sources/ShieldSecurity/SecCertificate.swift b/Sources/ShieldSecurity/SecCertificate.swift index 31dfe81fb..551e72551 100644 --- a/Sources/ShieldSecurity/SecCertificate.swift +++ b/Sources/ShieldSecurity/SecCertificate.swift @@ -163,18 +163,10 @@ public extension SecCertificate { return } - let errorDesc: String - if let error = error { - errorDesc = (CFErrorCopyFailureReason(error) ?? CFErrorCopyDescription(error)) as String? ?? "" - } - else { - errorDesc = "" - } - logTrustEvaluation(level: .error, trust: trust, result: trustResult, - description: errorDesc) + error: error) throw SecCertificateError.trustValidationFailed } @@ -182,7 +174,7 @@ public extension SecCertificate { private func logTrustEvaluation(level: OSLogType, trust: SecTrust, result: SecTrustResultType, - description: String) { + error: CFError?) { var anchorCertificatesArray: CFArray? let anchorCertificates: [SecCertificate] @@ -194,17 +186,25 @@ public extension SecCertificate { anchorCertificates = [] } + let errorDesc: String + if let error = error { + errorDesc = "\n" + error.humanReadableDescriptionLines.map { " \($0)" }.joined(separator: "\n") + } + else { + errorDesc = "None" + } + Logger.default.log( level: level, """ Trust evaulation failed: - Result: \(trustResultDescription(result: result), privacy: .public), - Description: \(description, privacy: .public) + Result: \(trustResultDescription(result: result), privacy: .public) + Error: \(errorDesc) Certificate: \(self, privacy: .public) Anchor Certificates: \(anchorCertificates.enumerated().map { (idx, cert) in - "\(idx): \(cert)" }.joined(separator: "\n "), privacy: .public) + "\(idx): \(cert)" }.joined(separator: "\n "), privacy: .public) """ ) } @@ -224,7 +224,7 @@ public extension SecCertificate { let query = [ kSecReturnAttributes as String: kCFBooleanTrue!, kSecValueRef as String: self, - ] as CFDictionary + ] as [String: Any] as CFDictionary var data: CFTypeRef? @@ -257,7 +257,7 @@ public extension SecCertificate { let query = [ kSecClass as String: kSecClassCertificate, kSecValueRef as String: self, - ] as CFDictionary + ] as [String: Any] as CFDictionary var data: CFTypeRef? diff --git a/Tests/ErrorsTests.swift b/Tests/ErrorsTests.swift new file mode 100644 index 000000000..222fa30aa --- /dev/null +++ b/Tests/ErrorsTests.swift @@ -0,0 +1,88 @@ +// +// ErrorsTests.swift +// Shield +// +// Copyright © 2021 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation +@testable import ShieldSecurity +import XCTest + +class ErrorsTests: XCTestCase { + + func testCFErrorHumanReadable() { + + let error = CFErrorCreate(nil, "TestErrorDomain" as CFString, -1234, [ + kCFErrorLocalizedDescriptionKey: "An Error Occurred" as CFString, + kCFErrorLocalizedFailureReasonKey: "Invalid Parameters" as CFString, + kCFErrorLocalizedRecoverySuggestionKey: "Pass valid parameters" as CFString, + kCFErrorUnderlyingErrorKey: CFErrorCreate(nil, kCFErrorDomainPOSIX, 1, nil)!, + ] as [CFString: Any] as CFDictionary)! + + XCTAssertEqual( + error.humanReadableDescription, + """ + Code: -1234 + Domain: TestErrorDomain + Description: An Error Occurred + Failure Reason: Invalid Parameters + Recovery Suggestion: Pass valid parameters + Underlying Error: + Code: 1 + Domain: NSPOSIXErrorDomain + Description: The operation couldn’t be completed. Operation not permitted + Failure Reason: Operation not permitted + """ + ) + } + + @available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *) + func testNSErrorHumanReadable() { + + let error = NSError(domain: "TestErrorDomain", code: -1234, userInfo: [ + kCFErrorLocalizedDescriptionKey: "An Error Occurred" as CFString, + kCFErrorLocalizedFailureReasonKey: "Invalid Parameters" as CFString, + kCFErrorLocalizedRecoverySuggestionKey: "Pass valid parameters" as CFString, + NSLocalizedRecoveryOptionsErrorKey as CFString: [ + "Parameter 1 must be a string", + "Parameter 2 must be a boolean", + ], + kCFErrorUnderlyingErrorKey: POSIXError(.EPERM), + NSMultipleUnderlyingErrorsKey as CFString: [POSIXError(.EPERM), POSIXError(.ENOENT)], + ] as [String: Any]) + + XCTAssertEqual( + error.humanReadableDescription, + """ + Code: -1234 + Domain: TestErrorDomain + Description: An Error Occurred + Failure Reason: Invalid Parameters + Recovery Suggestion: Pass valid parameters + Recovery Options: + Option 0: Parameter 1 must be a string + Option 1: Parameter 2 must be a boolean + Underlying Error: + Code: 1 + Domain: NSPOSIXErrorDomain + Description: The operation couldn’t be completed. Operation not permitted + Failure Reason: Operation not permitted + Underlying Errors: + Error 0: + Code: 1 + Domain: NSPOSIXErrorDomain + Description: The operation couldn’t be completed. Operation not permitted + Failure Reason: Operation not permitted + Error 1: + Code: 2 + Domain: NSPOSIXErrorDomain + Description: The operation couldn’t be completed. No such file or directory + Failure Reason: No such file or directory + """ + ) + } +} diff --git a/Tests/SecKeyPairTests.swift b/Tests/SecKeyPairTests.swift index b1314a7a0..702f6480a 100644 --- a/Tests/SecKeyPairTests.swift +++ b/Tests/SecKeyPairTests.swift @@ -40,7 +40,7 @@ class SecKeyPairTests: XCTestCase { kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecReturnRef: kCFBooleanTrue!, - ] as CFDictionary + ] as [String: Any] as CFDictionary var privateKeyRef: CFTypeRef? XCTAssertEqual(SecItemCopyMatching(privateKeyAttrs, &privateKeyRef), errSecSuccess) XCTAssertNotNil(privateKeyRef) @@ -50,7 +50,7 @@ class SecKeyPairTests: XCTestCase { kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPublic, kSecReturnRef: kCFBooleanTrue!, - ] as CFDictionary + ] as [String: Any] as CFDictionary var publicKeyRef: CFTypeRef? XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs as CFDictionary, &publicKeyRef), errSecSuccess) XCTAssertNotNil(publicKeyRef) @@ -63,7 +63,7 @@ class SecKeyPairTests: XCTestCase { kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecReturnRef: kCFBooleanTrue!, - ] as CFDictionary + ] as [String: Any] as CFDictionary var privateKeyRef: CFTypeRef? XCTAssertEqual(SecItemCopyMatching(privateKeyAttrs, &privateKeyRef), errSecSuccess) XCTAssertNotNil(privateKeyRef) @@ -73,7 +73,7 @@ class SecKeyPairTests: XCTestCase { kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPublic, kSecReturnRef: kCFBooleanTrue!, - ] as CFDictionary + ] as [String: Any] as CFDictionary var publicKeyRef: CFTypeRef? XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs as CFDictionary, &publicKeyRef), errSecSuccess) XCTAssertNotNil(publicKeyRef) @@ -236,7 +236,7 @@ class SecKeyPairTests: XCTestCase { kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecReturnRef: kCFBooleanTrue!, - ] as CFDictionary + ] as [String: Any] as CFDictionary var privateKeyRef: CFTypeRef? XCTAssertEqual(SecItemCopyMatching(privateKeyAttrs, &privateKeyRef), errSecSuccess) XCTAssertNotNil(privateKeyRef) @@ -246,7 +246,7 @@ class SecKeyPairTests: XCTestCase { kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPublic, kSecReturnRef: kCFBooleanTrue!, - ] as CFDictionary + ] as [String: Any] as CFDictionary var publicKeyRef: CFTypeRef? XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs as CFDictionary, &publicKeyRef), errSecSuccess) XCTAssertNotNil(publicKeyRef)