Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): Adding setting in mobile app to TLS client certificate #10860

Merged
merged 7 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions mobile/assets/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,5 +531,19 @@
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack"
}
"viewer_unstack": "Un-Stack",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"client_cert_title": "SSL Client Certificate",
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
"client_cert_import": "Import",
"client_cert_remove": "Remove",
"client_cert_remove_msg": "Client certificate is removed",
"client_cert_import_success_msg": "Client certificate is imported",
"client_cert_invalid_msg": "Invalid certificate file or wrong password",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Enter Password"
}
50 changes: 50 additions & 0 deletions mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ PODS:
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
Expand Down Expand Up @@ -46,6 +80,9 @@ PODS:
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- SAMKeychain (1.5.3)
- SDWebImage (5.19.4):
- SDWebImage/Core (= 5.19.4)
- SDWebImage/Core (5.19.4)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
Expand All @@ -54,6 +91,7 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- SwiftyGif (5.4.5)
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
Expand All @@ -66,6 +104,7 @@ PODS:
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
Expand All @@ -91,17 +130,23 @@ DEPENDENCIES:

SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FMDB
- MapLibre
- ReachabilitySwift
- SAMKeychain
- SDWebImage
- SwiftyGif
- Toast

EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
Expand Down Expand Up @@ -150,6 +195,9 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
Expand All @@ -170,9 +218,11 @@ SPEC CHECKSUMS:
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
Expand Down
35 changes: 35 additions & 0 deletions mobile/lib/entities/store.entity.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:isar/isar.dart';
Expand Down Expand Up @@ -140,6 +143,36 @@ class StoreValue {
}
}

class SSLClientCertStoreVal {
final Uint8List data;
final String? password;

SSLClientCertStoreVal(this.data, this.password);

void save() {
final b64Str = base64Encode(data);
Store.put(StoreKey.sslClientCertData, b64Str);
if (password != null) {
Store.put(StoreKey.sslClientPasswd, password!);
}
}

static SSLClientCertStoreVal? load() {
final b64Str = Store.tryGet<String>(StoreKey.sslClientCertData);
if (b64Str == null) {
return null;
}
final Uint8List certData = base64Decode(b64Str);
final passwd = Store.tryGet<String>(StoreKey.sslClientPasswd);
return SSLClientCertStoreVal(certData, passwd);
}

static void delete() {
Store.delete(StoreKey.sslClientCertData);
Store.delete(StoreKey.sslClientPasswd);
}
}

class StoreKeyNotFoundException implements Exception {
final StoreKey key;
StoreKeyNotFoundException(this.key);
Expand All @@ -164,6 +197,8 @@ enum StoreKey<T> {
serverEndpoint<String>(12, type: String),
autoBackup<bool>(13, type: bool),
backgroundBackup<bool>(14, type: bool),
sslClientCertData<String>(15, type: String),
sslClientPasswd<String>(16, type: String),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>(100, type: bool),
loadOriginal<bool>(101, type: bool),
Expand Down
43 changes: 40 additions & 3 deletions mobile/lib/utils/http_ssl_cert_override.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,49 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:logging/logging.dart';

class HttpSSLCertOverride extends HttpOverrides {
static final Logger _log = Logger("HttpSSLCertOverride");
final SSLClientCertStoreVal? _clientCert;
late final SecurityContext? _ctxWithCert;

HttpSSLCertOverride() : _clientCert = SSLClientCertStoreVal.load() {
if (_clientCert != null) {
_ctxWithCert = SecurityContext(withTrustedRoots: true);
if (_ctxWithCert != null) {
setClientCert(_ctxWithCert, _clientCert);
} else {
_log.severe("Failed to create security context with client cert!");
}
} else {
_ctxWithCert = null;
}
}

static bool setClientCert(SecurityContext ctx, SSLClientCertStoreVal cert) {
try {
_log.info("Setting client certificate");
ctx.usePrivateKeyBytes(cert.data, password: cert.password);
if (!Platform.isIOS) {
ctx.useCertificateChainBytes(cert.data, password: cert.password);
}
} catch (e) {
_log.severe("Failed to set SSL client cert: $e");
return false;
}
return true;
}

@override
HttpClient createHttpClient(SecurityContext? context) {
if (context != null) {
if (_clientCert != null) {
setClientCert(context, _clientCert);
}
} else {
context = _ctxWithCert;
}

return super.createHttpClient(context)
..badCertificateCallback = (X509Certificate cert, String host, int port) {
var log = Logger("HttpSSLCertOverride");

AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert;

// Check if user has allowed self signed SSL certificates.
Expand All @@ -28,7 +65,7 @@ class HttpSSLCertOverride extends HttpOverrides {
}

if (!selfSignedCertsAllowed) {
log.severe("Invalid SSL certificate for $host:$port");
_log.severe("Invalid SSL certificate for $host:$port");
}

return selfSignedCertsAllowed;
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 @@ -13,6 +13,7 @@ import 'package:immich_mobile/services/app_settings.service.dart';
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:logging/logging.dart';

class AdvancedSettings extends HookConsumerWidget {
Expand Down Expand Up @@ -64,6 +65,7 @@ class AdvancedSettings extends HookConsumerWidget {
onChanged: (_) => HttpOverrides.global = HttpSSLCertOverride(),
),
const CustomeProxyHeaderSettings(),
SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
];

return SettingsSubPageScaffold(settings: advancedSettings);
Expand Down
Loading
Loading