Skip to content

Commit

Permalink
Feat #13555 add server cert field, similar to client cert
Browse files Browse the repository at this point in the history
  • Loading branch information
crisoagf committed Nov 25, 2024
1 parent 01a9cda commit 678fd71
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 6 deletions.
8 changes: 8 additions & 0 deletions mobile/assets/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@
"client_cert_remove_msg": "Client certificate is removed",
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
"client_cert_title": "SSL Client Certificate",
"server_cert_dialog_msg_confirm": "OK",
"server_cert_import": "Import",
"server_cert_import_success_msg": "Server certificate is imported",
"server_cert_invalid_msg": "Invalid certificate file or wrong password",
"server_cert_remove": "Remove",
"server_cert_remove_msg": "Server certificate is removed",
"server_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
"server_cert_title": "SSL Server Certificate",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
Expand Down
25 changes: 25 additions & 0 deletions mobile/lib/entities/store.entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,30 @@ class SSLClientCertStoreVal {
}
}

class SSLServerCertStoreVal {
final Uint8List data;

SSLServerCertStoreVal(this.data);

void save() {
final b64Str = base64Encode(data);
Store.put(StoreKey.sslServerCertData, b64Str);
}

static SSLServerCertStoreVal? load() {
final b64Str = Store.tryGet<String>(StoreKey.sslServerCertData);
if (b64Str == null) {
return null;
}
final Uint8List certData = base64Decode(b64Str);
return SSLServerCertStoreVal(certData);
}

static void delete() {
Store.delete(StoreKey.sslServerCertData);
}
}

class StoreKeyNotFoundException implements Exception {
final StoreKey key;
StoreKeyNotFoundException(this.key);
Expand All @@ -199,6 +223,7 @@ enum StoreKey<T> {
backgroundBackup<bool>(14, type: bool),
sslClientCertData<String>(15, type: String),
sslClientPasswd<String>(16, type: String),
sslServerCertData<String>(17, type: String),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>(100, type: bool),
loadOriginal<bool>(101, type: bool),
Expand Down
36 changes: 30 additions & 6 deletions mobile/lib/utils/http_ssl_cert_override.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@ import 'package:logging/logging.dart';
class HttpSSLCertOverride extends HttpOverrides {
static final Logger _log = Logger("HttpSSLCertOverride");
final SSLClientCertStoreVal? _clientCert;
final SSLServerCertStoreVal? _rootCert;
late final SecurityContext? _ctxWithCert;

HttpSSLCertOverride() : _clientCert = SSLClientCertStoreVal.load() {
if (_clientCert != null) {
HttpSSLCertOverride() : _clientCert = SSLClientCertStoreVal.load(), _rootCert = SSLServerCertStoreVal.load() {
if (_clientCert != null || _rootCert != null) {
_ctxWithCert = SecurityContext(withTrustedRoots: true);
if (_ctxWithCert != null) {
setClientCert(_ctxWithCert, _clientCert);
} else {
_log.severe("Failed to create security context with client cert!");
if (_clientCert != null) {
if (_ctxWithCert != null) {
setClientCert(_ctxWithCert, _clientCert);
} else {
_log.severe("Failed to create security context with client cert!");
}
}
if (_rootCert != null) {
if (_ctxWithCert != null) {
setRootCert(_ctxWithCert, _rootCert);
} else {
_log.severe("Failed to create security context with server cert!");
}
}
} else {
_ctxWithCert = null;
Expand All @@ -33,12 +43,26 @@ class HttpSSLCertOverride extends HttpOverrides {
return true;
}

static bool setRootCert(SecurityContext ctx, SSLServerCertStoreVal cert) {
try {
_log.info("Setting server certificate");
ctx.setTrustedCertificatesBytes(cert.data);
} catch (e) {
_log.severe("Failed to set SSL server cert: $e");
return false;
}
return true;
}

@override
HttpClient createHttpClient(SecurityContext? context) {
if (context != null) {
if (_clientCert != null) {
setClientCert(context, _clientCert);
}
if (_rootCert != null) {
setRootCert(context, _rootCert);
}
} else {
context = _ctxWithCert;
}
Expand Down
2 changes: 2 additions & 0 deletions mobile/lib/widgets/settings/advanced_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/immich_logger.service.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart';
import 'package:immich_mobile/widgets/settings/ssl_server_cert_settings.dart';
import 'package:logging/logging.dart';

class AdvancedSettings extends HookConsumerWidget {
Expand Down Expand Up @@ -66,6 +67,7 @@ class AdvancedSettings extends HookConsumerWidget {
),
const CustomeProxyHeaderSettings(),
SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
SslServerCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
];

return SettingsSubPageScaffold(settings: advancedSettings);
Expand Down
133 changes: 133 additions & 0 deletions mobile/lib/widgets/settings/ssl_server_cert_settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import 'dart:io';

import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';

class SslServerCertSettings extends StatefulWidget {
const SslServerCertSettings({super.key, required this.isLoggedIn});

final bool isLoggedIn;

@override
State<StatefulWidget> createState() => _SslServerCertSettingsState();
}

class _SslServerCertSettingsState extends State<SslServerCertSettings> {
_SslServerCertSettingsState()
: isCertExist = SSLServerCertStoreVal.load() != null;

bool isCertExist;

@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
horizontalTitleGap: 20,
isThreeLine: true,
title: Text(
"server_cert_title".tr(),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"server_cert_subtitle".tr(),
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
),
const SizedBox(
height: 6,
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
onPressed: widget.isLoggedIn ? null : () => importCert(context),
child: Text("server_cert_import".tr()),
),
const SizedBox(
width: 15,
),
ElevatedButton(
onPressed: widget.isLoggedIn || !isCertExist
? null
: () => removeCert(context),
child: Text("server_cert_remove".tr()),
),
],
),
],
),
);
}

void showMessage(BuildContext context, String message) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
content: Text(message),
actions: [
TextButton(
onPressed: () => ctx.pop(),
child: Text("server_cert_dialog_msg_confirm".tr()),
),
],
),
);
}

void storeCert(BuildContext context, Uint8List data) {
final cert = SSLServerCertStoreVal(data);
// Test whether the certificate is valid
final isCertValid = HttpSSLCertOverride.setRootCert(
SecurityContext(withTrustedRoots: true),
cert,
);
if (!isCertValid) {
showMessage(context, "server_cert_invalid_msg".tr());
return;
}
cert.save();
HttpOverrides.global = HttpSSLCertOverride();
setState(
() => isCertExist = true,
);
showMessage(context, "client_cert_import_success_msg".tr());
}

Future<void> importCert(BuildContext ctx) async {
FilePickerResult? res = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: [
'p12',
'pfx',
],
);
if (res != null) {
File file = File(res.files.single.path!);
final data = await file.readAsBytes();
storeCert(context, data);
}
}

void removeCert(BuildContext context) {
SSLServerCertStoreVal.delete();
HttpOverrides.global = HttpSSLCertOverride();
setState(
() => isCertExist = false,
);
showMessage(context, "server_cert_remove_msg".tr());
}
}

0 comments on commit 678fd71

Please sign in to comment.