freezer/lib/ui/login_on_other_device.dart

253 lines
7.7 KiB
Dart
Raw Normal View History

2024-02-12 02:37:26 +00:00
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/utils.dart';
import 'package:pointycastle/export.dart' as pc;
import 'package:pointycastle/src/platform_check/platform_check.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:freezer/translations.i18n.dart';
class LoginOnOtherDevice extends StatefulWidget {
const LoginOnOtherDevice({super.key});
@override
State<LoginOnOtherDevice> createState() => _LoginOnOtherDeviceState();
}
class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
String _ipAddress = '';
String _code = '';
String? _error;
bool _loading = false;
bool _step2 = false;
final GlobalKey<FormState> _formKey = GlobalKey();
late final encrypt.Key key;
void _doHandshake() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
// generate keypair
final keyGen = pc.RSAKeyGenerator();
final secureRandom = pc.SecureRandom('Fortuna')
..seed(pc.KeyParameter(
Platform.instance.platformEntropySource().getBytes(32)));
keyGen.init(pc.ParametersWithRandom(
pc.RSAKeyGeneratorParameters(BigInt.parse('65537'), 2048, 64),
secureRandom));
final keyPair = keyGen.generateKeyPair();
final privKey = keyPair.privateKey as pc.RSAPrivateKey;
final pubKey = keyPair.publicKey as pc.RSAPublicKey;
// initial handshake
final hash = Hmac(sha512, Utils.splitDigits(int.parse(_code)))
.convert(Utils.serializeBigInt(pubKey.exponent! ^ pubKey.modulus!))
.toString();
late final Response res;
try {
res = await Dio().post('http://$_ipAddress/',
data: jsonEncode({
'_': 'handshake',
'pubKey': [
base64.encode(Utils.serializeBigInt(pubKey.modulus!)),
base64.encode(Utils.serializeBigInt(pubKey.exponent!))
],
'hash': hash,
}),
options: Options(responseType: ResponseType.json));
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
if (e.response?.statusCode == 403) {
setState(() {
_error = 'Wrong code';
_loading = false;
});
return;
}
final data = e.response?.data as Map?;
if (data != null && data['message'] is String) {
setState(() {
_error = 'Request failed: ${data['message']}';
_loading = false;
});
return;
}
setState(() {
_error = 'Request failed: $e';
_loading = false;
});
return;
}
}
print(res);
final data = res.data as Map;
if (!data['ok']) {
setState(() {
_error = data['message'] as String? ?? 'Unknown server error';
_loading = false;
});
return;
}
final encryptedKey = data['key'] as String;
final encrypter = encrypt.Encrypter(encrypt.RSA(privateKey: privKey));
key = encrypt.Key(Uint8List.fromList(
encrypter.decryptBytes(encrypt.Encrypted.fromBase64(encryptedKey))));
setState(() {
_loading = false;
_step2 = true;
});
}
void _loginUsingArl(String arl) async {
setState(() {
_loading = true;
});
final encrypter = encrypt.Encrypter(
encrypt.AES(key),
);
final iv = encrypt.IV.fromSecureRandom(16);
final encryptedARL = encrypter.encrypt(arl, iv: iv);
// send it over with dio
late final Response res;
try {
res = await Dio().post('http://$_ipAddress/',
data: jsonEncode({
'_': 'arl',
'arl': encryptedARL.base64,
'iv': iv.base64,
}),
options: Options(responseType: ResponseType.json));
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
final data = e.response?.data as Map?;
if (data != null && data['message'] is String) {
setState(() {
_error = 'Request failed: ${data['message']}';
_loading = false;
});
}
}
setState(() {
_error = 'Request failed: $e';
_loading = false;
});
return;
}
if (!res.data['ok']) {
setState(() {
_error = res.data['message'] as String? ?? 'Unknown server error.';
_loading = false;
});
return;
}
Navigator.pop(context);
ScaffoldMessenger.of(context).snack('Logged in successfully!'.i18n);
}
2024-02-12 02:46:39 +00:00
@override
void dispose() async {
2024-02-12 02:37:26 +00:00
if (_step2) {
final hash =
Hmac(sha512, key.bytes).convert(utf8.encode(_code)).toString();
await Dio().post('http://$_ipAddress/',
data: jsonEncode({'_': 'cancel', 'hash': hash}));
}
2024-02-12 02:46:39 +00:00
super.dispose();
2024-02-12 02:37:26 +00:00
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Login on other device'.i18n),
content: _step2
? Column(mainAxisSize: MainAxisSize.min, children: [
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
OutlinedButton(
onPressed:
_loading ? null : () => _loginUsingArl(settings.arl!),
child: Text('Login with current ARL'.i18n)),
])
: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
TextFormField(
validator: (value) {
value ??= '';
final p = value.split(':');
if (p.length != 2) return 'Invalid IP and Port';
final ip = p[0];
final port = int.tryParse(p[1]);
if (port == null || port > 65535) return 'Invalid port';
final ipParts = ip.split('.');
if (ipParts.length != 4) return 'Invalid IP';
for (final part in ipParts) {
final a = int.tryParse(part);
if (a == null || a < 0 || a > 255) {
return 'Invalid IP';
}
}
return null;
},
onChanged: (value) => _ipAddress = value,
decoration:
InputDecoration(label: Text('IP Address'.i18n)),
),
TextFormField(
validator: (value) {
value ??= '';
if (value.length != 6 || int.tryParse(value) == null) {
return 'Invalid code';
}
return null;
},
onChanged: (value) => _code = value,
decoration: InputDecoration(label: Text('Code'.i18n)),
)
]),
),
actions: [
2024-02-12 02:46:39 +00:00
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.i18n)),
2024-02-12 02:37:26 +00:00
if (!_step2)
TextButton(
onPressed: _loading ? null : _doHandshake,
child: Text('Login'.i18n),
),
],
);
}
}