A lightweight wrapper for sensitive string values (auth tokens, API keys,
passwords) that reduces — but does not eliminate — how long a secret's
plaintext remains recoverable from process memory, inspired by Rust's
secrecy/zeroize crates.
Once a secret is read from any storage (including secure storage packages
like flutter_secure_storage), it exists as a plain object in your app's
memory. Dart's String type is immutable, so there is no way to zero it
out when you're done with it — the old value simply waits to be garbage
collected, on no guaranteed schedule.
dart_secret stores the secret as a mutable byte buffer instead, so it
can be explicitly overwritten with zeros when you're done — via
dispose().
-
This does not guarantee zero memory presence. If you already have
a
Stringbefore wrapping it (e.g. fromflutter_secure_storage.read()), that original String is untouched — Dart gives no public API to zero an existing String's memory.dart_secretcan only guarantee zeroing of the byte buffer it owns internally, from the point you wrap the value onward. - In an empirical test (full methodology and raw data in the
companion research repository),
calling
dispose()measurably reduced the number of recoverable copies of a test secret in a raw memory snapshot (iOS Simulator,lldb-based full memory dump) compared to simply dropping aStringreference — but did not reduce it to zero. Treat this package as defense-in-depth, not a complete solution. - This is a young, unaudited package. It has not undergone independent security review. Do not treat it as a substitute for OS-level secure storage (Keychain/Keystore) for data at rest — use it alongside such storage, for the in-memory portion of a secret's lifetime.
import 'package:dart_secret/dart_secret.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
// Read the secret and wrap it as early as possible.
final rawValue = await storage.read(key: 'auth_token');
final secret = DartSecret(rawValue!);
// Use it.
final token = secret.reveal();
await apiCall(token);
// When done, dispose it explicitly.
secret.dispose();-
DartSecret(String value)— wrap an existing secret string. -
DartSecret.fromBytes(Uint8List bytes)— wrap raw bytes directly, skipping String creation if your source can already give you bytes. -
.reveal()→String— get the secret as a string (creates a new, untracked copy; use immediately). -
.revealBytes()→Uint8List— get the secret as raw bytes. -
.dispose()— overwrite the internal buffer with zeros. -
.isDisposed→bool.
This package was built alongside an empirical study measuring secret
memory persistence in flutter_secure_storage, which found that
neither nulling a variable reference nor calling delete() reliably
clears a previously-read secret from memory. Full methodology, raw
data, and a from-scratch (native Keychain byte-path) experimental
variant are documented in the
companion research repository.
- Investigate a native (platform-channel, byte-level) storage path that
bypasses
flutter_secure_storage's String-returning API entirely, to test whether avoiding String creation on the read path (not just wrapping after the fact) reduces memory persistence further. An early prototype exists but did not show a measurable additional benefit over the pure-Dart wrapper in initial testing — see the research repository for details. Further investigation needed before shipping this as a package feature.
MIT