🔒 Secure authToken storage with flutter_secure_storage

Closed
UN
2mo ago
+755-111

🎯 What: Store auth tokens securely
authToken was stored in plaintext in SharedPreferences (NSUserDefaults on iOS). Migrate it to flutter_secure_storage (Android Keystore / iOS Keychain).

⚠️ Risk: Plaintext tokens on rooted/jailbroken devices or in backups enable session hijacking.

🛡️ Solution:

  • Migrate the legacy SharedPreferences token into secure storage at SharedPreferencesUtil.init(); remove the plaintext copy once secure write succeeds. A failed migration write keeps the in-memory token and the legacy value so startup never fails hard, and cleanup retries on the next init.
  • persistAuthToken() is now awaited by refresh/sign-out/session-expiration callers, with bounded retries and delete-pending markers so a token refresh does not silently disappear across restart and sign-out does not silently leave a secure-storage token behind.
  • Native Android background bridge: Android background BLE transcription (OmiBackgroundAudioStreamer) runs after the Flutter engine exits and can only read SharedPreferences. Keep an explicit nativeAuthToken mirror there, written only on Android, deleted on sign-out. This is a documented tradeoff: a plaintext mirror remains on Android to preserve background transcription, while the Dart-side token no longer lives in plaintext. The alternative (reading the encrypted Keystore from native Kotlin without the plugin) is not viable for a background service.
  • Tests mock secure storage (and use a fake storage seam) so the unit suite stays hermetic.

Failure-Class: none


[!NOTE]
High Risk
Touches authentication token storage, sign-out/session lifecycle, and Android background transcription auth; a regression could strand users logged out or break native BLE streaming.

Overview
Moves authToken out of plaintext SharedPreferences into flutter_secure_storage (Keystore / Keychain), with startup migration from the legacy key and removal of the plaintext copy once secure storage succeeds.

Introduces AuthTokenPersistence and persistAuthToken(), which serialize writes, retry with verification, and use a delete-pending flag so failed sign-out or slow writes can finish cleanup on the next launch. AuthService, HTTP refresh, and sign-out/session expiry now await persistence; refresh can keep an in-memory token if secure write fails.

On Android, a nativeAuthToken mirror in prefs feeds OmiBackgroundAudioStreamer when Flutter is not running (still plaintext by necessity); native code prefers nativeAuthToken over legacy authToken. android:allowBackup="false" reduces backup exposure; iOS adds keychain-access-groups entitlements and flutter_secure_storage wiring.

Tests mock secure storage and add auth_token_persistence_test.dart for migration, mirror, and failure paths.

Reviewed by Cursor Bugbot for commit 6c6195e6e0f90eb958c30ec40ccc673fad2b7509. Configure here.