forked from core/mobile_nebula
Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Nate Brown | 41a382d218 |
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
class IPAndPort {
|
||||
String ip;
|
||||
int port;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ class _CAListScreenState extends State<CAListScreen> {
|
|||
onPressed: () {
|
||||
Utils.openPage(context, (context) {
|
||||
return CertificateDetailsScreen(
|
||||
certificate: ca,
|
||||
ca,
|
||||
onDelete: () {
|
||||
setState(() {
|
||||
changed = true;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(() {});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue