Chepuhagram/lib/presentation/screens/qr_scan_screen.dart

264 lines
9.1 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 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:local_auth/local_auth.dart';
import '../../domain/services/crypto_service.dart';
import '../../domain/services/api_service.dart';
class QrScanScreen extends StatefulWidget {
const QrScanScreen({super.key});
@override
State<QrScanScreen> createState() => _QrScanScreenState();
}
class _QrScanScreenState extends State<QrScanScreen> {
final _mobileScannerController = MobileScannerController();
final _localAuth = LocalAuthentication();
final _cryptoService = CryptoService();
bool _isProcessing = false;
String? _statusMessage;
@override
void dispose() {
_mobileScannerController.dispose();
super.dispose();
}
Future<void> _handleQrDetected(BarcodeCapture capture) async {
if (_isProcessing) return;
final barcode = capture.barcodes.firstOrNull;
if (barcode == null || barcode.rawValue == null) return;
final scannedValue = barcode.rawValue!;
print('Сканирован QR: $scannedValue');
try {
final uri = Uri.parse(scannedValue);
if (uri.scheme != 'chepuhagram' || !scannedValue.contains('qr_login')) {
setState(() {
_statusMessage = 'Неверный формат QR-кода Чепухаграм';
});
return;
}
final roomId = uri.queryParameters['room_id'];
final tempPublicKeyBase64 = uri.queryParameters['public_key']?.replaceAll(' ', '+');
final connectingDeviceName = uri.queryParameters['device_name'];
if (roomId == null || tempPublicKeyBase64 == null) {
setState(() {
_statusMessage = 'Отсутствуют параметры сопряжения';
});
return;
}
// Нашли валидный QR, останавливаем сканер и начинаем обработку
setState(() {
_isProcessing = true;
_statusMessage = 'Обработка запроса сопряжения...';
});
_mobileScannerController.stop();
// Запрашиваем подтверждение
final confirmed = await _showConfirmDialog(context);
if (!confirmed) {
_resetScanner();
return;
}
// Биометрическая аутентификация с ручным фолбеком при ошибке/отсутствии настроек
bool authenticated = false;
try {
authenticated = await _localAuth.authenticate(
localizedReason: 'Подтвердите личность для сопряжения нового устройства',
options: const AuthenticationOptions(
biometricOnly: false,
useErrorDialogs: true,
stickyAuth: false,
),
);
} catch (e) {
print('Исключение local_auth, пробуем ручное подтверждение: $e');
authenticated = await _showManualConfirmationDialog(context);
}
if (!authenticated) {
_resetScanner();
return;
}
// 1. Извлекаем локальный закрытый E2EE-ключ
final privateKey = await _cryptoService.getPrivateKey();
if (privateKey == null) {
throw Exception('Локальный закрытый ключ шифрования не найден');
}
// 2. Генерируем свои временные ключи сопряжения
final phoneKeyPair = await _cryptoService.generateTempKeyPair();
final phonePublicKey = await phoneKeyPair.extractPublicKey();
final phonePublicKeyBase64 = base64Encode(phonePublicKey.bytes);
// 3. Шифруем приватный ключ на временном общем секрете
final encPayload = await _cryptoService.encryptPrivateKeyWithEcdh(
tempKeyPair: phoneKeyPair,
tempRemotePublicKeyBase64: tempPublicKeyBase64,
privateKeyToEncrypt: privateKey,
);
// 4. Отправляем одобрение на сервер
final apiService = ApiService();
final success = await apiService.approveQrLogin(
roomId: roomId,
phoneTempPub: phonePublicKeyBase64,
encPrivateKeyPayload: encPayload,
deviceName: connectingDeviceName ?? 'Новое устройство',
);
if (success) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Новое устройство успешно авторизовано')),
);
Navigator.pop(context);
}
} else {
throw Exception('Сервер отклонил авторизацию сопряжения');
}
} catch (e) {
print(' Ошибка: $e');
setState(() {
_statusMessage = 'Ошибка: $e';
});
_resetScanner();
}
}
void _resetScanner() {
setState(() {
_isProcessing = false;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
_mobileScannerController.start();
}
});
}
Future<bool> _showConfirmDialog(BuildContext context) async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
title: const Text('Подтверждение входа'),
content: const Text(
'Вы действительно хотите войти на новом устройстве? Это перенесет ваши зашифрованные ключи для доступа к чатам.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Отмена'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Войти'),
),
],
);
},
) ??
false;
}
Future<bool> _showManualConfirmationDialog(BuildContext context) async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
title: const Text('Подтверждение сопряжения'),
content: const Text(
'Биометрическая защита или блокировка экрана не настроены на этом телефоне.\n\nВы действительно подтверждаете вход на новом устройстве?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Отмена'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Подтвердить'),
),
],
);
},
) ??
false;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Сканировать QR-код'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
// Сканер камеры
MobileScanner(
controller: _mobileScannerController,
onDetect: _handleQrDetected,
),
// Полупрозрачная рамка фокуса по центру
Center(
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
border: Border.all(color: colorScheme.primary, width: 3),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
spreadRadius: 2000,
),
],
),
),
),
// Текстовая подсказка
Positioned(
bottom: 60,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(16),
),
child: Text(
_statusMessage ?? 'Наведите камеру на QR-код на новом устройстве',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
),
),
],
),
);
}
}