Neda/call/lib/ui/screens/settings_screen.dart

276 lines
11 KiB
Dart

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<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final _ctrl = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _loading = false;
bool _obscure = true;
String? _fingerprint;
String? _errorMsg;
@override
void initState() {
super.initState();
_loadFingerprint();
}
Future<void> _loadFingerprint() async {
final fp = await widget.session.getSavedFingerprint();
if (mounted) setState(() => _fingerprint = fp);
}
Future<void> _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<void> _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')),
],
),
);
}