Skip to content

Commit

Permalink
Merge pull request #5 from narek-sv/unified-storage
Browse files Browse the repository at this point in the history
Unified storage
  • Loading branch information
narek-sv authored Mar 3, 2024
2 parents 000eb0c + 26623a4 commit 3b11ef4
Show file tree
Hide file tree
Showing 40 changed files with 129 additions and 123 deletions.
19 changes: 12 additions & 7 deletions KeyValueStorageSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
Pod::Spec.new do |spec|
spec.name = "KeyValueStorageSwift"
spec.version = "1.1.0"
spec.version = "2.0.0"

spec.summary = "Key-value storage written in Swift."
spec.description = "An elegant, multipurpose key-value storage, compatible with all Apple platforms."
spec.homepage = "https://github.com/narek-sv/KeyValueStorage"
spec.license = { :type => "MIT", :file => "LICENSE" }
spec.author = { "Narek Sahakyan" => "[email protected]" }

spec.swift_version = "5.6"
spec.ios.deployment_target = "12.0"
spec.osx.deployment_target = "10.13"
spec.watchos.deployment_target = "4.0"
spec.tvos.deployment_target = "12.0"
spec.swift_version = "5.9"
spec.ios.deployment_target = "13.0"
spec.osx.deployment_target = "10.15"
spec.watchos.deployment_target = "6.0"
spec.tvos.deployment_target = "13.0"

spec.source = { :git => "https://github.com/narek-sv/KeyValueStorage.git", :tag => "v1.1.0" }
spec.source = { :git => "https://github.com/narek-sv/KeyValueStorage.git", :tag => "v2.0.0" }
spec.source_files = "Sources/**/*"

spec.test_spec 'Tests' do |test_spec|
test_spec.source_files = "Tests/**/*"
end

end
39 changes: 20 additions & 19 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,44 @@ let package = Package(
.tvOS(.v13)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "KeyValueStorage",
targets: ["KeyValueStorage"]),

.library(
name: "KeyValueStorageLegacy",
targets: ["KeyValueStorageLegacy"]),
.library(
name: "KeyValueStorageWrapper",
targets: ["KeyValueStorageWrapper"]),
.library(
name: "KeyValueStorageSwiftUI",
targets: ["KeyValueStorageSwiftUI"]),
name: "KeyValueStorageLegacyWrapper",
targets: ["KeyValueStorageLegacyWrapper"]),
.library(
name: "UnifiedStorage",
targets: ["UnifiedStorage"]),
name: "KeyValueStorageLegacySwiftUI",
targets: ["KeyValueStorageLegacySwiftUI"]),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "UnifiedStorage",
name: "KeyValueStorage",
dependencies: [],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]),
.testTarget(
name: "KeyValueStorageTests",
dependencies: ["KeyValueStorage"]),

.target(
name: "KeyValueStorage",
name: "KeyValueStorageLegacy",
dependencies: []),
.target(
name: "KeyValueStorageWrapper",
dependencies: [.target(name: "KeyValueStorage")]),
name: "KeyValueStorageLegacyWrapper",
dependencies: [.target(name: "KeyValueStorageLegacy")]),
.target(
name: "KeyValueStorageSwiftUI",
dependencies: [.target(name: "KeyValueStorageWrapper")]),
.testTarget(
name: "KeyValueStorageTests",
dependencies: ["KeyValueStorage", "KeyValueStorageWrapper", "KeyValueStorageSwiftUI"]),
name: "KeyValueStorageLegacySwiftUI",
dependencies: [.target(name: "KeyValueStorageLegacyWrapper")]),
.testTarget(
name: "UnifiedStorageTests",
dependencies: ["UnifiedStorage"]),
name: "KeyValueStorageLegacyTests",
dependencies: ["KeyValueStorageLegacy", "KeyValueStorageLegacyWrapper", "KeyValueStorageLegacySwiftUI"]),
]
)
76 changes: 32 additions & 44 deletions Sources/KeyValueStorage/Helpers/KeychainHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,27 @@
import Foundation
import Security

enum KeychainHelperError: Error {
case status(OSStatus)
}

