This commit is contained in:
Nate Brown 2021-04-29 16:39:02 -05:00
parent 4c28cc196e
commit 41a382d218
15 changed files with 403 additions and 211 deletions

View File

@ -59,7 +59,7 @@ class NebulaVpnService : VpnService() {
// Link active site config in Main to avoid this
site = Site(File(path))
if (site!!.cert == null) {
if (site!!.certInfos == null || site!!.certInfos.isEmpty()) {
announceExit(id, "Site is missing a certificate")
//TODO: can we signal failure?
return super.onStartCommand(intent, flags, startId)
@ -74,7 +74,7 @@ class NebulaVpnService : VpnService() {
var ipNet: CIDR
try {
ipNet = mobileNebula.MobileNebula.parseCIDR(site!!.cert!!.cert.details.ips[0])
ipNet = mobileNebula.MobileNebula.parseCIDR(site!!.primaryCertInfo!!.cert.details.ips[0])
} catch (err: Exception) {
return announceExit(site!!.id, err.message ?: "$err")
}

View File

@ -46,7 +46,7 @@ class Sites(private var engine: FlutterEngine) {
this.sites[site.id] = SiteContainer(site, updater)
} catch (err: Exception) {
siteDir.deleteRecursively()
// siteDir.deleteRecursively()
Log.e(TAG, "Deleting non conforming site ${siteDir.absolutePath}", err)
}
}
@ -101,9 +101,10 @@ class SiteUpdater(private var site: Site, engine: FlutterEngine): EventChannel.S
}
data class CertificateInfo(
@SerializedName("Cert") val cert: Certificate,
@SerializedName("RawCert") val rawCert: String,
@SerializedName("Validity") val validity: CertificateValidity
@SerializedName("Cert") val cert: Certificate,
@SerializedName("RawCert") val rawCert: String,
@SerializedName("Validity") val validity: CertificateValidity,
var primary: Boolean
)
data class Certificate(
@ -134,8 +135,8 @@ class Site {
val id: String
val staticHostmap: HashMap<String, StaticHosts>
val unsafeRoutes: List<UnsafeRoute>
var cert: CertificateInfo? = null
var ca: Array<CertificateInfo>
val certInfos: ArrayList<CertificateInfo> = ArrayList()
lateinit var caInfos: Array<CertificateInfo>
val lhDuration: Int
val port: Int
val mtu: Int
@ -154,6 +155,9 @@ class Site {
// Strong representation of the site config
@Expose(serialize = false)
val config: String
@Expose(serialize = false)
lateinit var primaryCertInfo: CertificateInfo
constructor(siteDir: File) {
val gson = Gson()
@ -176,26 +180,23 @@ class Site {
connected = false
status = "Disconnected"
try {
val rawDetails = mobileNebula.MobileNebula.parseCerts(incomingSite.cert)
val certs = gson.fromJson(rawDetails, Array<CertificateInfo>::class.java)
if (certs.isEmpty()) {
throw IllegalArgumentException("No certificate found")
incomingSite.certs?.forEach { certContainer ->
val certInfo = getCertDetails(certContainer.cert, gson) ?: return
certInfo.primary = certContainer.primary
if (certInfo.primary) {
this.primaryCertInfo = certInfo
}
cert = certs[0]
if (!cert!!.validity.valid) {
errors.add("Certificate is invalid: ${cert!!.validity.reason}")
}
} catch (err: Exception) {
errors.add("Error while loading certificate: ${err.message}")
this.certInfos.add(certInfo)
}
// Upgrade the old cert property if present
upgradeCert(incomingSite, gson)
try {
val rawCa = mobileNebula.MobileNebula.parseCerts(incomingSite.ca)
ca = gson.fromJson(rawCa, Array<CertificateInfo>::class.java)
caInfos = gson.fromJson(rawCa, Array<CertificateInfo>::class.java)
var hasErrors = false
ca.forEach {
caInfos.forEach {
if (!it.validity.valid) {
hasErrors = true
}
@ -206,7 +207,7 @@ class Site {
}
} catch (err: Exception) {
ca = arrayOf()
caInfos = arrayOf()
errors.add("Error while loading certificate authorities: ${err.message}")
}
@ -219,8 +220,65 @@ class Site {
}
}
// Upgrades cert -> certs in the stored site config if needed
private fun upgradeCert(site: IncomingSite, gson: Gson) {
if (site.cert == null) {
// Nothing to do
return
}
val context = MainActivity.getContext()!!
// Try to get the details
val certInfo = getCertDetails(site.cert!!, gson) ?: return
// Push this cert in as the primary certificate
certInfo.primary
certInfos.add(certInfo)
// Upgrade the persisted object
site.cert = null
site.certs = arrayListOf(IncomingCert(certInfo.cert.fingerprint, certInfo.rawCert,true, null))
// Get the old key contents and delete the key
val oldKeyPath = File(path).resolve("key")
val oldKeyFile = EncFile(context).openRead(oldKeyPath)
val key = oldKeyFile.readText()
oldKeyFile.close()
oldKeyPath.delete()
// /data/data/net.defined.mobile_nebula/files/sites/8554c7fc-c2a5-4cba-9bc4-fe6e0eb3129a
// Write to the new path
val newKeyFile = EncFile(context).openWrite(File(path).resolve("key.${certInfo.cert.fingerprint}"))
newKeyFile.use { it.write(key) }
newKeyFile.close()
site.save(context)
}
private fun getCertDetails(rawCert: String, gson: Gson): CertificateInfo? {
var cert: CertificateInfo? = null
try {
val rawDetails = mobileNebula.MobileNebula.parseCerts(rawCert)
val certs = gson.fromJson(rawDetails, Array<CertificateInfo>::class.java)
if (certs.isEmpty()) {
throw IllegalArgumentException("No certificate found")
}
cert = certs[0]
if (!cert.validity.valid) {
errors.add("Certificate is invalid: ${cert.validity.reason}")
}
} catch (err: Exception) {
errors.add("Error while loading certificate: ${err.message}")
}
return cert
}
fun getKey(context: Context): String? {
val f = EncFile(context).openRead(File(path).resolve("key"))
val f = EncFile(context).openRead(File(path).resolve("key.${primaryCertInfo.cert.fingerprint}"))
val k = f.readText()
f.close()
return k
@ -238,12 +296,21 @@ data class UnsafeRoute(
val mtu: Int?
)
data class IncomingCert(
// fingerprint is the cert fingerprint, only used as part of the key file name
val fingerprint: String,
val cert: String,
val primary: Boolean,
@Expose(serialize = false)
var key: String?
)
class IncomingSite(
val name: String,
val id: String,
val staticHostmap: HashMap<String, StaticHosts>,
val unsafeRoutes: List<UnsafeRoute>?,
val cert: String,
var certs: List<IncomingCert>?,
val ca: String,
val lhDuration: Int,
val port: Int,
@ -251,8 +318,9 @@ class IncomingSite(
val cipher: String,
val sortKey: Int?,
var logVerbosity: String?,
@Expose(serialize = false)
var key: String?
@Deprecated("certs is the new property")
var cert: String?
) {
fun save(context: Context) {
@ -261,13 +329,16 @@ class IncomingSite(
siteDir.mkdir()
}
if (key != null) {
val f = EncFile(context).openWrite(siteDir.resolve("key"))
f.use { it.write(key) }
f.close()
certs?.forEach { cert ->
if (cert.key != null) {
val f = EncFile(context).openWrite(siteDir.resolve("key.${cert.fingerprint}"))
f.use { it.write(cert.key) }
f.close()
}
cert.key = null
}
key = null
val gson = Gson()
val confFile = siteDir.resolve("config.json")
confFile.writeText(gson.toJson(this))

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.primaryCertInfo.cert != null && site.primaryCertInfo.cert.details.ips.length > 0) {
ip = site.primaryCertInfo.cert.details.ips[0];
}
return SpecialButton(

View File

@ -620,7 +620,7 @@ class _SpecialSelectableTextState extends State<SpecialSelectableText> with Auto
toolbarOptions: widget.toolbarOptions,
minLines: widget.minLines,
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
selectionColor: themeData.textSelectionColor,
selectionColor: TextSelectionTheme.of(context).selectionColor,
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
onSelectionChanged: _handleSelectionChanged,
onSelectionHandleTapped: _handleSelectionHandleTapped,

View File

@ -4,7 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
TextStyle basicTextStyle(BuildContext context) =>
Platform.isIOS ? CupertinoTheme.of(context).textTheme.textStyle : Theme.of(context).textTheme.subhead;
Platform.isIOS ? CupertinoTheme.of(context).textTheme.textStyle : Theme.of(context).textTheme.subtitle1;
const double _headerFontSize = 13.0;

View File

@ -2,6 +2,10 @@ class CertificateInfo {
Certificate cert;
String rawCert;
CertificateValidity validity;
bool primary;
// Key is only present when a new certificate is being installed, provided to the backend by the UI
String key;
CertificateInfo.debug({this.rawCert = ""})
: this.cert = Certificate.debug(),
@ -10,6 +14,7 @@ class CertificateInfo {
CertificateInfo.fromJson(Map<String, dynamic> json)
: cert = Certificate.fromJson(json['Cert']),
rawCert = json['RawCert'],
primary = json['primary'],
validity = CertificateValidity.fromJson(json['Validity']);
CertificateInfo({this.cert, this.rawCert, this.validity});
@ -17,6 +22,15 @@ class CertificateInfo {
static List<CertificateInfo> fromJsonList(List<dynamic> list) {
return list.map((v) => CertificateInfo.fromJson(v));
}
Map<String, dynamic> toJson() {
return {
'cert': rawCert,
'key': key,
'primary': primary,
'fingerprint': cert.fingerprint
};
}
}
class Certificate {
@ -80,4 +94,4 @@ class CertificateValidity {
CertificateValidity.fromJson(Map<String, dynamic> json)
: valid = json['Valid'],
reason = json['Reason'];
}
}

View File

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

View File

@ -26,9 +26,9 @@ class Site {
List<UnsafeRoute> unsafeRoutes;
// pki fields
List<CertificateInfo> ca;
CertificateInfo cert;
String key;
List<CertificateInfo> caInfos;
List<CertificateInfo> certInfos;
CertificateInfo primaryCertInfo;
// lighthouse options
int lhDuration; // in seconds
@ -51,8 +51,8 @@ class Site {
{this.name,
id,
staticHostmap,
ca,
this.cert,
caInfos,
certInfos,
this.lhDuration = 0,
this.port = 0,
this.cipher = "aes",
@ -67,7 +67,8 @@ class Site {
: staticHostmap = staticHostmap ?? {},
unsafeRoutes = unsafeRoutes ?? [],
errors = errors ?? [],
ca = ca ?? [],
caInfos = caInfos ?? [],
certInfos = certInfos ?? [],
id = id ?? uuid.v4();
Site.fromJson(Map<String, dynamic> json) {
@ -88,15 +89,21 @@ class Site {
});
}
List<dynamic> rawCA = json['ca'];
ca = [];
List<dynamic> rawCA = json['caInfos'];
caInfos = [];
rawCA.forEach((val) {
ca.add(CertificateInfo.fromJson(val));
caInfos.add(CertificateInfo.fromJson(val));
});
if (json['cert'] != null) {
cert = CertificateInfo.fromJson(json['cert']);
}
List<dynamic> rawCerts = json['certInfos'];
certInfos = [];
rawCerts.forEach((val) {
final certInfo = CertificateInfo.fromJson(val);
if (certInfo.primary) {
primaryCertInfo = certInfo;
}
certInfos.add(certInfo);
});
lhDuration = json['lhDuration'];
port = json['port'];
@ -142,12 +149,11 @@ class Site {
'id': id,
'staticHostmap': staticHostmap,
'unsafeRoutes': unsafeRoutes,
'ca': ca?.map((cert) {
'ca': caInfos?.map((cert) {
return cert.rawCert;
})?.join('\n') ??
"",
'cert': cert?.rawCert,
'key': key,
'certs': certInfos,
'lhDuration': lhDuration,
'port': port,
'mtu': mtu,

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(CertificateInfo(cert: hostInfo.cert))))
: Container(),
]);
}

View File

@ -73,7 +73,7 @@ class _MainScreenState extends State<MainScreen> {
if (!ready) {
return Center(
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 50);
return CupertinoProgressIndicatorData(radius: 500);
}),
);
}
@ -172,21 +172,23 @@ CjkKB3Rlc3QgY2EopYyK9wUwpfOOhgY6IHj4yrtHbq+rt4hXTYGrxuQOS0412uKT
mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
-----END NEBULA CERTIFICATE-----''';
var certInfo = CertificateInfo.debug(rawCert: cert);
certInfo.primary = true;
certInfo.key = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
rmXnR1yvDZi1VPVmnNVY8NMsQpEpbbYlq7rul+ByQvg=
-----END NEBULA X25519 PRIVATE KEY-----''';
var s = Site(
name: "DEBUG TEST",
id: uuid.v4(),
staticHostmap: {
"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),
caInfos: [CertificateInfo.debug(rawCert: ca)],
certInfos: [certInfo],
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
);
s.key = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
rmXnR1yvDZi1VPVmnNVY8NMsQpEpbbYlq7rul+ByQvg=
-----END NEBULA X25519 PRIVATE KEY-----''';
var err = await s.save();
if (err != null) {
Utils.popError(context, "Failed to save the site", err);

View File

@ -1,56 +1,47 @@
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';
class CertificateResult {
CertificateInfo cert;
String key;
import 'CertificateDetailsScreen.dart';
CertificateResult({this.cert, this.key});
}
class AddCertificateScreen extends StatefulWidget {
const AddCertificateScreen({Key key, this.onSave, this.choosePrimary = false}) : super(key: key);
class CertificateScreen extends StatefulWidget {
const CertificateScreen({Key key, this.cert, this.onSave}) : super(key: key);
final CertificateInfo cert;
final ValueChanged<CertificateResult> onSave;
final ValueChanged<CertificateInfo> onSave;
final choosePrimary;
@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;
CertificateInfo certInfo;
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 +53,30 @@ 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 {
items.addAll(_buildShare());
items.addAll(_buildLoadCert());
}
} else {
items.addAll(_buildCertList());
hideSave = false;
if (pubKey == null) {
return Center(
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 500);
}),
);
}
return FormPage(
List<Widget> items = [];
items.addAll(_buildShare());
items.addAll(_buildLoadCert());
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 +98,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 +145,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 +165,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 +188,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 +196,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;
@ -280,20 +206,31 @@ class _CertificateScreenState extends State<CertificateScreen> {
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.');
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);
}
//TODO: test that the pubkey matches the privkey
cert = tryCert;
//TODO: test that the pubkey we generated equals the pub key in the cert
certInfo = tryCertInfo;
certInfo.primary = !widget.choosePrimary;
certInfo.key = privKey;
}
} on PlatformException catch (err) {
error = err.details ?? err.message;
return Utils.popError(context, 'Error loading certificate content', err.details ?? err.message);
}
if (callback != null) {
callback(error);
}
// We have a cert, pop this screen and replace it with the cert detail screen and a save dialog
Utils.openPage(context, (context) {
//TODO: thread on save
return CertificateDetailsScreen(certInfo, newCert: true, choosePrimary: widget.choosePrimary, onSave: (isPrimary) {
Navigator.pop(context);
certInfo.primary = isPrimary;
widget.onSave(certInfo);
});
});
}
}

View File

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

View File

@ -2,30 +2,50 @@ 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/services/utils.dart';
//TODO: add a primary toggle if we have multiple sites
/// 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(this.certInfo, {Key key, this.onDelete, this.onSave, this.newCert = false, this.choosePrimary = false}) : super(key: key);
final CertificateInfo certificate;
final CertificateInfo certInfo;
final Function onDelete;
final ValueChanged<bool> onSave;
final bool newCert;
final bool choosePrimary;
@override
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
}
class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
bool primary;
bool changed = false;
@override
void initState() {
primary = widget.certInfo.primary ?? false;
super.initState();
}
@override
Widget build(BuildContext context) {
return SimplePage(
return FormPage(
title: 'Certificate Details',
onSave: () {
Navigator.pop(context);
widget.onSave(widget.choosePrimary == true ? primary : false);
},
changed: widget.newCert || changed,
child: Column(children: [
widget.choosePrimary ? _buildPrimaryChooser() : Container(),
_buildID(),
_buildFilters(),
_buildValid(),
@ -35,19 +55,37 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
);
}
Widget _buildPrimaryChooser() {
return ConfigSection(children: <Widget>[
ConfigItem(
label: Text('Primary Certificate'),
content: Container(
alignment: Alignment.centerRight,
child: Switch.adaptive(
value: primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (v) {
setState(() {
changed = true;
primary = v;
});
})),
)]);
}
Widget _buildID() {
return ConfigSection(children: <Widget>[
ConfigItem(label: Text('Name'), content: SpecialSelectableText(widget.certificate.cert.details.name)),
ConfigItem(label: Text('Name'), content: SpecialSelectableText(widget.certInfo.cert.details.name)),
ConfigItem(
label: Text('Type'),
content: Text(widget.certificate.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
content: Text(widget.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 (widget.certInfo.validity != null && !widget.certInfo.validity.valid) {
valid = Text(widget.certInfo.validity.valid ? 'yes' : widget.certInfo.validity.reason,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
}
return ConfigSection(
@ -56,33 +94,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(widget.certInfo.cert.details.notBefore.toLocal().toString())),
ConfigItem(
label: Text('Expires'),
content: SpecialSelectableText(widget.certificate.cert.details.notAfter.toLocal().toString())),
content: SpecialSelectableText(widget.certInfo.cert.details.notAfter.toLocal().toString())),
],
);
}
Widget _buildFilters() {
List<Widget> items = [];
if (widget.certificate.cert.details.groups.length > 0) {
if (widget.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(widget.certInfo.cert.details.groups.join(', '))));
}
if (widget.certificate.cert.details.ips.length > 0) {
if (widget.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(widget.certInfo.cert.details.ips.join(', '))));
}
if (widget.certificate.cert.details.subnets.length > 0) {
if (widget.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(widget.certInfo.cert.details.subnets.join(', '))));
}
return items.length > 0
? ConfigSection(label: widget.certificate.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
? ConfigSection(label: widget.certInfo.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
: Container();
}
@ -91,18 +129,18 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
children: <Widget>[
ConfigItem(
label: Text('Fingerprint'),
content: SpecialSelectableText(widget.certificate.cert.fingerprint,
content: SpecialSelectableText(widget.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(widget.certInfo.cert.details.publicKey,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start),
widget.certificate.rawCert != null
widget.certInfo.rawCert != null
? ConfigItem(
label: Text('PEM Format'),
content: SpecialSelectableText(widget.certificate.rawCert,
content: SpecialSelectableText(widget.certInfo.rawCert,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start)
: Container(),
@ -115,7 +153,7 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
return Container();
}
var title = widget.certificate.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
var title = widget.certInfo.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
return Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),

View File

@ -0,0 +1,115 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/FormPage.dart';
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Certificate.dart';
import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart';
import 'package:mobile_nebula/services/utils.dart';
import 'AddCertificateScreen.dart';
class CertificatesScreen extends StatefulWidget {
const CertificatesScreen({Key key, this.site, this.onSave}) : super(key: key);
final Site site;
final ValueChanged<Site> onSave;
@override
_CertificatesScreenState createState() => _CertificatesScreenState();
}
class _CertificatesScreenState extends State<CertificatesScreen> {
Site site;
CertificateInfo primary;
bool changed = false;
@override
void initState() {
site = widget.site;
super.initState();
}
@override
Widget build(BuildContext context) {
var items = _buildCertList();
items.add(ConfigButtonItem(
content: Text('Add a certificate'),
onPressed: () {
Utils.openPage(context, (context) {
//TODO: thread through the primary choice
return AddCertificateScreen(choosePrimary: true, onSave: (certInfo) {
setState(() {
changed = true;
site.certInfos.add(certInfo);
if (certInfo.primary) {
_setPrimary(certInfo);
}
});
});
});
},
));
return FormPage(
title: 'Certificates',
changed: changed,
onSave: () {
Navigator.pop(context);
if (widget.onSave != null) {
widget.onSave(site);
}
},
child: ConfigSection(children: items));
}
List<Widget> _buildCertList() {
List<Widget> list = [];
site.certInfos.forEach((certInfo) {
var title = certInfo.cert.details.name;
if (certInfo.primary ?? false) {
title += " (primary)";
}
list.add(ConfigPageItem(
content: Text(title),
onPressed: () {
Utils.openPage(context, (context) {
return CertificateDetailsScreen(
certInfo,
choosePrimary: site.certInfos.length > 1,
onSave: (isPrimary) {
if (isPrimary) {
_setPrimary(certInfo);
}
},
onDelete: () {
setState(() {
changed = true;
site.certInfos.remove(certInfo);
if (primary.cert.fingerprint)
});
}
);
});
},
));
});
return list;
}
_setPrimary(CertificateInfo certInfo) {
// Turn every certInfo object to non primary
site.certInfos.forEach((certInfo) {
certInfo.primary = false;
});
// Flip this new primary on
certInfo.primary = true;
site.primaryCertInfo = certInfo;
setState(() {});
}
}

View File

@ -11,9 +11,10 @@ import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/screens/siteConfig/AddCertificateScreen.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/CertificatesScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/StaticHostsScreen.dart';
import 'package:mobile_nebula/services/utils.dart';
@ -105,10 +106,10 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
}
Widget _keys() {
final certError = site.cert == null || !site.cert.validity.valid;
var caError = site.ca.length == 0;
final certError = site.primaryCertInfo == null || !site.primaryCertInfo.validity.valid;
var caError = site.caInfos.length == 0;
if (!caError) {
site.ca.forEach((ca) {
site.caInfos.forEach((ca) {
if (!ca.validity.valid) {
caError = true;
}
@ -126,19 +127,29 @@ 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.primaryCertInfo.cert.details.name)
]),
onPressed: () {
Utils.openPage(context, (context) {
return CertificateScreen(
cert: site.cert,
onSave: (result) {
setState(() {
changed = true;
site.cert = result.cert;
site.key = result.key;
if (site.certInfos.length > 0) {
return CertificatesScreen(
site: site,
onSave: (newSite) {
setState(() {
changed = true;
site = site;
});
});
});
}
return AddCertificateScreen(onSave: (certInfo) {
//TODO: onSave we may want to route them to the certificates screen somehow
changed = true;
certInfo.primary = true;
site.primaryCertInfo = certInfo;
site.certInfos.add(certInfo);
setState(() {});
});
});
},
),
@ -151,16 +162,16 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5))
: Container(),
caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.ca.length))
caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.caInfos.length))
]),
onPressed: () {
Utils.openPage(context, (context) {
return CAListScreen(
cas: site.ca,
cas: site.caInfos,
onSave: (ca) {
setState(() {
changed = true;
site.ca = ca;
site.caInfos = ca;
});
});
});