Support replacing certs (#33)

This commit is contained in:
Nate Brown 2021-05-03 15:16:00 -05:00 committed by GitHub
parent a283bf8010
commit 0bb2a30829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 150 deletions

View File

@ -263,9 +263,11 @@ class IncomingSite(
}
if (key != null) {
val f = EncFile(context).openWrite(siteDir.resolve("key"))
f.use { it.write(key) }
f.close()
val keyFile = siteDir.resolve("key")
keyFile.delete()
val encFile = EncFile(context).openWrite(keyFile)
encFile.use { it.write(key) }
encFile.close()
}
key = null

View File

@ -8,12 +8,13 @@ import 'package:mobile_nebula/services/utils.dart';
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made
class FormPage extends StatefulWidget {
const FormPage(
{Key key, this.title, @required this.child, @required this.onSave, @required this.changed, this.hideSave = false})
{Key key, this.title, @required this.child, @required this.onSave, @required this.changed, this.hideSave = false, this.scrollController})
: super(key: key);
final String title;
final Function onSave;
final Widget child;
final ScrollController scrollController;
/// If you need the page to progress to a certain point before saving, control it here
final bool hideSave;
@ -50,6 +51,7 @@ class _FormPageState extends State<FormPage> {
child: SimplePage(
leadingAction: _buildLeader(context),
trailingActions: _buildTrailer(context),
scrollController: widget.scrollController,
title: widget.title,
child: Form(
key: _formKey,

View File

@ -28,8 +28,8 @@ class SiteItem extends StatelessWidget {
Widget _buildContent(BuildContext context) {
final border = BorderSide(color: Utils.configSectionBorder(context));
var ip = "Error";
if (site.cert != null && site.cert.cert.details.ips.length > 0) {
ip = site.cert.cert.details.ips[0];
if (site.certInfo != null && site.certInfo.cert.details.ips.length > 0) {
ip = site.certInfo.cert.details.ips[0];
}
return SpecialButton(

View File

@ -1,5 +1,3 @@
import 'dart:io';
class IPAndPort {
String ip;
int port;

View File

@ -27,7 +27,7 @@ class Site {
// pki fields
List<CertificateInfo> ca;
CertificateInfo cert;
CertificateInfo certInfo;
String key;
// lighthouse options
@ -52,7 +52,7 @@ class Site {
id,
staticHostmap,
ca,
this.cert,
this.certInfo,
this.lhDuration = 0,
this.port = 0,
this.cipher = "aes",
@ -95,7 +95,7 @@ class Site {
});
if (json['cert'] != null) {
cert = CertificateInfo.fromJson(json['cert']);
certInfo = CertificateInfo.fromJson(json['cert']);
}
lhDuration = json['lhDuration'];
@ -146,7 +146,7 @@ class Site {
return cert.rawCert;
})?.join('\n') ??
"",
'cert': cert?.rawCert,
'cert': certInfo?.rawCert,
'key': key,
'lhDuration': lhDuration,
'port': port,

View File

@ -68,7 +68,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
labelWidth: 150,
content: Text(hostInfo.cert.details.name),
onPressed: () => Utils.openPage(
context, (context) => CertificateDetailsScreen(certificate: CertificateInfo(cert: hostInfo.cert))))
context, (context) => CertificateDetailsScreen(certInfo: CertificateInfo(cert: hostInfo.cert))))
: Container(),
]);
}

View File

@ -179,7 +179,7 @@ mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
"10.1.0.1": StaticHost(lighthouse: true, destinations: [IPAndPort(ip: '10.1.1.53', port: 4242), IPAndPort(ip: '1::1', port: 4242)])
},
ca: [CertificateInfo.debug(rawCert: ca)],
cert: CertificateInfo.debug(rawCert: cert),
certInfo: CertificateInfo.debug(rawCert: cert),
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
);

View File

@ -1,56 +1,54 @@
import 'dart:convert';
import 'package:barcode_scan/barcode_scan.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/FormPage.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigPageItem.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/screens/siteConfig/CertificateDetailsScreen.dart';
import 'package:mobile_nebula/services/share.dart';
import 'package:mobile_nebula/services/utils.dart';
import 'CertificateDetailsScreen.dart';
class CertificateResult {
CertificateInfo cert;
CertificateInfo certInfo;
String key;
CertificateResult({this.cert, this.key});
CertificateResult({this.certInfo, this.key});
}
class CertificateScreen extends StatefulWidget {
const CertificateScreen({Key key, this.cert, this.onSave}) : super(key: key);
class AddCertificateScreen extends StatefulWidget {
const AddCertificateScreen({Key key, this.onSave, this.onReplace}) : super(key: key);
final CertificateInfo cert;
// onSave will pop a new CertificateDetailsScreen
final ValueChanged<CertificateResult> onSave;
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen
final ValueChanged<CertificateResult> onReplace;
@override
_CertificateScreenState createState() => _CertificateScreenState();
_AddCertificateScreenState createState() => _AddCertificateScreenState();
}
class _CertificateScreenState extends State<CertificateScreen> {
class _AddCertificateScreenState extends State<AddCertificateScreen> {
String pubKey;
String privKey;
bool changed = false;
CertificateInfo cert;
String inputType = 'paste';
bool shared = false;
final pasteController = TextEditingController();
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
@override
void initState() {
cert = widget.cert;
_generateKeys();
super.initState();
}
@ -62,71 +60,29 @@ class _CertificateScreenState extends State<CertificateScreen> {
@override
Widget build(BuildContext context) {
List<Widget> items = [];
bool hideSave = true;
if (cert == null) {
if (pubKey == null) {
items = _buildGenerate();
} else {
return Center(
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 500);
}),
);
}
List<Widget> items = [];
items.addAll(_buildShare());
items.addAll(_buildLoadCert());
}
} else {
items.addAll(_buildCertList());
hideSave = false;
}
return FormPage(
return SimplePage(
title: 'Certificate',
changed: changed,
hideSave: hideSave,
onSave: () {
Navigator.pop(context);
if (widget.onSave != null) {
widget.onSave(CertificateResult(cert: cert, key: privKey));
}
},
child: Column(children: items));
}
_buildCertList() {
//TODO: generate a full list
return [
ConfigSection(
children: [
ConfigPageItem(
content: Text(cert.cert.details.name),
onPressed: () {
Utils.openPage(context, (context) {
//TODO: wire on delete
return CertificateDetailsScreen(certificate: cert);
});
},
)
],
)
];
}
List<Widget> _buildGenerate() {
return [
ConfigSection(label: 'Please generate a new public and private key', children: [
ConfigButtonItem(
content: Text('Generate Keys'),
onPressed: () => _generateKeys(),
)
])
];
}
_generateKeys() async {
try {
var kp = await platform.invokeMethod("nebula.generateKeyPair");
Map<String, dynamic> keyPair = jsonDecode(kp);
setState(() {
changed = true;
pubKey = keyPair['PublicKey'];
privKey = keyPair['PrivateKey'];
});
@ -148,9 +104,6 @@ class _CertificateScreenState extends State<CertificateScreen> {
content: Text('Share Public Key'),
onPressed: () async {
await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
setState(() {
shared = true;
});
},
),
])
@ -198,14 +151,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
ConfigButtonItem(
content: Center(child: Text('Load Certificate')),
onPressed: () {
_addCertEntry(pasteController.text, (err) {
if (err != null) {
return Utils.popError(context, 'Failed to parse certificate content', err);
}
pasteController.text = '';
setState(() {});
});
_addCertEntry(pasteController.text);
}),
],
)
@ -225,13 +171,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
return;
}
_addCertEntry(content, (err) {
if (err != null) {
Utils.popError(context, 'Error loading certificate file', err);
} else {
setState(() {});
}
});
_addCertEntry(content);
} catch (err) {
return Utils.popError(context, 'Failed to load certificate file', err.toString());
}
@ -254,13 +194,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
var result = await BarcodeScanner.scan(options: options);
if (result.rawContent != "") {
_addCertEntry(result.rawContent, (err) {
if (err != null) {
Utils.popError(context, 'Error loading certificate content', err);
} else {
setState(() {});
}
});
_addCertEntry(result.rawContent);
}
}),
],
@ -268,9 +202,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
];
}
_addCertEntry(String rawCert, ValueChanged<String> callback) async {
String error;
_addCertEntry(String rawCert) async {
// Allow for app store review testing cert to override the generated key
if (rawCert.trim() == _testCert) {
privKey = _testKey;
@ -278,21 +210,36 @@ class _CertificateScreenState extends State<CertificateScreen> {
try {
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
List<dynamic> certs = jsonDecode(rawCerts);
if (certs.length > 0) {
var tryCert = CertificateInfo.fromJson(certs.first);
if (tryCert.cert.details.isCa) {
return callback('A certificate authority is not appropriate for a client certificate.');
}
//TODO: test that the pubkey matches the privkey
cert = tryCert;
}
} on PlatformException catch (err) {
error = err.details ?? err.message;
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.');
} else if (!tryCertInfo.validity.valid) {
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity.reason);
}
if (callback != null) {
callback(error);
//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;
}
// 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));
});
});
}
} on PlatformException catch (err) {
return Utils.popError(context, 'Error loading certificate content', err.details ?? err.message);
}
}
}