/// A wrapper class which allows to use Keychain it in a similar manner to User Defaults.
final class KeychainHelper {
open class KeychainHelper: @unchecked Sendable {

/// `serviceName` is used to uniquely identify this keychain accessor.
private(set) var serviceName: String
let serviceName: String

/// `accessGroup` is used to identify which Keychain Access Group this entry belongs to. This allows you to use shared keychain access between different applications.
private(set) var accessGroup: String?
let accessGroup: String?

init(serviceName: String, accessGroup: String? = nil) {
self.serviceName = serviceName
self.accessGroup = accessGroup
}

/// Returns a Data object for a specified key.
///
/// - parameter forKey: The key to lookup data for.
/// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
/// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false.
/// - returns: The Data object associated with the key if it exists. If no data exists, returns nil.
func get(forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) -> Data? {
isSynchronizable: Bool = false) throws -> Data? {
var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
keychainQueryDictionary[KeychainHelper.matchLimit] = kSecMatchLimitOne
keychainQueryDictionary[KeychainHelper.returnData] = kCFBooleanTrue
Expand All @@ -39,75 +37,65 @@ final class KeychainHelper {
var result: AnyObject?
let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)

return status == noErr ? result as? Data : nil
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}

return result as? Data
}

/// Save a Data object to the keychain associated with a specified key. If data already exists for the given key, the data will be overwritten with the new value.
///
/// - parameter value: The Data object to save.
/// - parameter forKey: The key to save the object under.
/// - parameter withAccessibility: Optional accessibility to use when setting the keychain item.
/// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false.
/// - returns: True if the save was successful, false otherwise.
@discardableResult
func set(_ value: Data,
forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) -> Bool {
isSynchronizable: Bool = false) throws {
var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
keychainQueryDictionary[KeychainHelper.valueData] = value
keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key ?? KeychainAccessibility.whenUnlocked.key

let status = SecItemAdd(keychainQueryDictionary as CFDictionary, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
return update(value, forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
if status == errSecDuplicateItem {
try update(value, forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
} else if status != errSecSuccess {
throw KeychainHelperError.status(status)
}

return false
}

/// Remove an object associated with a specified key. If re-using a key but with a different accessibility, first remove the previous key value using removeObjectForKey(:withAccessibility) using the same accessibilty it was saved with.
///
/// - parameter forKey: The key value to remove data for.
/// - parameter withAccessibility: Optional accessibility level to use when looking up the keychain item.
/// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false.
/// - returns: True if successful, false otherwise.
@discardableResult

func remove(forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) -> Bool {
isSynchronizable: Bool = false) throws {
let keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)

let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
return status == errSecSuccess
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}
}

/// Remove all keychain data added through KeychainWrapper. This will only delete items matching the currnt ServiceName and AccessGroup if one is set.
/// - returns: True if successful, false otherwise.
@discardableResult
func removeAll() -> Bool {
func removeAll() throws {
var keychainQueryDictionary: [String: Any] = [KeychainHelper.class: kSecClassGenericPassword]
keychainQueryDictionary[KeychainHelper.attrService] = serviceName
keychainQueryDictionary[KeychainHelper.attrAccessGroup] = accessGroup

let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
return status == errSecSuccess
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}
}

// MARK: - Helpers

private func update(_ value: Data,
forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) -> Bool {
isSynchronizable: Bool = false) throws {
let updateDictionary = [KeychainHelper.valueData: value]
var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key

let status = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary)
return status == errSecSuccess
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}
}

