mobile_nebula/lib/screens/siteConfig/AddCertificateScreen.dart

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

302 lines
9.6 KiB
Dart
Raw Permalink Normal View History

Update Gradle, replace QR code reader library (#162) I ran the Gradle Upgrade Assistant to get us on the latest version of gradle, but two of our dependencies didn't support it. - https://pub.dev/documentation/package_info/latest/ - https://github.com/AmolGangadhare/flutter_barcode_scanner `pacakge_info` is officially deprecated and replaced by `package_info_plus`, which is what I've swapped to here. A bigger change was switching to https://github.com/juliansteenbakker/mobile_scanner. It does seem to work a bit better than the other one, and does not throw an error now when cancelling the QR code collection, as it did before. I've tested on android in the simulator, and iOS with an actual device. To test adding a cert: 1. Create a CA on your computer with `nebula-cert ca -name test-mobile`. This will create a `ca.crt` and `ca.key` 2. Tap the + button in the mobile app to add a site 3. Tap the "Certificate" row 4. Copy the public key to a file on your computer like `test.pub` 5. Create a signed cert with `nebula-cert sign -name test-mobile -ip 192.168.0.20/24 -in-pub test.pub` 6. Create a QR code for it: `nebula-cert print -out-qr "qr.png" -path ./test-mobile.crt` 7. In Android studio, in the "Running devices" tab, open the simulator's extended controls: <img width="509" alt="Android Studio 2024-09-23 13 08 08" src="https://github.com/user-attachments/assets/c1f8288e-374c-457c-942a-4109240102ab"> 8. Choose the Camera option, and add the qr code image to the wall of the virtual scene <img width="679" alt="image" src="https://github.com/user-attachments/assets/bafaa9af-72e4-4444-9704-9876c53c883c"> 9. Back in the app, when you choose QR Code and "Scan a QR code`, the virtual scene should open. Hold shift, then move your mouse to look around. Turn around 180 degrees, and walk forward into the other room (can go through the walls) using the `w` key. When you get the QR code into the white border, the scanner should close and apply the certificate settings. If you use a nonsense QR code, or a QR code with a non-matching key, or a CA QR code, you should get an error message when it scans. The process for scanning a CA qr code is similar. 1. Run `nebula-cert print -out-qr "qr-ca.png" -path ./ca.crt` 2. Replace the QR in the extended controls 3. Tap CA when adding a site 4. The rest of the process is the same as above. iOS is similar, except you'll need to use a real device, as the simulator does not include a virtual scene like Android does.
2024-09-24 11:25:09 +00:00
import 'dart:async';
2020-07-27 20:43:58 +00:00
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
2021-05-03 20:16:00 +00:00
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
2020-07-27 20:43:58 +00:00
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
import 'package:mobile_nebula/models/Certificate.dart';
Update Gradle, replace QR code reader library (#162) I ran the Gradle Upgrade Assistant to get us on the latest version of gradle, but two of our dependencies didn't support it. - https://pub.dev/documentation/package_info/latest/ - https://github.com/AmolGangadhare/flutter_barcode_scanner `pacakge_info` is officially deprecated and replaced by `package_info_plus`, which is what I've swapped to here. A bigger change was switching to https://github.com/juliansteenbakker/mobile_scanner. It does seem to work a bit better than the other one, and does not throw an error now when cancelling the QR code collection, as it did before. I've tested on android in the simulator, and iOS with an actual device. To test adding a cert: 1. Create a CA on your computer with `nebula-cert ca -name test-mobile`. This will create a `ca.crt` and `ca.key` 2. Tap the + button in the mobile app to add a site 3. Tap the "Certificate" row 4. Copy the public key to a file on your computer like `test.pub` 5. Create a signed cert with `nebula-cert sign -name test-mobile -ip 192.168.0.20/24 -in-pub test.pub` 6. Create a QR code for it: `nebula-cert print -out-qr "qr.png" -path ./test-mobile.crt` 7. In Android studio, in the "Running devices" tab, open the simulator's extended controls: <img width="509" alt="Android Studio 2024-09-23 13 08 08" src="https://github.com/user-attachments/assets/c1f8288e-374c-457c-942a-4109240102ab"> 8. Choose the Camera option, and add the qr code image to the wall of the virtual scene <img width="679" alt="image" src="https://github.com/user-attachments/assets/bafaa9af-72e4-4444-9704-9876c53c883c"> 9. Back in the app, when you choose QR Code and "Scan a QR code`, the virtual scene should open. Hold shift, then move your mouse to look around. Turn around 180 degrees, and walk forward into the other room (can go through the walls) using the `w` key. When you get the QR code into the white border, the scanner should close and apply the certificate settings. If you use a nonsense QR code, or a QR code with a non-matching key, or a CA QR code, you should get an error message when it scans. The process for scanning a CA qr code is similar. 1. Run `nebula-cert print -out-qr "qr-ca.png" -path ./ca.crt` 2. Replace the QR in the extended controls 3. Tap CA when adding a site 4. The rest of the process is the same as above. iOS is similar, except you'll need to use a real device, as the simulator does not include a virtual scene like Android does.
2024-09-24 11:25:09 +00:00
import 'package:mobile_nebula/screens/siteConfig/ScanQRScreen.dart';
import 'package:mobile_nebula/services/share.dart';
2020-07-27 20:43:58 +00:00
import 'package:mobile_nebula/services/utils.dart';
2021-05-03 20:16:00 +00:00
import 'CertificateDetailsScreen.dart';
2020-07-27 20:43:58 +00:00
class CertificateResult {
2021-05-03 20:16:00 +00:00
CertificateInfo certInfo;
2020-07-27 20:43:58 +00:00
String key;
CertificateResult({required this.certInfo, required this.key});
2020-07-27 20:43:58 +00:00
}
2021-05-03 20:16:00 +00:00
class AddCertificateScreen extends StatefulWidget {
const AddCertificateScreen({
Key? key,
this.onSave,
this.onReplace,
required this.pubKey,
required this.privKey,
required this.supportsQRScanning,
}) : super(key: key);
// onSave will pop a new CertificateDetailsScreen.
// If onSave is null, onReplace must be set.
final ValueChanged<CertificateResult>? onSave;
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen.
// If onReplace is null, onSave must be set.
final ValueChanged<CertificateResult>? onReplace;
2020-07-27 20:43:58 +00:00
final String pubKey;
final String privKey;
final bool supportsQRScanning;
2020-07-27 20:43:58 +00:00
@override
2021-05-03 20:16:00 +00:00
_AddCertificateScreenState createState() => _AddCertificateScreenState();
2020-07-27 20:43:58 +00:00
}
2021-05-03 20:16:00 +00:00
class _AddCertificateScreenState extends State<AddCertificateScreen> {
late String pubKey;
bool showKey = false;
2020-07-27 20:43:58 +00:00
String inputType = 'paste';
final keyController = TextEditingController();
2020-07-27 20:43:58 +00:00
final pasteController = TextEditingController();
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
@override
void initState() {
pubKey = widget.pubKey;
keyController.text = widget.privKey;
2020-07-27 20:43:58 +00:00
super.initState();
}
@override
void dispose() {
pasteController.dispose();
keyController.dispose();
super.dispose();
}
2020-07-27 20:43:58 +00:00
@override
Widget build(BuildContext context) {
2021-05-03 20:16:00 +00:00
List<Widget> items = [];
items.addAll(_buildShare());
items.add(_buildKey());
2021-05-03 20:16:00 +00:00
items.addAll(_buildLoadCert());
return SimplePage(title: Text('Certificate'), child: Column(children: items));
2020-07-27 20:43:58 +00:00
}
List<Widget> _buildShare() {
return [
ConfigSection(
label: 'Share your public key with a nebula CA so they can sign and return a certificate',
children: [
ConfigItem(
labelWidth: 0,
content: SelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
2020-07-27 20:43:58 +00:00
),
2023-05-15 20:12:24 +00:00
Builder(
builder: (BuildContext context) {
return ConfigButtonItem(
content: Text('Share Public Key'),
onPressed: () async {
await Share.share(context,
title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
2023-05-15 20:12:24 +00:00
},
);
},
2020-07-27 20:43:58 +00:00
),
])
];
}
List<Widget> _buildLoadCert() {
Map<String, Widget> children = {
'paste': Text('Copy/Paste'),
'file': Text('File'),
};
// not all devices have a camera for QR codes
if (widget.supportsQRScanning) {
children['qr'] = Text('QR Code');
}
2020-07-27 20:43:58 +00:00
List<Widget> items = [
Padding(
padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
child: CupertinoSlidingSegmentedControl(
groupValue: inputType,
onValueChanged: (v) {
if (v != null) {
setState(() {
inputType = v;
});
}
2020-07-27 20:43:58 +00:00
},
children: children,
2020-07-27 20:43:58 +00:00
))
];
if (inputType == 'paste') {
items.addAll(_addPaste());
} else if (inputType == 'file') {
items.addAll(_addFile());
} else if (inputType == 'qr') {
2020-07-27 20:43:58 +00:00
items.addAll(_addQr());
}
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)),
],
);
}
2020-07-27 20:43:58 +00:00
List<Widget> _addPaste() {
return [
ConfigSection(
children: [
ConfigTextItem(
placeholder: 'Certificate PEM Contents',
controller: pasteController,
),
ConfigButtonItem(
content: Center(child: Text('Load Certificate')),
onPressed: () {
2021-05-03 20:16:00 +00:00
_addCertEntry(pasteController.text);
2020-07-27 20:43:58 +00:00
}),
],
)
];
}
List<Widget> _addFile() {
return [
ConfigSection(
children: [
ConfigButtonItem(
content: Center(child: Text('Choose a file')),
onPressed: () async {
try {
2021-04-23 17:33:28 +00:00
final content = await Utils.pickFile(context);
if (content == null) {
2020-07-27 20:43:58 +00:00
return;
}
2021-05-03 20:16:00 +00:00
_addCertEntry(content);
2020-07-27 20:43:58 +00:00
} catch (err) {
2021-04-23 17:33:28 +00:00
return Utils.popError(context, 'Failed to load certificate file', err.toString());
2020-07-27 20:43:58 +00:00
}
})
],
)
];
}
List<Widget> _addQr() {
return [
ConfigSection(
children: [
ConfigButtonItem(
Update Gradle, replace QR code reader library (#162) I ran the Gradle Upgrade Assistant to get us on the latest version of gradle, but two of our dependencies didn't support it. - https://pub.dev/documentation/package_info/latest/ - https://github.com/AmolGangadhare/flutter_barcode_scanner `pacakge_info` is officially deprecated and replaced by `package_info_plus`, which is what I've swapped to here. A bigger change was switching to https://github.com/juliansteenbakker/mobile_scanner. It does seem to work a bit better than the other one, and does not throw an error now when cancelling the QR code collection, as it did before. I've tested on android in the simulator, and iOS with an actual device. To test adding a cert: 1. Create a CA on your computer with `nebula-cert ca -name test-mobile`. This will create a `ca.crt` and `ca.key` 2. Tap the + button in the mobile app to add a site 3. Tap the "Certificate" row 4. Copy the public key to a file on your computer like `test.pub` 5. Create a signed cert with `nebula-cert sign -name test-mobile -ip 192.168.0.20/24 -in-pub test.pub` 6. Create a QR code for it: `nebula-cert print -out-qr "qr.png" -path ./test-mobile.crt` 7. In Android studio, in the "Running devices" tab, open the simulator's extended controls: <img width="509" alt="Android Studio 2024-09-23 13 08 08" src="https://github.com/user-attachments/assets/c1f8288e-374c-457c-942a-4109240102ab"> 8. Choose the Camera option, and add the qr code image to the wall of the virtual scene <img width="679" alt="image" src="https://github.com/user-attachments/assets/bafaa9af-72e4-4444-9704-9876c53c883c"> 9. Back in the app, when you choose QR Code and "Scan a QR code`, the virtual scene should open. Hold shift, then move your mouse to look around. Turn around 180 degrees, and walk forward into the other room (can go through the walls) using the `w` key. When you get the QR code into the white border, the scanner should close and apply the certificate settings. If you use a nonsense QR code, or a QR code with a non-matching key, or a CA QR code, you should get an error message when it scans. The process for scanning a CA qr code is similar. 1. Run `nebula-cert print -out-qr "qr-ca.png" -path ./ca.crt` 2. Replace the QR in the extended controls 3. Tap CA when adding a site 4. The rest of the process is the same as above. iOS is similar, except you'll need to use a real device, as the simulator does not include a virtual scene like Android does.
2024-09-24 11:25:09 +00:00
content: Text('Scan a QR code'),
onPressed: () async {
var result = await Navigator.push(
context,
platformPageRoute(
context: context,
builder: (context) => new ScanQRScreen(),
),
);
if (result != null) {
_addCertEntry(result);
}
},
),
2020-07-27 20:43:58 +00:00
],
)
];
}
2021-05-03 20:16:00 +00:00
_addCertEntry(String rawCert) async {
2020-08-07 22:44:40 +00:00
// Allow for app store review testing cert to override the generated key
if (rawCert.trim() == _testCert) {
keyController.text = _testKey;
2020-08-07 22:44:40 +00:00
}
2020-07-27 20:43:58 +00:00
try {
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
2021-05-03 20:16:00 +00:00
2020-07-27 20:43:58 +00:00
List<dynamic> certs = jsonDecode(rawCerts);
if (certs.length > 0) {
2021-05-03 20:16:00 +00:00
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);
2021-05-03 20:16:00 +00:00
}
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.');
}
2021-05-03 20:16:00 +00:00
if (widget.onReplace != null) {
// If we are replacing we just return the results now
2021-05-03 20:16:00 +00:00
Navigator.pop(context);
widget.onReplace!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
2021-05-03 20:16:00 +00:00
return;
} else if (widget.onSave != null) {
// We have a cert, pop the details screen where they can hit save
Utils.openPage(context, (context) {
return CertificateDetailsScreen(
certInfo: tryCertInfo,
onSave: () {
Navigator.pop(context);
widget.onSave!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
},
supportsQRScanning: widget.supportsQRScanning,
);
});
}
2020-07-27 20:43:58 +00:00
}
} on PlatformException catch (err) {
2021-05-03 20:16:00 +00:00
return Utils.popError(context, 'Error loading certificate content', err.details ?? err.message);
2020-07-27 20:43:58 +00:00
}
}
}
2020-08-07 22:44:40 +00:00
// This a cert that if presented will swap the key to assist the app review process
const _testCert = '''-----BEGIN NEBULA CERTIFICATE-----
CpMBChdBcHAgU3RvcmUgUmV2aWV3IERldmljZRIKgpSghQyA/v//DyIGcmV2aWV3
IhRiNzJjZThiZWM5MDYwYTA3MmNmMSjvk7f5BTCPnYf0BzogYHa3YoNcFJxKX8bU
jK4pg0aIYxDkwk8aM7w1c+CQXSpKICx06NYtozgKaA2R9NO311D8T86iTXxLmjI4
0wzAXCSmEkCi9ocqtyQhNp75eKphqVlZNl1RXBo4hdY9jBdc9+b9o0bU4zxFxIRT
uDneQqytYS+BUfgNnGX5wsMxOEst/kkC
-----END NEBULA CERTIFICATE-----''';
const _testKey = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
UlyDdFn/2mLFykeWjCEwWVRSDHtMF7nz3At3O77Faf4=
-----END NEBULA X25519 PRIVATE KEY-----''';