Allow import of private key, make it so key material isn't removed when navigating off the add a cert page (#64)
This commit is contained in:
parent
457952b5ed
commit
958b15d711
|
@ -49,6 +49,7 @@ class MainActivity: FlutterActivity() {
|
|||
"nebula.parseCerts" -> nebulaParseCerts(call, result)
|
||||
"nebula.generateKeyPair" -> nebulaGenerateKeyPair(result)
|
||||
"nebula.renderConfig" -> nebulaRenderConfig(call, result)
|
||||
"nebula.verifyCertAndKey" -> nebulaVerifyCertAndKey(call, result)
|
||||
|
||||
"listSites" -> listSites(result)
|
||||
"deleteSite" -> deleteSite(call, result)
|
||||
|
@ -104,6 +105,25 @@ class MainActivity: FlutterActivity() {
|
|||
return result.success(yaml)
|
||||
}
|
||||
|
||||
private fun nebulaVerifyCertAndKey(call: MethodCall, result: MethodChannel.Result) {
|
||||
val cert = call.argument<String>("cert")
|
||||
if (cert == "") {
|
||||
return result.error("required_argument", "cert is a required argument", null)
|
||||
}
|
||||
|
||||
val key = call.argument<String>("key")
|
||||
if (key == "") {
|
||||
return result.error("required_argument", "key is a required argument", null)
|
||||
}
|
||||
|
||||
return try {
|
||||
val json = mobileNebula.MobileNebula.verifyCertAndKey(cert, key)
|
||||
result.success(json)
|
||||
} catch (err: Exception) {
|
||||
result.error("unhandled_error", err.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listSites(result: MethodChannel.Result) {
|
||||
sites!!.refreshSites(activeSiteId)
|
||||
val sites = sites!!.getSites()
|
||||
|
|
|
@ -34,6 +34,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
|
||||
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
|
||||
case "nebula.renderConfig": return self.nebulaRenderConfig(call: call, result: result)
|
||||
case "nebula.verifyCertAndKey": return self.nebulaVerifyCertAndKey(call: call, result: result)
|
||||
|
||||
case "listSites": return self.listSites(result: result)
|
||||
case "deleteSite": return self.deleteSite(call: call, result: result)
|
||||
|
@ -71,6 +72,21 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
return result(json)
|
||||
}
|
||||
|
||||
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
|
||||
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||
guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) }
|
||||
guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) }
|
||||
|
||||
var err: NSError?
|
||||
var validd: ObjCBool = false
|
||||
let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err)
|
||||
if (err != nil) {
|
||||
return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription))
|
||||
}
|
||||
|
||||
return result(valid)
|
||||
}
|
||||
|
||||
func nebulaGenerateKeyPair(result: FlutterResult) {
|
||||
var err: NSError?
|
||||
let kp = MobileNebulaGenerateKeyPair(&err)
|
||||
|
|
|
@ -5,10 +5,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:mobile_nebula/components/SpecialTextField.dart';
|
||||
|
||||
class ConfigTextItem extends StatelessWidget {
|
||||
const ConfigTextItem({Key key, this.placeholder, this.controller}) : super(key: key);
|
||||
const ConfigTextItem({Key key, this.placeholder, this.controller, this.style = const TextStyle(fontFamily: 'RobotoMono')}) : super(key: key);
|
||||
|
||||
final String placeholder;
|
||||
final TextEditingController controller;
|
||||
final TextStyle style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -19,7 +20,7 @@ class ConfigTextItem extends StatelessWidget {
|
|||
minLines: 3,
|
||||
maxLines: 10,
|
||||
placeholder: placeholder,
|
||||
style: TextStyle(fontFamily: 'RobotoMono'),
|
||||
style: style,
|
||||
controller: controller));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,69 +24,54 @@ class CertificateResult {
|
|||
}
|
||||
|
||||
class AddCertificateScreen extends StatefulWidget {
|
||||
const AddCertificateScreen({Key key, this.onSave, this.onReplace}) : super(key: key);
|
||||
const AddCertificateScreen({Key key, this.onSave, this.onReplace, this.pubKey, this.privKey}) : super(key: key);
|
||||
|
||||
// onSave will pop a new CertificateDetailsScreen
|
||||
final ValueChanged<CertificateResult> onSave;
|
||||
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen
|
||||
final ValueChanged<CertificateResult> onReplace;
|
||||
|
||||
final String pubKey;
|
||||
final String privKey;
|
||||
|
||||
@override
|
||||
_AddCertificateScreenState createState() => _AddCertificateScreenState();
|
||||
}
|
||||
|
||||
class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||
String pubKey;
|
||||
String privKey;
|
||||
bool showKey = false;
|
||||
|
||||
String inputType = 'paste';
|
||||
|
||||
final keyController = TextEditingController();
|
||||
final pasteController = TextEditingController();
|
||||
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_generateKeys();
|
||||
pubKey = widget.pubKey;
|
||||
keyController.text = widget.privKey;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pasteController.dispose();
|
||||
keyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (pubKey == null) {
|
||||
return Center(
|
||||
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
|
||||
return CupertinoProgressIndicatorData(radius: 500);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> items = [];
|
||||
items.addAll(_buildShare());
|
||||
items.add(_buildKey());
|
||||
items.addAll(_buildLoadCert());
|
||||
|
||||
return SimplePage(title: 'Certificate', child: Column(children: items));
|
||||
}
|
||||
|
||||
_generateKeys() async {
|
||||
try {
|
||||
var kp = await platform.invokeMethod("nebula.generateKeyPair");
|
||||
Map<String, dynamic> keyPair = jsonDecode(kp);
|
||||
|
||||
setState(() {
|
||||
pubKey = keyPair['PublicKey'];
|
||||
privKey = keyPair['PrivateKey'];
|
||||
});
|
||||
} on PlatformException catch (err) {
|
||||
Utils.popError(context, 'Failed to generate key pair', err.details ?? err.message);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildShare() {
|
||||
return [
|
||||
ConfigSection(
|
||||
|
@ -136,6 +121,33 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
return items;
|
||||
}
|
||||
|
||||
Widget _buildKey() {
|
||||
if (!showKey) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 20, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: Text('Show/Import Private Key'),
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
onPressed: () => Utils.confirmDelete(context, 'Show/Import Private Key?', () {
|
||||
setState(() {
|
||||
showKey = true;
|
||||
});
|
||||
}, deleteLabel: 'Yes'))));
|
||||
}
|
||||
|
||||
return ConfigSection(
|
||||
label: 'Import a private key generated on another device',
|
||||
children: [
|
||||
ConfigTextItem(
|
||||
controller: keyController,
|
||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _addPaste() {
|
||||
return [
|
||||
ConfigSection(
|
||||
|
@ -201,7 +213,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
_addCertEntry(String rawCert) async {
|
||||
// Allow for app store review testing cert to override the generated key
|
||||
if (rawCert.trim() == _testCert) {
|
||||
privKey = _testKey;
|
||||
keyController.text = _testKey;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -217,12 +229,20 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity.reason);
|
||||
}
|
||||
|
||||
//TODO: test that the pubkey we generated equals the pub key in the cert
|
||||
var certMatch = await platform.invokeMethod(
|
||||
"nebula.verifyCertAndKey",
|
||||
<String, String>{"cert": rawCert, "key": keyController.text}
|
||||
);
|
||||
if (!certMatch) {
|
||||
// The method above will throw if there is a mismatch, this is just here in case we introduce a bug in the future
|
||||
return Utils.popError(context, 'Error loading certificate content',
|
||||
'The provided certificates public key is not compatible with the private key.');
|
||||
}
|
||||
|
||||
// If we are replacing we just return the results now
|
||||
if (widget.onReplace != null) {
|
||||
Navigator.pop(context);
|
||||
widget.onReplace(CertificateResult(certInfo: tryCertInfo, key: privKey));
|
||||
widget.onReplace(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -232,7 +252,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
certInfo: tryCertInfo,
|
||||
onSave: () {
|
||||
Navigator.pop(context);
|
||||
widget.onSave(CertificateResult(certInfo: tryCertInfo, key: privKey));
|
||||
widget.onSave(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'package:mobile_nebula/services/utils.dart';
|
|||
|
||||
/// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert)
|
||||
class CertificateDetailsScreen extends StatefulWidget {
|
||||
const CertificateDetailsScreen({Key key, this.certInfo, this.onDelete, this.onSave, this.onReplace})
|
||||
const CertificateDetailsScreen({Key key, this.certInfo, this.onDelete, this.onSave, this.onReplace, this.pubKey, this.privKey})
|
||||
: super(key: key);
|
||||
|
||||
final CertificateInfo certInfo;
|
||||
|
@ -24,6 +24,9 @@ class CertificateDetailsScreen extends StatefulWidget {
|
|||
// onReplace is used to install a new certificate over top of the old one
|
||||
final ValueChanged<CertificateResult> onReplace;
|
||||
|
||||
final String pubKey;
|
||||
final String privKey;
|
||||
|
||||
@override
|
||||
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
|
||||
}
|
||||
|
@ -164,7 +167,7 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
|||
// Slam the page back to the top
|
||||
controller.animateTo(0,
|
||||
duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
|
||||
});
|
||||
}, pubKey: widget.pubKey, privKey: widget.privKey, );
|
||||
});
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'dart:convert';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' as fpw;
|
||||
import 'package:mobile_nebula/components/FormPage.dart';
|
||||
import 'package:mobile_nebula/components/PlatformTextFormField.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||
|
@ -36,11 +38,16 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
bool newSite = false;
|
||||
bool debug = false;
|
||||
Site site;
|
||||
String pubKey;
|
||||
String privKey;
|
||||
|
||||
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
||||
final nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//NOTE: this is slightly wasteful since a keypair will be generated every time this page is opened
|
||||
_generateKeys();
|
||||
if (widget.site == null) {
|
||||
newSite = true;
|
||||
site = Site();
|
||||
|
@ -54,6 +61,14 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (pubKey == null) {
|
||||
return Center(
|
||||
child: fpw.PlatformCircularProgressIndicator(cupertino: (_, __) {
|
||||
return fpw.CupertinoProgressIndicatorData(radius: 50);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return FormPage(
|
||||
title: newSite ? 'New Site' : 'Edit Site',
|
||||
changed: changed,
|
||||
|
@ -121,7 +136,7 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
return ConfigSection(
|
||||
return ConfigSection(
|
||||
label: "IDENTITY",
|
||||
children: [
|
||||
ConfigPageItem(
|
||||
|
@ -139,6 +154,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
if (site.certInfo != null) {
|
||||
return CertificateDetailsScreen(
|
||||
certInfo: site.certInfo,
|
||||
pubKey: pubKey,
|
||||
privKey: privKey,
|
||||
onReplace: (result) {
|
||||
setState(() {
|
||||
changed = true;
|
||||
|
@ -148,7 +165,7 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
return AddCertificateScreen(onSave: (result) {
|
||||
return AddCertificateScreen(pubKey: pubKey, privKey: privKey, onSave: (result) {
|
||||
setState(() {
|
||||
changed = true;
|
||||
site.certInfo = result.certInfo;
|
||||
|
@ -243,4 +260,18 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
_generateKeys() async {
|
||||
try {
|
||||
var kp = await platform.invokeMethod("nebula.generateKeyPair");
|
||||
Map<String, dynamic> keyPair = jsonDecode(kp);
|
||||
|
||||
setState(() {
|
||||
pubKey = keyPair['PublicKey'];
|
||||
privKey = keyPair['PrivateKey'];
|
||||
});
|
||||
} on PlatformException catch (err) {
|
||||
Utils.popError(context, 'Failed to generate key pair', err.details ?? err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,11 @@ class Utils {
|
|||
|
||||
static Widget trailingSaveWidget(BuildContext context, Function onPressed) {
|
||||
return CupertinoButton(
|
||||
child: Text('Save', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
child: Text('Save', style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
//TODO: For some reason on android if inherit is the default of true the text color here turns to the background color
|
||||
inherit: Platform.isIOS ? true : false
|
||||
)),
|
||||
padding: Platform.isAndroid ? null : EdgeInsets.zero,
|
||||
onPressed: () => onPressed());
|
||||
}
|
||||
|
|
|
@ -250,6 +250,20 @@ func x25519Keypair() ([]byte, []byte, error) {
|
|||
return pubkey[:], privkey[:], nil
|
||||
}
|
||||
|
||||
//func VerifyCertAndKey(cert string, key string) (string, error) {
|
||||
//
|
||||
//}
|
||||
func VerifyCertAndKey(rawCert string, pemPrivateKey string) (bool, error) {
|
||||
rawKey, _, err := cert.UnmarshalX25519PrivateKey([]byte(pemPrivateKey))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error while unmarshaling private key: %s", err)
|
||||
}
|
||||
|
||||
nebulaCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(rawCert))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error while unmarshaling cert: %s", err)
|
||||
}
|
||||
|
||||
if err = nebulaCert.VerifyPrivateKey(rawKey); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue