m_security 0.3.0 copy "m_security: ^0.3.0" to clipboard
m_security: ^0.3.0 copied to clipboard

A high-performance cryptographic SDK for Flutter powered by native Rust via FFI. Provides authenticated encryption (AES-256-GCM, ChaCha20-Poly1305), modern hashing (BLAKE3, SHA-3, Argon2id), key deriv [...]

example/lib/main.dart

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:m_security/m_security.dart';
import 'package:m_security/src/rust/api/encryption.dart' as rust_enc;
import 'package:m_security/src/rust/api/hashing.dart' as hashing;
import 'package:m_security/src/rust/api/compression.dart';
import 'package:m_security/src/rust/api/evfs.dart' as rust_evfs;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await RustLib.init();
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'M-Security v0.3.0',
      theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
      home: const DemoHome(),
    );
  }
}

class DemoHome extends StatefulWidget {
  const DemoHome({super.key});

  @override
  State<DemoHome> createState() => _DemoHomeState();
}

class _DemoHomeState extends State<DemoHome> {
  int _tab = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('M-Security v0.3.0')),
      body: IndexedStack(
        index: _tab,
        children: const [
          _HashingTab(),
          _EncryptionTab(),
          _KdfTab(),
          _StreamingTab(),
          _VaultTab(),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _tab,
        onDestinationSelected: (i) => setState(() => _tab = i),
        destinations: const [
          NavigationDestination(icon: Icon(Icons.tag), label: 'Hash'),
          NavigationDestination(icon: Icon(Icons.lock), label: 'Encrypt'),
          NavigationDestination(icon: Icon(Icons.key), label: 'KDF'),
          NavigationDestination(icon: Icon(Icons.stream), label: 'Stream'),
          NavigationDestination(icon: Icon(Icons.folder), label: 'Vault'),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Hashing Tab
// ---------------------------------------------------------------------------

class _HashingTab extends StatefulWidget {
  const _HashingTab();
  @override
  State<_HashingTab> createState() => _HashingTabState();
}

class _HashingTabState extends State<_HashingTab> {
  final _input = TextEditingController(text: 'Hello, M-Security!');
  final _results = <String, String>{};
  bool _loading = false;

  Future<void> _run(String name, Future<String> Function() fn) async {
    setState(() => _loading = true);
    try {
      _results[name] = await fn();
    } catch (e) {
      _results[name] = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        TextField(
          controller: _input,
          decoration: const InputDecoration(
            labelText: 'Input text',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 12),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: [
            FilledButton(
              onPressed: _loading
                  ? null
                  : () => _run('BLAKE3', () async {
                        final h = await hashing.blake3Hash(
                            data: utf8.encode(_input.text));
                        return _hex(h);
                      }),
              child: const Text('BLAKE3'),
            ),
            FilledButton(
              onPressed: _loading
                  ? null
                  : () => _run('SHA-3', () async {
                        final h = await hashing.sha3Hash(
                            data: utf8.encode(_input.text));
                        return _hex(h);
                      }),
              child: const Text('SHA-3'),
            ),
            FilledButton(
              onPressed: _loading
                  ? null
                  : () => _run('Argon2id',
                      () => argon2IdHash(password: _input.text)),
              child: const Text('Argon2id'),
            ),
            FilledButton.tonal(
              onPressed: _loading
                  ? null
                  : () async {
                      final argonHash = _results['Argon2id'];
                      if (argonHash == null ||
                          argonHash.startsWith('Error')) {
                        _run('Verify', () async => 'Hash first with Argon2id');
                        return;
                      }
                      _run('Verify', () async {
                        try {
                          await argon2IdVerify(
                              phcHash: argonHash, password: _input.text);
                          return 'PASS - password matches';
                        } catch (_) {
                          return 'FAIL - password does not match';
                        }
                      });
                    },
              child: const Text('Verify Argon2'),
            ),
          ],
        ),
        if (_loading) const _Loader(),
        ..._results.entries
            .map((e) => _ResultCard(e.key, e.value)),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Encryption Tab
// ---------------------------------------------------------------------------

class _EncryptionTab extends StatefulWidget {
  const _EncryptionTab();
  @override
  State<_EncryptionTab> createState() => _EncryptionTabState();
}

class _EncryptionTabState extends State<_EncryptionTab> {
  final _input = TextEditingController(text: 'Secret message');
  String _algo = 'AES-256-GCM';
  Uint8List? _encrypted;
  rust_enc.CipherHandle? _cipher;
  String _encHex = '';
  String _decrypted = '';
  bool _loading = false;

  Future<void> _encrypt() async {
    setState(() {
      _loading = true;
      _decrypted = '';
      _encHex = '';
    });
    try {
      final key = _algo == 'AES-256-GCM'
          ? await rust_enc.generateAes256GcmKey()
          : await rust_enc.generateChacha20Poly1305Key();
      _cipher = _algo == 'AES-256-GCM'
          ? await rust_enc.createAes256Gcm(key: key)
          : await rust_enc.createChacha20Poly1305(key: key);
      _encrypted = await rust_enc.encrypt(
        cipher: _cipher!,
        plaintext: Uint8List.fromList(utf8.encode(_input.text)),
        aad: Uint8List(0),
      );
      _encHex = _hex(_encrypted!);
    } catch (e) {
      _encHex = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _decrypt() async {
    if (_encrypted == null || _cipher == null) return;
    setState(() => _loading = true);
    try {
      final plain = await rust_enc.decrypt(
        cipher: _cipher!,
        ciphertext: _encrypted!,
        aad: Uint8List(0),
      );
      _decrypted = utf8.decode(plain);
    } catch (e) {
      _decrypted = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        TextField(
          controller: _input,
          decoration: const InputDecoration(
            labelText: 'Plaintext',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 12),
        SegmentedButton<String>(
          segments: const [
            ButtonSegment(value: 'AES-256-GCM', label: Text('AES-GCM')),
            ButtonSegment(value: 'ChaCha20', label: Text('ChaCha20')),
          ],
          selected: {_algo},
          onSelectionChanged: (s) => setState(() => _algo = s.first),
        ),
        const SizedBox(height: 12),
        Row(
          children: [
            Expanded(
              child: FilledButton(
                onPressed: _loading ? null : _encrypt,
                child: const Text('Encrypt'),
              ),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: FilledButton.tonal(
                onPressed: _encrypted == null || _loading ? null : _decrypt,
                child: const Text('Decrypt'),
              ),
            ),
          ],
        ),
        if (_loading) const _Loader(),
        if (_encHex.isNotEmpty)
          _ResultCard('Ciphertext (${_encrypted!.length}B)', _encHex),
        if (_decrypted.isNotEmpty) _ResultCard('Decrypted', _decrypted),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// KDF Tab (HKDF)
// ---------------------------------------------------------------------------

class _KdfTab extends StatefulWidget {
  const _KdfTab();
  @override
  State<_KdfTab> createState() => _KdfTabState();
}

class _KdfTabState extends State<_KdfTab> {
  final _ikmInput = TextEditingController(text: 'master-secret');
  final _infoInput = TextEditingController(text: 'subkey-1');
  final _lenInput = TextEditingController(text: '32');
  String _derived = '';
  String _prk = '';
  bool _loading = false;

  Future<void> _derive() async {
    setState(() => _loading = true);
    try {
      final ikm = Uint8List.fromList(utf8.encode(_ikmInput.text));
      final info = Uint8List.fromList(utf8.encode(_infoInput.text));
      final len = int.parse(_lenInput.text);
      final result = MHKDF.derive(ikm: ikm, info: info, outputLen: len);
      _derived = _hex(result);
    } catch (e) {
      _derived = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _extract() async {
    setState(() => _loading = true);
    try {
      final ikm = Uint8List.fromList(utf8.encode(_ikmInput.text));
      final result = MHKDF.extract(ikm: ikm);
      _prk = _hex(result);
    } catch (e) {
      _prk = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        TextField(
          controller: _ikmInput,
          decoration: const InputDecoration(
            labelText: 'Input Key Material (IKM)',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: _infoInput,
          decoration: const InputDecoration(
            labelText: 'Info (context string)',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: _lenInput,
          keyboardType: TextInputType.number,
          decoration: const InputDecoration(
            labelText: 'Output length (bytes)',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 12),
        Row(
          children: [
            Expanded(
              child: FilledButton(
                onPressed: _loading ? null : _derive,
                child: const Text('HKDF Derive'),
              ),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: FilledButton.tonal(
                onPressed: _loading ? null : _extract,
                child: const Text('HKDF Extract'),
              ),
            ),
          ],
        ),
        if (_loading) const _Loader(),
        if (_derived.isNotEmpty) _ResultCard('Derived Key', _derived),
        if (_prk.isNotEmpty) _ResultCard('PRK (Extract)', _prk),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Streaming Tab (encrypt/decrypt/hash files + compression)
// ---------------------------------------------------------------------------

class _StreamingTab extends StatefulWidget {
  const _StreamingTab();
  @override
  State<_StreamingTab> createState() => _StreamingTabState();
}

class _StreamingTabState extends State<_StreamingTab> {
  String _status = '';
  double _progress = 0;
  bool _loading = false;
  String _algo = 'AES-256-GCM';
  String _compAlgo = 'None';
  final _sizeKb = TextEditingController(text: '256');

  Future<void> _testStreamEncryptDecrypt() async {
    setState(() {
      _loading = true;
      _status = 'Creating test file...';
      _progress = 0;
    });
    try {
      final dir = await Directory.systemTemp.createTemp('stream_test');
      final inputPath = '${dir.path}/input.bin';
      final encPath = '${dir.path}/encrypted.bin';
      final decPath = '${dir.path}/decrypted.bin';

      // Create test file
      final sizeBytes = int.parse(_sizeKb.text) * 1024;
      final data = Uint8List(sizeBytes);
      for (int i = 0; i < data.length; i++) {
        data[i] = i % 256;
      }
      await File(inputPath).writeAsBytes(data);

      // Create cipher
      final key = _algo == 'AES-256-GCM'
          ? await rust_enc.generateAes256GcmKey()
          : await rust_enc.generateChacha20Poly1305Key();
      final cipher = _algo == 'AES-256-GCM'
          ? await rust_enc.createAes256Gcm(key: key)
          : await rust_enc.createChacha20Poly1305(key: key);

      // Encrypt
      setState(() => _status = 'Encrypting ${_sizeKb.text}KB...');
      if (_compAlgo == 'None') {
        await StreamingService.encryptFile(
          inputPath: inputPath,
          outputPath: encPath,
          cipher: cipher,
        ).listen((p) => setState(() => _progress = p)).asFuture();
      } else {
        final comp = _compAlgo == 'Zstd'
            ? CompressionAlgorithm.zstd
            : CompressionAlgorithm.brotli;
        await CompressionService.compressAndEncryptFile(
          inputPath: inputPath,
          outputPath: encPath,
          cipher: cipher,
          config: CompressionConfig(algorithm: comp),
        ).listen((p) => setState(() => _progress = p)).asFuture();
      }

      final encSize = await File(encPath).length();
      setState(() {
        _status = 'Encrypted: ${encSize}B. Decrypting...';
        _progress = 0;
      });

      // Decrypt
      final cipher2 = _algo == 'AES-256-GCM'
          ? await rust_enc.createAes256Gcm(key: key)
          : await rust_enc.createChacha20Poly1305(key: key);

      if (_compAlgo == 'None') {
        await StreamingService.decryptFile(
          inputPath: encPath,
          outputPath: decPath,
          cipher: cipher2,
        ).listen((p) => setState(() => _progress = p)).asFuture();
      } else {
        await CompressionService.decryptAndDecompressFile(
          inputPath: encPath,
          outputPath: decPath,
          cipher: cipher2,
        ).listen((p) => setState(() => _progress = p)).asFuture();
      }

      // Verify
      final original = await File(inputPath).readAsBytes();
      final decrypted = await File(decPath).readAsBytes();
      final match = _bytesEqual(original, decrypted);

      setState(() {
        _progress = 1;
        _status =
            'Done! Roundtrip ${match ? "PASS" : "FAIL"}\n'
            'Input: ${original.length}B -> Encrypted: ${encSize}B -> Decrypted: ${decrypted.length}B';
      });

      await dir.delete(recursive: true);
    } catch (e) {
      setState(() => _status = 'Error: $e');
    }
    setState(() => _loading = false);
  }

  Future<void> _testStreamHash() async {
    setState(() {
      _loading = true;
      _status = 'Creating test file for hashing...';
      _progress = 0;
    });
    try {
      final dir = await Directory.systemTemp.createTemp('hash_test');
      final filePath = '${dir.path}/input.bin';

      final sizeBytes = int.parse(_sizeKb.text) * 1024;
      final data = Uint8List(sizeBytes);
      for (int i = 0; i < data.length; i++) {
        data[i] = i % 256;
      }
      await File(filePath).writeAsBytes(data);

      // One-shot hash for comparison
      final oneshotHash = await hashing.blake3Hash(data: data);

      // Streaming hash
      setState(() => _status = 'Streaming BLAKE3 hash...');
      final hasher = await hashing.createBlake3();
      final streamHash = await StreamingService.hashFile(
        filePath: filePath,
        hasher: hasher,
      );

      final match = _bytesEqual(oneshotHash, streamHash);
      setState(() {
        _progress = 1;
        _status =
            'Streaming hash ${match ? "PASS" : "FAIL"}\n'
            'One-shot: ${_hex(oneshotHash).substring(0, 16)}...\n'
            'Stream:   ${_hex(streamHash).substring(0, 16)}...';
      });

      await dir.delete(recursive: true);
    } catch (e) {
      setState(() => _status = 'Error: $e');
    }
    setState(() => _loading = false);
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        TextField(
          controller: _sizeKb,
          keyboardType: TextInputType.number,
          decoration: const InputDecoration(
            labelText: 'Test file size (KB)',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 12),
        Text('Cipher', style: Theme.of(context).textTheme.labelMedium),
        SegmentedButton<String>(
          segments: const [
            ButtonSegment(value: 'AES-256-GCM', label: Text('AES-GCM')),
            ButtonSegment(value: 'ChaCha20', label: Text('ChaCha20')),
          ],
          selected: {_algo},
          onSelectionChanged: (s) => setState(() => _algo = s.first),
        ),
        const SizedBox(height: 8),
        Text('Compression', style: Theme.of(context).textTheme.labelMedium),
        SegmentedButton<String>(
          segments: const [
            ButtonSegment(value: 'None', label: Text('None')),
            ButtonSegment(value: 'Zstd', label: Text('Zstd')),
            ButtonSegment(value: 'Brotli', label: Text('Brotli')),
          ],
          selected: {_compAlgo},
          onSelectionChanged: (s) => setState(() => _compAlgo = s.first),
        ),
        const SizedBox(height: 12),
        Row(
          children: [
            Expanded(
              child: FilledButton(
                onPressed: _loading ? null : _testStreamEncryptDecrypt,
                child: const Text('Encrypt/Decrypt'),
              ),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: FilledButton.tonal(
                onPressed: _loading ? null : _testStreamHash,
                child: const Text('Stream Hash'),
              ),
            ),
          ],
        ),
        const SizedBox(height: 12),
        if (_loading || _progress > 0)
          LinearProgressIndicator(value: _loading ? _progress : 1),
        if (_loading) const _Loader(),
        if (_status.isNotEmpty) _ResultCard('Status', _status),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Vault Tab (EVFS)
// ---------------------------------------------------------------------------

class _VaultTab extends StatefulWidget {
  const _VaultTab();
  @override
  State<_VaultTab> createState() => _VaultTabState();
}

class _VaultTabState extends State<_VaultTab> {
  final _vaultSizeMb = TextEditingController(text: '5');
  final _segName = TextEditingController(text: 'secret.txt');
  final _segData = TextEditingController(text: 'Vault data here');
  String _status = '';
  List<String> _segments = [];
  String _readResult = '';
  String _capacityInfo = '';
  bool _loading = false;
  bool _vaultOpen = false;
  String _compAlgo = 'Zstd';

  rust_evfs.VaultHandle? _handle;
  Uint8List? _key;
  String? _vaultPath;

  Future<void> _createVault() async {
    setState(() => _loading = true);
    try {
      final dir = await Directory.systemTemp.createTemp('demo_vault');
      _vaultPath = '${dir.path}/demo.vault';
      _key = await rust_enc.generateAes256GcmKey();
      final sizeMb = int.tryParse(_vaultSizeMb.text) ?? 5;
      _handle = await VaultService.create(
        path: _vaultPath!,
        key: _key!,
        algorithm: 'aes-256-gcm',
        capacityBytes: sizeMb * 1024 * 1024,
      );
      _vaultOpen = true;
      _status = 'Vault created (${sizeMb}MB, AES-256-GCM)';
      await _refreshList();
      await _refreshCapacity();
    } catch (e) {
      _status = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _closeVault() async {
    if (!_vaultOpen || _handle == null) return;
    try {
      await VaultService.close(handle: _handle!);
      _handle = null;
      _vaultOpen = false;
      _segments = [];
      _readResult = '';
      _capacityInfo = '';
      _status = 'Vault closed';
    } catch (e) {
      _status = 'Error: $e';
    }
    setState(() {});
  }

  Future<void> _reopenVault() async {
    if (_vaultPath == null || _key == null) return;
    setState(() => _loading = true);
    try {
      _handle = await VaultService.open(path: _vaultPath!, key: _key!);
      _vaultOpen = true;
      _status = 'Vault reopened (WAL recovery ran)';
      await _refreshList();
      await _refreshCapacity();
    } catch (e) {
      _status = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _writeSegment() async {
    if (!_vaultOpen || _handle == null) return;
    setState(() => _loading = true);
    try {
      final data = Uint8List.fromList(utf8.encode(_segData.text));
      CompressionConfig? comp;
      if (_compAlgo == 'Zstd') {
        comp = const CompressionConfig(algorithm: CompressionAlgorithm.zstd);
      } else if (_compAlgo == 'Brotli') {
        comp = const CompressionConfig(algorithm: CompressionAlgorithm.brotli);
      }
      await VaultService.write(
        handle: _handle!,
        name: _segName.text,
        data: data,
        compression: comp,
      );
      _status = 'Wrote "${_segName.text}" (${data.length}B, $_compAlgo)';
      await _refreshList();
      await _refreshCapacity();
    } catch (e) {
      _status = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _readSegment(String name) async {
    if (!_vaultOpen || _handle == null) return;
    setState(() => _loading = true);
    try {
      final data = await VaultService.read(handle: _handle!, name: name);
      // Try decoding as UTF-8, fallback to hex
      try {
        _readResult = '[$name] ${utf8.decode(data)}';
      } catch (_) {
        _readResult = '[$name] ${_hex(data)}';
      }
    } catch (e) {
      _readResult = 'Error reading "$name": $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _deleteSegment(String name) async {
    if (!_vaultOpen || _handle == null) return;
    setState(() => _loading = true);
    try {
      await VaultService.delete(handle: _handle!, name: name);
      _status = 'Deleted "$name" (securely erased)';
      _readResult = '';
      await _refreshList();
      await _refreshCapacity();
    } catch (e) {
      _status = 'Error: $e';
    }
    setState(() => _loading = false);
  }

  Future<void> _refreshList() async {
    if (!_vaultOpen || _handle == null) return;
    _segments = await VaultService.list(handle: _handle!);
  }

  Future<void> _refreshCapacity() async {
    if (!_vaultOpen || _handle == null) return;
    final cap = await VaultService.capacity(handle: _handle!);
    _capacityInfo = 'Total: ${_fmtBytes(cap.totalBytes)}  |  '
        'Used: ${_fmtBytes(cap.usedBytes)}  |  '
        'Free-list: ${_fmtBytes(cap.freeListBytes)}  |  '
        'Unallocated: ${_fmtBytes(cap.unallocatedBytes)}';
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        // Vault size
        if (!_vaultOpen)
          Padding(
            padding: const EdgeInsets.only(bottom: 12),
            child: TextField(
              controller: _vaultSizeMb,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: 'Vault size (MB)',
                border: OutlineInputBorder(),
                isDense: true,
              ),
            ),
          ),

        // Vault lifecycle
        Row(
          children: [
            Expanded(
              child: FilledButton(
                onPressed: _vaultOpen || _loading ? null : _createVault,
                child: const Text('Create'),
              ),
            ),
            const SizedBox(width: 6),
            Expanded(
              child: FilledButton.tonal(
                onPressed: !_vaultOpen || _loading
                    ? null
                    : _closeVault,
                child: const Text('Close'),
              ),
            ),
            const SizedBox(width: 6),
            Expanded(
              child: OutlinedButton(
                onPressed: _vaultOpen || _vaultPath == null || _loading
                    ? null
                    : _reopenVault,
                child: const Text('Reopen'),
              ),
            ),
          ],
        ),
        const SizedBox(height: 8),

        if (_status.isNotEmpty)
          Card(
            color: Theme.of(context).colorScheme.primaryContainer,
            child: Padding(
              padding: const EdgeInsets.all(10),
              child: Text(_status,
                  style: TextStyle(
                      fontSize: 13,
                      color:
                          Theme.of(context).colorScheme.onPrimaryContainer)),
            ),
          ),

        if (_capacityInfo.isNotEmpty)
          Card(
            color: Theme.of(context).colorScheme.tertiaryContainer,
            child: Padding(
              padding: const EdgeInsets.all(10),
              child: Text(_capacityInfo,
                  style: TextStyle(
                      fontSize: 11,
                      fontFamily: 'monospace',
                      color: Theme.of(context)
                          .colorScheme
                          .onTertiaryContainer)),
            ),
          ),

        const SizedBox(height: 12),

        // Write segment
        if (_vaultOpen) ...[
          TextField(
            controller: _segName,
            decoration: const InputDecoration(
              labelText: 'Segment name',
              border: OutlineInputBorder(),
              isDense: true,
            ),
          ),
          const SizedBox(height: 8),
          TextField(
            controller: _segData,
            maxLines: 2,
            decoration: const InputDecoration(
              labelText: 'Segment data',
              border: OutlineInputBorder(),
              isDense: true,
            ),
          ),
          const SizedBox(height: 8),
          Text('Compression',
              style: Theme.of(context).textTheme.labelMedium),
          SegmentedButton<String>(
            segments: const [
              ButtonSegment(value: 'None', label: Text('None')),
              ButtonSegment(value: 'Zstd', label: Text('Zstd')),
              ButtonSegment(value: 'Brotli', label: Text('Brotli')),
            ],
            selected: {_compAlgo},
            onSelectionChanged: (s) => setState(() => _compAlgo = s.first),
          ),
          const SizedBox(height: 8),
          FilledButton.icon(
            onPressed: _loading ? null : _writeSegment,
            icon: const Icon(Icons.save, size: 18),
            label: const Text('Write Segment'),
          ),
          const Divider(height: 24),

          // Segment list
          Text('Segments (${_segments.length})',
              style: Theme.of(context).textTheme.titleSmall),
          if (_segments.isEmpty)
            const Padding(
              padding: EdgeInsets.symmetric(vertical: 8),
              child: Text('No segments yet',
                  style: TextStyle(color: Colors.grey)),
            ),
          ..._segments.map((name) => ListTile(
                dense: true,
                leading: const Icon(Icons.insert_drive_file, size: 20),
                title: Text(name),
                trailing: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    IconButton(
                      icon: const Icon(Icons.visibility, size: 20),
                      tooltip: 'Read',
                      onPressed: () => _readSegment(name),
                    ),
                    IconButton(
                      icon:
                          const Icon(Icons.delete, size: 20, color: Colors.red),
                      tooltip: 'Delete',
                      onPressed: () => _deleteSegment(name),
                    ),
                  ],
                ),
              )),
          if (_readResult.isNotEmpty) _ResultCard('Read', _readResult),
        ],

        if (_loading) const _Loader(),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Shared widgets & helpers
// ---------------------------------------------------------------------------

class _ResultCard extends StatelessWidget {
  final String label;
  final String value;
  const _ResultCard(this.label, this.value);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(top: 10),
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(label,
                style: Theme.of(context)
                    .textTheme
                    .labelMedium
                    ?.copyWith(fontWeight: FontWeight.bold)),
            const SizedBox(height: 4),
            SelectableText(value,
                style: Theme.of(context)
                    .textTheme
                    .bodySmall
                    ?.copyWith(fontFamily: 'monospace')),
          ],
        ),
      ),
    );
  }
}

class _Loader extends StatelessWidget {
  const _Loader();
  @override
  Widget build(BuildContext context) => const Padding(
        padding: EdgeInsets.all(16),
        child: Center(child: CircularProgressIndicator()),
      );
}

String _hex(Uint8List bytes) =>
    bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();

String _fmtBytes(BigInt bytes) {
  final b = bytes.toInt();
  if (b < 1024) return '${b}B';
  if (b < 1024 * 1024) return '${(b / 1024).toStringAsFixed(1)}KB';
  return '${(b / (1024 * 1024)).toStringAsFixed(1)}MB';
}

bool _bytesEqual(Uint8List a, Uint8List b) {
  if (a.length != b.length) return false;
  for (int i = 0; i < a.length; i++) {
    if (a[i] != b[i]) return false;
  }
  return true;
}
1
likes
140
points
0
downloads

Publisher

unverified uploader

Weekly Downloads

A high-performance cryptographic SDK for Flutter powered by native Rust via FFI. Provides authenticated encryption (AES-256-GCM, ChaCha20-Poly1305), modern hashing (BLAKE3, SHA-3, Argon2id), key derivation (HKDF), streaming encryption with compression (Zstd, Brotli), and an encrypted virtual file system (EVFS).

Repository (GitHub)
View/report issues
Contributing

Topics

#cryptography #encryption #security #rust #ffi

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, flutter_rust_bridge, freezed_annotation, plugin_platform_interface

More

Packages that depend on m_security

Packages that implement m_security