freezer/lib/ui/login_on_other_device.dart
Pato05 c42b9bc8e2
use pipe API for lyrics
Hive persistent cookie jar
Translated lyrics
2024-02-13 02:48:39 +01:00

314 lines
10 KiB
Dart

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/ui/login_screen.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 _loginWithArl(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);
}
@override
void dispose() async {
if (_step2) {
final hash =
Hmac(sha512, key.bytes).convert(utf8.encode(_code)).toString();
await Dio().post('http://$_ipAddress/',
data: jsonEncode({'_': 'cancel', 'hash': hash}));
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Login on other device'.i18n),
content: _step2
? (_loading
? const CircularProgressIndicator()
: Column(mainAxisSize: MainAxisSize.min, children: [
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
OutlinedButton(
onPressed: () => _loginWithArl(settings.arl!),
child: Text('Login with current ARL (one-click)'.i18n)),
const SizedBox(height: 16.0),
Text('Or, login with external ARL'.i18n),
const SizedBox(height: 16.0),
if (LoginBrowser.supported()) ...[
OutlinedButton(
onPressed: () {
Navigator.of(context).pushRoute(
builder: (context) =>
LoginBrowser(_loginWithArl));
},
child: Text('Login using browser'.i18n)),
const SizedBox(height: 16.0),
],
OutlinedButton(
onPressed: () {
String arl = '';
final key = GlobalKey<FormFieldState<String>>();
showDialog(
context: context,
builder: (context) {
submit(String arl) {
if (!key.currentState!.validate()) return;
Navigator.pop(context);
_loginWithArl(arl);
}
return AlertDialog(
title: Text('Enter ARL'.i18n),
content: TextFormField(
key: key,
validator: (value) =>
value?.trim().length == 192
? null
: 'Invalid ARL length!'.i18n,
onChanged: (value) => arl = value,
decoration: InputDecoration(
labelText: 'Token (ARL)'.i18n),
onFieldSubmitted: submit,
),
actions: <Widget>[
TextButton(
child: Text('Save'.i18n),
onPressed: () => submit(arl),
)
],
);
});
},
child: Text('Login with 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'.i18n;
}
final ip = p[0];
final port = int.tryParse(p[1]);
if (port == null || port > 65535) {
return 'Invalid port'.i18n;
}
final ipParts = ip.split('.');
if (ipParts.length != 4) {
return 'Invalid IP'.i18n;
}
for (final part in ipParts) {
final a = int.tryParse(part);
if (a == null || a < 0 || a > 255) {
return 'Invalid IP'.i18n;
}
}
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'.i18n;
}
return null;
},
onChanged: (value) => _code = value,
decoration: InputDecoration(label: Text('Code'.i18n)),
)
]),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.i18n)),
if (!_step2)
TextButton(
onPressed: _loading ? null : _doHandshake,
child: Text('Login'.i18n),
),
],
);
}
}