libsignal 1.1.0 copy "libsignal: ^1.1.0" to clipboard
libsignal: ^1.1.0 copied to clipboard

Dart FFI bindings for libsignal — Signal Protocol implementation for end-to-end encryption, sealed sender, group messaging, and secure cryptographic operations.

example/lib/main.dart

import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:libsignal/libsignal.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  bool _isInitialized = false;

  // State for each demo
  String? _keysResult;
  String? _cryptoResult;
  String? _groupsResult;
  String? _fingerprintResult;

  bool _keysLoading = false;
  bool _cryptoLoading = false;
  bool _groupsLoading = false;
  bool _fingerprintLoading = false;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
    _initLibSignal();
  }

  void _initLibSignal() {
    LibSignal.init();
    setState(() => _isInitialized = true);
  }

  @override
  void dispose() {
    _tabController.dispose();
    LibSignal.cleanup();
    super.dispose();
  }

  // Helper: Generate random bytes
  Uint8List _randomBytes(int length) {
    final random = Random.secure();
    return Uint8List.fromList(
      List.generate(length, (_) => random.nextInt(256)),
    );
  }

  // Helper: Convert bytes to hex string
  String _bytesToHex(Uint8List bytes, {int? maxLength}) {
    final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
    if (maxLength != null && hex.length > maxLength) {
      return '${hex.substring(0, maxLength)}...';
    }
    return hex;
  }

  // Helper: Format fingerprint (60 digits -> groups of 5)
  String _formatFingerprint(String fingerprint) {
    final buffer = StringBuffer();
    for (var i = 0; i < fingerprint.length; i += 5) {
      if (i > 0) buffer.write(' ');
      final end = (i + 5 < fingerprint.length) ? i + 5 : fingerprint.length;
      buffer.write(fingerprint.substring(i, end));
    }
    return buffer.toString();
  }

  // ============================================
  // Keys Demo
  // ============================================
  Future<void> _runKeysDemo() async {
    setState(() {
      _keysLoading = true;
      _keysResult = null;
    });

    PrivateKey? privateKey;
    PublicKey? publicKey;
    IdentityKeyPair? identityKeyPair;

    try {
      final result = StringBuffer();

      // 1. Generate PrivateKey
      privateKey = PrivateKey.generate();
      final privateBytes = privateKey.serialize();
      result.writeln('1. PrivateKey generated');
      result.writeln('   Size: ${privateBytes.bytes.length} bytes');
      result.writeln(
        '   Hex: ${_bytesToHex(privateBytes.bytes, maxLength: 32)}',
      );
      result.writeln();

      // 2. Get PublicKey from PrivateKey
      publicKey = privateKey.getPublicKey();
      final publicBytes = publicKey.serialize();
      result.writeln('2. PublicKey derived');
      result.writeln('   Size: ${publicBytes.length} bytes (1 type + 32 key)');
      result.writeln('   Hex: ${_bytesToHex(publicBytes, maxLength: 32)}');
      result.writeln();

      // 3. Sign a message
      const messageText = 'Hello, Signal Protocol!';
      final message = Uint8List.fromList(utf8.encode(messageText));
      final signature = privateKey.sign(message);
      result.writeln('3. Message signed (Ed25519)');
      result.writeln('   Message: "$messageText"');
      result.writeln('   Signature size: ${signature.length} bytes');
      result.writeln('   Signature: ${_bytesToHex(signature, maxLength: 32)}');
      result.writeln();

      // 4. Verify signature
      final isValid = publicKey.verify(message, signature);
      result.writeln('4. Signature verification');
      result.writeln('   Valid: $isValid');
      result.writeln();

      // 5. Verify with wrong message fails
      final wrongMessage = Uint8List.fromList(utf8.encode('Wrong message'));
      final isInvalid = publicKey.verify(wrongMessage, signature);
      result.writeln('5. Wrong message verification');
      result.writeln('   Valid: $isInvalid (expected: false)');
      result.writeln();

      // 6. Generate IdentityKeyPair
      identityKeyPair = IdentityKeyPair.generate();
      final identitySerialized = identityKeyPair.serialize();
      result.writeln('6. IdentityKeyPair generated');
      result.writeln(
        '   Serialized size: ${identitySerialized.bytes.length} bytes',
      );
      result.writeln(
        '   Public key: ${_bytesToHex(identityKeyPair.publicKey.serialize(), maxLength: 32)}',
      );

      setState(() => _keysResult = result.toString());
    } catch (e) {
      setState(() => _keysResult = 'Error: $e');
    } finally {
      // Proper disposal
      privateKey?.dispose();
      publicKey?.dispose();
      identityKeyPair?.dispose();
      setState(() => _keysLoading = false);
    }
  }

  // ============================================
  // Crypto Demo (AES-256-GCM-SIV)
  // ============================================
  Future<void> _runCryptoDemo() async {
    setState(() {
      _cryptoLoading = true;
      _cryptoResult = null;
    });

    Aes256GcmSiv? cipher;

    try {
      final result = StringBuffer();

      // 1. Generate key and nonce
      final key = _randomBytes(32);
      final nonce = _randomBytes(12);
      result.writeln('1. Key and nonce generated');
      result.writeln('   Key size: ${key.length} bytes (256 bits)');
      result.writeln('   Key: ${_bytesToHex(key, maxLength: 32)}');
      result.writeln('   Nonce size: ${nonce.length} bytes (96 bits)');
      result.writeln('   Nonce: ${_bytesToHex(nonce)}');
      result.writeln();

      // 2. Create cipher
      cipher = Aes256GcmSiv(key);
      result.writeln('2. AES-256-GCM-SIV cipher created');
      result.writeln();

      // 3. Encrypt message
      const messageText = 'Secret message for encryption';
      final plaintext = Uint8List.fromList(utf8.encode(messageText));
      final ciphertext = cipher.encrypt(plaintext: plaintext, nonce: nonce);
      result.writeln('3. Message encrypted');
      result.writeln('   Plaintext: "$messageText"');
      result.writeln('   Plaintext size: ${plaintext.length} bytes');
      result.writeln('   Ciphertext size: ${ciphertext.length} bytes');
      result.writeln(
        '   Size diff: +${ciphertext.length - plaintext.length} bytes (auth tag)',
      );
      result.writeln(
        '   Ciphertext: ${_bytesToHex(ciphertext, maxLength: 40)}',
      );
      result.writeln();

      // 4. Decrypt message
      final decrypted = cipher.decrypt(ciphertext: ciphertext, nonce: nonce);
      final decryptedText = utf8.decode(decrypted);
      result.writeln('4. Message decrypted');
      result.writeln('   Decrypted: "$decryptedText"');
      result.writeln('   Match: ${decryptedText == messageText}');
      result.writeln();

      // 5. Demonstrate determinism
      final ciphertext2 = cipher.encrypt(plaintext: plaintext, nonce: nonce);
      final isSame = _bytesToHex(ciphertext) == _bytesToHex(ciphertext2);
      result.writeln('5. Determinism test (same key + nonce)');
      result.writeln('   Same ciphertext: $isSame');
      result.writeln();

      // 6. Different nonce = different ciphertext
      final nonce2 = _randomBytes(12);
      final ciphertext3 = cipher.encrypt(plaintext: plaintext, nonce: nonce2);
      final isDifferent = _bytesToHex(ciphertext) != _bytesToHex(ciphertext3);
      result.writeln('6. Different nonce test');
      result.writeln('   Different ciphertext: $isDifferent');

      setState(() => _cryptoResult = result.toString());
    } catch (e) {
      setState(() => _cryptoResult = 'Error: $e');
    } finally {
      cipher?.dispose();
      setState(() => _cryptoLoading = false);
    }
  }

  // ============================================
  // Groups Demo (SenderKey)
  // ============================================
  Future<void> _runGroupsDemo() async {
    setState(() {
      _groupsLoading = true;
      _groupsResult = null;
    });

    ProtocolAddress? aliceAddress;
    ProtocolAddress? bobAddress;
    SenderKeyDistributionMessage? distMessage;

    try {
      final result = StringBuffer();

      // 1. Create protocol addresses
      aliceAddress = ProtocolAddress('alice', 1);
      bobAddress = ProtocolAddress('bob', 1);
      result.writeln('1. Protocol addresses created');
      result.writeln('   Alice: ${aliceAddress.name}:${aliceAddress.deviceId}');
      result.writeln('   Bob: ${bobAddress.name}:${bobAddress.deviceId}');
      result.writeln();

      // 2. Generate distribution ID (UUID)
      final distributionId = GroupSession.uuidFromString(
        '01234567-89ab-cdef-0123-456789abcdef',
      );
      final uuidString = GroupSession.uuidToString(distributionId);
      result.writeln('2. Distribution ID (UUID)');
      result.writeln('   UUID: $uuidString');
      result.writeln('   Size: ${distributionId.length} bytes');
      result.writeln();

      // 3. Create stores and sessions
      final aliceStore = InMemorySenderKeyStore();
      final bobStore = InMemorySenderKeyStore();
      final aliceSession = GroupSession(
        aliceAddress,
        distributionId,
        aliceStore,
      );
      final bobSession = GroupSession(bobAddress, distributionId, bobStore);
      result.writeln('3. Group sessions created');
      result.writeln('   Alice store: InMemorySenderKeyStore');
      result.writeln('   Bob store: InMemorySenderKeyStore');
      result.writeln();

      // 4. Alice creates distribution message
      distMessage = await aliceSession.createDistributionMessage();
      result.writeln('4. Alice created distribution message');
      result.writeln(
        '   Distribution ID: ${GroupSession.uuidToString(distMessage.distributionId)}',
      );
      result.writeln('   Chain key size: ${distMessage.chainKey.length} bytes');
      result.writeln('   Iteration: ${distMessage.iteration}');
      result.writeln();

      // 5. Bob processes distribution message
      await bobSession.processDistributionMessage(aliceAddress, distMessage);
      result.writeln('5. Bob processed distribution message');
      result.writeln('   Bob store entries: ${bobStore.length}');
      result.writeln();

      // 6. Alice encrypts message
      const messageText = 'Hello, group!';
      final plaintext = Uint8List.fromList(utf8.encode(messageText));
      final ciphertext = await aliceSession.encrypt(plaintext);
      result.writeln('6. Alice encrypted message');
      result.writeln('   Message: "$messageText"');
      result.writeln('   Ciphertext size: ${ciphertext.length} bytes');
      result.writeln();

      // 7. Bob decrypts message
      final decrypted = await bobSession.decrypt(aliceAddress, ciphertext);
      final decryptedText = utf8.decode(decrypted);
      result.writeln('7. Bob decrypted message');
      result.writeln('   Decrypted: "$decryptedText"');
      result.writeln('   Match: ${decryptedText == messageText}');

      setState(() => _groupsResult = result.toString());
    } catch (e) {
      setState(() => _groupsResult = 'Error: $e');
    } finally {
      aliceAddress?.dispose();
      bobAddress?.dispose();
      distMessage?.dispose();
      setState(() => _groupsLoading = false);
    }
  }

  // ============================================
  // Fingerprint Demo
  // ============================================
  Future<void> _runFingerprintDemo() async {
    setState(() {
      _fingerprintLoading = true;
      _fingerprintResult = null;
    });

    IdentityKeyPair? aliceIdentity;
    IdentityKeyPair? bobIdentity;
    Fingerprint? aliceFingerprint;
    Fingerprint? bobFingerprint;

    try {
      final result = StringBuffer();

      // 1. Generate identity keys
      aliceIdentity = IdentityKeyPair.generate();
      bobIdentity = IdentityKeyPair.generate();
      result.writeln('1. Identity keys generated');
      result.writeln(
        '   Alice public key: ${_bytesToHex(aliceIdentity.publicKey.serialize(), maxLength: 24)}',
      );
      result.writeln(
        '   Bob public key: ${_bytesToHex(bobIdentity.publicKey.serialize(), maxLength: 24)}',
      );
      result.writeln();

      // 2. Create identifiers
      final aliceId = Uint8List.fromList(utf8.encode('alice-uuid-12345'));
      final bobId = Uint8List.fromList(utf8.encode('bob-uuid-67890'));
      result.writeln('2. User identifiers');
      result.writeln('   Alice: ${utf8.decode(aliceId)}');
      result.writeln('   Bob: ${utf8.decode(bobId)}');
      result.writeln();

      // 3. Create Alice's fingerprint (her view)
      aliceFingerprint = Fingerprint.create(
        localIdentifier: aliceId,
        localKey: aliceIdentity.publicKey,
        remoteIdentifier: bobId,
        remoteKey: bobIdentity.publicKey,
      );
      final aliceDisplay = aliceFingerprint.displayString;
      result.writeln('3. Alice\'s Safety Number');
      result.writeln('   ${_formatFingerprint(aliceDisplay)}');
      result.writeln('   Length: ${aliceDisplay.length} digits');
      result.writeln();

      // 4. Create Bob's fingerprint (his view - swapped)
      bobFingerprint = Fingerprint.create(
        localIdentifier: bobId,
        localKey: bobIdentity.publicKey,
        remoteIdentifier: aliceId,
        remoteKey: aliceIdentity.publicKey,
      );
      final bobDisplay = bobFingerprint.displayString;
      result.writeln('4. Bob\'s Safety Number');
      result.writeln('   ${_formatFingerprint(bobDisplay)}');
      result.writeln();

      // 5. Verify symmetry
      final isSymmetric = aliceDisplay == bobDisplay;
      result.writeln('5. Symmetry verification');
      result.writeln('   Alice and Bob see same number: $isSymmetric');
      result.writeln();

      // 6. Scannable encoding
      final scannable = aliceFingerprint.scannableEncoding;
      result.writeln('6. Scannable encoding (for QR codes)');
      result.writeln('   Size: ${scannable.length} bytes');
      result.writeln('   Hex: ${_bytesToHex(scannable, maxLength: 32)}');

      setState(() => _fingerprintResult = result.toString());
    } catch (e) {
      setState(() => _fingerprintResult = 'Error: $e');
    } finally {
      aliceIdentity?.dispose();
      bobIdentity?.dispose();
      aliceFingerprint?.dispose();
      bobFingerprint?.dispose();
      setState(() => _fingerprintLoading = false);
    }
  }

  // ============================================
  // UI
  // ============================================

  Widget _buildDemoCard({
    required String title,
    required String description,
    required VoidCallback onRun,
    required bool isLoading,
    String? result,
  }) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 8),
            Text(
              description,
              style: Theme.of(
                context,
              ).textTheme.bodyMedium?.copyWith(color: Colors.grey[600]),
            ),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: isLoading ? null : onRun,
              icon: isLoading
                  ? const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.play_arrow),
              label: Text(isLoading ? 'Running...' : 'Run Demo'),
            ),
            if (result != null) ...[
              const SizedBox(height: 16),
              const Divider(),
              const SizedBox(height: 8),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    'Result:',
                    style: Theme.of(context).textTheme.titleSmall,
                  ),
                  IconButton(
                    icon: const Icon(Icons.copy, size: 18),
                    tooltip: 'Copy to clipboard',
                    onPressed: () {
                      Clipboard.setData(ClipboardData(text: result));
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('Copied to clipboard'),
                          duration: Duration(seconds: 1),
                        ),
                      );
                    },
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.grey[300]!),
                ),
                child: SelectableText(
                  result,
                  style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildKeysTab() {
    return SingleChildScrollView(
      child: _buildDemoCard(
        title: 'Key Generation & Signatures',
        description:
            'Generate Curve25519 key pairs, sign messages with Ed25519, '
            'and verify signatures. Demonstrates PrivateKey, PublicKey, '
            'and IdentityKeyPair classes.',
        onRun: _runKeysDemo,
        isLoading: _keysLoading,
        result: _keysResult,
      ),
    );
  }

  Widget _buildCryptoTab() {
    return SingleChildScrollView(
      child: _buildDemoCard(
        title: 'AES-256-GCM-SIV Encryption',
        description:
            'Symmetric encryption using AES-256-GCM-SIV (nonce-misuse resistant). '
            'Demonstrates encryption, decryption, and deterministic behavior.',
        onRun: _runCryptoDemo,
        isLoading: _cryptoLoading,
        result: _cryptoResult,
      ),
    );
  }

  Widget _buildGroupsTab() {
    return SingleChildScrollView(
      child: _buildDemoCard(
        title: 'Group Messaging (SenderKey)',
        description:
            'Efficient group encryption using Sender Keys. Alice creates a '
            'distribution message, Bob processes it, then they can exchange '
            'encrypted group messages.',
        onRun: _runGroupsDemo,
        isLoading: _groupsLoading,
        result: _groupsResult,
      ),
    );
  }

  Widget _buildFingerprintTab() {
    return SingleChildScrollView(
      child: _buildDemoCard(
        title: 'Safety Number Verification',
        description:
            'Create fingerprints for identity verification. Both parties '
            'see the same 60-digit Safety Number, which can be compared '
            'to verify encryption keys.',
        onRun: _runFingerprintDemo,
        isLoading: _fingerprintLoading,
        result: _fingerprintResult,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'libsignal Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('libsignal Example'),
          centerTitle: true,
          bottom: TabBar(
            controller: _tabController,
            tabs: const [
              Tab(icon: Icon(Icons.key), text: 'Keys'),
              Tab(icon: Icon(Icons.lock), text: 'Crypto'),
              Tab(icon: Icon(Icons.group), text: 'Groups'),
              Tab(icon: Icon(Icons.fingerprint), text: 'Verify'),
            ],
          ),
        ),
        body: _isInitialized
            ? TabBarView(
                controller: _tabController,
                children: [
                  _buildKeysTab(),
                  _buildCryptoTab(),
                  _buildGroupsTab(),
                  _buildFingerprintTab(),
                ],
              )
            : const Center(child: CircularProgressIndicator()),
      ),
    );
  }
}
1
likes
140
points
187
downloads

Publisher

unverified uploader

Weekly Downloads

Dart FFI bindings for libsignal — Signal Protocol implementation for end-to-end encryption, sealed sender, group messaging, and secure cryptographic operations.

Repository (GitHub)
View/report issues
Contributing

Topics

#encryption #signal-protocol #e2e-encryption #cryptography #messaging

Documentation

API reference

License

AGPL-3.0 (license)

Dependencies

code_assets, crypto, ffi, hooks

More

Packages that depend on libsignal