.
675 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "KeychainManager",
8 | platforms: [
9 | .macOS(.v11), .iOS(.v13), .tvOS(.v11), .watchOS(.v6)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "KeychainManager",
15 | targets: ["KeychainManager"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "KeychainManager",
26 | dependencies: [],
27 | path: "Sources"),
28 | .testTarget(
29 | name: "KeychainManagerTests",
30 | dependencies: ["KeychainManager"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Keychain Manager
4 | **Keychain Manager** is a Layer-2 framework built over Keychain API which helps in using Keychain in all your Apple devices with easiness and flexibility. It focuses on using all the power of Keychain with high simplicity. The easy to use methods of Keychain Manager helps to setup Keychain on any Apple device with great convenience.
5 |
6 | ## 📔 Usage
7 |
8 | ### ⚙️ Intilisation
9 | Before using any Keychain Manager methods we need to intialise the class. Keychain Manager supports various types of inilisation which depends upon variety of use cases
10 |
11 | ### 🗳 Basic Initialisation
12 | * This initilisation stores all the Keychain items on the local device.
13 | * Such initilisations are best used when the app is single login based.
14 |
15 | ```swift
16 | let KCM = KeychainManager()
17 | ```
18 |
19 | ### 🗳 Prefix Initiliser
20 | * This initiliser helps to add a prefix value in your account string.
21 | * Such initilisations are best used when performing tests (***Eg: test_account1_***).
22 | ```swift
23 | let KCM = KeychainManager(keyPrefix: "test")
24 | ```
25 |
26 |
27 | ### 🗳 Sharable Initiliser
28 | * Keychain Manger allowes developers to share the keychain values to other apps also synchronise with iCloud.
29 | * Such initilisations are best used when you need to share Keychain values among apps.
30 | * ***Eg: A same app running on two different devices with same iCloudID & To share data between Different apps running on same or different device***
31 |
32 | ```swift
33 | let KCM = KeychainManager(accessGroup: "TeamID.KeychainGroupID", synchronizable: true)
34 | ```
35 | * To use this you need to enable the Keychain sharing in capabilities. ([How to add Keychain Sharing Capability?](https://github.com/gokulnair2001/KeychainManager#-keychain-sharing))
36 | * Here **TeamID** is which you get from your developer profile from [Developer Account](http://developer.apple.com).
37 | * **KeychainGroupID** is the string which you add in the Keychain Sharing Capability.
38 |
39 |
40 | ### 🗳 Prefix + Sharable
41 | * When you need to add both prefix and sharable propert on keychain then this initialisation is the best one to use
42 |
43 | ```swift
44 | let KCM = KeychainManager(keyPrefix: "test", accessGroup: "TeamID.KeychainGroupID", synchronizable: true)
45 | ```
46 | ## 🛠 Operations
47 |
48 | Following are the methods which help to perfrom various operations:
49 |
50 | ## 🔑 SET
51 | * Used to save data on keychain.
52 | * Keychain Manager Supports variety of data storage
53 |
54 | #### String
55 | ```swift
56 | KCM.set(value: "value", service: service_ID, account: account_name)
57 | ```
58 | #### Bool
59 | ```swift
60 | KCM.set(value: true, service: service_ID, account: account_name)
61 | ```
62 | #### Custom Object
63 | ```swift
64 | KCM.set(object: Any_Codable_Object, service: service_ID, account: account_name)
65 | ```
66 |
67 | #### Web Credentials
68 | ```swift
69 | KCM.set(server: server_ID, account: account_name, password: password)
70 | ```
71 | **Tip: Make sure Account, Service & Server parameter must be unique for every item.**
72 |
73 |
74 | ## 🔑 GET
75 | * Used to get Keychain Items.
76 | * Keychain Manager helps to GET variety of format of Data from Keychain Storage
77 |
78 | #### String
79 | ```swift
80 | let value = KCM.get(service: service_ID, account: account_name)
81 | ```
82 | #### Bool
83 | ```swift
84 | let value = KCM.getBool(service: service_ID, account: account_name)
85 | ```
86 | #### Custom Object
87 | ```swift
88 | let value = KCM.get(object: Any_Codable_Object, service: service_ID, account: account_name)
89 | ```
90 |
91 | #### Web Credentials
92 | ```swift
93 | let value = KCM.get(server: server_ID, account: account_name)
94 | ```
95 |
96 | #### Get All Values
97 | * Generic Password
98 | ```swift
99 | let value = KCM.getAllValues(secClass: .genericPassword)
100 | ```
101 | * Web Credentials
102 | ```swift
103 | let value = KCM.getAllValues(secClass: .webCredentials)
104 | ```
105 |
106 | ## 🔑 UPDATE
107 |
108 | * Used to update Kechain Item Values
109 | * Since we have variety of SET and GET methods, similarly to update them we have variety of UPDATE methods
110 |
111 | #### String
112 | ```swift
113 | KCM.update(value: "value", service: service_ID, account: account_name)
114 | ```
115 | #### Bool
116 | ```swift
117 | KCM.update(value: true, service: service_ID, account: account_name)
118 | ```
119 | #### Custom Object
120 | ```swift
121 | KCM.update(object: Any_Codable_Object, service: service_ID, account: account_name)
122 | ```
123 |
124 | #### Web Credentials
125 | ```swift
126 | KCM.update(server: server_ID, account: account_name, password: password)
127 | ```
128 |
129 | ## 🔑 DELETE
130 | * Used to delete Keychain Items
131 |
132 | #### Service Deletion
133 | ```swift
134 | do {
135 | try KCMTest.delete(service: service_ID, isCustomObjectType: false)
136 | }
137 | catch {
138 | print(error.localizedDescription)
139 | }
140 | ```
141 | * **isCustomObjectType** is used to explicitly tell Keychain Manager to delete a custom Object type.
142 | * By default the value of **isCustomObjectType** is ```false```
143 |
144 | #### Server Deletion
145 | ```swift
146 | do {
147 | try KCMTest.delete(server: server_ID)
148 | }
149 | catch {
150 | print(error.localizedDescription)
151 | }
152 | ```
153 |
154 | ## 🔑 VALIDATE
155 | * Is used to check if a certain Server or Service based keychain is valid/present.
156 |
157 | #### Service
158 | ```swift
159 | if KCM.isValidService(service: service_ID, account: account_name) {
160 | print("🙂")
161 | } else {
162 | print("☹️")
163 | }
164 | ```
165 | #### Server
166 | ```swift
167 | if KCM.isValidService(server: server_ID, account: account_name) {
168 | print("🙂")
169 | } else {
170 | print("☹️")
171 | }
172 | ```
173 |
174 | ## ☁️ iCloud Sync
175 | * iCloud synchronisation needs to be set during initilisation.
176 | * Make sure to use the sharable initialisation at every method to save all changes on cloud.
177 |
178 |
179 | ## 📱 Device Supported
180 | | No | Device | Version |
181 | | -- | -- | -- |
182 | | 1 | iOS | 13.0.0 + |
183 | | 2 | iPadOS | 13.0.0 + |
184 | | 3 | WatchOS | 6.0.0 +|
185 | | 4 | MacOS | 11.0.0 + |
186 | | 5 | tvOS | 11.0.0 + |
187 |
188 | ## 📌 Keynotes
189 | Make sure you know these keynotes before using Keychain Manager
190 | * GET Bool will return false even after deleting the Keychain item.
191 | * To delete a custom object make sure you explicitly tell Keychain Manager that its a custom object in the delete method.
192 | * By enabling iCloud sync you also enable Keychain Access Group, thus adding Keychain Sharing capability is important ([How to do it?](https://github.com/gokulnair2001/KeychainManager#-keychain-sharing)).
193 | * Every Keychain item stored through MacOS is also saved in form of iOS, making it easy for developers to share same keychain data among all platforms. Thus keychain manager will give access to your MacOS based keychain items on other platforms too ([Read this](https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain)).
194 | * Keychain Items stored on tvOS will not sync with other platforms ([Read this](https://developer.apple.com/documentation/security/ksecattrsynchronizable)).
195 |
196 |
197 | ## 📦 SPM
198 | Keychain Manger is available through [Swift Package Manager](https://github.com/apple/swift-package-manager/). To add Keychain Manager through SPM
199 | * Open project in Xcode
200 | * **Select ```File > Add Packages```**
201 |
202 | ```swift
203 | https://github.com/gokulnair2001/KeychainManager
204 | ```
205 |
206 |
207 | ## 🌐 Keychain Sharing
208 | * Open your project target
209 | * Select ```Signing & Capabilities``` option
210 | * Click plus button and search for ```Keychain Sharing```
211 |
212 |
213 |
214 |
215 |
216 | * Make sure to use the same Keychain Item in other apps you add the Keychain access group ID of the initial project.
217 |
218 | ## 🪄 How to contribute ?
219 |
220 | * Use the framework through SPM
221 | * If you face issues in any step open a new issue.
222 | * To fix issues: Fork this repository, make your changes and make a Pull Request.
223 |
224 | ## ⚖️ License
225 | * Keychain Manager is available under GNU General Public [License](https://github.com/gokulnair2001/KeychainManager/blob/master/LICENSE).
226 |
227 | ## Like the framework ?
228 | * If you liked ```Keychain Manager``` do consider buying me a coffee 😊
229 |
230 | [
](https://www.buymeacoffee.com/gokulnair)
231 |
232 |
233 | Made with ❤️ in 🇮🇳 By Gokul Nair
234 |
235 |
--------------------------------------------------------------------------------
/Sources/KeychainManager/KeychainManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainManager.swift
3 | // KeyChain-Test
4 | //
5 | // Created by Gokul Nair on 18/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | open class KeychainManager {
11 |
12 | /// KeyPrefix: Prefix used to append to the account id.
13 | /// Such Prefix are best used when performing tests. Eg: test_account1_
14 | fileprivate var keyPrefix: String = ""
15 |
16 | /// AccessGroup: Unique access group ID
17 | /// Used to sync data among same ID devices
18 | fileprivate var accessGroup: String = ""
19 |
20 | /// Synchronizable: Bool which specifies if synchronizable data.
21 | /// When enabled all the keychains will be saved on the users iCloud account.
22 | fileprivate var synchronizable: Bool = false
23 |
24 | /// Initialiser to pre set KeyPrefix
25 | public init(keyPrefix: String) {
26 | self.keyPrefix = keyPrefix
27 | }
28 |
29 | /// Initialiser to pre set access group and iCloud sync state of Keychain
30 | public init(accessGroup:String, synchronizable: Bool) {
31 | self.accessGroup = accessGroup
32 | self.synchronizable = synchronizable
33 | }
34 |
35 | /// Initialiser to pre set keyPrefix, access group and sync state of Keychain
36 | public init(keyPrefix: String, accessGroup:String, synchronizable: Bool) {
37 | self.keyPrefix = keyPrefix
38 | self.accessGroup = accessGroup
39 | self.synchronizable = synchronizable
40 | }
41 |
42 | /// Empty Initialiser to use generic keyChain
43 | public init() {}
44 |
45 | }
46 |
47 | //MARK: - SET
48 | extension KeychainManager {
49 |
50 | //MARK: SET DRIVER CODE
51 | fileprivate func set(value: Data, service: String, account: String) throws {
52 |
53 | var query: [String: AnyObject] = [
54 | KMConstants.classType.value() : kSecClassGenericPassword,
55 | KMConstants.service.value() : service as AnyObject,
56 | KMConstants.account.value() : (keyPrefix + account) as AnyObject,
57 | KMConstants.valueData.value() : value as AnyObject,
58 | KMConstants.dataProtection.value() : kCFBooleanTrue as AnyObject,
59 | ]
60 |
61 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
62 |
63 | let status = SecItemAdd(query as CFDictionary, nil)
64 |
65 |
66 | guard status != errSecDuplicateItem else {
67 | throw KeychainError.duplicateEntry
68 | }
69 |
70 | guard status == errSecSuccess else {
71 | throw KeychainError.unknown(status)
72 | }
73 | }
74 |
75 | // MARK: Method to save boolean values
76 | /// Function to SET/SAVE keychain values as Bool
77 | /// - Parameters:
78 | /// - value: Bool value to save
79 | /// - service: String to specify the service associated with this item
80 | /// - account: Account name of keychain holder
81 | public func set(value: Bool, service: String, account: String) {
82 | let bytes: [UInt8] = value ? [1] : [0]
83 |
84 | do {
85 | try set(value: Data(bytes), service: service, account: account)
86 | }catch {
87 | print(error)
88 | }
89 | }
90 |
91 | // MARK: Method to store String directly to keychain
92 | /// Function to SET/SAVE keychain values as String
93 | /// - Parameters:
94 | /// - value: String value to save
95 | /// - service: String to specify the service associated with this item
96 | /// - account: Account name of keychain holder
97 | public func set(value: String, service: String, account: String) {
98 |
99 | do {
100 | try set(value: value.data(using: .utf8) ?? Data(), service: service, account: account)
101 | }catch {
102 | print(error)
103 | }
104 | }
105 |
106 | // MARK: Method to save Custom Data Object
107 | /// Function to SET/SAVE keychain values as Custom Objects
108 | /// - Parameters:
109 | /// - object: Custom Codable object to save
110 | /// - service: String to specify the service associated with this item
111 | /// - account: Account name of keychain holder
112 | public func set (object: T, service: String, account: String) {
113 |
114 | guard let userData = try? JSONEncoder().encode(object) else { return }
115 |
116 | do {
117 | try? KeychainManager().set(value: userData, service: service, account: account)
118 | }
119 | }
120 |
121 | //MARK: SET WEB CREDENTIALS DRIVER
122 | fileprivate func set(server: String, user: String, password: String) throws {
123 |
124 | let encryptedPassword = password.data(using: .utf8)
125 |
126 | var query: [String : AnyObject] = [
127 | KMConstants.classType.value() : kSecClassInternetPassword,
128 | KMConstants.account.value() : user as AnyObject,
129 | KMConstants.server.value() : server as AnyObject,
130 | KMConstants.valueData.value() : encryptedPassword as AnyObject,
131 | KMConstants.dataProtection.value() : kCFBooleanTrue as AnyObject,
132 | ]
133 |
134 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
135 |
136 | let status = SecItemAdd(query as CFDictionary, nil)
137 |
138 | guard status != errSecDuplicateItem else {
139 | throw KeychainError.duplicateEntry
140 | }
141 |
142 | guard status == errSecSuccess else {
143 | throw KeychainError.unknown(status)
144 | }
145 | }
146 |
147 | //MARK: Method to store web credentials
148 | /// Function to SET/SAVE Internet passwords on keychain
149 | /// - Parameters:
150 | /// - server: Contains the server's domain name or IP address
151 | /// - account: Account name of keychain holder
152 | /// - password: Password to save in keychain
153 | public func set(server: String, account: String, password: String) {
154 |
155 | do {
156 | try set(server: server, user: account, password: password)
157 | }catch {
158 | print(error.localizedDescription)
159 | }
160 | }
161 |
162 | }
163 |
164 | //MARK: - GET
165 | extension KeychainManager {
166 |
167 | //MARK: GET DRIVER CODE
168 | fileprivate func get(service: String, account: String) -> (value: Data?, status: OSStatus) {
169 |
170 | var query: [String: AnyObject] = [
171 | KMConstants.classType.value() : kSecClassGenericPassword,
172 | KMConstants.service.value() : service as AnyObject,
173 | KMConstants.account.value() : (keyPrefix + account) as AnyObject,
174 | KMConstants.returnData.value() : kCFBooleanTrue,
175 | KMConstants.matchLimit.value() : kSecMatchLimitOne
176 | ]
177 |
178 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
179 |
180 | var result: AnyObject?
181 | let status = SecItemCopyMatching(query as CFDictionary, &result)
182 |
183 | return (result as? Data, status)
184 | }
185 |
186 | //MARK: Method to fetch bool values
187 | @discardableResult
188 | /// Function to GET/FETCH keychain values as stored as Bool
189 | /// - Parameters:
190 | /// - service: String to specify the service associated with this item
191 | /// - account: Account name of keychain holder
192 | /// - Returns: Returns the Bool value stored
193 | public func getBool(service: String, account: String) -> Bool {
194 | guard let data = get(service: service, account: account).value else {return false}
195 | guard let firstBit = data.first else {return false}
196 |
197 | return firstBit == 1
198 | }
199 |
200 | //MARK: Method to get custom object type
201 | @discardableResult
202 | /// Function to GET/FETCH keychain values as stored as Custom Object
203 | /// - Parameters:
204 | /// - object: Custom Codable object to save
205 | /// - service: String to specify the service associated with this item
206 | /// - account: Account name of keychain holder
207 | /// - Returns: Returns the Codable object stored
208 | public func get (object: T, service: String, account: String) -> T? {
209 |
210 | guard let userData = get(service: service, account: account).value else {return nil}
211 |
212 | guard let decodedData = try? JSONDecoder().decode(T.self, from: userData) else { return nil}
213 |
214 | return decodedData
215 | }
216 |
217 | //MARK: Method to get String value
218 | @discardableResult
219 | /// Function to GET/FETCH keychain values as stored as String
220 | /// - Parameters:
221 | /// - service: String to specify the service associated with this item
222 | /// - account: Account name of keychain holder
223 | /// - Returns: Returns the String value stored
224 | public func get(service: String, account: String) -> String {
225 |
226 | let rawData: Data? = get(service: service, account: account).value
227 |
228 | let userData = String(decoding: rawData ?? Data(), as: UTF8.self)
229 |
230 | return userData
231 | }
232 |
233 | //MARK: GET WEB CREDENTIALS DRIVER
234 | fileprivate func get(server: String, account: String) -> (password: Data?, status: OSStatus) {
235 |
236 | var query: [String: AnyObject] = [
237 | KMConstants.classType.value() : kSecClassInternetPassword,
238 | KMConstants.account.value() : account as AnyObject,
239 | KMConstants.server.value() : server as AnyObject,
240 | KMConstants.returnData.value() : kCFBooleanTrue,
241 | KMConstants.matchLimit.value() : kSecMatchLimitOne,
242 | ]
243 |
244 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
245 |
246 | var result: AnyObject?
247 | let status = SecItemCopyMatching(query as CFDictionary, &result)
248 |
249 | return (result as? Data, status)
250 | }
251 |
252 | //MARK: Method to get Web Credential value
253 | @discardableResult
254 | /// Function to GET/FETCH internet password stored
255 | /// - Parameters:
256 | /// - server: Contains the server's domain name or IP address
257 | /// - account: Account name of keychain holder
258 | /// - Returns: Returns the password stored
259 | public func get(server: String, account: String) -> String {
260 |
261 | let rawData: Data? = get(server: server, account: account).password
262 |
263 | let userData = String(decoding: rawData ?? Data(), as: UTF8.self)
264 |
265 | return userData
266 | }
267 |
268 | //MARK: - VALIDATE SERVICE AND SERVER
269 | /// Function to validate the provided Service and Account combination
270 | /// - Parameters:
271 | /// - service: String to specify the service associated with this item
272 | /// - account: Account name of keychain holder
273 | /// - Returns: Returns the validation result
274 | public func isValidService(service: String, account: String) -> Bool {
275 |
276 | let status = get(service: service, account: account).status
277 |
278 | if status == 0 {
279 | return true
280 | }
281 |
282 | return false
283 | }
284 |
285 | /// Function to validate the provided Server and Account combination
286 | /// - Parameters:
287 | /// - server: Contains the server's domain name or IP address
288 | /// - account: Account name of keychain holder
289 | /// - Returns: Returns the validation result
290 | public func isValidServer(server: String, account: String) -> Bool {
291 |
292 | let status = get(server: server, account: account).status
293 |
294 | if status == 0 {
295 | return true
296 | }
297 |
298 | return false
299 | }
300 |
301 | //MARK: - GET ALL VALUES
302 | @discardableResult
303 | /// Function to GET/FETCH all the values stored in keychain
304 | /// - Parameter secClass: Specifies the keychain security class to fetch
305 | /// - Returns: Returns all the values stored
306 | public func getAllValues(secClass: secureClassType) -> [String:String] {
307 |
308 | var query: [String: AnyObject] = [
309 | KMConstants.classType.value() : secClass.value() as AnyObject,
310 | KMConstants.returnData.value() : kCFBooleanTrue,
311 | KMConstants.returnAttributes.value() : kCFBooleanTrue,
312 | KMConstants.returnReference.value() : kCFBooleanTrue,
313 | KMConstants.matchLimit.value() : kSecMatchLimitAll
314 | ]
315 |
316 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
317 |
318 | var result: AnyObject?
319 |
320 | let lastResultCode = withUnsafeMutablePointer(to: &result) {
321 | SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
322 | }
323 |
324 | var values = [String:String]()
325 | if lastResultCode == noErr {
326 | let array = result as? Array>
327 |
328 | for item in array! {
329 | if let key = item[kSecAttrAccount as String] as? String,
330 | let value = item[kSecValueData as String] as? Data {
331 | values[key] = String(data: value, encoding:.utf8)
332 | }
333 | }
334 | }
335 |
336 | return values
337 | }
338 | }
339 |
340 | //MARK: - UPDATE
341 | extension KeychainManager {
342 |
343 | //MARK: UPDATE DRIVER CODE FOR GENERIC PASSWORD
344 | fileprivate func update(value: Data, account: String, service: String, isCustomObjectType: Bool = false) throws {
345 |
346 | let attributes: [String: Any] = [
347 | KMConstants.account.value() : (keyPrefix + account) as AnyObject,
348 | KMConstants.valueData.value() : value,
349 | ]
350 |
351 | var query: [String: AnyObject] = [
352 | KMConstants.classType.value() : kSecClassGenericPassword,
353 | KMConstants.service.value() : service as AnyObject
354 | ]
355 | if !isCustomObjectType {
356 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
357 | }
358 |
359 | let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
360 |
361 | guard status != errSecItemNotFound else { throw KeychainError.noPassword }
362 |
363 | guard status == errSecSuccess else { throw KeychainError.unknown(status) }
364 | }
365 |
366 | //MARK: Update Bool Values
367 | /// Function to UPDATE any bool value stored in Keychain
368 | /// - Parameters:
369 | /// - value: specifies the new bool value to be updated
370 | /// - service: String to specify the service associated with this item
371 | /// - account: Account name of keychain holder
372 | public func update(value: Bool, service: String, account: String) {
373 | let bytes: [UInt8] = value ? [1] : [0]
374 |
375 | do {
376 | try update(value: Data(bytes), account: account, service: service)
377 | }catch {
378 | print(error.localizedDescription)
379 | }
380 | }
381 |
382 | //MARK: Update String Value
383 | /// Function to UPDATE any string value stored in Keychain
384 | /// - Parameters:
385 | /// - value: specifies the new string value to be updated
386 | /// - service: String to specify the service associated with this item
387 | /// - account: Account name of keychain holder
388 | public func update(value: String, service: String, account: String) {
389 | do {
390 | try update(value: value.data(using: .utf8) ?? Data(), account: account, service: service)
391 |
392 | }catch {
393 | print(error.localizedDescription)
394 | }
395 | }
396 |
397 | //MARK: Update Custom Objects
398 | /// Function to UPDATE any codable object stored in Keychain
399 | /// - Parameters:
400 | /// - object: specifies the new object to be updated
401 | /// - service: String to specify the service associated with this item
402 | /// - account: Account name of keychain holder
403 | public func update (object: T, service: String, account: String) {
404 |
405 | guard let userData = try? JSONEncoder().encode(object) else { return }
406 |
407 | do {
408 | try update(value: userData, account: account, service: service, isCustomObjectType: true)
409 |
410 | }catch {
411 | print(error.localizedDescription)
412 | }
413 | }
414 |
415 | //MARK: UPDATE WEB CREDENTIALS DRIVER
416 | fileprivate func update(server: String, account: String, password: Data) throws {
417 |
418 | let attributes: [String: Any] = [
419 | KMConstants.account.value() : (keyPrefix + account) as AnyObject,
420 | KMConstants.valueData.value() : password as AnyObject,
421 | ]
422 |
423 | var query: [String: AnyObject] = [
424 | KMConstants.classType.value() : kSecClassInternetPassword,
425 | KMConstants.server.value() : server as AnyObject,
426 | ]
427 |
428 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
429 |
430 | let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
431 |
432 | guard status != errSecItemNotFound else { throw KeychainError.noPassword }
433 |
434 | guard status == errSecSuccess else { throw KeychainError.unknown(status) }
435 | }
436 |
437 | //MARK: Update Web Credentials
438 | /// Function to UPDATE any password stored in Keychain
439 | /// - Parameters:
440 | /// - server: Contains the server's domain name or IP address
441 | /// - account: Account name of keychain holder
442 | /// - password: Specifies the new password to be updated
443 | public func update(server: String, account: String, password: String) {
444 | do {
445 | let encryptedPassword = password.data(using: .utf8) ?? Data()
446 | try update(server: server, account: account, password: encryptedPassword)
447 |
448 | }catch {
449 | print(error.localizedDescription)
450 | }
451 | }
452 | }
453 |
454 | //MARK: - DELETE
455 | extension KeychainManager {
456 |
457 | //MARK: DELETE SELECTED ITEM
458 | /// Function to DELETE / REMOVE a keychain value
459 | /// - Parameters:
460 | /// - service: String to specify the service associated with this item
461 | /// - isCustomObjectType: Explicitly tells the item to delete is a custom value type
462 | public func delete(service: String, isCustomObjectType: Bool = false) throws {
463 |
464 | var query: [String: AnyObject] = [
465 | KMConstants.classType.value() : kSecClassGenericPassword,
466 | KMConstants.service.value() : service as AnyObject,
467 | ]
468 |
469 | if !isCustomObjectType {
470 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
471 | }
472 |
473 | let status = SecItemDelete(query as CFDictionary)
474 | guard status == errSecSuccess || status == errSecItemNotFound else {
475 | throw KeychainError.unknown(status) }
476 | }
477 |
478 | //MARK: DELETE WEB CREDENTIALS
479 | /// FFunction to DELETE / REMOVE passwords saved on Keychain
480 | /// - Parameters:
481 | /// - server: Contains the server's domain name or IP address
482 | /// - account: Account name of keychain holder
483 | public func delete(server: String) throws {
484 |
485 | var query: [String: AnyObject] = [
486 | KMConstants.classType.value() : kSecClassInternetPassword,
487 | KMConstants.server.value() : server as AnyObject,
488 | ]
489 |
490 | query = addSyncIfRequired(queryItems: query, isSynchronizable: synchronizable)
491 |
492 | let status = SecItemDelete(query as CFDictionary)
493 | guard status == errSecSuccess || status == errSecItemNotFound else {
494 | throw KeychainError.unknown(status) }
495 | }
496 | }
497 |
498 | //MARK: - Tools
499 | extension KeychainManager {
500 |
501 | /// Method to enable iCloud Sync
502 | private func addSyncIfRequired(queryItems: [String: AnyObject], isSynchronizable: Bool) -> [String: AnyObject] {
503 |
504 | if isSynchronizable {
505 | var result: [String: AnyObject] = queryItems
506 | result[KMConstants.accessGroup.value()] = accessGroup as AnyObject
507 | result[KMConstants.synchronizable.value()] = isSynchronizable ? kCFBooleanTrue as AnyObject : kSecAttrSynchronizableAny as AnyObject
508 |
509 | return result
510 | }
511 |
512 | return queryItems
513 | }
514 | }
515 |
--------------------------------------------------------------------------------
/Sources/KeychainManager/KeychainManagerConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainManagerConstants.swift
3 | // KeyChain-Test
4 | //
5 | // Created by Gokul Nair on 19/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | //MARK: - Keychain Manager Secure Storage Items
11 | /// The KMConstants enum returns the String value of CFString based Items
12 | enum KMConstants {
13 |
14 | case classType
15 | case service
16 | case account
17 | case valueData
18 | case returnData
19 | case returnReference
20 | case returnAttributes
21 | case matchLimit
22 | case accessGroup
23 | case server
24 | case synchronizable
25 | case dataProtection
26 | case internetPassword
27 | case genericPassword
28 |
29 | /// Method to cast CFString to String
30 | private func castToString(_ value: CFString) -> String {
31 | return value as String
32 | }
33 |
34 | /// Method to return value of KMConstants Selected
35 | func value() -> String {
36 | switch self {
37 | case .classType:
38 | return castToString(kSecClass)
39 | case .service:
40 | return castToString(kSecAttrService)
41 | case .account:
42 | return castToString(kSecAttrAccount)
43 | case .valueData:
44 | return castToString(kSecValueData)
45 | case .returnData:
46 | return castToString(kSecReturnData)
47 | case .returnReference:
48 | return castToString(kSecReturnRef)
49 | case .returnAttributes:
50 | return castToString(kSecReturnAttributes)
51 | case .matchLimit:
52 | return castToString(kSecMatchLimit)
53 | case .accessGroup:
54 | return castToString(kSecAttrAccessGroup)
55 | case .server:
56 | return castToString(kSecAttrServer)
57 | case .synchronizable:
58 | return castToString(kSecAttrSynchronizable)
59 | case .dataProtection:
60 | return castToString(kSecUseDataProtectionKeychain)
61 | case .internetPassword:
62 | return castToString(kSecClassInternetPassword)
63 | case .genericPassword:
64 | return castToString(kSecClassGenericPassword)
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/KeychainManager/KeychainManagerEnums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Gokul Nair on 25/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | //MARK: - Keychain Error Type Enum
11 |
12 | public enum KeychainError: Error {
13 | case duplicateEntry
14 | case unknown(OSStatus)
15 | case noPassword
16 | }
17 |
18 | //MARK: - Keychain Security Class Type Enum
19 |
20 | public enum secureClassType {
21 |
22 | case webCredentials
23 | case genericPassword
24 |
25 | func value() -> String {
26 | switch self {
27 | case .webCredentials:
28 | return KMConstants.internetPassword.value()
29 | case .genericPassword:
30 | return KMConstants.genericPassword.value()
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/KeychainManagerTests/KeychainManagerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import KeychainManager
3 |
4 | //final class KeychainManagerTests: XCTestCase {
5 | // func testExample() throws {
6 | // // This is an example of a functional test case.
7 | // // Use XCTAssert and related functions to verify your tests produce the correct
8 | // // results.
9 | // XCTAssertEqual(KeychainManager().text, "Hello, World!")
10 | // }
11 | //}
12 |
--------------------------------------------------------------------------------