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:
Nate Brown 2022-08-05 16:42:17 -05:00 committed by GitHub
parent 457952b5ed
commit 958b15d711
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 39 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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));
}
}

View File

@ -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));
});
});
}

View File

@ -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, );
});
})));
}

View File

@ -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);
}
}
}

View File

@ -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());
}

View File

@ -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
}