import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:matrix/encryption.dart'; import '../matrix/client.dart'; import '../matrix/encryption.dart'; import '../state.dart'; import '../theme.dart'; import '../widgets/verification_sheet.dart'; /// Encryption for the account and this session: set it up, verify this /// session (with the recovery key or another session), or reset it. class EncryptionScreen extends ConsumerStatefulWidget { const EncryptionScreen({super.key}); @override ConsumerState createState() => _EncryptionScreenState(); } class _EncryptionScreenState extends ConsumerState { bool _busy = false; String? _error; Future _run(Future Function() action) async { setState(() { _busy = true; _error = null; }); try { await action(); } catch (e) { if (mounted) setState(() => _error = describeError(e)); } finally { if (mounted) setState(() => _busy = false); } } Future _setUp({required bool reset}) async { final encryption = ref.read(clientProvider).encryption!; if (reset) { final ok = await showDialog( context: context, builder: (dialog) => AlertDialog( title: const Text('Reset encryption?'), content: const Text( 'Only do this if you have lost your recovery key and have no ' 'other verified session.\n\nYou get a new recovery key. Messages ' 'only the old keys could read stay unreadable, and the people ' "you've verified will need to verify you again.", ), actions: [ TextButton( onPressed: () => Navigator.pop(dialog, false), child: const Text('Cancel'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: context.warp.danger, ), onPressed: () => Navigator.pop(dialog, true), child: const Text('Reset'), ), ], ), ); if (ok != true) return; } String? recoveryKey; await _run(() async => recoveryKey = await setUpEncryption(encryption)); final key = recoveryKey; if (key != null && mounted) { await Navigator.of(context).push( MaterialPageRoute( fullscreenDialog: true, builder: (_) => _RecoveryKeyScreen(recoveryKey: key), ), ); if (mounted) setState(() {}); } } Future _verifyWithKey() async { final controller = TextEditingController(); final input = await showDialog( context: context, builder: (dialog) => AlertDialog( title: const Text('Enter your recovery key'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'The key you saved when you set up encryption. It looks ' 'like EsTc 1234 …', ), const SizedBox(height: 16), TextField( controller: controller, autofocus: true, autocorrect: false, enableSuggestions: false, minLines: 2, maxLines: 3, style: const TextStyle(fontFamily: fontMono), decoration: const InputDecoration(labelText: 'Recovery key'), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(dialog), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(dialog, controller.text), child: const Text('Verify'), ), ], ), ); if (input == null || input.trim().isEmpty) return; final encryption = ref.read(clientProvider).encryption!; await _run(() async { try { await verifyWithRecoveryKey(encryption, input); } on InvalidPassphraseException { throw Exception("That recovery key isn't right."); } }); if (mounted && _error == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'This session is verified. Older messages are being restored.', ), ), ); } } Future _verifyWithSession() async { final client = ref.read(clientProvider); await _run(() async { final keys = client.userDeviceKeys[client.userID!]; if (keys == null) throw Exception('Your sessions are still loading.'); final request = await keys.startVerification(); if (mounted) await showVerificationSheet(context, request); }); } @override Widget build(BuildContext context) { ref.watch(syncProvider); final client = ref.watch(clientProvider); final encryption = client.encryption; final c = context.warp; final text = Theme.of(context).textTheme; final List body; if (encryption == null) { body = [ const _Para('End-to-end encryption is not available in this build.'), ]; } else if (!encryption.crossSigning.enabled) { body = [ const _Para( 'Encryption keeps your messages readable only by the people in ' 'the conversation. Set it up once and you get a recovery key: with ' 'it, any new session you sign in to can verify itself and read ' 'your older messages.', ), const SizedBox(height: 8), const _Para( 'You will be asked for your password. Keep the recovery key ' 'somewhere safe, like a password manager.', ), const SizedBox(height: 24), FilledButton( onPressed: _busy ? null : () => _setUp(reset: false), child: const Text('Set up encryption'), ), ]; } else if (client.isUnknownSession) { body = [ const _Para( 'Prove this session is you, so it can read your encrypted messages ' 'and the people you talk to trust it.', ), const SizedBox(height: 24), FilledButton.icon( onPressed: _busy ? null : _verifyWithSession, icon: const Icon(Icons.devices_outlined), label: const Text('Use another session'), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: _busy ? null : _verifyWithKey, icon: const Icon(Icons.key_outlined), label: const Text('Use recovery key'), ), const SizedBox(height: 32), TextButton( onPressed: _busy ? null : () => _setUp(reset: true), style: TextButton.styleFrom(foregroundColor: c.danger), child: const Text('Lost your recovery key? Reset encryption'), ), ]; } else { body = [ _Status( ok: true, title: 'This session is verified', detail: 'It can read your encrypted messages.', ), _Status( ok: encryption.keyManager.enabled, title: encryption.keyManager.enabled ? 'Key backup is on' : 'Key backup is off', detail: encryption.keyManager.enabled ? 'Your message keys are backed up, encrypted with your ' 'recovery key.' : 'New sessions may not be able to read older messages.', ), const SizedBox(height: 32), OutlinedButton( onPressed: _busy ? null : () => _setUp(reset: true), style: OutlinedButton.styleFrom(foregroundColor: c.danger), child: const Text('Reset encryption'), ), ]; } return Scaffold( appBar: AppBar(title: const Text('Encryption')), body: ListView( padding: const EdgeInsets.all(24), children: [ if (_busy) ...[ const LinearProgressIndicator(minHeight: 2), const SizedBox(height: 16), ], if (_error != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: c.dangerSoft, borderRadius: BorderRadius.circular(12), ), child: Text( _error!, style: text.bodyMedium?.copyWith(color: c.danger), ), ), const SizedBox(height: 16), ], ...body, ], ), ); } } class _Para extends StatelessWidget { const _Para(this.text); final String text; @override Widget build(BuildContext context) => Text(text, style: Theme.of(context).textTheme.bodyLarge); } class _Status extends StatelessWidget { const _Status({required this.ok, required this.title, required this.detail}); final bool ok; final String title; final String detail; @override Widget build(BuildContext context) { final c = context.warp; return ListTile( contentPadding: EdgeInsets.zero, leading: Icon( ok ? Icons.check_circle_outline : Icons.error_outline, color: ok ? c.tealText : c.warning, ), title: Text(title), subtitle: Text(detail), ); } } /// The new recovery key, shown once. The user has to say they saved it. class _RecoveryKeyScreen extends StatefulWidget { const _RecoveryKeyScreen({required this.recoveryKey}); final String recoveryKey; @override State<_RecoveryKeyScreen> createState() => _RecoveryKeyScreenState(); } class _RecoveryKeyScreenState extends State<_RecoveryKeyScreen> { bool _saved = false; @override Widget build(BuildContext context) { final c = context.warp; final text = Theme.of(context).textTheme; return PopScope( canPop: _saved, child: Scaffold( appBar: AppBar( title: const Text('Your recovery key'), automaticallyImplyLeading: false, ), body: ListView( padding: const EdgeInsets.all(24), children: [ Text( 'Save this key somewhere safe, like a password manager. It is ' 'the only way to read your encrypted messages on a new session ' "if you aren't signed in anywhere else. It won't be shown again.", style: text.bodyLarge, ), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: c.surfaceSunken, border: Border.all(color: c.line), borderRadius: BorderRadius.circular(12), ), child: SelectableText( widget.recoveryKey, textAlign: TextAlign.center, style: text.titleMedium?.copyWith( fontFamily: fontMono, letterSpacing: 1, height: 1.6, ), ), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: () { Clipboard.setData(ClipboardData(text: widget.recoveryKey)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Recovery key copied')), ); }, icon: const Icon(Icons.copy), label: const Text('Copy'), ), const SizedBox(height: 24), CheckboxListTile( contentPadding: EdgeInsets.zero, value: _saved, onChanged: (v) => setState(() => _saved = v ?? false), title: const Text("I've saved my recovery key"), controlAffinity: ListTileControlAffinity.leading, ), const SizedBox(height: 12), FilledButton( onPressed: _saved ? () => Navigator.pop(context) : null, child: const Text('Done'), ), ], ), ), ); } }