Chepuhagram/lib/presentation/screens/security_settings_screen.dart

317 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:chepuhagram/domain/services/api_service.dart';
import 'package:chepuhagram/domain/services/crypto_service.dart';
class SecuritySettingsScreen extends StatefulWidget {
const SecuritySettingsScreen({super.key});
@override
State<SecuritySettingsScreen> createState() => _SecuritySettingsScreenState();
}
class _SecuritySettingsScreenState extends State<SecuritySettingsScreen> {
final _passwordFormKey = GlobalKey<FormState>();
final _encryptionFormKey = GlobalKey<FormState>();
final _totpFormKey = GlobalKey<FormState>();
final _currentPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _currentEncryptPasswordController = TextEditingController();
final _newEncryptPasswordController = TextEditingController();
final _confirmEncryptPasswordController = TextEditingController();
final LocalAuthentication _localAuth = LocalAuthentication();
bool _isBiometricAvailable = false;
bool _isSavingPassword = false;
bool _isSavingEncryption = false;
bool _isSavingTotp = false;
@override
void initState() {
super.initState();
_checkBiometricSupport();
}
@override
void dispose() {
_currentPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
_currentEncryptPasswordController.dispose();
_newEncryptPasswordController.dispose();
_confirmEncryptPasswordController.dispose();
super.dispose();
}
Future<void> _checkBiometricSupport() async {
try {
final canCheckBiometrics = await _localAuth.canCheckBiometrics;
final isSupported = await _localAuth.isDeviceSupported();
final availableBiometrics = await _localAuth.getAvailableBiometrics();
if (!mounted) return;
setState(() {
_isBiometricAvailable = canCheckBiometrics && isSupported && availableBiometrics.isNotEmpty;
});
} catch (_) {
if (!mounted) return;
setState(() {
_isBiometricAvailable = false;
});
}
}
Future<bool> _authenticateBiometric() async {
try {
return await _localAuth.authenticate(
localizedReason: 'Подтвердите личность для смены пароля шифрования',
options: const AuthenticationOptions(
biometricOnly: false,
stickyAuth: false,
useErrorDialogs: true,
sensitiveTransaction: true,
),
);
} catch (error) {
debugPrint('Biometric authentication error: $error');
return false;
}
}
Future<void> _savePassword() async {
if (!_passwordFormKey.currentState!.validate()) return;
setState(() => _isSavingPassword = true);
try {
final api = ApiService();
final success = await api.changePassword(
_currentPasswordController.text.trim(),
_newPasswordController.text.trim(),
);
if (!success) {
throw Exception('Не удалось изменить пароль');
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Пароль успешно изменён')),
);
_currentPasswordController.clear();
_newPasswordController.clear();
_confirmPasswordController.clear();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))),
);
} finally {
if (!mounted) return;
setState(() => _isSavingPassword = false);
}
}
Future<void> _saveEncryptionPassword() async {
await _checkBiometricSupport();
if (!_encryptionFormKey.currentState!.validate()) return;
setState(() => _isSavingEncryption = true);
try {
final newPassword = _newEncryptPasswordController.text.trim();
final currentPassword = _currentEncryptPasswordController.text.trim();
final cryptoService = CryptoService();
String privateKeyBase64;
if (currentPassword.isEmpty) {
if (!_isBiometricAvailable) {
throw Exception('Биометрия не настроена. Введите текущий пароль.');
}
final authenticated = await _authenticateBiometric();
if (!authenticated) {
throw Exception('Биометрическая аутентификация не пройдена.');
}
final localPrivateKey = await cryptoService.getPrivateKey();
if (localPrivateKey == null || localPrivateKey.isEmpty) {
throw Exception('Локальный приватный ключ не найден.');
}
privateKeyBase64 = localPrivateKey;
} else {
final api = ApiService();
final userData = await api.getMe();
final encryptedPrivateKey = userData['encrypted_private_key']?.toString();
if (encryptedPrivateKey == null || encryptedPrivateKey.isEmpty) {
throw Exception('Зашифрованный ключ не найден на сервере.');
}
privateKeyBase64 = await cryptoService.decryptPrivateKey(
encryptedPrivateKey,
currentPassword,
);
await cryptoService.savePrivateKey(privateKeyBase64);
}
final updatedEncryptedPrivateKey = await cryptoService.encryptPrivateKeyWithPassword(
privateKeyBase64,
newPassword,
);
final success = await ApiService().updateEncryptedPrivateKey(updatedEncryptedPrivateKey);
if (!success) {
throw Exception('Не удалось обновить пароль шифрования на сервере.');
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Пароль шифрования успешно обновлён')),
);
_currentEncryptPasswordController.clear();
_newEncryptPasswordController.clear();
_confirmEncryptPasswordController.clear();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))),
);
} finally {
if (!mounted) return;
setState(() => _isSavingEncryption = false);
}
}
Future<void> _setupTotp() async {
setState(() => _isSavingTotp = true);
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
setState(() => _isSavingTotp = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TOTP пока не подключён на сервере')),
);
}
String? _currentEncryptionPasswordValidator(String? value) {
if (value == null || value.isEmpty) {
if (!_isBiometricAvailable) {
return 'Введите текущий пароль';
}
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Безопасность')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('Смена пароля аккаунта', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Form(
key: _passwordFormKey,
child: Column(
children: [
TextFormField(
controller: _currentPasswordController,
decoration: const InputDecoration(labelText: 'Текущий пароль'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) return 'Введите текущий пароль';
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _newPasswordController,
decoration: const InputDecoration(labelText: 'Новый пароль'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) return 'Введите новый пароль';
if (value.length < 6) return 'Пароль слишком короткий';
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _confirmPasswordController,
decoration: const InputDecoration(labelText: 'Повторите пароль'),
obscureText: true,
validator: (value) {
if (value != _newPasswordController.text) return 'Пароли не совпадают';
return null;
},
),
const SizedBox(height: 14),
ElevatedButton(
onPressed: _isSavingPassword ? null : _savePassword,
child: _isSavingPassword ? const CircularProgressIndicator(color: Colors.white) : const Text('Сохранить пароль'),
),
],
),
),
const SizedBox(height: 24),
const Text('Пароль шифрования сообщений', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Form(
key: _encryptionFormKey,
child: Column(
children: [
TextFormField(
controller: _currentEncryptPasswordController,
decoration: InputDecoration(
labelText: 'Текущий пароль шифрования',
helperText: _isBiometricAvailable
? 'Оставьте поле пустым и подтвердите биометрией'
: 'Требуется текущий пароль',
),
obscureText: true,
validator: _currentEncryptionPasswordValidator,
),
const SizedBox(height: 12),
TextFormField(
controller: _newEncryptPasswordController,
decoration: const InputDecoration(labelText: 'Новый пароль шифрования'),
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) return 'Пароль слишком короткий';
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _confirmEncryptPasswordController,
decoration: const InputDecoration(labelText: 'Повторите новый пароль'),
obscureText: true,
validator: (value) {
if (value != _newEncryptPasswordController.text) return 'Пароли не совпадают';
return null;
},
),
const SizedBox(height: 14),
ElevatedButton(
onPressed: _isSavingEncryption ? null : _saveEncryptionPassword,
child: _isSavingEncryption ? const CircularProgressIndicator(color: Colors.white) : const Text('Сохранить пароль шифрования'),
),
],
),
),
const SizedBox(height: 24),
const Text('TOTP', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
const Text('Настройка одноразового кода (TOTP) пока не подключена на сервере.'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _isSavingTotp ? null : _setupTotp,
child: _isSavingTotp ? const CircularProgressIndicator(color: Colors.white) : const Text('Установить TOTP код'),
),
],
),
);
}
}