eter_secure_buffer

A Flutter FFI package that provides secure off-heap memory buffers. Memory is allocated via malloc, locked to RAM with mlock (preventing swap to disk), and deterministically wiped with a volatile memset before free. The Dart garbage collector never sees plaintext.

What is this?

Flutter's Uint8List lives on the Dart GC heap. That means:

  • The runtime may copy it during GC compaction.
  • Copies linger until the next GC cycle — potentially seconds or minutes.
  • On a rooted device or with a memory dump, those bytes are recoverable.

SecureBuffer sidesteps all of this by allocating memory via native malloc, outside the Dart heap entirely. No GC. No copies. No lingering plaintext.

Security properties

Property Mechanism
Heap isolation malloc — memory is never on the Dart/VM heap
No GC exposure Dart GC never touches the pointer
Swap prevention mlock pins pages in RAM (best-effort)
Constant-time wipe Volatile memset(0x00) — compiler cannot optimize away
Explicit lifecycle You control alloc, write, read, wipe — no hidden copies

Platform support

Platform Architecture Status
Android arm64-v8a, x86_64 Supported
iOS arm64, arm64-simulator Supported
macOS arm64, x86_64 Supported
Linux x86_64 Supported

Installation

dependencies:
  eter_secure_buffer: ^1.0.0

Usage

import 'package:eter_secure_buffer/eter_secure_buffer.dart';

// 1. Allocate off-heap memory (malloc + mlock)
final buffer = SecureBuffer.alloc(32);

// 2. Write data into native memory
//    Data travels: Dart heap → temp native buffer → secure buffer → temp freed
final ciphertext = Uint8List.fromList([/* your bytes */]);
buffer.writeEncrypted(ciphertext);

// 3. Read data — asView() returns a LIVE pointer into native memory.
//    Use immediately. Do NOT store the reference.
final view = buffer.asView();
processData(view); // use here, then let view go out of scope

// 4. Wipe: volatile memset(0x00) → munlock → free
//    After this call, the buffer is invalid.
buffer.wipe();

// Checking state
print(buffer.isWiped); // true

WARNING: asView() returns a Uint8List that is a direct pointer into native memory. If you store this reference and then call wipe(), accessing the stored reference is undefined behavior (use-after-free). Always read and discard in the same scope.

Why not just use Uint8List?

Uint8List is backed by the Dart GC heap. When you write final key = Uint8List(32), that object:

  1. Lives at a Dart-managed address.
  2. May be moved during GC compaction (old address still readable in a memory dump).
  3. Is not wiped when GC'd — just marked as free, data still in RAM.
  4. May be paged to disk by the OS under memory pressure.

SecureBuffer avoids all four issues.

mlock caveat on Android

Android enforces RLIMIT_MEMLOCK (commonly 64 KB per process). mlock calls may fail silently for large buffers. SecureBuffer.alloc still succeeds — mlock failure is non-fatal by design. The volatile memset wipe is always applied regardless.

License

Apache 2.0 — see LICENSE.

Libraries

eter_secure_buffer