Files
m8chat-app2/lib/features/profile/presentation/key_restore_dialog.dart
help4bis 3568b17f0f feat: brand retheme to m8chat.au blues, fix black logo, v1.6.0+8
- m8logo.svg: inline fills (flutter_svg ignores <style> blocks; logo rendered black)
- theme.dart v2.0.0: purple -> site palette (#1265ED / #3B8BFF / navy darks)
- app_config.dart: appVersion 1.3.0 -> 1.6.0 (profile screen showed stale version)
- manifest.json: theme/background colours to match
- Remove em-dashes from user-visible UI strings (global style rule)
- chat_screen.dart: fix use_build_context_synchronously in _leaveRoom
- Deployed to app2.m8chat.au after full functionality audit (all green)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 05:16:11 +10:00

214 lines
6.8 KiB
Dart

// Version: 1.0.1 | Created: 2026-04-11 | Updated: 2026-07-03
// Dialog for entering a recovery key to restore encrypted message keys
// from the server-side key backup (SSSS / Secure Secret Storage).
import 'package:flutter/material.dart';
import 'package:matrix/encryption/ssss.dart';
import 'package:matrix/matrix.dart';
/// Shows a dialog that asks the user for their recovery key, unlocks SSSS,
/// and restores all Megolm session keys from the server backup.
///
/// Returns `true` if keys were restored successfully, `false` otherwise.
Future<bool> showKeyRestoreDialog(BuildContext context, Client client) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) => _KeyRestoreDialog(client: client),
);
return result ?? false;
}
class _KeyRestoreDialog extends StatefulWidget {
const _KeyRestoreDialog({required this.client});
final Client client;
@override
State<_KeyRestoreDialog> createState() => _KeyRestoreDialogState();
}
enum _RestorePhase { input, unlocking, downloading, done, error }
class _KeyRestoreDialogState extends State<_KeyRestoreDialog> {
final _controller = TextEditingController();
_RestorePhase _phase = _RestorePhase.input;
String? _error;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _restore() async {
final key = _controller.text.trim();
if (key.isEmpty) return;
final enc = widget.client.encryption;
if (enc == null) {
setState(() {
_phase = _RestorePhase.error;
_error = 'Encryption is not available.';
});
return;
}
// Phase 1: Unlock SSSS with the recovery key
setState(() => _phase = _RestorePhase.unlocking);
try {
final ssssKey = enc.ssss.open();
await ssssKey.unlock(keyOrPassphrase: key);
debugPrint('[KeyRestore] SSSS unlocked successfully');
} on InvalidPassphraseException {
setState(() {
_phase = _RestorePhase.error;
_error = 'Invalid recovery key. Please check and try again.';
});
return;
} on Exception catch (e) {
setState(() {
_phase = _RestorePhase.error;
_error = 'Could not unlock: ${e.toString().split('\n').first}';
});
return;
}
// Phase 2: Download and decrypt all keys from backup
setState(() => _phase = _RestorePhase.downloading);
try {
await enc.keyManager.loadAllKeys();
debugPrint('[KeyRestore] All keys loaded from backup');
setState(() => _phase = _RestorePhase.done);
} on MatrixException catch (e) {
if (e.errcode == 'M_NOT_FOUND') {
setState(() {
_phase = _RestorePhase.error;
_error = 'No key backup found on the server for this account.';
});
} else {
setState(() {
_phase = _RestorePhase.error;
_error = 'Server error: ${e.errorMessage}';
});
}
} on Exception catch (e) {
setState(() {
_phase = _RestorePhase.error;
_error = 'Failed to download keys: ${e.toString().split('\n').first}';
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(switch (_phase) {
_RestorePhase.input => 'Restore Message Keys',
_RestorePhase.unlocking => 'Unlocking...',
_RestorePhase.downloading => 'Downloading Keys...',
_RestorePhase.done => 'Keys Restored',
_RestorePhase.error => 'Restore Failed',
}),
content: switch (_phase) {
_RestorePhase.input => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Enter your recovery key to decrypt old messages. '
'This is the key you were given when you set up '
'encryption in Element.',
),
const SizedBox(height: 16),
TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'EsTc oRgW rqHN...',
labelText: 'Recovery key',
border: OutlineInputBorder(),
),
maxLines: 3,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _restore(),
),
],
),
_RestorePhase.unlocking => const _ProgressContent(
message: 'Verifying recovery key...',
),
_RestorePhase.downloading => const _ProgressContent(
message: 'Downloading and decrypting message keys...\n'
'This may take a moment.',
),
_RestorePhase.done => const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, color: Colors.green, size: 48),
SizedBox(height: 16),
Text(
'Message keys have been restored. '
'Go back to your rooms. Previously encrypted '
'messages should now be readable.',
),
],
),
_RestorePhase.error => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 48),
const SizedBox(height: 16),
Text(_error ?? 'An unknown error occurred.'),
],
),
},
actions: switch (_phase) {
_RestorePhase.input => [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _restore,
child: const Text('Restore'),
),
],
_RestorePhase.unlocking || _RestorePhase.downloading => [],
_RestorePhase.done => [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Done'),
),
],
_RestorePhase.error => [
TextButton(
onPressed: () =>
setState(() => _phase = _RestorePhase.input),
child: const Text('Try again'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Close'),
),
],
},
);
}
}
class _ProgressContent extends StatelessWidget {
const _ProgressContent({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(message, textAlign: TextAlign.center),
],
);
}
}