import 'package:flutter/material.dart'; import '../../services/call_session.dart'; /// Settings / key-management screen. /// /// Allows the user to enter a shared passphrase and derive the AES-256 key. /// The passphrase is never stored — only a non-reversible fingerprint is kept /// via [CallSession.getSavedFingerprint]. class SettingsScreen extends StatefulWidget { final CallSession session; const SettingsScreen({super.key, required this.session}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { final _ctrl = TextEditingController(); final _formKey = GlobalKey(); bool _loading = false; bool _obscure = true; String? _fingerprint; String? _errorMsg; @override void initState() { super.initState(); _loadFingerprint(); } Future _loadFingerprint() async { final fp = await widget.session.getSavedFingerprint(); if (mounted) setState(() => _fingerprint = fp); } Future _deriveKey() async { if (!(_formKey.currentState?.validate() ?? false)) return; setState(() { _loading = true; _errorMsg = null; }); // Capture messenger before async gap to avoid BuildContext-across-await lint. final messenger = ScaffoldMessenger.of(context); final ok = await widget.session.loadPassphrase(_ctrl.text.trim()); if (!mounted) return; if (ok) { await _loadFingerprint(); messenger.showSnackBar( const SnackBar( content: Text('Key derived successfully'), backgroundColor: Color(0xFF00C853), ), ); } else { setState(() => _errorMsg = widget.session.lastError); } setState(() => _loading = false); } Future _clearKey() async { await widget.session.clearKey(); _ctrl.clear(); if (mounted) { setState(() => _fingerprint = null); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Key cleared')), ); } } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0D0D1A), appBar: AppBar( backgroundColor: const Color(0xFF0D0D1A), title: const Text('Encryption Key', style: TextStyle(color: Colors.white, letterSpacing: 1.5)), iconTheme: const IconThemeData(color: Colors.white), ), body: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Info card ───────────────────────────────────────── Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1A1A2E), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blueAccent.withValues(alpha: 0.3)), ), child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.lock_outline, color: Colors.blueAccent, size: 18), SizedBox(width: 8), Text('Pre-shared key setup', style: TextStyle( color: Colors.blueAccent, fontWeight: FontWeight.bold)), ]), SizedBox(height: 8), Text( 'Both devices must use the SAME passphrase before ' 'starting a call. The passphrase is converted to a ' '256-bit AES key using PBKDF2-HMAC-SHA256 ' '(100 000 iterations).\n\n' 'Exchange the passphrase over a separate secure channel ' '(in person, encrypted message, etc.).', style: TextStyle(color: Colors.white60, height: 1.5, fontSize: 13), ), ], ), ), const SizedBox(height: 28), // ── Passphrase field ────────────────────────────────── TextFormField( controller: _ctrl, obscureText: _obscure, style: const TextStyle(color: Colors.white, letterSpacing: 1.2), decoration: InputDecoration( labelText: 'Shared passphrase', labelStyle: const TextStyle(color: Colors.white54), filled: true, fillColor: const Color(0xFF1A1A2E), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), suffixIcon: IconButton( icon: Icon( _obscure ? Icons.visibility_off : Icons.visibility, color: Colors.white38), onPressed: () => setState(() => _obscure = !_obscure), ), ), validator: (v) { if (v == null || v.trim().isEmpty) return 'Enter a passphrase'; if (v.trim().length < 8) return 'Minimum 8 characters'; return null; }, ), if (_errorMsg != null) ...[ const SizedBox(height: 10), Text(_errorMsg!, style: const TextStyle(color: Colors.redAccent, fontSize: 13)), ], const SizedBox(height: 20), // ── Derive button ───────────────────────────────────── ElevatedButton.icon( onPressed: _loading ? null : _deriveKey, icon: _loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.vpn_key_rounded), label: const Text('DERIVE KEY', style: TextStyle(letterSpacing: 1.5)), style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ), const SizedBox(height: 32), // ── Key fingerprint ─────────────────────────────────── if (_fingerprint != null) ...[ const Text('ACTIVE KEY FINGERPRINT', style: TextStyle( color: Colors.white38, fontSize: 11, letterSpacing: 1.5)), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: const Color(0xFF1A1A2E), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF00C853).withValues(alpha: 0.4)), ), child: Row( children: [ const Icon(Icons.fingerprint, color: Color(0xFF00C853), size: 20), const SizedBox(width: 12), Text( _fingerprint!, style: const TextStyle( color: Color(0xFF00C853), fontFamily: 'monospace', fontSize: 18, letterSpacing: 4, ), ), ], ), ), const SizedBox(height: 8), const Text( 'Show this fingerprint to your peer — it must match on both devices.', style: TextStyle(color: Colors.white38, fontSize: 12, height: 1.4), ), const SizedBox(height: 24), OutlinedButton.icon( onPressed: _clearKey, icon: const Icon(Icons.delete_outline, color: Colors.redAccent), label: const Text('CLEAR KEY', style: TextStyle(color: Colors.redAccent, letterSpacing: 1.5)), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.redAccent), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ), ], const SizedBox(height: 40), // ── Technical info ──────────────────────────────────── const Divider(color: Colors.white12), const SizedBox(height: 16), const Text('SYSTEM PARAMETERS', style: TextStyle( color: Colors.white24, fontSize: 11, letterSpacing: 1.5)), const SizedBox(height: 12), _paramRow('Cipher', 'AES-256-CTR'), _paramRow('KDF', 'PBKDF2-HMAC-SHA256 (100k iter)'), _paramRow('FEC', 'Reed-Solomon RS(15,11) GF(16)'), _paramRow('Modulation', '4-FSK 600 baud 1200 bps'), _paramRow('Voice', 'LPC-10 ~360 bps 200 ms frames'), _paramRow('Channel', 'Acoustic FSK over SIM call'), ], ), ), ), ); } static Widget _paramRow(String label, String value) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(color: Colors.white38, fontSize: 12)), Text(value, style: const TextStyle( color: Colors.white60, fontSize: 12, fontFamily: 'monospace')), ], ), ); }