dart_secret

A lightweight wrapper for sensitive strings that reduces (does not eliminate) in-memory persistence via explicit zeroize-on-dispose, inspired by Rust's secrecy/zeroize crates.


License
MIT

Documentation

dart_secret

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.

Why

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().

Honest limitations (please read)

  • This does not guarantee zero memory presence. If you already have a String before wrapping it (e.g. from flutter_secure_storage.read()), that original String is untouched — Dart gives no public API to zero an existing String's memory. dart_secret can 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 a String reference — 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.

Usage

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();

API

  • 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.
  • .isDisposedbool.

Research background

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.

Roadmap

  • 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.

License

MIT