// 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 showKeyRestoreDialog(BuildContext context, Client client) async { final result = await showDialog( 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 _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), ], ); } }