Skip to content

fix(mobile): make secure storage fallible and fail-closed - #5923

Open
mortenolsrud wants to merge 2 commits into
wailsapp:masterfrom
mortenolsrud:fix/mobile-secure-storage
Open

fix(mobile): make secure storage fallible and fail-closed#5923
mortenolsrud wants to merge 2 commits into
wailsapp:masterfrom
mortenolsrud:fix/mobile-secure-storage

Conversation

@mortenolsrud

@mortenolsrud mortenolsrud commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

The mobile secure storage API (SecureSet, SecureGet, SecureDelete) silently swallows all native failures, cannot distinguish a missing key from an empty stored value, and on Android falls back to plaintext preferences when encrypted storage initialization fails.

This PR makes all three methods fallible, removes the plaintext fallback, and fixes the destructive delete-before-add Keychain pattern on iOS.

// New signatures (MobileManager interface)
SecureSet(key, value string) error
SecureGet(key string) (value string, found bool, err error)
SecureDelete(key string) error

iOS — non-destructive Keychain updates:

  • Uses SecItemUpdate first, falls back to SecItemAdd only on errSecItemNotFound
  • All OSStatus values are checked and propagated as typed errors
  • Distinguishes missing key (found=false) from empty stored value (found=true)

Android — fail-closed encrypted storage:

  • Removed wails_secure_plain plaintext fallback entirely
  • Requires API 23+ for EncryptedSharedPreferences; returns explicit error below
  • Uses commit() instead of apply() for durable, verifiable writes
  • Uses contains(key) to distinguish missing from empty
  • Returns structured JSON envelope across JNI bridge

Desktop stub returns ErrSecureStorageUnsupported (previously returned apparent success).

Empty key is rejected at the Go layer before crossing either native bridge. Delete of a non-existent key succeeds on both platforms.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Desktop build (GTK4) — compiles, stub returns ErrSecureStorageUnsupported.
  • Android cross-compile (android/arm64, CGO_ENABLED=0 nocgo path) — compiles.
  • iOS cannot be verified without Xcode/SDK but Go source parses cleanly.
  • Example app updated to use new signatures and compiles on desktop.

The change is backwards-compatible for callers that used the methods as standalone statements (fire-and-forget). Callers that captured SecureGet's return value are the source-breaking change.

  • Windows
  • macOS
  • Linux

Test Configuration

  • Wails CLI: v3.0.0-beta.3
  • Go: go1.26.5
  • Ubuntu 24.04.4

Checklist:

  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Notes:

Summary by CodeRabbit

  • New Features
    • Added secure storage support with set, retrieve, and delete operations across Android and iOS.
    • Retrieval now indicates whether a key exists and safely handles stored values.
  • Bug Fixes
    • Added validation and clear error reporting for invalid keys, unavailable storage, failed operations, and malformed responses.
    • Missing keys are handled explicitly, while deleting an absent key succeeds safely.
  • Compatibility
    • Android secure storage requires API 23 or later.
    • Desktop platforms now clearly report that secure storage is unsupported.

BREAKING CHANGE: SecureSet, SecureGet, and SecureDelete now return errors.

SecureSet(key, value string) error
SecureGet(key string) (value string, found bool, err error)
SecureDelete(key string) error

iOS:
- Use SecItemUpdate before falling back to SecItemAdd (non-destructive)
- Check and propagate all OSStatus values
- Distinguish missing key (found=false) from empty value (found=true)

Android:
- Remove plaintext fallback (wails_secure_plain eliminated)
- Require API 23+ for EncryptedSharedPreferences; return error below
- Use commit() instead of apply() for durable writes
- Use contains() to distinguish missing from empty
- Return structured JSON envelope across JNI bridge

Desktop stub returns ErrSecureStorageUnsupported for all three methods.

Empty key is rejected at the Go layer before crossing any bridge.
Delete of a non-existent key succeeds on both platforms.

Closes wailsapp#5913 (partially — secure storage portion of the mobile hardening audit)
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 71ae9a4e-3487-44cd-bef4-1a5a1f17bbfe

📥 Commits

Reviewing files that changed from the base of the PR and between d71f916 and 840645f.

📒 Files selected for processing (2)
  • v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java
  • v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java

Walkthrough

Secure storage now uses error-returning APIs across mobile platforms. Android and iOS return structured JSON results, distinguish missing values, validate keys, and propagate native failures. Unsupported platforms return ErrSecureStorageUnsupported.

Changes

Secure storage API contract

