eter_secure_buffer 1.0.0
eter_secure_buffer: ^1.0.0 copied to clipboard
Secure off-heap memory buffer for Flutter via FFI. Prevents plaintext from touching the Dart GC heap — malloc/mlock/memset/munlock outside Dart heap.
example/lib/main.dart
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:eter_secure_buffer/eter_secure_buffer.dart';
void main() {
runApp(const SecureBufferExampleApp());
}
class SecureBufferExampleApp extends StatelessWidget {
const SecureBufferExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SecureBuffer Example',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const SecureBufferDemo(),
);
}
}
class SecureBufferDemo extends StatefulWidget {
const SecureBufferDemo({super.key});
@override
State<SecureBufferDemo> createState() => _SecureBufferDemoState();
}
class _SecureBufferDemoState extends State<SecureBufferDemo> {
SecureBuffer? _buffer;
String _status = 'No buffer allocated.';
String _mlockStatus = '';
String _readPreview = '';
void _allocate() {
_wipeExisting();
try {
final buf = SecureBuffer.alloc(32);
setState(() {
_buffer = buf;
_mlockStatus = 'mlock: attempted (best-effort on Android)';
_status = 'Allocated 32 bytes of off-heap secure memory.';
_readPreview = '';
});
} catch (e) {
setState(() {
_status = 'Allocation failed: $e';
});
}
}
void _write() {
final buf = _buffer;
if (buf == null || buf.isWiped) {
setState(() => _status = 'No active buffer. Allocate first.');
return;
}
final random = Random.secure();
final bytes = Uint8List.fromList(
List.generate(32, (_) => random.nextInt(256)),
);
try {
buf.writeEncrypted(bytes);
setState(() {
_status = 'Wrote 32 random bytes into native memory.';
_readPreview = '';
});
} catch (e) {
setState(() => _status = 'Write failed: $e');
}
}
void _read() {
final buf = _buffer;
if (buf == null || buf.isWiped) {
setState(() => _status = 'No active buffer. Allocate first.');
return;
}
try {
// asView() returns a live pointer — read immediately, do not store
final view = buf.asView();
final hex = view
.take(4)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(' ');
setState(() {
_readPreview = 'First 4 bytes: $hex (use and discard immediately)';
_status = 'Read successful. WARNING: asView() is a live native pointer.';
});
} catch (e) {
setState(() => _status = 'Read failed: $e');
}
}
void _wipe() {
final buf = _buffer;
if (buf == null) {
setState(() => _status = 'No buffer to wipe.');
return;
}
buf.wipe();
setState(() {
_status = 'Buffer wiped: memset(0x00) → munlock → free. Memory cleared.';
_mlockStatus = '';
_readPreview = '';
});
}
void _wipeExisting() {
final buf = _buffer;
if (buf != null && !buf.isWiped) buf.wipe();
_buffer = null;
}
@override
void dispose() {
_wipeExisting();
super.dispose();
}
@override
Widget build(BuildContext context) {
final buf = _buffer;
final hasActiveBuffer = buf != null && !buf.isWiped;
return Scaffold(
appBar: AppBar(title: const Text('SecureBuffer Demo')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_InfoCard(
label: 'Status',
value: _status,
color: Colors.indigo.shade50,
),
if (_mlockStatus.isNotEmpty)
_InfoCard(
label: 'mlock',
value: _mlockStatus,
color: Colors.green.shade50,
),
if (_readPreview.isNotEmpty)
_InfoCard(
label: 'Read preview',
value: _readPreview,
color: Colors.orange.shade50,
),
const SizedBox(height: 24),
_ActionButton(
label: 'Allocate (32 bytes)',
icon: Icons.memory,
onPressed: _allocate,
),
const SizedBox(height: 12),
_ActionButton(
label: 'Write random bytes',
icon: Icons.edit,
onPressed: hasActiveBuffer ? _write : null,
),
const SizedBox(height: 12),
_ActionButton(
label: 'Read (first 4 bytes)',
icon: Icons.visibility,
onPressed: hasActiveBuffer ? _read : null,
),
const SizedBox(height: 12),
_ActionButton(
label: 'Wipe buffer',
icon: Icons.delete_forever,
color: Colors.red,
onPressed: hasActiveBuffer ? _wipe : null,
),
const SizedBox(height: 24),
const Text(
'Memory lifecycle: alloc → mlock → write → use → wipe (memset + munlock + free).\n'
'The buffer lives outside the Dart GC heap — the garbage collector never sees plaintext.',
style: TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class _InfoCard extends StatelessWidget {
final String label;
final String value;
final Color color;
const _InfoCard({
required this.label,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style:
const TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 13)),
],
),
);
}
}
class _ActionButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback? onPressed;
final Color? color;
const _ActionButton({
required this.label,
required this.icon,
this.onPressed,
this.color,
});
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: color != null ? Colors.white : null,
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: onPressed,
);
}
}