6128953e28
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>
205 lines
6.8 KiB
Dart
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|