mobile_nebula/lib/screens/siteConfig/AddCertificateScreen.dart

259 lines
8.0 KiB
Dart
Raw Normal View History

2020-07-27 20:43:58 +00:00
import 'dart:convert';
import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.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';
import 'package:mobile_nebula/components/SpecialSelectableText.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';
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;
2021-05-03 20:16:00 +00:00
CertificateResult({this.certInfo, this.key});
2020-07-27 20:43:58 +00:00
}
2021-05-03 20:16:00 +00:00
class AddCertificateScreen extends StatefulWidget {
const AddCertificateScreen({Key key, this.onSave, this.onReplace}) : super(key: key);
2020-07-27 20:43:58 +00:00
2021-05-03 20:16:00 +00:00
// onSave will pop a new CertificateDetailsScreen
2020-07-27 20:43:58 +00:00
final ValueChanged<CertificateResult> onSave;
2021-05-03 20:16:00 +00:00
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen
final ValueChanged<CertificateResult> onReplace;
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> {
2020-07-27 20:43:58 +00:00
String pubKey;
String privKey;
String inputType = 'paste';
final pasteController = TextEditingController();
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
@override
void initState() {
2021-05-03 20:16:00 +00:00
_generateKeys();
2020-07-27 20:43:58 +00:00
super.initState();
}
@override
void dispose() {
pasteController.dispose();
super.dispose();
}
2020-07-27 20:43:58 +00:00
@override
Widget build(BuildContext context) {
2021-05-03 20:16:00 +00:00
if (pubKey == null) {
return Center(
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 500);
}),
);
2020-07-27 20:43:58 +00:00
}
2021-05-03 20:16:00 +00:00
List<Widget> items = [];
items.addAll(_buildShare());
items.addAll(_buildLoadCert());
return SimplePage(title: 'Certificate', child: Column(children: items));
2020-07-27 20:43:58 +00:00
}
_generateKeys() async {
try {
var kp = await platform.invokeMethod("nebula.generateKeyPair");
Map<String, dynamic> keyPair = jsonDecode(kp);
setState(() {
pubKey = keyPair['PublicKey'];
privKey = keyPair['PrivateKey'];
});
} on PlatformException catch (err) {
Utils.popError(context, 'Failed to generate key pair', err.details ?? err.message);
}
}
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,
content: SpecialSelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
2020-07-27 20:43:58 +00:00
),
ConfigButtonItem(
content: Text('Share Public Key'),
onPressed: () async {
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) {
setState(() {
inputType = v;
});
},
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;
}
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 {
var options = ScanOptions(
restrictFormat: [BarcodeFormat.qr],
);
var result = await BarcodeScanner.scan(options: options);
if (result.rawContent != "") {
2021-05-03 20:16:00 +00:00
_addCertEntry(result.rawContent);
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) {
privKey = _testKey;
}
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.');
2021-05-03 20:16:00 +00:00
} else if (!tryCertInfo.validity.valid) {
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity.reason);
}
//TODO: test that the pubkey we generated equals the pub key in the cert
// If we are replacing we just return the results now
if (widget.onReplace != null) {
Navigator.pop(context);
widget.onReplace(CertificateResult(certInfo: tryCertInfo, key: privKey));
return;
}
2021-05-03 20:16:00 +00:00
// 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: privKey));
});
2021-05-03 20:16: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-----''';