Layer / File(s) Summary
Common contract and unsupported-platform behavior
v3/pkg/application/mobile.go, v3/pkg/application/mobile_stub.go
The API now provides SecureSet, error-returning SecureDelete, and (value, found, error) from SecureGet. Unsupported platforms return ErrSecureStorageUnsupported.
Android secure storage implementation
v3/pkg/application/mobile_features_android.go, v3/pkg/application/application_android_nocgo.go, v3/.../android/app/src/main/java/.../WailsBridge.java, v3/examples/mobile/native_features_android.go, v3/examples/mobile/go.mod
Android storage requires encrypted preferences on API 23+, returns JSON result envelopes, validates keys, reports commit failures, and propagates errors through event handlers.
iOS secure storage implementation
v3/pkg/application/mobile_features_ios.go, v3/pkg/application/mobile_features_ios.h, v3/pkg/application/mobile_features_ios.m, v3/examples/mobile/native_features_ios.go
Keychain operations return JSON result envelopes, validate keys, distinguish missing entries, escape values, and propagate native errors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MobileManager
  participant PlatformAdapter
  participant NativeSecureStorage
  participant EventHandler
  MobileManager->>PlatformAdapter: SecureSet, SecureGet, or SecureDelete
  PlatformAdapter->>NativeSecureStorage: Validated request
  NativeSecureStorage-->>PlatformAdapter: JSON result or native error
  PlatformAdapter-->>MobileManager: Value, found status, or error
  EventHandler-->>MobileManager: common:secureValue or common:secureError
Loading

Suggested reviewers: taliesin-ai

Poem

I tap my paws on keys so bright,
Secure JSON guards them tight.
Missing leaves a clear-found sign,
Errors travel down the line.
Android, iOS, side by side—
A careful rabbit hops with pride.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states that mobile secure storage now returns failures and uses fail-closed behavior.
Description check ✅ Passed The description covers the change, motivation, platform behavior, testing, configuration, change type, and checklist status.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java (1)

748-763: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

securePrefs() recreates the encrypted store on every call in both WailsBridge.java copies. Each secure storage operation builds a MasterKey and calls EncryptedSharedPreferences.create, which performs Android Keystore and Tink keyset work.

  • v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java#L748-L763: store the created SharedPreferences in a field and return the cached instance on later calls, keeping the null return for the unavailable case.
  • v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java#L748-L763: apply the same caching so the generated example matches the template.
🤖 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
`@v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java`
around lines 748 - 763, Cache the SharedPreferences instance created by
securePrefs() in a field, returning the cached value on subsequent calls while
preserving the existing null result for unsupported Android versions or
initialization failures. Apply the same change at
v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java
lines 748-763 and
v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java
lines 748-763 so both copies remain consistent.
🤖 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.

Inline comments:
In
`@v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java`:
- Around line 782-784: In both WailsBridge.java files at
v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java:782-784
and
v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java:782-784,
add a private secureError(String) helper that builds the error envelope with
JSONObject, safely handling null messages and JSON escaping. Update the catch
blocks in secureSet, secureGet, and secureDelete in both files to call this
helper instead of manually concatenating the message, keeping both copies
identical.

---

Nitpick comments:
In
`@v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java`:
- Around line 748-763: Cache the SharedPreferences instance created by
securePrefs() in a field, returning the cached value on subsequent calls while
preserving the existing null result for unsupported Android versions or
initialization failures. Apply the same change at
v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java
lines 748-763 and
v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java
lines 748-763 so both copies remain consistent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d5c1d08-358c-41b3-9c6e-6ff3b11e9d1a

📥 Commits

Reviewing files that changed from the base of the PR and between bd139c0 and d71f916.

⛔ Files ignored due to path filters (1)
  • v3/examples/mobile/go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • v3/examples/mobile/build/android/app/src/main/java/com/wails/app/WailsBridge.java
  • v3/examples/mobile/go.mod
  • v3/examples/mobile/native_features_android.go
  • v3/examples/mobile/native_features_ios.go
  • v3/internal/commands/build_assets/android/app/src/main/java/com/wails/app/WailsBridge.java
  • v3/pkg/application/application_android_nocgo.go
  • v3/pkg/application/mobile.go
  • v3/pkg/application/mobile_features_android.go
  • v3/pkg/application/mobile_features_ios.go
  • v3/pkg/application/mobile_features_ios.h
  • v3/pkg/application/mobile_features_ios.m
  • v3/pkg/application/mobile_stub.go

Address CodeRabbit review feedback:
- Cache EncryptedSharedPreferences instance after first initialization
- Add secureError() helper using JSONObject to safely handle null
  getMessage() and avoid JSON injection from special characters
- Replace manual string concatenation in all catch blocks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant