Files
pyramid/lib/widgets/settings/encryption/fingerprint_passphrase.dart
T
Bernd Steckmeister 6128953e28 refactor: settings_encryption.dart (1470 Zeilen) in 4 thematische part-Dateien unter encryption/ geteilt (M2, zeichenidentisch verifiziert)
Vorbereitung fuer Schnitt-Doc Schritt 3 (verification-Modul), ohne den
Verschluesselungs-heiligen Code anzufassen: megolm_crypto (Export/Import-
Krypto-Helfer), encryption_section (Enums + _EncryptionSection),
recovery_key_dialogs (_ChangeRecoveryKeyDialog/_ShowKeyBody/_ConfirmKeyBody),
fingerprint_passphrase (_FingerprintGroup/_PassphraseDialog).
Rekonstruktion zeichenidentisch (1468 Code-Zeilen, ordinaler Vergleich),
analyze sauber, 29 Tests gruen, App-Start ok (auth_log 17:43:14 loggedIn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:43:53 +02:00

205 lines
6.8 KiB
Dart

part of '../../settings_modal.dart';
class _FingerprintGroup extends ConsumerWidget {
final PyramidTheme pt;
const _FingerprintGroup({required this.pt});
static String _fmt(String key) {
final clean = key.replaceAll(RegExp(r'\s'), '');
final groups = <String>[];
for (var i = 0; i < clean.length; i += 4) {
groups.add(clean.substring(i, (i + 4).clamp(0, clean.length)));
}
return groups.join(' ');
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final clientAsync = ref.watch(matrixClientProvider);
final client = clientAsync.valueOrNull;
if (client == null) return const SizedBox.shrink();
final fingerprint = _fmt(client.fingerprintKey);
final deviceId = client.deviceID ?? '';
return _SettingsGroup(title: 'Dieses Gerät', pt: pt, children: [
_SettingsRow(
title: 'Geräte-ID',
desc: deviceId,
pt: pt,
control: IconButton(
icon: Icon(Icons.copy_rounded, size: 14, color: pt.fgDim),
tooltip: 'Kopieren',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(maxWidth: 28, maxHeight: 28),
onPressed: () => Clipboard.setData(ClipboardData(text: deviceId)),
),
),
_Divider(pt: pt),
Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Ed25519-Fingerabdruck',
style: TextStyle(color: pt.fg, fontSize: 13, fontWeight: FontWeight.w500)),
const Spacer(),
IconButton(
icon: Icon(Icons.copy_rounded, size: 14, color: pt.fgDim),
tooltip: 'Fingerabdruck kopieren',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(maxWidth: 28, maxHeight: 28),
onPressed: () => Clipboard.setData(ClipboardData(text: client.fingerprintKey)),
),
],
),
const SizedBox(height: 4),
SelectableText(
fingerprint,
style: TextStyle(
color: pt.fgMuted, fontSize: 11,
fontFamily: 'monospace', letterSpacing: 0.5, height: 1.6,
),
),
],
),
),
]);
}
}
// Passphrase dialog (for key export/import)
class _PassphraseDialog extends StatefulWidget {
final PyramidTheme pt;
final bool forExport;
const _PassphraseDialog({required this.pt, required this.forExport});
@override
State<_PassphraseDialog> createState() => _PassphraseDialogState();
}
class _PassphraseDialogState extends State<_PassphraseDialog> {
final _ctrl1 = TextEditingController();
final _ctrl2 = TextEditingController();
bool _obscure = true;
String? _error;
@override
void dispose() {
_ctrl1.dispose();
_ctrl2.dispose();
super.dispose();
}
void _submit() {
final pass = _ctrl1.text.trim();
if (pass.isEmpty) {
setState(() => _error = 'Bitte ein Passwort eingeben.');
return;
}
if (widget.forExport && pass != _ctrl2.text.trim()) {
setState(() => _error = 'Passwörter stimmen nicht überein.');
return;
}
Navigator.pop(context, pass);
}
InputDecoration _inputDeco(String label, PyramidTheme pt) => InputDecoration(
labelText: label,
labelStyle: TextStyle(color: pt.fgMuted, fontSize: 13),
errorText: _error,
filled: true,
fillColor: pt.bg3,
prefixIcon: Icon(Icons.lock_outline_rounded, color: pt.fgDim, size: 18),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.border)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.border)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.accent)),
isDense: true,
errorMaxLines: 2,
suffixIcon: IconButton(
icon: Icon(
_obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined,
size: 18,
color: pt.fgDim,
),
onPressed: () => setState(() => _obscure = !_obscure),
),
);
@override
Widget build(BuildContext context) {
final pt = widget.pt;
return AlertDialog(
backgroundColor: pt.bg2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(pt.rXl),
side: BorderSide(color: pt.border),
),
title: Text(
widget.forExport ? 'Schlüssel exportieren' : 'Schlüssel importieren',
style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700),
),
content: SizedBox(
width: 340,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.forExport
? 'Wähle ein Passwort zum Verschlüsseln der Schlüsseldatei.'
: 'Gib das Passwort ein, mit dem die Datei verschlüsselt wurde.',
style: TextStyle(color: pt.fgMuted, fontSize: 13, height: 1.5),
),
const SizedBox(height: 16),
TextField(
controller: _ctrl1,
obscureText: _obscure,
autofocus: true,
style: TextStyle(color: pt.fg, fontSize: 13),
decoration: _inputDeco('Passwort', pt).copyWith(
// Only show error on confirm field if export, else here
errorText: widget.forExport ? null : _error,
),
cursorColor: pt.accent,
onSubmitted: widget.forExport ? null : (_) => _submit(),
),
if (widget.forExport) ...[
const SizedBox(height: 12),
TextField(
controller: _ctrl2,
obscureText: _obscure,
style: TextStyle(color: pt.fg, fontSize: 13),
decoration: _inputDeco('Passwort bestätigen', pt),
cursorColor: pt.accent,
onSubmitted: (_) => _submit(),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: pt.accent,
foregroundColor: pt.accentFg,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _submit,
child: Text(widget.forExport ? 'Exportieren' : 'Importieren'),
),
],
);
}
}