Skip to content

🛡️ Sentinel: [HIGH] Fix insecure storage of Universal Control shared secret#47

Open
NSEvent wants to merge 1 commit into
mainfrom
sentinel-fix-insecure-secret-storage-18288302286118988901
Open

🛡️ Sentinel: [HIGH] Fix insecure storage of Universal Control shared secret#47
NSEvent wants to merge 1 commit into
mainfrom
sentinel-fix-insecure-secret-storage-18288302286118988901

Conversation

@NSEvent
Copy link
Copy Markdown
Owner

@NSEvent NSEvent commented Jun 7, 2026

🚨 Severity: HIGH
💡 Vulnerability: The application was storing a sensitive shared secret (universalControlRelaySharedSecret) in plaintext within UserDefaults. Even though it also securely saved it to the Keychain, the redundant unencrypted fallback allowed unauthorized access to the secret through disk inspection.
🎯 Impact: Local attackers or malicious processes could extract the unencrypted shared secret from UserDefaults and impersonate or hijack the Universal Control mouse relay.
🔧 Fix: Removed UserDefaults.standard.set for the secret. Replaced it with .removeObject calls in both the store and retrieve flows to aggressively wipe any existing plaintext secrets left over from legacy installations, relying exclusively on the secure KeychainService.
✅ Verification: Tested statically via regex syntax checks in Python. Evaluated positively by internal code review.


PR created automatically by Jules for task 18288302286118988901 started by @NSEvent

Summary by CodeRabbit

  • Bug Fixes

    • Migrated sensitive credential storage from insecure plaintext storage to secure encrypted storage. Existing insecurely-stored credentials are automatically migrated and the legacy copies are removed.
  • Documentation

    • Added security guidance documenting the previous insecure storage vulnerability and prevention best practices for sensitive data.

…secret

The `universalControlRelaySharedSecret` was being stored in plaintext in `UserDefaults` as a redundant fallback. This commit removes writes to the unencrypted `UserDefaults` storage and explicitly deletes the key when accessing data, ensuring the secret only persists within the secure Keychain.

Co-authored-by: NSEvent <44446865+NSEvent@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The relay shared secret is migrated from insecure plaintext storage in UserDefaults to Keychain. Legacy storage entries are removed during migration and on new secret generation, and the security issue is documented.

Changes

Secret Storage Security Fix

Layer / File(s) Summary
Security issue documentation
.Jules/sentinel.md
A new sentinel document describes the insecure plaintext secret storage of universalControlRelaySharedSecret in UserDefaults, explains the risk of legacy redundant copies, and recommends avoiding UserDefaults for sensitive data and deleting legacy entries after migration.
Remove UserDefaults secret persistence
XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift
Secret migration from UserDefaults to Keychain now removes the legacy plaintext entry from UserDefaults. Secret generation/storage also stops writing to UserDefaults entirely, storing only in Keychain.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A secret stored in plain sight, oh dear,
Now locked in Keychain, safe and clear!
We've scrubbed the old defaults away,
Security wins the day! 🔐✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: removing insecure plaintext storage of a shared secret in UserDefaults and securing it in Keychain, which aligns perfectly with the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentinel-fix-insecure-secret-storage-18288302286118988901

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift (1)

2805-2811: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Surface and handle Keychain write failures explicitly.

Line 2805 and Line 2835 ignore KeychainService.storePassword results. With plaintext defaults now removed, a failed Keychain write silently drops durable secret persistence and can break pairing continuity after restart while configureRelayPairingSecret still reports success.

Proposed fix
 func configureRelayPairingSecret(_ secret: String) -> Bool {
     guard let data = Self.decodeRelaySecret(secret) else { return false }
-    storeRelaySharedSecret(data)
-    return true
+    return storeRelaySharedSecret(data)
 }

 private func relaySharedSecretData() -> Data {
     if let configured = UserDefaults.standard.string(forKey: relaySharedSecretDefaultsKey),
        let data = Self.decodeRelaySecret(configured) {
-        _ = KeychainService.storePassword(
+        let stored = KeychainService.storePassword(
             data.base64EncodedString(),
             key: relaySecretKey,
             service: relaySecretKeychainService
-        )
+        ) != nil
         UserDefaults.standard.removeObject(forKey: relaySharedSecretDefaultsKey)
-        return data
+        if stored { return data }
+        NSLog("[UCMouseRelay] Failed to migrate relay secret to Keychain")
     }

     if let stored = KeychainService.retrievePassword(
         key: relaySecretKey,
         service: relaySecretKeychainService
@@
- private func storeRelaySharedSecret(_ data: Data) {
+ `@discardableResult`
+ private func storeRelaySharedSecret(_ data: Data) -> Bool {
     let normalized = data.base64EncodedString()
     UserDefaults.standard.removeObject(forKey: relaySharedSecretDefaultsKey)
-    _ = KeychainService.storePassword(
+    guard KeychainService.storePassword(
         normalized,
         key: relaySecretKey,
         service: relaySecretKeychainService
-    )
+    ) != nil else {
+        NSLog("[UCMouseRelay] Failed to persist relay secret to Keychain")
+        return false
+    }
     resetAuthenticator(secretData: data)
+    return true
 }

Also applies to: 2832-2840

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift`
around lines 2805 - 2811, The Keychain write result from
KeychainService.storePassword is currently ignored which can silently drop
secret persistence; update the code paths that call
KeychainService.storePassword (the block that stores data.base64EncodedString()
with key relaySecretKey and service relaySecretKeychainService and the similar
block around lines 2832–2840) to check the returned success/failure, handle
failures by returning an error or throwing so configureRelayPairingSecret does
not report success on failure, and only remove
UserDefaults.standard.removeObject(forKey: relaySharedSecretDefaultsKey) after a
successful store; ensure failures surface up to the caller (or log and return
nil/error) so pairing persistence problems are detectable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift`:
- Around line 2805-2811: The Keychain write result from
KeychainService.storePassword is currently ignored which can silently drop
secret persistence; update the code paths that call
KeychainService.storePassword (the block that stores data.base64EncodedString()
with key relaySecretKey and service relaySecretKeychainService and the similar
block around lines 2832–2840) to check the returned success/failure, handle
failures by returning an error or throwing so configureRelayPairingSecret does
not report success on failure, and only remove
UserDefaults.standard.removeObject(forKey: relaySharedSecretDefaultsKey) after a
successful store; ensure failures surface up to the caller (or log and return
nil/error) so pairing persistence problems are detectable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72bcd6c8-dbbb-4a6a-a970-423a7252a421

📥 Commits

Reviewing files that changed from the base of the PR and between 56769ad and 2539fae.

📒 Files selected for processing (2)
  • .Jules/sentinel.md
  • XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant