iOS: cloudSync: false is treated as true — items written to iCloud keychain when the caller explicitly opted out

Open Beginner friendly
#800 1 comment 0 reactions 0 assignees View on GitHub

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
Domain
mobile, security

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 = true and 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: false everywhere") 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

  1. On a device/simulator signed into iCloud with iCloud Keychain enabled:
    await Keychain.setGenericPassword('user', 'super-secret', {
      service: 'com.example.test',
      cloudSync: false,
    });
    
  2. Inspect the stored item's attributes (e.g. via SecItemCopyMatching with kSecReturnAttributes, or observe it appearing on a second device in the same iCloud Keychain circle).
  3. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from oblador/react-native-keychain

All issues in oblador/react-native-keychain

Similar issues

More Kotlin issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.