Skip to content
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

Make context parameter optional #151

Merged
merged 3 commits into from
Jun 24, 2022
Merged
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
1 change: 0 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

Expand Down
2 changes: 1 addition & 1 deletion SimpleKeychain.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Pod::Spec.new do |s|
s.summary = 'A simple Keychain wrapper for iOS, macOS, tvOS, and watchOS'
s.description = <<-DESC
Easily store your user's credentials in the Keychain.
Supports sharing credentials with an Access Group and integrating Touch ID / Face ID through a LAContext instance.
Supports sharing credentials with an Access Group or through iCloud, and integrating Touch ID / Face ID.
DESC
s.homepage = 'https://github.com/auth0/SimpleKeychain'
s.license = 'MIT'
Expand Down
30 changes: 19 additions & 11 deletions SimpleKeychain/SimpleKeychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@ import Security
import LocalAuthentication
#endif

typealias StoreFunction = (_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
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 items with an **Access Group** and integrating **Touch ID / Face ID** through a `LAContext` instance.
/// Supports sharing credentials with an **Access Group** or through **iCloud**, and integrating **Touch ID / Face ID**.
public struct SimpleKeychain {
let service: String
let accessGroup: String?
let accessibility: Accessibility
let accessControlFlags: SecAccessControlCreateFlags?
let isSynchronizable: Bool

var store: StoreFunction = SecItemAdd
var retrieve: RetrieveFunction = SecItemCopyMatching
var remove: RemoveFunction = SecItemDelete

#if canImport(LocalAuthentication)
let context: LAContext
let context: LAContext?

/// Initializes a ``SimpleKeychain`` instance.
///
/// - Parameter service: Name of the service to save items under. Defaults to the bundle identifier.
/// - Parameter service: Name of the service under which to save items. Defaults to the bundle identifier.
/// - Parameter accessGroup: Access Group for sharing Keychain items. Defaults to `nil`.
/// - Parameter accessibility: ``Accessibility`` type the stored items will have. Defaults to ``Accessibility/afterFirstUnlock``.
/// - Parameter accessControlFlags: Access control conditions for `kSecAttrAccessControl`.
Expand All @@ -36,7 +36,7 @@ public struct SimpleKeychain {
accessGroup: String? = nil,
accessibility: Accessibility = .afterFirstUnlock,
accessControlFlags: SecAccessControlCreateFlags? = nil,
context: LAContext = LAContext(),
context: LAContext? = nil,
synchronizable: Bool = false) {
self.service = service
self.accessGroup = accessGroup
Expand All @@ -48,7 +48,7 @@ public struct SimpleKeychain {
#else
/// Initializes a ``SimpleKeychain`` instance.
///
/// - Parameter service: Name of the service to save items under. Defaults to the bundle identifier.
/// - Parameter service: Name of the service under which to save items. Defaults to the bundle identifier.
/// - Parameter accessGroup: Access Group for sharing Keychain items. Defaults to `nil`.
/// - Parameter accessibility: ``Accessibility`` type the stored items will have. Defaults to ``Accessibility/afterFirstUnlock``.
/// - Parameter accessControlFlags: Access control conditions for `kSecAttrAccessControl`.
Expand Down Expand Up @@ -148,7 +148,7 @@ public extension SimpleKeychain {
/// - Throws: A ``SimpleKeychainError`` when the SimpleKeychain operation fails.
func set(_ data: Data, forKey key: String) throws {
let addItemQuery = self.setQuery(forKey: key, data: data)
let addStatus = store(addItemQuery as CFDictionary, nil)
let addStatus = SecItemAdd(addItemQuery as CFDictionary, nil)

if addStatus == SimpleKeychainError.duplicateItem.status {
let updateQuery = self.baseQuery(withKey: key)
Expand All @@ -174,7 +174,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: SecItemDelete(query as CFDictionary))
try assertSuccess(forStatus: remove(query as CFDictionary))
}

/// Deletes all items from the Keychain for the service and access group values.
Expand All @@ -189,7 +189,7 @@ public extension SimpleKeychain {
#if os(macOS)
query[kSecMatchLimit as String] = kSecMatchLimitAll
#endif
let status = SecItemDelete(query as CFDictionary)
let status = remove(query as CFDictionary)
guard SimpleKeychainError.Code(rawValue: status) != SimpleKeychainError.Code.itemNotFound else { return }
try assertSuccess(forStatus: status)
}
Expand Down Expand Up @@ -269,7 +269,9 @@ extension SimpleKeychain {
query[kSecAttrSynchronizable as String] = kCFBooleanTrue
}
#if canImport(LocalAuthentication)
query[kSecUseAuthenticationContext as String] = self.context
if let context = self.context {
query[kSecUseAuthenticationContext as String] = context
}
#endif
return query
}
Expand All @@ -295,7 +297,13 @@ extension SimpleKeychain {
let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, self.accessibility.rawValue, flags, nil) {
query[kSecAttrAccessControl as String] = access
} else {
#if os(macOS)
if self.isSynchronizable {
query[kSecAttrAccessible as String] = self.accessibility.rawValue
}
#else
query[kSecAttrAccessible as String] = self.accessibility.rawValue
#endif
}

return query
Expand Down
56 changes: 52 additions & 4 deletions SimpleKeychainTests/SimpleKeychainSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,36 @@ class SimpleKeychainSpec: QuickSpec {
expect(try sut.deleteItem(forKey: "SHOULDNOTEXIST")).to(throwError())
}

it("should clear all items") {
it("should delete all items") {
try sut.deleteAll()
expect(try sut.string(forKey: key)).to(throwError(SimpleKeychainError.itemNotFound))
}

#if os(macOS)
it("should include limit all attribute when deleting all items") {
var limit: String?
sut = SimpleKeychain(service: KeychainService)
sut.remove = { query in
let key = kSecMatchLimit as String
limit = (query as NSDictionary).value(forKey: key) as? String
return errSecSuccess
}
try sut.deleteAll()
expect(limit).toEventually(equal(kSecMatchLimitAll as String))
}
#else
it("should not include limit all attribute when deleting all items") {
var limit: String? = ""
sut = SimpleKeychain(service: KeychainService)
sut.remove = { query in
let key = kSecMatchLimit as String
limit = (query as NSDictionary).value(forKey: key) as? String
return errSecSuccess
}
try sut.deleteAll()
expect(limit).toEventually(beNil())
}
#endif
}

describe("retrieving items") {
Expand Down Expand Up @@ -221,13 +247,13 @@ class SimpleKeychainSpec: QuickSpec {
let query = sut.baseQuery()
expect((query[kSecClass as String] as? String)) == kSecClassGenericPassword as String
expect((query[kSecAttrService as String] as? String)) == sut.service
#if canImport(LocalAuthentication)
expect((query[kSecUseAuthenticationContext as String] as? LAContext)) == sut.context
#endif
expect((query[kSecAttrAccount as String] as? String)).to(beNil())
expect((query[kSecValueData as String] as? Data)).to(beNil())
expect((query[kSecAttrAccessGroup as String] as? String)).to(beNil())
expect((query[kSecAttrSynchronizable as String] as? Bool)).to(beNil())
#if canImport(LocalAuthentication)
expect((query[kSecUseAuthenticationContext as String] as? LAContext)).to(beNil())
#endif
}

it("should include account attribute") {
Expand All @@ -253,6 +279,14 @@ class SimpleKeychainSpec: QuickSpec {
let query = sut.baseQuery()
expect((query[kSecAttrSynchronizable as String] as? Bool)) == sut.isSynchronizable
}

#if canImport(LocalAuthentication)
it("should include context attribute") {
sut = SimpleKeychain(service: KeychainService, context: LAContext())
let query = sut.baseQuery()
expect((query[kSecUseAuthenticationContext as String] as? LAContext)) == sut.context
}
#endif
}

context("get all query") {
Expand Down Expand Up @@ -303,11 +337,25 @@ class SimpleKeychainSpec: QuickSpec {
expect(query[kSecAttrAccessControl as String]).toNot(beNil())
}

#if os(macOS)
it("should include accessibility attribute when iCloud sharing is enabled") {
sut = SimpleKeychain(service: KeychainService, synchronizable: true)
let query = sut.setQuery(forKey: "foo", data: Data())
let expectedAccessibility = sut.accessibility.rawValue as String
expect((query[kSecAttrAccessible as String] as? String)) == expectedAccessibility
}

it("should not include accessibility attribute when iCloud sharing is disabled") {
let query = sut.setQuery(forKey: "foo", data: Data())
expect((query[kSecAttrAccessible as String] as? String)).to(beNil())
}
#else
it("should include accessibility attribute") {
let query = sut.setQuery(forKey: "foo", data: Data())
let expectedAccessibility = sut.accessibility.rawValue as String
expect((query[kSecAttrAccessible as String] as? String)) == expectedAccessibility
}
#endif
}
}
}
Expand Down