View File

@ -77,7 +77,7 @@ class _CAListScreenState extends State<CAListScreen> {
onPressed: () {
Utils.openPage(context, (context) {
return CertificateDetailsScreen(
certificate: ca,
certInfo: ca,
onDelete: () {
setState(() {
changed = true;

View File

@ -2,34 +2,73 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/components/FormPage.dart';
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Certificate.dart';
import 'package:mobile_nebula/screens/siteConfig/AddCertificateScreen.dart';
import 'package:mobile_nebula/services/utils.dart';
/// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert)
class CertificateDetailsScreen extends StatefulWidget {
const CertificateDetailsScreen({Key key, this.certificate, this.onDelete}) : super(key: key);
const CertificateDetailsScreen({Key key, this.certInfo, this.onDelete, this.onSave, this.onReplace}) : super(key: key);
final CertificateInfo certificate;
final CertificateInfo certInfo;
// onDelete is used to remove a CA cert
final Function onDelete;
// onSave is used to install a new certificate
final Function onSave;
// onReplace is used to install a new certificate over top of the old one
final ValueChanged<CertificateResult> onReplace;
@override
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
}
class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
bool changed = false;
CertificateResult certResult;
CertificateInfo certInfo;
ScrollController controller = ScrollController();
@override
void initState() {
certInfo = widget.certInfo;
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SimplePage(
return FormPage(
title: 'Certificate Details',
scrollController: controller,
changed: widget.onSave != null || changed,
onSave: () {
if (widget.onSave != null) {
Navigator.pop(context);
widget.onSave();
} else if (widget.onReplace != null) {
Navigator.pop(context);
widget.onReplace(certResult);
}
},
hideSave: widget.onSave == null && widget.onReplace == null,
child: Column(children: [
_buildID(),
_buildFilters(),
_buildValid(),
_buildAdvanced(),
_buildReplace(),
_buildDelete(),
]),
);
@ -37,17 +76,17 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
Widget _buildID() {
return ConfigSection(children: <Widget>[
ConfigItem(label: Text('Name'), content: SpecialSelectableText(widget.certificate.cert.details.name)),
ConfigItem(label: Text('Name'), content: SpecialSelectableText(certInfo.cert.details.name)),
ConfigItem(
label: Text('Type'),
content: Text(widget.certificate.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
]);
}
Widget _buildValid() {
var valid = Text('yes');
if (widget.certificate.validity != null && !widget.certificate.validity.valid) {
valid = Text(widget.certificate.validity.valid ? 'yes' : widget.certificate.validity.reason,
if (certInfo.validity != null && !certInfo.validity.valid) {
valid = Text(certInfo.validity.valid ? 'yes' : certInfo.validity.reason,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
}
return ConfigSection(
@ -56,33 +95,33 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
ConfigItem(label: Text('Valid?'), content: valid),
ConfigItem(
label: Text('Created'),
content: SpecialSelectableText(widget.certificate.cert.details.notBefore.toLocal().toString())),
content: SpecialSelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
ConfigItem(
label: Text('Expires'),
content: SpecialSelectableText(widget.certificate.cert.details.notAfter.toLocal().toString())),
content: SpecialSelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
],
);
}
Widget _buildFilters() {
List<Widget> items = [];
if (widget.certificate.cert.details.groups.length > 0) {
if (certInfo.cert.details.groups.length > 0) {
items.add(ConfigItem(
label: Text('Groups'), content: SpecialSelectableText(widget.certificate.cert.details.groups.join(', '))));
label: Text('Groups'), content: SpecialSelectableText(certInfo.cert.details.groups.join(', '))));
}
if (widget.certificate.cert.details.ips.length > 0) {
if (certInfo.cert.details.ips.length > 0) {
items
.add(ConfigItem(label: Text('IPs'), content: SpecialSelectableText(widget.certificate.cert.details.ips.join(', '))));
.add(ConfigItem(label: Text('IPs'), content: SpecialSelectableText(certInfo.cert.details.ips.join(', '))));
}
if (widget.certificate.cert.details.subnets.length > 0) {
if (certInfo.cert.details.subnets.length > 0) {
items.add(ConfigItem(
label: Text('Subnets'), content: SpecialSelectableText(widget.certificate.cert.details.subnets.join(', '))));
label: Text('Subnets'), content: SpecialSelectableText(certInfo.cert.details.subnets.join(', '))));
}
return items.length > 0
? ConfigSection(label: widget.certificate.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
? ConfigSection(label: certInfo.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
: Container();
}
@ -91,18 +130,18 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
children: <Widget>[
ConfigItem(
label: Text('Fingerprint'),
content: SpecialSelectableText(widget.certificate.cert.fingerprint,
content: SpecialSelectableText(certInfo.cert.fingerprint,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start),
ConfigItem(
label: Text('Public Key'),
content: SpecialSelectableText(widget.certificate.cert.details.publicKey,
content: SpecialSelectableText(certInfo.cert.details.publicKey,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start),
widget.certificate.rawCert != null
certInfo.rawCert != null
? ConfigItem(
label: Text('PEM Format'),
content: SpecialSelectableText(widget.certificate.rawCert,
content: SpecialSelectableText(certInfo.rawCert,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start)
: Container(),
@ -110,12 +149,40 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
);
}
Widget _buildReplace() {
if (widget.onReplace == null) {
return Container();
}
return Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox(
width: double.infinity,
child: PlatformButton(
child: Text('Replace certificate'),
color: CupertinoColors.systemRed.resolveFrom(context),
onPressed: () {
Utils.openPage(context, (context) {
return AddCertificateScreen(
onReplace: (result) {
setState(() {
changed = true;
certResult = result;
certInfo = certResult.certInfo;
});
// Slam the page back to the top
controller.animateTo(0, duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
});
});
})));
}
Widget _buildDelete() {
if (widget.onDelete == null) {
return Container();
}
var title = widget.certificate.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
var title = certInfo.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
return Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),

View File

@ -13,11 +13,13 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/screens/siteConfig/AdvancedScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/CAListScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/CertificateScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/AddCertificateScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/StaticHostsScreen.dart';
import 'package:mobile_nebula/services/utils.dart';
//TODO: Add a config test mechanism
//TODO: Enforce a name
class SiteConfigScreen extends StatefulWidget {
const SiteConfigScreen({Key key, this.site, this.onSave}) : super(key: key);
@ -100,12 +102,18 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
content: PlatformTextFormField(
placeholder: 'Required',
controller: nameController,
validator: (name) {
if (name == null || name == "") {
return "A name is required";
}
return null;
},
))
]);
}
Widget _keys() {
final certError = site.cert == null || !site.cert.validity.valid;
final certError = site.certInfo == null || !site.certInfo.validity.valid;
var caError = site.ca.length == 0;
if (!caError) {
site.ca.forEach((ca) {
@ -126,16 +134,28 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5))
: Container(),
certError ? Text('Needs attention') : Text(site.cert.cert.details.name)
certError ? Text('Needs attention') : Text(site.certInfo.cert.details.name)
]),
onPressed: () {
Utils.openPage(context, (context) {
return CertificateScreen(
cert: site.cert,
if (site.certInfo != null) {
return CertificateDetailsScreen(
certInfo: site.certInfo,
onReplace: (result) {
setState(() {
changed = true;
site.certInfo = result.certInfo;
site.key = result.key;
});
}
);
}
return AddCertificateScreen(
onSave: (result) {
setState(() {
changed = true;
site.cert = result.cert;
site.certInfo = result.certInfo;
site.key = result.key;
});
});