AuthKeychainServices.swift 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseCoreExtension
  15. import Foundation
  16. /// The prefix string for keychain item account attribute before the key.
  17. ///
  18. /// A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
  19. private let kAccountPrefix = "firebase_auth_1_"
  20. /// The utility class to manipulate data in iOS Keychain.
  21. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  22. final class AuthKeychainServices {
  23. /// The name of the keychain service.
  24. let service: String
  25. let keychainStorage: AuthKeychainStorage
  26. // MARK: - Internal methods for shared keychain operations
  27. required init(service: String = "Unset service",
  28. storage: AuthKeychainStorage = AuthKeychainStorageReal()) {
  29. self.service = service
  30. keychainStorage = storage
  31. }
  32. /// Get the item from keychain by given query.
  33. /// - Parameter query: The query to query the keychain.
  34. /// - Returns: The item of the given query. `nil` if it doesn't exist.
  35. func getItem(query: [String: Any]) throws -> Data? {
  36. var mutableQuery = query
  37. mutableQuery[kSecReturnData as String] = true
  38. mutableQuery[kSecReturnAttributes as String] = true
  39. // Using a match limit of 2 means that we can check whether more than one
  40. // item is returned by the query.
  41. mutableQuery[kSecMatchLimit as String] = 2
  42. var result: AnyObject?
  43. let status = keychainStorage.get(query: mutableQuery, result: &result)
  44. if let items = result as? [[String: Any]], status == noErr {
  45. if items.count != 1 {
  46. throw AuthErrorUtils.keychainError(function: "SecItemCopyMatching", status: status)
  47. }
  48. return items[0][kSecValueData as String] as? Data
  49. }
  50. if status == errSecItemNotFound {
  51. return nil
  52. } else {
  53. throw AuthErrorUtils.keychainError(function: "SecItemCopyMatching", status: status)
  54. }
  55. }
  56. /// Set the item into keychain with given query.
  57. /// - Parameter item: The item to be added into keychain.
  58. /// - Parameter query: The query to query the keychain.
  59. /// - Returns: Whether the operation succeed.
  60. func setItem(_ item: Data, withQuery query: [String: Any]) throws {
  61. let status: OSStatus
  62. let function: String
  63. if try (getItem(query: query)) != nil {
  64. let attributes: [String: Any] = [kSecValueData as String: item]
  65. status = keychainStorage.update(query: query, attributes: attributes)
  66. function = "SecItemUpdate"
  67. } else {
  68. var queryWithItem = query
  69. queryWithItem[kSecValueData as String] = item
  70. status = keychainStorage.add(query: queryWithItem)
  71. function = "SecItemAdd"
  72. }
  73. if status == noErr {
  74. return
  75. }
  76. throw AuthErrorUtils.keychainError(function: function, status: status)
  77. }
  78. /// Remove the item with given queryfrom keychain.
  79. /// - Parameter query: The query to query the keychain.
  80. func removeItem(query: [String: Any]) throws {
  81. let status = keychainStorage.delete(query: query)
  82. if status == noErr || status == errSecItemNotFound {
  83. return
  84. }
  85. throw AuthErrorUtils.keychainError(function: "SecItemDelete", status: status)
  86. }
  87. /// Indicates whether or not this class knows that the legacy item for a particular key has
  88. /// been deleted.
  89. ///
  90. /// This dictionary is to avoid unnecessary keychain operations against legacy items.
  91. private var legacyEntryDeletedForKey: Set<String> = []
  92. static func storage(identifier: String) -> Self {
  93. return Self(service: identifier)
  94. }
  95. func data(forKey key: String) throws -> Data? {
  96. if let data = try getItemLegacy(query: genericPasswordQuery(key: key)) {
  97. return data
  98. }
  99. // Check for legacy form.
  100. if legacyEntryDeletedForKey.contains(key) {
  101. return nil
  102. }
  103. if let data = try getItemLegacy(query: legacyGenericPasswordQuery(key: key)) {
  104. // Move the data to current form.
  105. try setData(data, forKey: key)
  106. deleteLegacyItem(key: key)
  107. return data
  108. } else {
  109. // Mark legacy data as non-existing so we don't have to query it again.
  110. legacyEntryDeletedForKey.insert(key)
  111. return nil
  112. }
  113. }
  114. func setData(_ data: Data, forKey key: String) throws {
  115. try setItemLegacy(data, withQuery: genericPasswordQuery(key: key))
  116. }
  117. func removeData(forKey key: String) throws {
  118. try removeItem(query: genericPasswordQuery(key: key))
  119. // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
  120. // current form item is removed, leading to incorrect semantics.
  121. deleteLegacyItem(key: key)
  122. }
  123. // MARK: - Internal methods for non-sharing keychain operations
  124. // TODO: This function can go away in favor of `getItem` if we can delete the legacy processing.
  125. func getItemLegacy(query: [String: Any]) throws -> Data? {
  126. var returningQuery = query
  127. returningQuery[kSecReturnData as String] = true
  128. returningQuery[kSecReturnAttributes as String] = true
  129. // Using a match limit of 2 means that we can check whether there is more than one item.
  130. // If we used a match limit of 1 we would never find out.
  131. returningQuery[kSecMatchLimit as String] = 2
  132. var result: AnyObject?
  133. let status = keychainStorage.get(query: returningQuery, result: &result)
  134. if let items = result as? [[String: Any]], status == noErr {
  135. if items.isEmpty {
  136. // The keychain query returned no error, but there were no items found.
  137. throw AuthErrorUtils.keychainError(function: "SecItemCopyMatching", status: status)
  138. } else if items.count > 1 {
  139. // More than one keychain item was found, all but the first will be ignored.
  140. FirebaseLogger.log(
  141. level: .warning,
  142. service: "[FirebaseAuth]",
  143. code: "I-AUT000005",
  144. message: "Keychain query returned multiple results, all but the first will be ignored: \(items)"
  145. )
  146. }
  147. // Return the non-legacy item.
  148. for item in items {
  149. if item[kSecAttrService as String] != nil {
  150. return item[kSecValueData as String] as? Data
  151. }
  152. }
  153. // If they were all legacy items, just return the first one.
  154. // This should not happen, since only one account should be
  155. // stored.
  156. return items[0][kSecValueData as String] as? Data
  157. }
  158. if status == errSecItemNotFound {
  159. return nil
  160. } else {
  161. throw AuthErrorUtils.keychainError(function: "SecItemCopyMatching", status: status)
  162. }
  163. }
  164. // TODO: This function can go away in favor of `setItem` if we can delete the legacy processing.
  165. func setItemLegacy(_ item: Data, withQuery query: [String: Any]) throws {
  166. let attributes: [String: Any] = [
  167. kSecValueData as String: item,
  168. kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
  169. ]
  170. let combined = attributes.merging(query, uniquingKeysWith: { _, last in last })
  171. var hasItem = false
  172. var status = keychainStorage.add(query: combined)
  173. if status == errSecDuplicateItem {
  174. hasItem = true
  175. status = keychainStorage.update(query: query, attributes: attributes)
  176. }
  177. if status == noErr {
  178. return
  179. }
  180. let function = hasItem ? "SecItemUpdate" : "SecItemAdd"
  181. throw AuthErrorUtils.keychainError(function: function, status: status)
  182. }
  183. /// Deletes legacy item from the keychain if it is not already known to be deleted.
  184. /// - Parameter key: The key for the item.
  185. private func deleteLegacyItem(key: String) {
  186. if legacyEntryDeletedForKey.contains(key) {
  187. return
  188. }
  189. let query = legacyGenericPasswordQuery(key: key)
  190. keychainStorage.delete(query: query)
  191. legacyEntryDeletedForKey.insert(key)
  192. }
  193. /// Returns a keychain query of generic password to be used to manipulate key'ed value.
  194. /// - Parameter key: The key for the value being manipulated, used as the account field in the
  195. /// query.
  196. private func genericPasswordQuery(key: String) -> [String: Any] {
  197. if key.isEmpty {
  198. fatalError("The key cannot be empty.")
  199. }
  200. var query: [String: Any] = [
  201. kSecClass as String: kSecClassGenericPassword,
  202. kSecAttrAccount as String: kAccountPrefix + key,
  203. kSecAttrService as String: service,
  204. ]
  205. query[kSecUseDataProtectionKeychain as String] = true
  206. return query
  207. }
  208. /// Returns a keychain query of generic password without service field, which is used by
  209. /// previous version of this class .
  210. /// - Parameter key: The key for the value being manipulated, used as the account field in the
  211. /// query.
  212. private func legacyGenericPasswordQuery(key: String) -> [String: Any] {
  213. [
  214. kSecClass as String: kSecClassGenericPassword,
  215. kSecAttrAccount as String: key,
  216. ]
  217. }
  218. }