iOS: cloudSync: false is treated as true — items written to iCloud keychain when the caller explicitly opted out
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 85/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- ios, objective-c, react-native
Research direction
Start in ios/RNKeychainManager/RNKeychainManager.m at cloudSyncValue(), then inspect its uses in set, lookup, and delete operations. Verify that true enables synchronization while false and an omitted option do not, using the provided setGenericPassword reproduction and checking the item's kSecAttrSynchronizable attribute.
Written by the indexing model from the issue text.
Description
Summary
On iOS, cloudSyncValue() tests the cloudSync option for presence rather than for its boolean value. A JavaScript false crosses the bridge as a non-nil NSNumber (@NO), which is truthy as an Objective-C object pointer, so { cloudSync: false } produces kSecAttrSynchronizable = true — identical to { cloudSync: true }.
The consequence is the reverse of what the caller asked for: passing cloudSync: false enables iCloud Keychain synchronization. The only way to get a non-synchronized item today is to omit the option entirely. Any app that explicitly passes cloudSync: false to keep secrets device-local is instead syncing them to iCloud.
Affected code
ios/RNKeychainManager/RNKeychainManager.m (v10.0.0, latest; the pattern dates to the introduction of cloudSync in v9.1.0):
CFBooleanRef cloudSyncValue(NSDictionary *options)
{
if (options && options[@"cloudSync"]) { // presence check, not value check
return kCFBooleanTrue;
}
return kCFBooleanFalse;
}
options[@"cloudSync"] is a non-nil NSNumber for both true and false, so the if is taken in both cases.
This helper feeds kSecAttrSynchronizable on every operation that reads it — setGenericPassword, setInternetCredentials, hasGenericPassword/hasInternetCredentials, getGenericPassword/getInternetCredentials, and the delete paths (deletePasswordsForOptions, deleteCredentialsForServer). So the bug affects not only writes but lookups and deletes: a delete issued with cloudSync: false builds a query with kSecAttrSynchronizable = true and therefore will not match the intended (locally-created, but actually-synced) item in the way the caller expects.
Impact / severity
cloudSync is a security control. Apps use cloudSync: false specifically to keep high-value secrets (wallet seed phrases, private keys, tokens) on the device and out of iCloud. This bug silently inverts that control:
- Secrets the developer intended to be device-local are written with
kSecAttrSynchronizable = trueand propagate to every device in the user's iCloud Keychain circle. - This is invisible: the API call succeeds, and code review of the JS ("we pass
cloudSync: falseeverywhere") looks correct. - It broadens the threat model from "attacker must obtain the device" to "attacker must obtain the iCloud account + a device passcode, or subvert account recovery," without the developer's or user's knowledge.
iCloud Keychain is end-to-end encrypted, so this is not server-side plaintext exposure — but it is a material, unconsented change to where a secret lives and who can reach it, which is exactly what the flag exists to control.
Reproduction
- On a device/simulator signed into iCloud with iCloud Keychain enabled:
await Keychain.setGenericPassword('user', 'super-secret', { service: 'com.example.test', cloudSync: false, }); - Inspect the stored item's attributes (e.g. via
SecItemCopyMatchingwithkSecReturnAttributes, or observe it appearing on a second device in the same iCloud Keychain circle). - Observed:
kSecAttrSynchronizable = 1. Expected:0(item stays on-device).
Suggested fix
Test the boolean value rather than presence:
CFBooleanRef cloudSyncValue(NSDictionary *options)
{
if (options && [options[@"cloudSync"] boolValue]) {
return kCFBooleanTrue;
}
return kCFBooleanFalse;
}
[nil boolValue] is NO, so omitting the option continues to mean non-synchronized. This makes cloudSync: false and an omitted option both non-synchronized, and only cloudSync: true synchronized, matching the documented contract.
Note for the fix's release
Because affected apps already have synchronized copies of items in users' iCloud keychains, the code fix alone changes behavior only for new writes; existing synced items remain in iCloud until explicitly deleted (with a query that also matches synchronizable items). A changelog note calling this out would help downstream apps plan a migration/cleanup rather than assume the fix retroactively un-syncs data.
Environment
- react-native-keychain 10.0.0 (also present in 9.1.0+)
- iOS (all versions; the bug is in the option marshaling, not the OS)
- Dominant language
- Kotlin
- Stars
- 3.5k
- Forks
- 544
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from oblador/react-native-keychain
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
oblador/react-native-keychain#768 · 1 comment · 1 reaction ·
-
Difficulty 3/5 1-2 days Newbie friendliness 42/100
oblador/react-native-keychain#815 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 74/100
oblador/react-native-keychain#813 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
oblador/react-native-keychain#802 · 3 comments · 2 reactions ·
-
Difficulty 4/5 3-5 days Newbie friendliness 52/100
oblador/react-native-keychain#801 · 2 comments · 3 reactions ·
All issues in oblador/react-native-keychain
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bitcoindevkit/bdk-ffi#1125 ·
-
helsemelding-json-schema json-schema-core
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
contributor: external needs review
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100