Skip to content

Bug: [Android] v10 sharedPreferencesName/preferencesKeyPrefix silently ignored on subsequent FlutterSecureStorage instances #1109

@TesteurManiak

Description

@TesteurManiak

Description

In flutter_secure_storage 10.0.0 on Android, the first sharedPreferencesName (and preferencesKeyPrefix) used in a process wins for the lifetime of that process. Every later Dart FlutterSecureStorage instance constructed with a different sharedPreferencesName is silently routed to the first instance's SharedPreferences file. The plugin's config field is updated on each call, but the preferences field is cached, so the symptom is "wrong file, right prefix" — silent data corruption, no exception.

This is a behaviour regression from v9, where ensureInitialized() re-resolved the SharedPreferences instance on every call.

Minimal reproducible example

import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  const a = FlutterSecureStorage(
    aOptions: AndroidOptions(sharedPreferencesName: 'storage_a'),
  );
  const b = FlutterSecureStorage(
    aOptions: AndroidOptions(sharedPreferencesName: 'storage_b'),
  );

  await a.write(key: 'k', value: 'from-A'); // first call: opens storage_a
  await b.write(key: 'k', value: 'from-B'); // expected: opens storage_b

  debugPrint('a.read = ${await a.read(key: "k")}');
  debugPrint('b.read = ${await b.read(key: "k")}');
}

Expected behavior

v9 behaviour:

a.read = from-A   // a's write to storage_a
b.read = from-B   // b's write to storage_b

Actual on v10:

a.read = from-B   // b's write landed in storage_a, overwriting a's value
b.read = from-B   // b reads from storage_a too

Context

  • flutter_secure_storage: v10.0.0
  • Platform: Pixel 9 Android emulator (API 35)
  • Flutter: 3.41.7 (also reproduced on 3.41.6)

Platforms

  • Android
  • iOS

Root cause

Two changes between v9 and v10 combine to produce the regression.

1. v10 caches preferences and short-circuits initialize()
android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java, lines 148-153:

protected void initialize(FlutterSecureStorageConfig config, SecurePreferencesCallback<Void> callback) {
    this.config = config;          // config IS updated…
    if (preferences != null) {     // …but preferences is reused
        callback.onSuccess(null);
        return;
    }
    // …open SharedPreferences with config.getSharedPreferencesName() and cache it
}

In v9 the equivalent guard was explicitly commented out, so the SharedPreferences reference was re-resolved each call:

// android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java (v9, line 152)
// if (preferences != null) return;

2. There is exactly one FlutterSecureStorage per process
android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java, line 32:

secureStorage = new FlutterSecureStorage(context);

So the cache from change (1) is shared across every Dart-side instance, regardless of sharedPreferencesName.

The combined effect: config.getSharedPreferencesName() is read only once, when the cache is empty. After that, the file is locked in for the rest of the process; later instances quietly write into it.

Might be related to #914 and #1043

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions