Skip to content

Commit

Permalink
Closes #193, #280 - Allowed user to define ssl-cert for pinning
Browse files Browse the repository at this point in the history
  • Loading branch information
Clon1998 committed Dec 7, 2023
1 parent 55a42e9 commit 8377c22
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 25 deletions.
7 changes: 6 additions & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,18 @@
"moonraker_api_key": "Moonraker - API Key",
"moonraker_api_desc": "Only needed if youre using trusted clients. FluiddPI enforces this!",
"full_url": "Full URL",
"self_signed": "Trust Self-Signed Certificates",
"timeout_label": "Client timeout",
"timeout_helper": "The timeout for the client connection in seconds",
"theme": "UI-Theme",
"theme_helper": "UI theme for printer",
"theme_unavailable": "UI theme for printer, only available for Supporters"
},
"ssl": {
"title": "SSL - Settings",
"pin_certificate_label": "Certificate Fingerprint Pinning",
"pin_certificate_helper": "Select a certificate file in PEM format for SSL-Pinning",
"self_signed": "Trust Self-Signed Certificates"
},
"motion_system": {
"title": "Motion System",
"invert_x": "Invert X - Axis",
Expand Down
5 changes: 2 additions & 3 deletions common/lib/network/dio_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ BaseOptions baseOptions(BaseOptionsRef ref, String machineUUID, ClientType clien
throw MobilerakerException('Machine with UUID "$machineUUID" was not found!');
}

var pinnedSha256Fp = machine.pinnedCertificateDER?.let((it) => HashDigest(fromHex(it)));
var pinnedSha256Fp = machine.pinnedCertificateDERBase64?.let((it) => sha256.convert(fromBase64(it)));

return switch (clientType) {
ClientType.octo => BaseOptions(
Expand Down Expand Up @@ -101,7 +101,7 @@ HttpClient httpClient(HttpClientRef ref, String machineUUID, ClientType clientTy
..idleTimeout = const Duration(seconds: 3)
..connectionTimeout = options.connectTimeout;

if (!options.trustUntrustedCertificate) return client;
if (!options.trustUntrustedCertificate && options.pinnedCertificateFingerPrint == null) return client;

var fingerPrint = options.pinnedCertificateFingerPrint;

Expand All @@ -110,7 +110,6 @@ HttpClient httpClient(HttpClientRef ref, String machineUUID, ClientType clientTy
if (fingerPrint == null) {
return true;
}

// Manually verified that using DER of cert is correctly working to generate a SHA256 FP for the cert
HashDigest sha256Fp = sha256.convert(cert.der);
return fingerPrint == sha256Fp;
Expand Down
24 changes: 24 additions & 0 deletions common/lib/ui/components/decorator_suffix_icon_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2023. Patrick Schmidt.
* All rights reserved.
*/

import 'package:flutter/material.dart';

class DecoratorSuffixIconButton extends StatelessWidget {
const DecoratorSuffixIconButton({super.key, required this.icon, this.onPressed});

final IconData icon;
final GestureTapCallback? onPressed;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: InkWell(
onTap: onPressed,
child: Icon(icon, size: 18),
),
);
}
}
10 changes: 8 additions & 2 deletions lib/ui/screens/printers/components/section_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
import 'package:flutter/material.dart';

class SectionHeader extends StatelessWidget {
const SectionHeader({Key? key, required this.title, this.trailing}) : super(key: key);
const SectionHeader({
Key? key,
required this.title,
this.trailing,
this.padding = const EdgeInsets.only(top: 16.0),
}) : super(key: key);

final String title;
final Widget? trailing;
final EdgeInsets padding;

@override
Widget build(BuildContext context) {
Expand All @@ -18,7 +24,7 @@ class SectionHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
padding: padding,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
Expand Down
164 changes: 164 additions & 0 deletions lib/ui/screens/printers/components/ssl_settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) 2023. Patrick Schmidt.
* All rights reserved.
*/

import 'dart:convert';

import 'package:common/data/model/hive/machine.dart';
import 'package:common/ui/components/decorator_suffix_icon_button.dart';
import 'package:common/util/logger.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hashlib/hashlib.dart';
import 'package:hashlib_codecs/hashlib_codecs.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mobileraker/ui/screens/printers/components/section_header.dart';
import 'package:pem/pem.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'ssl_settings.freezed.dart';

part 'ssl_settings.g.dart';

class SslSettings extends HookConsumerWidget {
const SslSettings({super.key, required this.machine});

final Machine machine;

@override
Widget build(BuildContext context, WidgetRef ref) {
var provider = sslSettingsControllerProvider(machine);
var model = ref.watch(provider);
var controller = ref.watch(provider.notifier);

var textController = useTextEditingController(text: model.certificateDER);
useEffect(
() {
if (model.fingerprintSHA256 == null) {
textController.clear();
} else {
textController.text = model.fingerprintSHA256!.toUpperCase();
}
},
[model.fingerprintSHA256],
);

return Column(
mainAxisSize: MainAxisSize.min,
children: [
SectionHeader(title: tr('pages.printer_edit.ssl.title')),
InputDecorator(
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.all(0),
),
child: SwitchListTile(
dense: true,
isThreeLine: false,
contentPadding: EdgeInsets.zero,
title: const Text('pages.printer_edit.ssl.self_signed').tr(),
value: model.trustSelfSigned || model.fingerprintSHA256 != null,
onChanged: model.fingerprintSHA256 == null ? (_) => controller.toggleTrustSelfSigned() : null,
controlAffinity: ListTileControlAffinity.trailing,
),
),
TextField(
canRequestFocus: false,
controller: textController,
decoration: InputDecoration(
labelText: tr('pages.printer_edit.ssl.pin_certificate_label'),
helperText: tr('pages.printer_edit.ssl.pin_certificate_helper'),
hintText: 'DER-Encoded Certificate',
helperMaxLines: 100,
suffix: model.fingerprintSHA256?.isNotEmpty == true
? DecoratorSuffixIconButton(
icon: Icons.close,
onPressed: controller.clearCertificate,
)
: null,
),
readOnly: true,
onTap: controller.pickCertificate,
),
],
);
}
}

@riverpod
class SslSettingsController extends _$SslSettingsController {
@override
SslSettingsModel build(Machine machine) {
bool trustSelfSigned = machine.trustUntrustedCertificate;

if (machine.pinnedCertificateDERBase64 == null) {
return SslSettingsModel(certificateDER: null, fingerprintSHA256: null, trustSelfSigned: trustSelfSigned);
}

return SslSettingsModel(
certificateDER: machine.pinnedCertificateDERBase64,
fingerprintSHA256: _fingerPrint(machine.pinnedCertificateDERBase64!),
trustSelfSigned: trustSelfSigned,
);
}

void pickCertificate() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pem', 'crt', 'cer'],
withReadStream: true,
withData: false,
);

if (result != null) {
var file = result.files.first;
var content = await utf8.decodeStream(file.readStream!);

PemCodec pemCodec = PemCodec(PemLabel.certificate);
var derBytes = pemCodec.decode(content);
var base64 = toBase64(derBytes);

state = SslSettingsModel(
certificateDER: base64,
fingerprintSHA256: _fingerPrint(base64),
trustSelfSigned: state.trustSelfSigned,
);
} else {
// User canceled the picker
logger.i('User canceled certificate picker');
}
}

void clearCertificate() {
state = SslSettingsModel(certificateDER: null, fingerprintSHA256: null, trustSelfSigned: state.trustSelfSigned);
}

void toggleTrustSelfSigned() {
state = SslSettingsModel(
certificateDER: state.certificateDER,
fingerprintSHA256: state.fingerprintSHA256,
trustSelfSigned: !state.trustSelfSigned,
);
}

String _fingerPrint(String derB64) {
var b64 = fromBase64(derB64);

String hex = sha256.convert(b64).hex();

return [for (int i = 0; i < hex.length; i += 2) hex.substring(i, i + 2)].join(':');
}
}

@freezed
class SslSettingsModel with _$SslSettingsModel {
const factory SslSettingsModel({
required String? certificateDER,
required String? fingerprintSHA256,
@Default(false) bool trustSelfSigned,
}) = _SslSettingsModel;
}
5 changes: 3 additions & 2 deletions lib/ui/screens/printers/edit/components/macro_group_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:common/service/machine_service.dart';
import 'package:common/service/ui/bottom_sheet_service_interface.dart';
import 'package:common/service/ui/dialog_service_interface.dart';
import 'package:common/service/ui/snackbar_service_interface.dart';
import 'package:common/ui/components/decorator_suffix_icon_button.dart';
import 'package:common/util/extensions/async_ext.dart';
import 'package:common/util/extensions/object_extension.dart';
import 'package:common/util/logger.dart';
Expand Down Expand Up @@ -163,8 +164,8 @@ class _MacroGroup extends HookConsumerWidget {
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'pages.printer_edit.general.displayname'.tr(),
suffix: IconButton(
icon: const Icon(Icons.delete),
suffix: DecoratorSuffixIconButton(
icon: Icons.delete,
onPressed: enabled ? () => controller.removeMacroGroup(macroGroup) : null,
),
),
Expand Down
6 changes: 5 additions & 1 deletion lib/ui/screens/printers/edit/printers_edit_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import 'package:mobileraker/ui/components/dialog/import_settings/import_settings
import 'package:mobileraker/ui/components/dialog/webcam_preview_dialog.dart';
import 'package:mobileraker/ui/screens/printers/components/http_headers.dart';
import 'package:mobileraker/ui/screens/printers/components/ssid_preferences_list.dart';
import 'package:mobileraker/ui/screens/printers/components/ssl_settings.dart';
import 'package:mobileraker/ui/screens/qr_scanner/qr_scanner_page.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
Expand Down Expand Up @@ -234,7 +235,10 @@ class PrinterEditController extends _$PrinterEditController {
if (wsUri != null) {
_machine.wsUri = wsUri;
}
_machine.trustUntrustedCertificate = storedValues['trustSelfSigned'];
var sslSettings = ref.read(sslSettingsControllerProvider(_machine));
_machine.trustUntrustedCertificate = sslSettings.trustSelfSigned;
_machine.pinnedCertificateDERBase64 = sslSettings.certificateDER;

_machine.httpHeaders = ref.read(headersControllerProvider(_machine.httpHeaders));
_machine.localSsids = ref.read(ssidPreferenceListControllerProvider(_machine.localSsids));
await ref.read(machineServiceProvider).updateMachine(_machine);
Expand Down
35 changes: 19 additions & 16 deletions lib/ui/screens/printers/edit/printers_edit_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:common/service/misc_providers.dart';
import 'package:common/service/moonraker/printer_service.dart';
import 'package:common/service/payment_service.dart';
import 'package:common/service/ui/theme_service.dart';
import 'package:common/ui/components/decorator_suffix_icon_button.dart';
import 'package:common/ui/components/supporter_only_feature.dart';
import 'package:common/ui/theme/theme_pack.dart';
import 'package:common/util/extensions/async_ext.dart';
Expand All @@ -33,6 +34,7 @@ import 'package:mobileraker/ui/components/warning_card.dart';
import 'package:mobileraker/ui/screens/printers/components/http_headers.dart';
import 'package:mobileraker/ui/screens/printers/components/section_header.dart';
import 'package:mobileraker/ui/screens/printers/components/ssid_preferences_list.dart';
import 'package:mobileraker/ui/screens/printers/components/ssl_settings.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:progress_indicators/progress_indicators.dart';
import 'package:stringr/stringr.dart';
Expand Down Expand Up @@ -116,10 +118,13 @@ class _Body extends ConsumerWidget {
key: ref.watch(editPrinterFormKeyProvider),
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Padding(
padding: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(6.0),
child: Column(
children: <Widget>[
SectionHeader(title: 'pages.setting.general.title'.tr()),
SectionHeader(
title: 'pages.setting.general.title'.tr(),
padding: EdgeInsets.zero,
),
FormBuilderTextField(
enableInteractiveSelection: true,
keyboardType: TextInputType.text,
Expand Down Expand Up @@ -174,9 +179,12 @@ class _Body extends ConsumerWidget {
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'pages.printer_edit.general.moonraker_api_key'.tr(),
suffix: IconButton(
icon: const Icon(Icons.qr_code_sharp),
onPressed: () => controller.openQrScanner(context),
suffix: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: InkWell(
child: const Icon(Icons.qr_code_sharp, size: 18),
onTap: () => controller.openQrScanner(context),
),
),
helperText: 'pages.printer_edit.general.moonraker_api_desc'.tr(),
helperMaxLines: 3,
Expand Down Expand Up @@ -204,12 +212,7 @@ class _Body extends ConsumerWidget {
FormBuilderValidators.integer(),
]),
),
FormBuilderCheckbox(
name: 'trustSelfSigned',
title: const Text('pages.printer_edit.general.self_signed').tr(),
controlAffinity: ListTileControlAffinity.trailing,
initialValue: machine.trustUntrustedCertificate,
),
SslSettings(machine: machine),
HttpHeaders(initialValue: machine.httpHeaders),
const Divider(),
// if (machine.hasRemoteConnection)
Expand Down Expand Up @@ -333,8 +336,8 @@ class _WebCamItem extends HookConsumerWidget {
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'pages.printer_edit.general.displayname'.tr(),
suffix: IconButton(
icon: const Icon(Icons.delete),
suffix: DecoratorSuffixIconButton(
icon: Icons.delete,
onPressed: ref.watch(printerEditControllerProvider)
? null
: () => ref.read(webcamListControllerProvider.notifier).removeWebcam(cam),
Expand Down Expand Up @@ -737,8 +740,8 @@ class _TempPresetItem extends HookConsumerWidget {
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'pages.printer_edit.general.displayname'.tr(),
suffix: IconButton(
icon: const Icon(Icons.delete),
suffix: DecoratorSuffixIconButton(
icon: Icons.delete,
onPressed: ref.watch(printerEditControllerProvider)
? null
: () => ref
Expand Down Expand Up @@ -999,7 +1002,7 @@ class _ThemeSelector extends ConsumerWidget {
decoration: InputDecoration(
labelStyle: Theme.of(context).textTheme.labelLarge,
labelText: tr('pages.printer_edit.general.theme'),
helperText: tr('pages.printer_edit.general.theme_helper'),
helperText: isSupporter ? tr('pages.printer_edit.general.theme_helper') : null,
suffix: isSupporter
? null
: IconButton(
Expand Down
Loading

0 comments on commit 8377c22

Please sign in to comment.