Skip to content

Reduce the scope of @unchecked Sendable conformances #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions SimpleKeychain.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
objects = {

/* Begin PBXBuildFile section */
1DD9E19C2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */; };
1DD9E19D2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */; };
1DD9E19E2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */; };
1DD9E19F2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */; };
1DD9E1A02DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */; };
1DD9E1A22DD0036800430020 /* SimpleKeychainOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */; };
1DD9E1A32DD0036800430020 /* SimpleKeychainOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */; };
1DD9E1A42DD0036800430020 /* SimpleKeychainOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */; };
1DD9E1A52DD0036800430020 /* SimpleKeychainOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */; };
1DD9E1A62DD0036800430020 /* SimpleKeychainOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */; };
5B0D47641EA63CD1009FF1BF /* SimpleKeychainSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4D27651BCE995C003C27B3 /* SimpleKeychainSpec.swift */; };
5C29744623FF457A00BC18FA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C29744523FF457A00BC18FA /* AppDelegate.swift */; };
5C29744823FF457A00BC18FA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C29744723FF457A00BC18FA /* ViewController.swift */; };
Expand Down Expand Up @@ -117,6 +127,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleKeychainAttributes.swift; sourceTree = "<group>"; };
1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleKeychainOperations.swift; sourceTree = "<group>"; };
5B0AB18F2088E2DB002D7109 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
5B0D47591EA63C74009FF1BF /* SimpleKeychainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleKeychainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
5B108AA91EA62F6100ED4DD2 /* SimpleKeychain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleKeychain.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -323,7 +335,9 @@
children = (
5FEEB99F1B7BD70A00501415 /* Supporting Files */,
5CDF40592852D88C003840E6 /* SimpleKeychain.swift */,
1DD9E19B2DCFDC2400430020 /* SimpleKeychainAttributes.swift */,
5C737B2A285A7C0200B4BB25 /* SimpleKeychainError.swift */,
1DD9E1A12DD0036100430020 /* SimpleKeychainOperations.swift */,
5C840111285AFF7B00689C01 /* Accessibility.swift */,
);
path = SimpleKeychain;
Expand Down Expand Up @@ -938,7 +952,9 @@
buildActionMask = 2147483647;
files = (
5C737B2D285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */,
1DD9E1A22DD0036800430020 /* SimpleKeychainOperations.swift in Sources */,
5C840114285AFF7B00689C01 /* Accessibility.swift in Sources */,
1DD9E19E2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */,
5CDF405C2852D88C003840E6 /* SimpleKeychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -948,7 +964,9 @@
buildActionMask = 2147483647;
files = (
5C737B2E285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */,
1DD9E1A62DD0036800430020 /* SimpleKeychainOperations.swift in Sources */,
5C840115285AFF7B00689C01 /* Accessibility.swift in Sources */,
1DD9E1A02DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */,
5CDF405D2852D88C003840E6 /* SimpleKeychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -995,7 +1013,9 @@
buildActionMask = 2147483647;
files = (
5C737B2B285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */,
1DD9E1A32DD0036800430020 /* SimpleKeychainOperations.swift in Sources */,
5C840112285AFF7B00689C01 /* Accessibility.swift in Sources */,
1DD9E19F2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */,
5CDF405A2852D88C003840E6 /* SimpleKeychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -1005,7 +1025,9 @@
buildActionMask = 2147483647;
files = (
5C737B2C285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */,
1DD9E1A42DD0036800430020 /* SimpleKeychainOperations.swift in Sources */,
5C840113285AFF7B00689C01 /* Accessibility.swift in Sources */,
1DD9E19D2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */,
5CDF405B2852D88C003840E6 /* SimpleKeychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -1024,7 +1046,9 @@
buildActionMask = 2147483647;
files = (
C1D1FBAD2C2192FA008E9E3F /* SimpleKeychain.swift in Sources */,
1DD9E1A52DD0036800430020 /* SimpleKeychainOperations.swift in Sources */,
C1D1FBAE2C2192FA008E9E3F /* SimpleKeychainError.swift in Sources */,
1DD9E19C2DCFDC2400430020 /* SimpleKeychainAttributes.swift in Sources */,
C1D1FBAF2C2192FA008E9E3F /* Accessibility.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
24 changes: 9 additions & 15 deletions SimpleKeychain/SimpleKeychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@ import Security
@preconcurrency import LocalAuthentication
#endif

typealias RetrieveFunction = (_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
typealias RemoveFunction = (_ query: CFDictionary) -> OSStatus

/// A simple Keychain wrapper for iOS, macOS, tvOS, and watchOS.
/// Supports sharing credentials with an **access group** or through **iCloud**, and integrating
/// **Touch ID / Face ID**.
public struct SimpleKeychain: @unchecked Sendable {
public struct SimpleKeychain: Sendable {
let service: String
let accessGroup: String?
let accessibility: Accessibility
let accessControlFlags: SecAccessControlCreateFlags?
let isSynchronizable: Bool
let attributes: [String: Any]

var retrieve: RetrieveFunction = SecItemCopyMatching
var remove: RemoveFunction = SecItemDelete
let attributes: SimpleKeychainAttributes

#if canImport(LocalAuthentication) && !os(tvOS)
let context: LAContext?
Expand Down Expand Up @@ -48,7 +42,7 @@ public struct SimpleKeychain: @unchecked Sendable {
self.accessControlFlags = accessControlFlags
self.context = context
self.isSynchronizable = synchronizable
self.attributes = attributes
self.attributes = SimpleKeychainAttributes(attributes)
}
#else
/// Initializes a ``SimpleKeychain`` instance.
Expand Down Expand Up @@ -118,7 +112,7 @@ public extension SimpleKeychain {
func data(forKey key: String) throws -> Data {
let query = self.getOneQuery(byKey: key)
var result: AnyObject?
try assertSuccess(forStatus: retrieve(query as CFDictionary, &result))
try assertSuccess(forStatus: SimpleKeychainOperations.shared.retrieve(query as CFDictionary, &result))

guard let data = result as? Data else {
let message = "Unable to cast the retrieved item to a Data value"
Expand Down Expand Up @@ -182,7 +176,7 @@ public extension SimpleKeychain {
/// - Throws: A ``SimpleKeychainError`` when the SimpleKeychain operation fails.
func deleteItem(forKey key: String) throws {
let query = self.baseQuery(withKey: key)
try assertSuccess(forStatus: remove(query as CFDictionary))
try assertSuccess(forStatus: SimpleKeychainOperations.shared.remove(query as CFDictionary))
}

/// Deletes all items from the Keychain for the service and access group values.
Expand All @@ -197,7 +191,7 @@ public extension SimpleKeychain {
#if os(macOS)
query[kSecMatchLimit as String] = kSecMatchLimitAll
#endif
let status = remove(query as CFDictionary)
let status = SimpleKeychainOperations.shared.remove(query as CFDictionary)
guard SimpleKeychainError.Code(rawValue: status) != SimpleKeychainError.Code.itemNotFound else { return }
try assertSuccess(forStatus: status)
}
Expand All @@ -217,7 +211,7 @@ public extension SimpleKeychain {
/// - Throws: A ``SimpleKeychainError`` when the SimpleKeychain operation fails.
func hasItem(forKey key: String) throws -> Bool {
let query = self.baseQuery(withKey: key)
let status = retrieve(query as CFDictionary, nil)
let status = SimpleKeychainOperations.shared.retrieve(query as CFDictionary, nil)

if status == SimpleKeychainError.itemNotFound.status {
return false
Expand All @@ -239,7 +233,7 @@ public extension SimpleKeychain {
let query = self.getAllQuery
var keys: [String] = []
var result: AnyObject?
let status = retrieve(query as CFDictionary, &result)
let status = SimpleKeychainOperations.shared.retrieve(query as CFDictionary, &result)
guard SimpleKeychainError.Code(rawValue: status) != SimpleKeychainError.Code.itemNotFound else { return keys }
try assertSuccess(forStatus: status)

Expand All @@ -262,7 +256,7 @@ public extension SimpleKeychain {

extension SimpleKeychain {
func baseQuery(withKey key: String? = nil, data: Data? = nil) -> [String: Any] {
var query = self.attributes
var query = self.attributes.asDictionary
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = self.service

Expand Down
23 changes: 23 additions & 0 deletions SimpleKeychain/SimpleKeychainAttributes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public struct SimpleKeychainAttributes: @unchecked Sendable{
private let storage: [String: Any]

public init(_ attributes: [String: Any]) {
self.storage = attributes
}

public var asDictionary: [String: Any] {
return storage
}

public subscript(key: String) -> Any? {
return storage[key]
}

public var isEmpty: Bool {
return storage.isEmpty
}

public var count: Int {
return storage.count
}
}
11 changes: 11 additions & 0 deletions SimpleKeychain/SimpleKeychainOperations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Security

typealias RetrieveFunction = (_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
typealias RemoveFunction = (_ query: CFDictionary) -> OSStatus

class SimpleKeychainOperations: @unchecked Sendable {
static var shared = SimpleKeychainOperations()

var retrieve: RetrieveFunction = SecItemCopyMatching
var remove: RemoveFunction = SecItemDelete
}
12 changes: 8 additions & 4 deletions SimpleKeychainTests/SimpleKeychainSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ class SimpleKeychainTests: XCTestCase {
#if os(macOS)
func testIncludingLimitAllAttributeWhenDeletingAllItems() {
var limit: String?
sut.remove = { query in
SimpleKeychainOperations.shared.remove = { query in
let key = kSecMatchLimit as String
limit = (query as NSDictionary).value(forKey: key) as? String
return errSecSuccess
}
try? sut.deleteAll()
XCTAssertEqual(limit, kSecMatchLimitAll as String)
SimpleKeychainOperations.shared.remove = SecItemDelete
}
#else
func testNotIncludingLimitAllAttributeWhenDeletingAllItems() {
Expand Down Expand Up @@ -146,26 +147,28 @@ class SimpleKeychainTests: XCTestCase {
let key = UUID().uuidString
let message = "Unable to convert the retrieved item to a String value"
let expectedError = SimpleKeychainError(code: .unknown(message: message))
sut.retrieve = { _, result in
SimpleKeychainOperations.shared.retrieve = { _, result in
result?.pointee = .some(NSData(data: withUnsafeBytes(of: Date()) { Data($0) }))
return errSecSuccess
}
XCTAssertThrowsError(try sut.string(forKey: key)) { error in
XCTAssertEqual(error as? SimpleKeychainError, expectedError)
}
SimpleKeychainOperations.shared.retrieve = SecItemCopyMatching
}

func testRetrievingInvalidDataItem() {
let key = UUID().uuidString
let message = "Unable to cast the retrieved item to a Data value"
let expectedError = SimpleKeychainError(code: .unknown(message: message))
sut.retrieve = { _, result in
SimpleKeychainOperations.shared.retrieve = { _, result in
result?.pointee = .some(NSDate())
return errSecSuccess
}
XCTAssertThrowsError(try sut.string(forKey: key)) { error in
XCTAssertEqual(error as? SimpleKeychainError, expectedError)
}
SimpleKeychainOperations.shared.retrieve = SecItemCopyMatching
}

func testCheckingStoredItem() {
Expand Down Expand Up @@ -216,13 +219,14 @@ class SimpleKeychainTests: XCTestCase {
func testRetrievingInvalidAttributes() {
let message = "Unable to cast the retrieved items to a [[String: Any]] value"
let expectedError = SimpleKeychainError(code: .unknown(message: message))
sut.retrieve = { _, result in
SimpleKeychainOperations.shared.retrieve = { _, result in
result?.pointee = .some(NSDate())
return errSecSuccess
}
XCTAssertThrowsError(try sut.keys()) { error in
XCTAssertEqual(error as? SimpleKeychainError, expectedError)
}
SimpleKeychainOperations.shared.retrieve = SecItemCopyMatching
}

func testBaseQueryContainsDefaultAttributes() {
Expand Down