[ABANDONED] Mastodon iOS client.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

445 lines
21 KiB

6 years ago
  1. //
  2. // KeychainWrapper.swift
  3. // KeychainWrapper
  4. //
  5. // Created by Jason Rendel on 9/23/14.
  6. // Copyright (c) 2014 Jason Rendel. All rights reserved.
  7. //
  8. // The MIT License (MIT)
  9. //
  10. // Permission is hereby granted, free of charge, to any person obtaining a copy
  11. // of this software and associated documentation files (the "Software"), to deal
  12. // in the Software without restriction, including without limitation the rights
  13. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14. // copies of the Software, and to permit persons to whom the Software is
  15. // furnished to do so, subject to the following conditions:
  16. //
  17. // The above copyright notice and this permission notice shall be included in all
  18. // copies or substantial portions of the Software.
  19. //
  20. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  23. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  25. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  26. // SOFTWARE.
  27. import Foundation
  28. private let SecMatchLimit: String! = kSecMatchLimit as String
  29. private let SecReturnData: String! = kSecReturnData as String
  30. private let SecReturnPersistentRef: String! = kSecReturnPersistentRef as String
  31. private let SecValueData: String! = kSecValueData as String
  32. private let SecAttrAccessible: String! = kSecAttrAccessible as String
  33. private let SecClass: String! = kSecClass as String
  34. private let SecAttrService: String! = kSecAttrService as String
  35. private let SecAttrGeneric: String! = kSecAttrGeneric as String
  36. private let SecAttrAccount: String! = kSecAttrAccount as String
  37. private let SecAttrAccessGroup: String! = kSecAttrAccessGroup as String
  38. private let SecReturnAttributes: String = kSecReturnAttributes as String
  39. /// KeychainWrapper is a class to help make Keychain access in Swift more straightforward. It is designed to make accessing the Keychain services more like using NSUserDefaults, which is much more familiar to people.
  40. open class KeychainWrapper {
  41. @available(*, deprecated: 2.2.1, message: "KeychainWrapper.defaultKeychainWrapper is deprecated, use KeychainWrapper.standard instead")
  42. public static let defaultKeychainWrapper = KeychainWrapper.standard
  43. /// Default keychain wrapper access
  44. public static let standard = KeychainWrapper()
  45. /// ServiceName is used for the kSecAttrService property to uniquely identify this keychain accessor. If no service name is specified, KeychainWrapper will default to using the bundleIdentifier.
  46. private (set) public var serviceName: String
  47. /// AccessGroup is used for the kSecAttrAccessGroup property to identify which Keychain Access Group this entry belongs to. This allows you to use the KeychainWrapper with shared keychain access between different applications.
  48. private (set) public var accessGroup: String?
  49. private static let defaultServiceName: String = {
  50. return Bundle.main.bundleIdentifier ?? "SwiftKeychainWrapper"
  51. }()
  52. private convenience init() {
  53. self.init(serviceName: KeychainWrapper.defaultServiceName)
  54. }
  55. /// Create a custom instance of KeychainWrapper with a custom Service Name and optional custom access group.
  56. ///
  57. /// - parameter serviceName: The ServiceName for this instance. Used to uniquely identify all keys stored using this keychain wrapper instance.
  58. /// - parameter accessGroup: Optional unique AccessGroup for this instance. Use a matching AccessGroup between applications to allow shared keychain access.
  59. public init(serviceName: String, accessGroup: String? = nil) {
  60. self.serviceName = serviceName
  61. self.accessGroup = accessGroup
  62. }
  63. // MARK:- Public Methods
  64. /// Checks if keychain data exists for a specified key.
  65. ///
  66. /// - parameter forKey: The key to check for.
  67. /// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
  68. /// - returns: True if a value exists for the key. False otherwise.
  69. open func hasValue(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  70. if let _ = data(forKey: key, withAccessibility: accessibility) {
  71. return true
  72. } else {
  73. return false
  74. }
  75. }
  76. open func accessibilityOfKey(_ key: String) -> KeychainItemAccessibility? {
  77. var keychainQueryDictionary = setupKeychainQueryDictionary(forKey: key)
  78. // Remove accessibility attribute
  79. keychainQueryDictionary.removeValue(forKey: SecAttrAccessible)
  80. // Limit search results to one
  81. keychainQueryDictionary[SecMatchLimit] = kSecMatchLimitOne
  82. // Specify we want SecAttrAccessible returned
  83. keychainQueryDictionary[SecReturnAttributes] = kCFBooleanTrue
  84. // Search
  85. var result: AnyObject?
  86. let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)
  87. guard status == noErr, let resultsDictionary = result as? [String:AnyObject], let accessibilityAttrValue = resultsDictionary[SecAttrAccessible] as? String else {
  88. return nil
  89. }
  90. return KeychainItemAccessibility.accessibilityForAttributeValue(accessibilityAttrValue as CFString)
  91. }
  92. /// Get the keys of all keychain entries matching the current ServiceName and AccessGroup if one is set.
  93. open func allKeys() -> Set<String> {
  94. var keychainQueryDictionary: [String:Any] = [
  95. SecClass: kSecClassGenericPassword,
  96. SecAttrService: serviceName,
  97. SecReturnAttributes: kCFBooleanTrue,
  98. SecMatchLimit: kSecMatchLimitAll,
  99. ]
  100. if let accessGroup = self.accessGroup {
  101. keychainQueryDictionary[SecAttrAccessGroup] = accessGroup
  102. }
  103. var result: AnyObject?
  104. let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)
  105. guard status == errSecSuccess else { return [] }
  106. var keys = Set<String>()
  107. if let results = result as? [[AnyHashable: Any]] {
  108. for attributes in results {
  109. if let accountData = attributes[SecAttrAccount] as? Data,
  110. let account = String(data: accountData, encoding: String.Encoding.utf8) {
  111. keys.insert(account)
  112. }
  113. }
  114. }
  115. return keys
  116. }
  117. // MARK: Public Getters
  118. open func integer(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Int? {
  119. guard let numberValue = object(forKey: key, withAccessibility: accessibility) as? NSNumber else {
  120. return nil
  121. }
  122. return numberValue.intValue
  123. }
  124. open func float(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Float? {
  125. guard let numberValue = object(forKey: key, withAccessibility: accessibility) as? NSNumber else {
  126. return nil
  127. }
  128. return numberValue.floatValue
  129. }
  130. open func double(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Double? {
  131. guard let numberValue = object(forKey: key, withAccessibility: accessibility) as? NSNumber else {
  132. return nil
  133. }
  134. return numberValue.doubleValue
  135. }
  136. open func bool(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool? {
  137. guard let numberValue = object(forKey: key, withAccessibility: accessibility) as? NSNumber else {
  138. return nil
  139. }
  140. return numberValue.boolValue
  141. }
  142. /// Returns a string value for a specified key.
  143. ///
  144. /// - parameter forKey: The key to lookup data for.
  145. /// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
  146. /// - returns: The String associated with the key if it exists. If no data exists, or the data found cannot be encoded as a string, returns nil.
  147. open func string(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> String? {
  148. guard let keychainData = data(forKey: key, withAccessibility: accessibility) else {
  149. return nil
  150. }
  151. return String(data: keychainData, encoding: String.Encoding.utf8) as String?
  152. }
  153. /// Returns an object that conforms to NSCoding for a specified key.
  154. ///
  155. /// - parameter forKey: The key to lookup data for.
  156. /// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
  157. /// - returns: The decoded object associated with the key if it exists. If no data exists, or the data found cannot be decoded, returns nil.
  158. open func object(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> NSCoding? {
  159. guard let keychainData = data(forKey: key, withAccessibility: accessibility) else {
  160. return nil
  161. }
  162. return NSKeyedUnarchiver.unarchiveObject(with: keychainData) as? NSCoding
  163. }
  164. /// Returns a Data object for a specified key.
  165. ///
  166. /// - parameter forKey: The key to lookup data for.
  167. /// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
  168. /// - returns: The Data object associated with the key if it exists. If no data exists, returns nil.
  169. open func data(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Data? {
  170. var keychainQueryDictionary = setupKeychainQueryDictionary(forKey: key, withAccessibility: accessibility)
  171. // Limit search results to one
  172. keychainQueryDictionary[SecMatchLimit] = kSecMatchLimitOne
  173. // Specify we want Data/CFData returned
  174. keychainQueryDictionary[SecReturnData] = kCFBooleanTrue
  175. // Search
  176. var result: AnyObject?
  177. let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)
  178. return status == noErr ? result as? Data : nil
  179. }
  180. /// Returns a persistent data reference object for a specified key.
  181. ///
  182. /// - parameter forKey: The key to lookup data for.
  183. /// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item.
  184. /// - returns: The persistent data reference object associated with the key if it exists. If no data exists, returns nil.
  185. open func dataRef(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Data? {
  186. var keychainQueryDictionary = setupKeychainQueryDictionary(forKey: key, withAccessibility: accessibility)
  187. // Limit search results to one
  188. keychainQueryDictionary[SecMatchLimit] = kSecMatchLimitOne
  189. // Specify we want persistent Data/CFData reference returned
  190. keychainQueryDictionary[SecReturnPersistentRef] = kCFBooleanTrue
  191. // Search
  192. var result: AnyObject?
  193. let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)
  194. return status == noErr ? result as? Data : nil
  195. }
  196. // MARK: Public Setters
  197. @discardableResult open func set(_ value: Int, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  198. return set(NSNumber(value: value), forKey: key, withAccessibility: accessibility)
  199. }
  200. @discardableResult open func set(_ value: Float, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  201. return set(NSNumber(value: value), forKey: key, withAccessibility: accessibility)
  202. }
  203. @discardableResult open func set(_ value: Double, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  204. return set(NSNumber(value: value), forKey: key, withAccessibility: accessibility)
  205. }
  206. @discardableResult open func set(_ value: Bool, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  207. return set(NSNumber(value: value), forKey: key, withAccessibility: accessibility)
  208. }
  209. /// Save a String value to the keychain associated with a specified key. If a String value already exists for the given key, the string will be overwritten with the new value.
  210. ///
  211. /// - parameter value: The String value to save.
  212. /// - parameter forKey: The key to save the String under.
  213. /// - parameter withAccessibility: Optional accessibility to use when setting the keychain item.
  214. /// - returns: True if the save was successful, false otherwise.
  215. @discardableResult open func set(_ value: String, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  216. if let data = value.data(using: .utf8) {
  217. return set(data, forKey: key, withAccessibility: accessibility)
  218. } else {
  219. return false
  220. }
  221. }
  222. /// Save an NSCoding compliant object to the keychain associated with a specified key. If an object already exists for the given key, the object will be overwritten with the new value.
  223. ///
  224. /// - parameter value: The NSCoding compliant object to save.
  225. /// - parameter forKey: The key to save the object under.
  226. /// - parameter withAccessibility: Optional accessibility to use when setting the keychain item.
  227. /// - returns: True if the save was successful, false otherwise.
  228. @discardableResult open func set(_ value: NSCoding, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  229. let data = NSKeyedArchiver.archivedData(withRootObject: value)
  230. return set(data, forKey: key, withAccessibility: accessibility)
  231. }
  232. /// 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.
  233. ///
  234. /// - parameter value: The Data object to save.
  235. /// - parameter forKey: The key to save the object under.
  236. /// - parameter withAccessibility: Optional accessibility to use when setting the keychain item.
  237. /// - returns: True if the save was successful, false otherwise.
  238. @discardableResult open func set(_ value: Data, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  239. var keychainQueryDictionary: [String:Any] = setupKeychainQueryDictionary(forKey: key, withAccessibility: accessibility)
  240. keychainQueryDictionary[SecValueData] = value
  241. if let accessibility = accessibility {
  242. keychainQueryDictionary[SecAttrAccessible] = accessibility.keychainAttrValue
  243. } else {
  244. // Assign default protection - Protect the keychain entry so it's only valid when the device is unlocked
  245. keychainQueryDictionary[SecAttrAccessible] = KeychainItemAccessibility.whenUnlocked.keychainAttrValue
  246. }
  247. let status: OSStatus = SecItemAdd(keychainQueryDictionary as CFDictionary, nil)
  248. if status == errSecSuccess {
  249. return true
  250. } else if status == errSecDuplicateItem {
  251. return update(value, forKey: key, withAccessibility: accessibility)
  252. } else {
  253. return false
  254. }
  255. }
  256. @available(*, deprecated: 2.2.1, message: "remove is deprecated, use removeObject instead")
  257. @discardableResult open func remove(key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  258. return removeObject(forKey: key, withAccessibility: accessibility)
  259. }
  260. /// 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.
  261. ///
  262. /// - parameter forKey: The key value to remove data for.
  263. /// - parameter withAccessibility: Optional accessibility level to use when looking up the keychain item.
  264. /// - returns: True if successful, false otherwise.
  265. @discardableResult open func removeObject(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  266. let keychainQueryDictionary: [String:Any] = setupKeychainQueryDictionary(forKey: key, withAccessibility: accessibility)
  267. // Delete
  268. let status: OSStatus = SecItemDelete(keychainQueryDictionary as CFDictionary)
  269. if status == errSecSuccess {
  270. return true
  271. } else {
  272. return false
  273. }
  274. }
  275. /// Remove all keychain data added through KeychainWrapper. This will only delete items matching the currnt ServiceName and AccessGroup if one is set.
  276. open func removeAllKeys() -> Bool {
  277. // Setup dictionary to access keychain and specify we are using a generic password (rather than a certificate, internet password, etc)
  278. var keychainQueryDictionary: [String:Any] = [SecClass:kSecClassGenericPassword]
  279. // Uniquely identify this keychain accessor
  280. keychainQueryDictionary[SecAttrService] = serviceName
  281. // Set the keychain access group if defined
  282. if let accessGroup = self.accessGroup {
  283. keychainQueryDictionary[SecAttrAccessGroup] = accessGroup
  284. }
  285. let status: OSStatus = SecItemDelete(keychainQueryDictionary as CFDictionary)
  286. if status == errSecSuccess {
  287. return true
  288. } else {
  289. return false
  290. }
  291. }
  292. /// Remove all keychain data, including data not added through keychain wrapper.
  293. ///
  294. /// - Warning: This may remove custom keychain entries you did not add via SwiftKeychainWrapper.
  295. ///
  296. open class func wipeKeychain() {
  297. deleteKeychainSecClass(kSecClassGenericPassword) // Generic password items
  298. deleteKeychainSecClass(kSecClassInternetPassword) // Internet password items
  299. deleteKeychainSecClass(kSecClassCertificate) // Certificate items
  300. deleteKeychainSecClass(kSecClassKey) // Cryptographic key items
  301. deleteKeychainSecClass(kSecClassIdentity) // Identity items
  302. }
  303. // MARK:- Private Methods
  304. /// Remove all items for a given Keychain Item Class
  305. ///
  306. ///
  307. @discardableResult private class func deleteKeychainSecClass(_ secClass: AnyObject) -> Bool {
  308. let query = [SecClass: secClass]
  309. let status: OSStatus = SecItemDelete(query as CFDictionary)
  310. if status == errSecSuccess {
  311. return true
  312. } else {
  313. return false
  314. }
  315. }
  316. /// Update existing data associated with a specified key name. The existing data will be overwritten by the new data.
  317. private func update(_ value: Data, forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> Bool {
  318. var keychainQueryDictionary: [String:Any] = setupKeychainQueryDictionary(forKey: key, withAccessibility: accessibility)
  319. let updateDictionary = [SecValueData:value]
  320. // on update, only set accessibility if passed in
  321. if let accessibility = accessibility {
  322. keychainQueryDictionary[SecAttrAccessible] = accessibility.keychainAttrValue
  323. }
  324. // Update
  325. let status: OSStatus = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary)
  326. if status == errSecSuccess {
  327. return true
  328. } else {
  329. return false
  330. }
  331. }
  332. /// Setup the keychain query dictionary used to access the keychain on iOS for a specified key name. Takes into account the Service Name and Access Group if one is set.
  333. ///
  334. /// - parameter forKey: The key this query is for
  335. /// - parameter withAccessibility: Optional accessibility to use when setting the keychain item. If none is provided, will default to .WhenUnlocked
  336. /// - returns: A dictionary with all the needed properties setup to access the keychain on iOS
  337. private func setupKeychainQueryDictionary(forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil) -> [String:Any] {
  338. // Setup default access as generic password (rather than a certificate, internet password, etc)
  339. var keychainQueryDictionary: [String:Any] = [SecClass:kSecClassGenericPassword]
  340. // Uniquely identify this keychain accessor
  341. keychainQueryDictionary[SecAttrService] = serviceName
  342. // Only set accessibiilty if its passed in, we don't want to default it here in case the user didn't want it set
  343. if let accessibility = accessibility {
  344. keychainQueryDictionary[SecAttrAccessible] = accessibility.keychainAttrValue
  345. }
  346. // Set the keychain access group if defined
  347. if let accessGroup = self.accessGroup {
  348. keychainQueryDictionary[SecAttrAccessGroup] = accessGroup
  349. }
  350. // Uniquely identify the account who will be accessing the keychain
  351. let encodedIdentifier: Data? = key.data(using: String.Encoding.utf8)
  352. keychainQueryDictionary[SecAttrGeneric] = encodedIdentifier
  353. keychainQueryDictionary[SecAttrAccount] = encodedIdentifier
  354. return keychainQueryDictionary
  355. }
  356. }