@@ -13,7 +13,9 @@ final class KeychainHelper {
1313 private let service = " com.TablePro "
1414 private static let logger = Logger ( subsystem: " com.TablePro " , category: " KeychainHelper " )
1515 private static let migrationKey = " com.TablePro.keychainMigratedToDataProtection "
16- private static let passwordSyncEnabledKey = " com.TablePro.keychainPasswordSyncEnabled "
16+ static let passwordSyncEnabledKey = " com.TablePro.keychainPasswordSyncEnabled "
17+
18+ private let migrationLock = NSLock ( )
1719
1820 private var isPasswordSyncEnabled : Bool {
1921 UserDefaults . standard. bool ( forKey: Self . passwordSyncEnabledKey)
@@ -40,6 +42,7 @@ final class KeychainHelper {
4042 var status = SecItemAdd ( addQuery as CFDictionary , nil )
4143
4244 if status == errSecDuplicateItem {
45+ let synchronizable = isPasswordSyncEnabled
4346 let searchQuery : [ String : Any ] = [
4447 kSecClass as String : kSecClassGenericPassword,
4548 kSecAttrService as String : service,
@@ -48,7 +51,8 @@ final class KeychainHelper {
4851 kSecAttrSynchronizable as String : kSecAttrSynchronizableAny
4952 ]
5053 let updateAttributes : [ String : Any ] = [
51- kSecValueData as String : data
54+ kSecValueData as String : data,
55+ kSecAttrSynchronizable as String : synchronizable
5256 ]
5357 status = SecItemUpdate ( searchQuery as CFDictionary , updateAttributes as CFDictionary )
5458 }
@@ -195,7 +199,11 @@ final class KeychainHelper {
195199 // MARK: - Password Sync Migration
196200
197201 /// Migrates all TablePro keychain items between local-only and iCloud-synchronizable.
202+ /// Serialized via `migrationLock` to prevent concurrent migrations from rapid toggling.
198203 func migratePasswordSyncState( synchronizable: Bool ) {
204+ migrationLock. lock ( )
205+ defer { migrationLock. unlock ( ) }
206+
199207 Self . logger. info ( " Starting keychain sync migration: synchronizable= \( synchronizable) " )
200208
201209 let searchQuery : [ String : Any ] = [
@@ -260,7 +268,7 @@ final class KeychainHelper {
260268 kSecAttrService as String : service,
261269 kSecAttrAccount as String : account,
262270 kSecUseDataProtectionKeychain as String : true ,
263- kSecAttrSynchronizable as String : !synchronizable as CFBoolean
271+ kSecAttrSynchronizable as String : !synchronizable
264272 ]
265273 let deleteStatus = SecItemDelete ( deleteQuery as CFDictionary )
266274 if deleteStatus != errSecSuccess, deleteStatus != errSecItemNotFound {
0 commit comments