private func query(forKey key: String,
Expand Down Expand Up @@ -144,7 +132,7 @@ extension KeychainHelper {

// MARK: - Accessibility

public enum KeychainAccessibility {
public enum KeychainAccessibility: Sendable {
case afterFirstUnlock
case afterFirstUnlockThisDeviceOnly
case whenPasscodeSetThisDeviceOnly
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,29 @@
import Foundation
import Security

enum KeychainHelperError: Error {
case status(OSStatus)
}

/// A wrapper class which allows to use Keychain it in a similar manner to User Defaults.
open class KeychainHelper: @unchecked Sendable {
final class KeychainHelper {

/// `serviceName` is used to uniquely identify this keychain accessor.
let serviceName: String
private(set) var serviceName: String

/// `accessGroup` is used to identify which Keychain Access Group this entry belongs to. This allows you to use shared keychain access between different applications.
let accessGroup: String?
private(set) var accessGroup: String?

init(serviceName: String, accessGroup: String? = nil) {
self.serviceName = serviceName
self.accessGroup = accessGroup
}

/// Returns a Data object for a specified key.
///
/// - parameter forKey: The key to lookup data for.
/// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
/// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false.
/// - returns: The Data object associated with the key if it exists. If no data exists, returns nil.
func get(forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) throws -> Data? {
isSynchronizable: Bool = false) -> Data? {
var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
keychainQueryDictionary[KeychainHelper.matchLimit] = kSecMatchLimitOne
keychainQueryDictionary[KeychainHelper.returnData] = kCFBooleanTrue
Expand All @@ -37,65 +39,75 @@ open class KeychainHelper: @unchecked Sendable {
var result: AnyObject?
let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)

if status != errSecSuccess {
throw KeychainHelperError.status(status)
}

return result as? Data
return status == noErr ? result as? Data : nil
}

/// Save a Data object to the keychain associated with a specified key. If data already exists for the given key, the data will be overwritten with the new value.
///
/// - parameter value: The Data object to save.
/// - parameter forKey: The key to save the object under.
/// - parameter withAccessibility: Optional accessibility to use when setting the keychain item.
/// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false.
/// - returns: True if the save was successful, false otherwise.
@discardableResult
func set(_ value: Data,
forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) throws {
isSynchronizable: Bool = false) -> Bool {
var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
keychainQueryDictionary[KeychainHelper.valueData] = value
keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key ?? KeychainAccessibility.whenUnlocked.key

let status = SecItemAdd(keychainQueryDictionary as CFDictionary, nil)
if status == errSecDuplicateItem {
try update(value, forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
} else if status != errSecSuccess {
throw KeychainHelperError.status(status)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
return update(value, forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
}

return false
}


/// Remove an object associated with a specified key. If re-using a key but with a different accessibility, first remove the previous key value using removeObjectForKey(:withAccessibility) using the same accessibilty it was saved with.
///
/// - parameter forKey: The key value to remove data for.
/// - parameter withAccessibility: Optional accessibility level to use when looking up the keychain item.
/// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false.
/// - returns: True if successful, false otherwise.
@discardableResult
func remove(forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) throws {
isSynchronizable: Bool = false) -> Bool {
let keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)

let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}
return status == errSecSuccess
}

func removeAll() throws {
/// Remove all keychain data added through KeychainWrapper. This will only delete items matching the currnt ServiceName and AccessGroup if one is set.
/// - returns: True if successful, false otherwise.
@discardableResult
func removeAll() -> Bool {
var keychainQueryDictionary: [String: Any] = [KeychainHelper.class: kSecClassGenericPassword]
keychainQueryDictionary[KeychainHelper.attrService] = serviceName
keychainQueryDictionary[KeychainHelper.attrAccessGroup] = accessGroup

let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}
return status == errSecSuccess
}

// MARK: - Helpers

private func update(_ value: Data,
forKey key: String,
withAccessibility accessibility: KeychainAccessibility? = nil,
isSynchronizable: Bool = false) throws {
isSynchronizable: Bool = false) -> Bool {
let updateDictionary = [KeychainHelper.valueData: value]
var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable)
keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key

let status = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary)
if status != errSecSuccess {
throw KeychainHelperError.status(status)
}
return status == errSecSuccess
}

private func query(forKey key: String,
Expand Down Expand Up @@ -132,7 +144,7 @@ extension KeychainHelper {

// MARK: - Accessibility

public enum KeychainAccessibility: Sendable {
public enum KeychainAccessibility {
case afterFirstUnlock
case afterFirstUnlockThisDeviceOnly
case whenPasscodeSetThisDeviceOnly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import SwiftUI
import Combine
import KeyValueStorage
import KeyValueStorageWrapper
import KeyValueStorageLegacy
import KeyValueStorageLegacyWrapper

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper
Expand Down
Loading

0 comments on commit 3b11ef4

Please sign in to comment.