2020-07-27 20:43:58 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
2022-09-21 20:27:35 +00:00
|
|
|
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
|
2021-05-03 20:16:00 +00:00
|
|
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
|
|
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
2020-07-27 20:43:58 +00:00
|
|
|
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
|
|
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
|
|
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
|
|
|
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
|
|
|
|
import 'package:mobile_nebula/models/Certificate.dart';
|
2020-08-18 00:12:28 +00:00
|
|
|
import 'package:mobile_nebula/services/share.dart';
|
2020-07-27 20:43:58 +00:00
|
|
|
import 'package:mobile_nebula/services/utils.dart';
|
|
|
|
|
2021-05-03 20:16:00 +00:00
|
|
|
import 'CertificateDetailsScreen.dart';
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
class CertificateResult {
|
2021-05-03 20:16:00 +00:00
|
|
|
CertificateInfo certInfo;
|
2020-07-27 20:43:58 +00:00
|
|
|
String key;
|
|
|
|
|
2022-09-21 20:27:35 +00:00
|
|
|
CertificateResult({required this.certInfo, required this.key});
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
|
2021-05-03 20:16:00 +00:00
|
|
|
class AddCertificateScreen extends StatefulWidget {
|
2022-09-21 20:27:35 +00:00
|
|
|
const AddCertificateScreen({
|
|
|
|
Key? key,
|
|
|
|
this.onSave,
|
|
|
|
this.onReplace,
|
|
|
|
required this.pubKey,
|
|
|
|
required this.privKey,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
// onSave will pop a new CertificateDetailsScreen.
|
|
|
|
// If onSave is null, onReplace must be set.
|
|
|
|
final ValueChanged<CertificateResult>? onSave;
|
|
|
|
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen.
|
|
|
|
// If onReplace is null, onSave must be set.
|
|
|
|
final ValueChanged<CertificateResult>? onReplace;
|
2020-07-27 20:43:58 +00:00
|
|
|
|
2022-08-05 21:42:17 +00:00
|
|
|
final String pubKey;
|
|
|
|
final String privKey;
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
@override
|
2021-05-03 20:16:00 +00:00
|
|
|
_AddCertificateScreenState createState() => _AddCertificateScreenState();
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
|
2021-05-03 20:16:00 +00:00
|
|
|
class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
2022-09-21 20:27:35 +00:00
|
|
|
late String pubKey;
|
2022-08-05 21:42:17 +00:00
|
|
|
bool showKey = false;
|
2020-07-27 20:43:58 +00:00
|
|
|
|
|
|
|
String inputType = 'paste';
|
|
|
|
|
2022-08-05 21:42:17 +00:00
|
|
|
final keyController = TextEditingController();
|
2020-07-27 20:43:58 +00:00
|
|
|
final pasteController = TextEditingController();
|
|
|
|
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
2022-08-05 21:42:17 +00:00
|
|
|
pubKey = widget.pubKey;
|
|
|
|
keyController.text = widget.privKey;
|
2020-07-27 20:43:58 +00:00
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
2020-08-31 23:32:40 +00:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
pasteController.dispose();
|
2022-08-05 21:42:17 +00:00
|
|
|
keyController.dispose();
|
2020-08-31 23:32:40 +00:00
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2021-05-03 20:16:00 +00:00
|
|
|
List<Widget> items = [];
|
|
|
|
items.addAll(_buildShare());
|
2022-08-05 21:42:17 +00:00
|
|
|
items.add(_buildKey());
|
2021-05-03 20:16:00 +00:00
|
|
|
items.addAll(_buildLoadCert());
|
|
|
|
|
2022-11-17 21:43:16 +00:00
|
|
|
return SimplePage(title: Text('Certificate'), child: Column(children: items));
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> _buildShare() {
|
|
|
|
return [
|
|
|
|
ConfigSection(
|
|
|
|
label: 'Share your public key with a nebula CA so they can sign and return a certificate',
|
|
|
|
children: [
|
|
|
|
ConfigItem(
|
|
|
|
labelWidth: 0,
|
2022-01-19 19:29:30 +00:00
|
|
|
content: SelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
2020-07-27 20:43:58 +00:00
|
|
|
),
|
|
|
|
ConfigButtonItem(
|
|
|
|
content: Text('Share Public Key'),
|
|
|
|
onPressed: () async {
|
2020-08-18 00:12:28 +00:00
|
|
|
await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
|
2020-07-27 20:43:58 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
])
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> _buildLoadCert() {
|
|
|
|
List<Widget> items = [
|
|
|
|
Padding(
|
|
|
|
padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
|
|
|
|
child: CupertinoSlidingSegmentedControl(
|
|
|
|
groupValue: inputType,
|
|
|
|
onValueChanged: (v) {
|
2022-09-21 20:27:35 +00:00
|
|
|
if (v != null) {
|
|
|
|
setState(() {
|
|
|
|
inputType = v;
|
|
|
|
});
|
|
|
|
}
|
2020-07-27 20:43:58 +00:00
|
|
|
},
|
|
|
|
children: {
|
|
|
|
'paste': Text('Copy/Paste'),
|
|
|
|
'file': Text('File'),
|
|
|
|
'qr': Text('QR Code'),
|
|
|
|
},
|
|
|
|
))
|
|
|
|
];
|
|
|
|
|
|
|
|
if (inputType == 'paste') {
|
|
|
|
items.addAll(_addPaste());
|
|
|
|
} else if (inputType == 'file') {
|
|
|
|
items.addAll(_addFile());
|
|
|
|
} else {
|
|
|
|
items.addAll(_addQr());
|
|
|
|
}
|
|
|
|
|
|
|
|
return items;
|
|
|
|
}
|
|
|
|
|
2022-08-05 21:42:17 +00:00
|
|
|
Widget _buildKey() {
|
|
|
|
if (!showKey) {
|
|
|
|
return Padding(
|
|
|
|
padding: EdgeInsets.only(top: 20, bottom: 10, left: 10, right: 10),
|
|
|
|
child: SizedBox(
|
|
|
|
width: double.infinity,
|
|
|
|
child: PlatformElevatedButton(
|
|
|
|
child: Text('Show/Import Private Key'),
|
|
|
|
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
|
|
|
onPressed: () => Utils.confirmDelete(context, 'Show/Import Private Key?', () {
|
2022-09-21 20:27:35 +00:00
|
|
|
setState(() {
|
|
|
|
showKey = true;
|
|
|
|
});
|
|
|
|
}, deleteLabel: 'Yes'))));
|
2022-08-05 21:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ConfigSection(
|
|
|
|
label: 'Import a private key generated on another device',
|
|
|
|
children: [
|
2022-09-21 20:27:35 +00:00
|
|
|
ConfigTextItem(controller: keyController, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
2022-08-05 21:42:17 +00:00
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
List<Widget> _addPaste() {
|
|
|
|
return [
|
|
|
|
ConfigSection(
|
|
|
|
children: [
|
|
|
|
ConfigTextItem(
|
|
|
|
placeholder: 'Certificate PEM Contents',
|
|
|
|
controller: pasteController,
|
|
|
|
),
|
|
|
|
ConfigButtonItem(
|
|
|
|
content: Center(child: Text('Load Certificate')),
|
|
|
|
onPressed: () {
|
2021-05-03 20:16:00 +00:00
|
|
|
_addCertEntry(pasteController.text);
|
2020-07-27 20:43:58 +00:00
|
|
|
}),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> _addFile() {
|
|
|
|
return [
|
|
|
|
ConfigSection(
|
|
|
|
children: [
|
|
|
|
ConfigButtonItem(
|
|
|
|
content: Center(child: Text('Choose a file')),
|
|
|
|
onPressed: () async {
|
|
|
|
try {
|
2021-04-23 17:33:28 +00:00
|
|
|
final content = await Utils.pickFile(context);
|
|
|
|
if (content == null) {
|
2020-07-27 20:43:58 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-05-03 20:16:00 +00:00
|
|
|
_addCertEntry(content);
|
2020-07-27 20:43:58 +00:00
|
|
|
} catch (err) {
|
2021-04-23 17:33:28 +00:00
|
|
|
return Utils.popError(context, 'Failed to load certificate file', err.toString());
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
],
|
|
|
|
)
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> _addQr() {
|
|
|
|
return [
|
|
|
|
ConfigSection(
|
|
|
|
children: [
|
|
|
|
ConfigButtonItem(
|
|
|
|
content: Text('Scan a QR code'),
|
|
|
|
onPressed: () async {
|
2022-09-21 20:27:35 +00:00
|
|
|
try {
|
|
|
|
var result = await FlutterBarcodeScanner.scanBarcode('#ff6666', 'Cancel', true, ScanMode.QR);
|
|
|
|
if (result != "") {
|
|
|
|
_addCertEntry(result);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
return Utils.popError(context, 'Error scanning QR code', err.toString());
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2021-05-03 20:16:00 +00:00
|
|
|
_addCertEntry(String rawCert) async {
|
2020-08-07 22:44:40 +00:00
|
|
|
// Allow for app store review testing cert to override the generated key
|
|
|
|
if (rawCert.trim() == _testCert) {
|
2022-08-05 21:42:17 +00:00
|
|
|
keyController.text = _testKey;
|
2020-08-07 22:44:40 +00:00
|
|
|
}
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
try {
|
|
|
|
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
|
2021-05-03 20:16:00 +00:00
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
List<dynamic> certs = jsonDecode(rawCerts);
|
|
|
|
if (certs.length > 0) {
|
2021-05-03 20:16:00 +00:00
|
|
|
var tryCertInfo = CertificateInfo.fromJson(certs.first);
|
|
|
|
if (tryCertInfo.cert.details.isCa) {
|
|
|
|
return Utils.popError(context, 'Error loading certificate content',
|
|
|
|
'A certificate authority is not appropriate for a client certificate.');
|
2022-09-21 20:27:35 +00:00
|
|
|
} else if (!tryCertInfo.validity!.valid) {
|
|
|
|
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity!.reason);
|
2021-05-03 20:16:00 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 20:27:35 +00:00
|
|
|
var certMatch = await platform
|
|
|
|
.invokeMethod("nebula.verifyCertAndKey", <String, String>{"cert": rawCert, "key": keyController.text});
|
2022-08-05 21:42:17 +00:00
|
|
|
if (!certMatch) {
|
|
|
|
// The method above will throw if there is a mismatch, this is just here in case we introduce a bug in the future
|
|
|
|
return Utils.popError(context, 'Error loading certificate content',
|
|
|
|
'The provided certificates public key is not compatible with the private key.');
|
|
|
|
}
|
2021-05-03 20:16:00 +00:00
|
|
|
|
|
|
|
if (widget.onReplace != null) {
|
2022-09-21 20:27:35 +00:00
|
|
|
// If we are replacing we just return the results now
|
2021-05-03 20:16:00 +00:00
|
|
|
Navigator.pop(context);
|
2022-09-21 20:27:35 +00:00
|
|
|
widget.onReplace!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
|
2021-05-03 20:16:00 +00:00
|
|
|
return;
|
2022-09-21 20:27:35 +00:00
|
|
|
} else if (widget.onSave != null) {
|
|
|
|
// We have a cert, pop the details screen where they can hit save
|
|
|
|
Utils.openPage(context, (context) {
|
|
|
|
return CertificateDetailsScreen(
|
|
|
|
certInfo: tryCertInfo,
|
|
|
|
onSave: () {
|
|
|
|
Navigator.pop(context);
|
|
|
|
widget.onSave!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
|
|
|
|
});
|
|
|
|
});
|
2020-08-10 15:48:00 +00:00
|
|
|
}
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
} on PlatformException catch (err) {
|
2021-05-03 20:16:00 +00:00
|
|
|
return Utils.popError(context, 'Error loading certificate content', err.details ?? err.message);
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-07 22:44:40 +00:00
|
|
|
|
|
|
|
// This a cert that if presented will swap the key to assist the app review process
|
|
|
|
const _testCert = '''-----BEGIN NEBULA CERTIFICATE-----
|
|
|
|
CpMBChdBcHAgU3RvcmUgUmV2aWV3IERldmljZRIKgpSghQyA/v//DyIGcmV2aWV3
|
|
|
|
IhRiNzJjZThiZWM5MDYwYTA3MmNmMSjvk7f5BTCPnYf0BzogYHa3YoNcFJxKX8bU
|
|
|
|
jK4pg0aIYxDkwk8aM7w1c+CQXSpKICx06NYtozgKaA2R9NO311D8T86iTXxLmjI4
|
|
|
|
0wzAXCSmEkCi9ocqtyQhNp75eKphqVlZNl1RXBo4hdY9jBdc9+b9o0bU4zxFxIRT
|
|
|
|
uDneQqytYS+BUfgNnGX5wsMxOEst/kkC
|
|
|
|
-----END NEBULA CERTIFICATE-----''';
|
|
|
|
|
|
|
|
const _testKey = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
|
|
|
|
UlyDdFn/2mLFykeWjCEwWVRSDHtMF7nz3At3O77Faf4=
|
|
|
|
-----END NEBULA X25519 PRIVATE KEY-----''';
|