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';
|
2020-08-18 00:12:28 +00:00
|
|
|
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;
|
|
|
|
|
2022-09-21 20:27:35 +00:00
|
|
|
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 {
|
2022-09-21 20:27:35 +00:00
|
|
|
const AddCertificateScreen({
|
|
|
|
Key? key,
|
|
|
|
this.onSave,
|
|
|
|
this.onReplace,
|
|
|
|
required this.pubKey,
|
|
|
|
required this.privKey,
|
2022-11-22 21:50:11 +00:00
|
|
|
required this.supportsQRScanning,
|
2022-09-21 20:27:35 +00:00
|
|
|
}) : 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
|
|
|
|
2022-08-05 21:42:17 +00:00
|
|
|
final String pubKey;
|
|
|
|
final String privKey;
|
|
|
|
|
2022-11-22 21:50:11 +00:00
|
|
|
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> {
|
2022-09-21 20:27:35 +00:00
|
|
|
late String pubKey;
|
2022-08-05 21:42:17 +00:00
|
|
|
bool showKey = false;
|
2020-07-27 20:43:58 +00:00
|
|
|
|
|
|
|
String inputType = 'paste';
|
|
|
|
|
2022-08-05 21:42:17 +00:00
|
|
|
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() {
|
2022-08-05 21:42:17 +00:00
|
|
|
pubKey = widget.pubKey;
|
|
|
|
keyController.text = widget.privKey;
|
2020-07-27 20:43:58 +00:00
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
2020-08-31 23:32:40 +00:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
pasteController.dispose();
|
2022-08-05 21:42:17 +00:00
|
|
|
keyController.dispose();
|
2020-08-31 23:32:40 +00:00
|
|
|
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());
|
2022-08-05 21:42:17 +00:00
|
|
|
items.add(_buildKey());
|
2021-05-03 20:16:00 +00:00
|
|
|
items.addAll(_buildLoadCert());
|
|
|
|
|
2022-11-17 21:43:16 +00:00
|
|
|
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,
|
2022-01-19 19:29:30 +00:00
|
|
|
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,
|
2024-09-20 18:19:23 +00:00
|
|
|
title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
|
2023-05-15 20:12:24 +00:00
|
|
|
},
|
|
|
|
);
|
2024-09-20 18:19:23 +00:00
|
|
|
},
|
2020-07-27 20:43:58 +00:00
|
|
|
),
|
|
|
|
])
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> _buildLoadCert() {
|
2022-11-22 21:50:11 +00:00
|
|
|
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) {
|
2022-09-21 20:27:35 +00:00
|
|
|
if (v != null) {
|
|
|
|
setState(() {
|
|
|
|
inputType = v;
|
|
|
|
});
|
|
|
|
}
|
2020-07-27 20:43:58 +00:00
|
|
|
},
|
2022-11-22 21:50:11 +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());
|
2022-11-22 21:50:11 +00:00
|
|
|
} else if (inputType == 'qr') {
|
2020-07-27 20:43:58 +00:00
|
|
|
items.addAll(_addQr());
|
|
|
|
}
|
|
|
|
|
|
|
|
return items;
|
|
|
|
}
|
|
|
|
|
2022-08-05 21:42:17 +00:00
|
|
|
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?', () {
|
2022-09-21 20:27:35 +00:00
|
|
|
setState(() {
|
|
|
|
showKey = true;
|
|
|
|
});
|
|
|
|
}, deleteLabel: 'Yes'))));
|
2022-08-05 21:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ConfigSection(
|
|
|
|
label: 'Import a private key generated on another device',
|
|
|
|
children: [
|
2022-09-21 20:27:35 +00:00
|
|
|
ConfigTextItem(controller: keyController, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
2022-08-05 21:42:17 +00:00
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
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) {
|
2022-08-05 21:42:17 +00:00
|
|
|
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.');
|
2022-09-21 20:27:35 +00:00
|
|
|
} else if (!tryCertInfo.validity!.valid) {
|
|
|
|
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity!.reason);
|
2021-05-03 20:16:00 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 20:27:35 +00:00
|
|
|
var certMatch = await platform
|
|
|
|
.invokeMethod("nebula.verifyCertAndKey", <String, String>{"cert": rawCert, "key": keyController.text});
|
2022-08-05 21:42:17 +00:00
|
|
|
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) {
|
2022-09-21 20:27:35 +00:00
|
|
|
// If we are replacing we just return the results now
|
2021-05-03 20:16:00 +00:00
|
|
|
Navigator.pop(context);
|
2022-09-21 20:27:35 +00:00
|
|
|
widget.onReplace!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
|
2021-05-03 20:16:00 +00:00
|
|
|
return;
|
2022-09-21 20:27:35 +00:00
|
|
|
} else if (widget.onSave != null) {
|
|
|
|
// We have a cert, pop the details screen where they can hit save
|
|
|
|
Utils.openPage(context, (context) {
|
|
|
|
return CertificateDetailsScreen(
|
2024-09-20 18:19:23 +00:00
|
|
|
certInfo: tryCertInfo,
|
|
|
|
onSave: () {
|
|
|
|
Navigator.pop(context);
|
|
|
|
widget.onSave!(CertificateResult(certInfo: tryCertInfo, key: keyController.text));
|
|
|
|
},
|
|
|
|
supportsQRScanning: widget.supportsQRScanning,
|
2022-11-22 21:50:11 +00:00
|
|
|
);
|
2022-09-21 20:27:35 +00:00
|
|
|
});
|
2020-08-10 15:48:00 +00:00
|
|
|
}
|
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-----''';
|