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

Add support for iCloud synchronization [SDK-3453] #146

Merged
merged 4 commits into from
Jun 22, 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
14 changes: 12 additions & 2 deletions SimpleKeychain/SimpleKeychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public struct SimpleKeychain {
let accessGroup: String?
let accessibility: Accessibility
let accessControlFlags: SecAccessControlCreateFlags?
let isSynchronizable: Bool

var store: StoreFunction = SecItemAdd
var retrieve: RetrieveFunction = SecItemCopyMatching
Expand All @@ -29,17 +30,20 @@ public struct SimpleKeychain {
/// - Parameter accessControlFlags: Access control conditions for `kSecAttrAccessControl`.
/// When set, `kSecAttrAccessControl` will be used instead of `kSecAttrAccessible`. Defaults to `nil`.
/// - Parameter context: `LAContext` used to access Keychain items. Defaults to a new `LAContext` instance.
/// - Parameter synchronizable: Whether the items should be synchronized through iCloud. Defaults to `false`.
/// - Returns: A ``SimpleKeychain`` instance.
public init(service: String = Bundle.main.bundleIdentifier!,
accessGroup: String? = nil,
accessibility: Accessibility = .afterFirstUnlock,
accessControlFlags: SecAccessControlCreateFlags? = nil,
context: LAContext = LAContext()) {
context: LAContext = LAContext(),
synchronizable: Bool = false) {
self.service = service
self.accessGroup = accessGroup
self.accessibility = accessibility
self.accessControlFlags = accessControlFlags
self.context = context
self.isSynchronizable = synchronizable
}
#else
/// Initializes a ``SimpleKeychain`` instance.
Expand All @@ -49,15 +53,18 @@ public struct SimpleKeychain {
/// - Parameter accessibility: ``Accessibility`` type the stored items will have. Defaults to ``Accessibility/afterFirstUnlock``.
/// - Parameter accessControlFlags: Access control conditions for `kSecAttrAccessControl`.
/// When set, `kSecAttrAccessControl` will be used instead of `kSecAttrAccessible`. Defaults to `nil`.
/// - Parameter synchronizable: Whether the items should be synchronized through iCloud. Defaults to `false`.
/// - Returns: A ``SimpleKeychain`` instance.
public init(service: String = Bundle.main.bundleIdentifier!,
accessGroup: String? = nil,
accessibility: Accessibility = .afterFirstUnlock,
accessControlFlags: SecAccessControlCreateFlags? = nil) {
accessControlFlags: SecAccessControlCreateFlags? = nil,
synchronizable: Bool = false) {
self.service = service
self.accessGroup = accessGroup
self.accessibility = accessibility
self.accessControlFlags = accessControlFlags
self.isSynchronizable = synchronizable
}
#endif

Expand Down Expand Up @@ -258,6 +265,9 @@ extension SimpleKeychain {
if let data = data {
query[kSecValueData as String] = data
}
if isSynchronizable {
query[kSecAttrSynchronizable as String] = kCFBooleanTrue
}
#if canImport(LocalAuthentication)
query[kSecUseAuthenticationContext as String] = self.context
#endif
Expand Down
103 changes: 89 additions & 14 deletions SimpleKeychainTests/SimpleKeychainSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ class SimpleKeychainSpec: QuickSpec {
it("should init with default values") {
sut = SimpleKeychain()
expect(sut.accessGroup).to(beNil())
expect(sut.service).to(equal(Bundle.main.bundleIdentifier))
expect(sut.accessibility).to(equal(Accessibility.afterFirstUnlock))
expect(sut.service) == Bundle.main.bundleIdentifier
expect(sut.accessibility) == Accessibility.afterFirstUnlock
expect(sut.accessControlFlags).to(beNil())
expect(sut.isSynchronizable) == false
}

it("should init with custom values") {
sut = SimpleKeychain(service: KeychainService,
accessGroup: "Group",
accessibility: .whenUnlocked,
accessControlFlags: .userPresence)
expect(sut.accessGroup).to(equal("Group"))
expect(sut.service).to(equal(KeychainService))
expect(sut.accessibility).to(equal(Accessibility.whenUnlocked))
expect(sut.accessControlFlags).to(equal(.userPresence))
accessControlFlags: .userPresence,
synchronizable: true)
expect(sut.accessGroup) == "Group"
expect(sut.service) == KeychainService
expect(sut.accessibility) == Accessibility.whenUnlocked
expect(sut.accessControlFlags) == .userPresence
expect(sut.isSynchronizable) == true
}

#if canImport(LocalAuthentication)
Expand Down Expand Up @@ -64,6 +67,18 @@ class SimpleKeychainSpec: QuickSpec {
expect(try sut.set("value2", forKey: key)).toNot(throwError())
}

it("should store a string item with the default accessibility value") {
var accessible: String?
sut = SimpleKeychain(service: KeychainService)
sut.store = { query, _ in
let key = kSecAttrAccessible as String
accessible = (query as NSDictionary).value(forKey: key) as? String
return errSecSuccess
}
try sut.set("value", forKey: key)
expect(accessible).toEventually(equal(kSecAttrAccessibleAfterFirstUnlock as String))
}

it("should store a string item with a custom accessibility value") {
var accessible: String?
sut = SimpleKeychain(service: KeychainService, accessibility: .whenUnlocked)
Expand All @@ -87,6 +102,30 @@ class SimpleKeychainSpec: QuickSpec {
try sut.set("value", forKey: key)
expect(accessControl).toEventuallyNot(beNil())
}

it("should store a non-synchronizable string item by default") {
var synchronizable: Bool? = false
sut = SimpleKeychain(service: KeychainService)
sut.store = { query, _ in
let key = kSecAttrSynchronizable as String
synchronizable = (query as NSDictionary).value(forKey: key) as? Bool
return errSecSuccess
}
try sut.set("value", forKey: key)
expect(synchronizable).toEventually(beNil())
}

it("should store a synchronizable string item") {
var synchronizable: Bool?
sut = SimpleKeychain(service: KeychainService, synchronizable: true)
sut.store = { query, _ in
let key = kSecAttrSynchronizable as String
synchronizable = (query as NSDictionary).value(forKey: key) as? Bool
return errSecSuccess
}
try sut.set("value", forKey: key)
expect(synchronizable).toEventually(equal(true))
}
}

context("data items") {
Expand All @@ -99,6 +138,18 @@ class SimpleKeychainSpec: QuickSpec {
expect(try sut.set(Data(), forKey: key)).toNot(throwError())
}

it("should store a data item with the default accessibility value") {
var accessible: String?
sut = SimpleKeychain(service: KeychainService)
sut.store = { query, _ in
let key = kSecAttrAccessible as String
accessible = (query as NSDictionary).value(forKey: key) as? String
return errSecSuccess
}
try sut.set(Data(), forKey: key)
expect(accessible).toEventually(equal(kSecAttrAccessibleAfterFirstUnlock as String))
}

it("should store a string item with a custom accessibility value") {
sut = SimpleKeychain(service: KeychainService, accessibility: .whenUnlocked)
expect(try sut.set(Data(), forKey: key)).toNot(throwError())
Expand All @@ -115,6 +166,30 @@ class SimpleKeychainSpec: QuickSpec {
try sut.set(Data(), forKey: key)
expect(accessControl).toEventuallyNot(beNil())
}

it("should store a non-synchronizable data item by default") {
var synchronizable: Bool? = false
sut = SimpleKeychain(service: KeychainService)
sut.store = { query, _ in
let key = kSecAttrSynchronizable as String
synchronizable = (query as NSDictionary).value(forKey: key) as? Bool
return errSecSuccess
}
try sut.set(Data(), forKey: key)
expect(synchronizable).toEventually(beNil())
}

it("should store a synchronizable data item") {
var synchronizable: Bool?
sut = SimpleKeychain(service: KeychainService, synchronizable: true)
sut.store = { query, _ in
let key = kSecAttrSynchronizable as String
synchronizable = (query as NSDictionary).value(forKey: key) as? Bool
return errSecSuccess
}
try sut.set(Data(), forKey: key)
expect(synchronizable).toEventually(equal(true))
}
}
}

Expand Down Expand Up @@ -151,7 +226,7 @@ class SimpleKeychainSpec: QuickSpec {
}

it("should retrieve string item") {
expect(try sut.string(forKey: key)).to(equal("value1"))
expect(try sut.string(forKey: key)) == "value1"
}

it("should retrieve data item") {
Expand Down Expand Up @@ -199,11 +274,11 @@ class SimpleKeychainSpec: QuickSpec {
}

it("should return true when the item is stored") {
expect(try sut.hasItem(forKey: key)).to(beTrue())
expect(try sut.hasItem(forKey: key)) == true
}

it("should return false when the item is not stored") {
expect(try sut.hasItem(forKey: "SHOULDNOTEXIST")).to(beFalse())
expect(try sut.hasItem(forKey: "SHOULDNOTEXIST")) == false
}
}

Expand All @@ -222,20 +297,20 @@ class SimpleKeychainSpec: QuickSpec {
}

it("should return all the keys") {
expect(try sut.keys()).to(equal(keys))
expect(try sut.keys()) == keys
}

it("should return an empty array when there are no keys") {
for key in keys {
expect(try sut.data(forKey: key)).notTo(beNil())
}
expect(try sut.keys().count).to(equal(keys.count))
expect(try sut.keys().count) == keys.count
try sut.deleteAll()
let expectedError = SimpleKeychainError.itemNotFound
for key in keys {
expect(try sut.data(forKey: key)).to(throwError(expectedError))
}
expect(try sut.keys().count).to(equal(0))
expect(try sut.keys().count) == 0
}

it("should throw an error when retrieving invalid attributes") {
Expand Down