AuthStoredUserManager.swift 6.4 KB

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