forked from core/mobile_nebula
Support replacing certs (#33)
This commit is contained in:
parent
a283bf8010
commit
0bb2a30829
|
@ -263,9 +263,11 @@ class IncomingSite(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
val f = EncFile(context).openWrite(siteDir.resolve("key"))
|
val keyFile = siteDir.resolve("key")
|
||||||
f.use { it.write(key) }
|
keyFile.delete()
|
||||||
f.close()
|
val encFile = EncFile(context).openWrite(keyFile)
|
||||||
|
encFile.use { it.write(key) }
|
||||||
|
encFile.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
key = null
|
key = null
|
||||||
|
|
|
@ -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
|
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made
|
||||||
class FormPage extends StatefulWidget {
|
class FormPage extends StatefulWidget {
|
||||||
const FormPage(
|
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);
|
: super(key: key);
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final Function onSave;
|
final Function onSave;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
/// If you need the page to progress to a certain point before saving, control it here
|
/// If you need the page to progress to a certain point before saving, control it here
|
||||||
final bool hideSave;
|
final bool hideSave;
|
||||||
|
@ -50,6 +51,7 @@ class _FormPageState extends State<FormPage> {
|
||||||
child: SimplePage(
|
child: SimplePage(
|
||||||
leadingAction: _buildLeader(context),
|
leadingAction: _buildLeader(context),
|
||||||
trailingActions: _buildTrailer(context),
|
trailingActions: _buildTrailer(context),
|
||||||
|
scrollController: widget.scrollController,
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
|
|
|
@ -28,8 +28,8 @@ class SiteItem extends StatelessWidget {
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
final border = BorderSide(color: Utils.configSectionBorder(context));
|
final border = BorderSide(color: Utils.configSectionBorder(context));
|
||||||
var ip = "Error";
|
var ip = "Error";
|
||||||
if (site.cert != null && site.cert.cert.details.ips.length > 0) {
|
if (site.certInfo != null && site.certInfo.cert.details.ips.length > 0) {
|
||||||
ip = site.cert.cert.details.ips[0];
|
ip = site.certInfo.cert.details.ips[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return SpecialButton(
|
return SpecialButton(
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
class IPAndPort {
|
class IPAndPort {
|
||||||
String ip;
|
String ip;
|
||||||
int port;
|
int port;
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Site {
|
||||||
|
|
||||||
// pki fields
|
// pki fields
|
||||||
List<CertificateInfo> ca;
|
List<CertificateInfo> ca;
|
||||||
CertificateInfo cert;
|
CertificateInfo certInfo;
|
||||||
String key;
|
String key;
|
||||||
|
|
||||||
// lighthouse options
|
// lighthouse options
|
||||||
|
@ -52,7 +52,7 @@ class Site {
|
||||||
id,
|
id,
|
||||||
staticHostmap,
|
staticHostmap,
|
||||||
ca,
|
ca,
|
||||||
this.cert,
|
this.certInfo,
|
||||||
this.lhDuration = 0,
|
this.lhDuration = 0,
|
||||||
this.port = 0,
|
this.port = 0,
|
||||||
this.cipher = "aes",
|
this.cipher = "aes",
|
||||||
|
@ -95,7 +95,7 @@ class Site {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (json['cert'] != null) {
|
if (json['cert'] != null) {
|
||||||
cert = CertificateInfo.fromJson(json['cert']);
|
certInfo = CertificateInfo.fromJson(json['cert']);
|
||||||
}
|
}
|
||||||
|
|
||||||
lhDuration = json['lhDuration'];
|
lhDuration = json['lhDuration'];
|
||||||
|
@ -146,7 +146,7 @@ class Site {
|
||||||
return cert.rawCert;
|
return cert.rawCert;
|
||||||
})?.join('\n') ??
|
})?.join('\n') ??
|
||||||
"",
|
"",
|
||||||
'cert': cert?.rawCert,
|
'cert': certInfo?.rawCert,
|
||||||
'key': key,
|
'key': key,
|
||||||
'lhDuration': lhDuration,
|
'lhDuration': lhDuration,
|
||||||
'port': port,
|
'port': port,
|
||||||
|
|
|
@ -68,7 +68,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: Text(hostInfo.cert.details.name),
|
content: Text(hostInfo.cert.details.name),
|
||||||
onPressed: () => Utils.openPage(
|
onPressed: () => Utils.openPage(
|
||||||
context, (context) => CertificateDetailsScreen(certificate: CertificateInfo(cert: hostInfo.cert))))
|
context, (context) => CertificateDetailsScreen(certInfo: CertificateInfo(cert: hostInfo.cert))))
|
||||||
: Container(),
|
: Container(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)])
|
"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)],
|
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')]
|
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,56 +1,54 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:barcode_scan/barcode_scan.dart';
|
import 'package:barcode_scan/barcode_scan.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.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/SpecialSelectableText.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.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/ConfigSection.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
|
||||||
import 'package:mobile_nebula/models/Certificate.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/share.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
|
import 'CertificateDetailsScreen.dart';
|
||||||
|
|
||||||
class CertificateResult {
|
class CertificateResult {
|
||||||
CertificateInfo cert;
|
CertificateInfo certInfo;
|
||||||
String key;
|
String key;
|
||||||
|
|
||||||
CertificateResult({this.cert, this.key});
|
CertificateResult({this.certInfo, this.key});
|
||||||
}
|
}
|
||||||
|
|
||||||
class CertificateScreen extends StatefulWidget {
|
class AddCertificateScreen extends StatefulWidget {
|
||||||
const CertificateScreen({Key key, this.cert, this.onSave}) : super(key: key);
|
const AddCertificateScreen({Key key, this.onSave, this.onReplace}) : super(key: key);
|
||||||
|
|
||||||
final CertificateInfo cert;
|
// onSave will pop a new CertificateDetailsScreen
|
||||||
final ValueChanged<CertificateResult> onSave;
|
final ValueChanged<CertificateResult> onSave;
|
||||||
|
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen
|
||||||
|
final ValueChanged<CertificateResult> onReplace;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CertificateScreenState createState() => _CertificateScreenState();
|
_AddCertificateScreenState createState() => _AddCertificateScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CertificateScreenState extends State<CertificateScreen> {
|
class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
String pubKey;
|
String pubKey;
|
||||||
String privKey;
|
String privKey;
|
||||||
bool changed = false;
|
|
||||||
|
|
||||||
CertificateInfo cert;
|
|
||||||
|
|
||||||
String inputType = 'paste';
|
String inputType = 'paste';
|
||||||
bool shared = false;
|
|
||||||
|
|
||||||
final pasteController = TextEditingController();
|
final pasteController = TextEditingController();
|
||||||
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
cert = widget.cert;
|
_generateKeys();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,71 +60,29 @@ class _CertificateScreenState extends State<CertificateScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> items = [];
|
|
||||||
bool hideSave = true;
|
|
||||||
|
|
||||||
if (cert == null) {
|
|
||||||
if (pubKey == null) {
|
if (pubKey == null) {
|
||||||
items = _buildGenerate();
|
return Center(
|
||||||
} else {
|
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
|
||||||
|
return CupertinoProgressIndicatorData(radius: 500);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> items = [];
|
||||||
items.addAll(_buildShare());
|
items.addAll(_buildShare());
|
||||||
items.addAll(_buildLoadCert());
|
items.addAll(_buildLoadCert());
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.addAll(_buildCertList());
|
|
||||||
hideSave = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FormPage(
|
return SimplePage(
|
||||||
title: 'Certificate',
|
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));
|
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 {
|
_generateKeys() async {
|
||||||
try {
|
try {
|
||||||
var kp = await platform.invokeMethod("nebula.generateKeyPair");
|
var kp = await platform.invokeMethod("nebula.generateKeyPair");
|
||||||
Map<String, dynamic> keyPair = jsonDecode(kp);
|
Map<String, dynamic> keyPair = jsonDecode(kp);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
|
||||||
pubKey = keyPair['PublicKey'];
|
pubKey = keyPair['PublicKey'];
|
||||||
privKey = keyPair['PrivateKey'];
|
privKey = keyPair['PrivateKey'];
|
||||||
});
|
});
|
||||||
|
@ -148,9 +104,6 @@ class _CertificateScreenState extends State<CertificateScreen> {
|
||||||
content: Text('Share Public Key'),
|
content: Text('Share Public Key'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
|
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(
|
ConfigButtonItem(
|
||||||
content: Center(child: Text('Load Certificate')),
|
content: Center(child: Text('Load Certificate')),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_addCertEntry(pasteController.text, (err) {
|
_addCertEntry(pasteController.text);
|
||||||
if (err != null) {
|
|
||||||
return Utils.popError(context, 'Failed to parse certificate content', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
pasteController.text = '';
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -225,13 +171,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_addCertEntry(content, (err) {
|
_addCertEntry(content);
|
||||||
if (err != null) {
|
|
||||||
Utils.popError(context, 'Error loading certificate file', err);
|
|
||||||
} else {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return Utils.popError(context, 'Failed to load certificate file', err.toString());
|
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);
|
var result = await BarcodeScanner.scan(options: options);
|
||||||
if (result.rawContent != "") {
|
if (result.rawContent != "") {
|
||||||
_addCertEntry(result.rawContent, (err) {
|
_addCertEntry(result.rawContent);
|
||||||
if (err != null) {
|
|
||||||
Utils.popError(context, 'Error loading certificate content', err);
|
|
||||||
} else {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -268,9 +202,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
_addCertEntry(String rawCert, ValueChanged<String> callback) async {
|
_addCertEntry(String rawCert) async {
|
||||||
String error;
|
|
||||||
|
|
||||||
// Allow for app store review testing cert to override the generated key
|
// Allow for app store review testing cert to override the generated key
|
||||||
if (rawCert.trim() == _testCert) {
|
if (rawCert.trim() == _testCert) {
|
||||||
privKey = _testKey;
|
privKey = _testKey;
|
||||||
|
@ -278,21 +210,36 @@ class _CertificateScreenState extends State<CertificateScreen> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
|
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
|
||||||
|
|
||||||
List<dynamic> certs = jsonDecode(rawCerts);
|
List<dynamic> certs = jsonDecode(rawCerts);
|
||||||
if (certs.length > 0) {
|
if (certs.length > 0) {
|
||||||
var tryCert = CertificateInfo.fromJson(certs.first);
|
var tryCertInfo = CertificateInfo.fromJson(certs.first);
|
||||||
if (tryCert.cert.details.isCa) {
|
if (tryCertInfo.cert.details.isCa) {
|
||||||
return callback('A certificate authority is not appropriate for a client certificate.');
|
return Utils.popError(context, 'Error loading certificate content', 'A certificate authority is not appropriate for a client certificate.');
|
||||||
}
|
|
||||||
//TODO: test that the pubkey matches the privkey
|
} else if (!tryCertInfo.validity.valid) {
|
||||||
cert = tryCert;
|
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity.reason);
|
||||||
}
|
|
||||||
} on PlatformException catch (err) {
|
|
||||||
error = err.details ?? err.message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callback != null) {
|
//TODO: test that the pubkey we generated equals the pub key in the cert
|
||||||
callback(error);
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -77,7 +77,7 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return CertificateDetailsScreen(
|
return CertificateDetailsScreen(
|
||||||
certificate: ca,
|
certInfo: ca,
|
||||||
onDelete: () {
|
onDelete: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
|
@ -2,34 +2,73 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_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/SpecialSelectableText.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
import 'package:mobile_nebula/models/Certificate.dart';
|
import 'package:mobile_nebula/models/Certificate.dart';
|
||||||
|
import 'package:mobile_nebula/screens/siteConfig/AddCertificateScreen.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
/// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert)
|
/// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert)
|
||||||
class CertificateDetailsScreen extends StatefulWidget {
|
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;
|
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
|
@override
|
||||||
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
|
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SimplePage(
|
return FormPage(
|
||||||
title: 'Certificate Details',
|
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: [
|
child: Column(children: [
|
||||||
_buildID(),
|
_buildID(),
|
||||||
_buildFilters(),
|
_buildFilters(),
|
||||||
_buildValid(),
|
_buildValid(),
|
||||||
_buildAdvanced(),
|
_buildAdvanced(),
|
||||||
|
_buildReplace(),
|
||||||
_buildDelete(),
|
_buildDelete(),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
@ -37,17 +76,17 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
|
|
||||||
Widget _buildID() {
|
Widget _buildID() {
|
||||||
return ConfigSection(children: <Widget>[
|
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(
|
ConfigItem(
|
||||||
label: Text('Type'),
|
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() {
|
Widget _buildValid() {
|
||||||
var valid = Text('yes');
|
var valid = Text('yes');
|
||||||
if (widget.certificate.validity != null && !widget.certificate.validity.valid) {
|
if (certInfo.validity != null && !certInfo.validity.valid) {
|
||||||
valid = Text(widget.certificate.validity.valid ? 'yes' : widget.certificate.validity.reason,
|
valid = Text(certInfo.validity.valid ? 'yes' : certInfo.validity.reason,
|
||||||
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
|
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
|
||||||
}
|
}
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
|
@ -56,33 +95,33 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
ConfigItem(label: Text('Valid?'), content: valid),
|
ConfigItem(label: Text('Valid?'), content: valid),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Created'),
|
label: Text('Created'),
|
||||||
content: SpecialSelectableText(widget.certificate.cert.details.notBefore.toLocal().toString())),
|
content: SpecialSelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Expires'),
|
label: Text('Expires'),
|
||||||
content: SpecialSelectableText(widget.certificate.cert.details.notAfter.toLocal().toString())),
|
content: SpecialSelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilters() {
|
Widget _buildFilters() {
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
if (widget.certificate.cert.details.groups.length > 0) {
|
if (certInfo.cert.details.groups.length > 0) {
|
||||||
items.add(ConfigItem(
|
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
|
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(
|
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
|
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();
|
: Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,18 +130,18 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Fingerprint'),
|
label: Text('Fingerprint'),
|
||||||
content: SpecialSelectableText(widget.certificate.cert.fingerprint,
|
content: SpecialSelectableText(certInfo.cert.fingerprint,
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start),
|
crossAxisAlignment: CrossAxisAlignment.start),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Public Key'),
|
label: Text('Public Key'),
|
||||||
content: SpecialSelectableText(widget.certificate.cert.details.publicKey,
|
content: SpecialSelectableText(certInfo.cert.details.publicKey,
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start),
|
crossAxisAlignment: CrossAxisAlignment.start),
|
||||||
widget.certificate.rawCert != null
|
certInfo.rawCert != null
|
||||||
? ConfigItem(
|
? ConfigItem(
|
||||||
label: Text('PEM Format'),
|
label: Text('PEM Format'),
|
||||||
content: SpecialSelectableText(widget.certificate.rawCert,
|
content: SpecialSelectableText(certInfo.rawCert,
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start)
|
crossAxisAlignment: CrossAxisAlignment.start)
|
||||||
: Container(),
|
: 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() {
|
Widget _buildDelete() {
|
||||||
if (widget.onDelete == null) {
|
if (widget.onDelete == null) {
|
||||||
return Container();
|
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(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
|
|
|
@ -13,11 +13,13 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
import 'package:mobile_nebula/models/Site.dart';
|
import 'package:mobile_nebula/models/Site.dart';
|
||||||
import 'package:mobile_nebula/screens/siteConfig/AdvancedScreen.dart';
|
import 'package:mobile_nebula/screens/siteConfig/AdvancedScreen.dart';
|
||||||
import 'package:mobile_nebula/screens/siteConfig/CAListScreen.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/screens/siteConfig/StaticHostsScreen.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
//TODO: Add a config test mechanism
|
//TODO: Add a config test mechanism
|
||||||
|
//TODO: Enforce a name
|
||||||
|
|
||||||
class SiteConfigScreen extends StatefulWidget {
|
class SiteConfigScreen extends StatefulWidget {
|
||||||
const SiteConfigScreen({Key key, this.site, this.onSave}) : super(key: key);
|
const SiteConfigScreen({Key key, this.site, this.onSave}) : super(key: key);
|
||||||
|
@ -100,12 +102,18 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
content: PlatformTextFormField(
|
content: PlatformTextFormField(
|
||||||
placeholder: 'Required',
|
placeholder: 'Required',
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
|
validator: (name) {
|
||||||
|
if (name == null || name == "") {
|
||||||
|
return "A name is required";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
))
|
))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _keys() {
|
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;
|
var caError = site.ca.length == 0;
|
||||||
if (!caError) {
|
if (!caError) {
|
||||||
site.ca.forEach((ca) {
|
site.ca.forEach((ca) {
|
||||||
|
@ -126,16 +134,28 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||||
padding: EdgeInsets.only(right: 5))
|
padding: EdgeInsets.only(right: 5))
|
||||||
: Container(),
|
: Container(),
|
||||||
certError ? Text('Needs attention') : Text(site.cert.cert.details.name)
|
certError ? Text('Needs attention') : Text(site.certInfo.cert.details.name)
|
||||||
]),
|
]),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return CertificateScreen(
|
if (site.certInfo != null) {
|
||||||
cert: site.cert,
|
return CertificateDetailsScreen(
|
||||||
|
certInfo: site.certInfo,
|
||||||
|
onReplace: (result) {
|
||||||
|
setState(() {
|
||||||
|
changed = true;
|
||||||
|
site.certInfo = result.certInfo;
|
||||||
|
site.key = result.key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddCertificateScreen(
|
||||||
onSave: (result) {
|
onSave: (result) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
site.cert = result.cert;
|
site.certInfo = result.certInfo;
|
||||||
site.key = result.key;
|
site.key = result.key;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue