AuthStoredUserManager.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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 Foundation
  15. // TODO(ncooke3): Remove this type after all tests and internal call sites are converted to Swift
  16. // This is added since a throwing function returning Optional values in Swift cannot be
  17. // exposed to Objective-C due to convention and meaning of nil values in Objective-C.
  18. // This wrapper allows us to always return a value, thus allowing us to expose Objective-C api.
  19. @objc(FIRUserWrapper) public class UserWrapper: NSObject {
  20. @objc public let user: User?
  21. @objc public init(user: User?) {
  22. self.user = user
  23. }
  24. }
  25. @objc(FIRAuthStoredUserManager) public class AuthStoredUserManager: NSObject {
  26. /// Key of user access group stored in user defaults. Used for retrieve the
  27. /// user access group at launch.
  28. private static let storedUserAccessGroupKey = "firebase_auth_stored_user_access_group"
  29. /// Default value for kSecAttrAccount of shared keychain items.
  30. private static let sharedKeychainAccountValue = "firebase_auth_firebase_user"
  31. /// The key to encode and decode the stored user.
  32. private static let storedUserCoderKey = "firebase_auth_stored_user_coder_key"
  33. // TODO: Should keychainServices be AuthStorage
  34. /// Mediator object used to access the keychain.
  35. private let keychainServices: AuthSharedKeychainServices
  36. /// Mediator object used to access user defaults.
  37. private let userDefaults: AuthUserDefaults
  38. /// Designated initializer.
  39. /// - Parameter serviceName: The service name to initialize with.
  40. @objc public init(serviceName: String) {
  41. // TODO: keychainServices should be set by parameter.
  42. keychainServices = AuthSharedKeychainServices()
  43. userDefaults = AuthUserDefaults(service: serviceName)
  44. }
  45. /// Get the user access group stored locally.
  46. /// - Returns: The stored user access group; otherwise, `nil`.
  47. @objc public func getStoredUserAccessGroup() -> String? {
  48. if let data = try? userDefaults.data(forKey: Self.storedUserAccessGroupKey) {
  49. let userAccessGroup = String(data: data, encoding: .utf8)
  50. return userAccessGroup
  51. } else {
  52. return nil
  53. }
  54. }
  55. /// The setter of the user access group stored locally.
  56. /// - Parameter accessGroup: The access group to be store.
  57. @objc(setStoredUserAccessGroup:)
  58. public func setStoredUserAccessGroup(accessGroup: String?) {
  59. if let data = accessGroup?.data(using: .utf8) {
  60. try? userDefaults.setData(data, forKey: Self.storedUserAccessGroupKey)
  61. } else {
  62. try? userDefaults.removeData(forKey: Self.storedUserAccessGroupKey)
  63. }
  64. }
  65. // MARK: - User for Access Group
  66. /// The getter of the user stored locally.
  67. /// - Parameters:
  68. /// - accessGroup: The access group to retrieve the user from.
  69. /// - shareAuthStateAcrossDevices: If `true`, the keychain will be synced
  70. /// across the end-user's iCloud.
  71. /// - projectIdentifier: An identifier of the project that the user
  72. /// associates with.
  73. /// - Returns: The stored user for the given attributes.
  74. /// - Throws: An error if the operation failed.
  75. @objc(getStoredUserForAccessGroup:shareAuthStateAcrossDevices:projectIdentifier:error:)
  76. public func getStoredUser(accessGroup: String,
  77. shareAuthStateAcrossDevices: Bool,
  78. projectIdentifier: String) throws -> UserWrapper {
  79. let query = keychainQuery(
  80. accessGroup: accessGroup,
  81. shareAuthStateAcrossDevices: shareAuthStateAcrossDevices,
  82. projectIdentifier: projectIdentifier
  83. )
  84. guard let data = try? keychainServices.getItem(query: query) else {
  85. return UserWrapper(user: nil)
  86. }
  87. // TODO(ncooke3): The Objective-C code has an #if for watchOS here.
  88. // Does this work for watchOS?
  89. guard let unarchiver = try? NSKeyedUnarchiver(forReadingFrom: data) else {
  90. return UserWrapper(user: nil)
  91. }
  92. let user = unarchiver.decodeObject(of: User.self, forKey: Self.storedUserCoderKey)
  93. return UserWrapper(user: user)
  94. }
  95. /// The setter of the user stored locally.
  96. /// - Parameters:
  97. /// - user: The user to be stored.
  98. /// - accessGroup: The access group to store the user in.
  99. /// - shareAuthStateAcrossDevices: If `true`, the keychain will be
  100. /// synced across the end-user's iCloud.
  101. /// - projectIdentifier: An identifier of the project that the user
  102. /// associates with.
  103. /// - Throws: An error if the operation failed.
  104. @objc(setStoredUser:forAccessGroup:shareAuthStateAcrossDevices:projectIdentifier:error:)
  105. public func setStoredUser(user: User,
  106. accessGroup: String,
  107. shareAuthStateAcrossDevices: Bool,
  108. projectIdentifier: String) throws {
  109. var query = keychainQuery(
  110. accessGroup: accessGroup,
  111. shareAuthStateAcrossDevices: shareAuthStateAcrossDevices,
  112. projectIdentifier: projectIdentifier
  113. )
  114. if shareAuthStateAcrossDevices {
  115. query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
  116. } else {
  117. query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
  118. }
  119. // TODO(ncooke3): The Objective-C code has an #if for watchOS here.
  120. // Does this work for watchOS?
  121. let archiver = NSKeyedArchiver(requiringSecureCoding: false)
  122. archiver.encode(user, forKey: Self.storedUserCoderKey)
  123. archiver.finishEncoding()
  124. try keychainServices.setItem(archiver.encodedData, withQuery: query)
  125. }
  126. /// Remove the user that stored locally.
  127. /// - Parameters:
  128. /// - accessGroup: The access group to remove the user from.
  129. /// - shareAuthStateAcrossDevices: If `true`, the keychain will be
  130. /// synced across the end-user's iCloud.
  131. /// - projectIdentifier: An identifier of the project that the user
  132. /// associates with.
  133. /// - Throws: An error if the operation failed.
  134. @objc(removeStoredUserForAccessGroup:shareAuthStateAcrossDevices:projectIdentifier:error:)
  135. public func removeStoredUser(accessGroup: String,
  136. shareAuthStateAcrossDevices: Bool,
  137. projectIdentifier: String) throws {
  138. var query = keychainQuery(
  139. accessGroup: accessGroup,
  140. shareAuthStateAcrossDevices: shareAuthStateAcrossDevices,
  141. projectIdentifier: projectIdentifier
  142. )
  143. if shareAuthStateAcrossDevices {
  144. query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
  145. } else {
  146. query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
  147. }
  148. try keychainServices.removeItem(query: query)
  149. }
  150. // MARK: - Private Helpers
  151. private func keychainQuery(accessGroup: String,
  152. shareAuthStateAcrossDevices: Bool,
  153. projectIdentifier: String) -> [String: Any] {
  154. var query: [String: Any] = [
  155. kSecClass as String: kSecClassGenericPassword,
  156. kSecAttrAccessGroup as String: accessGroup,
  157. kSecAttrService as String: projectIdentifier,
  158. kSecAttrAccount as String: Self.sharedKeychainAccountValue,
  159. ]
  160. if #available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *) {
  161. query[kSecUseDataProtectionKeychain as String] = true
  162. }
  163. return query
  164. }
  165. }