mirror of https://github.com/immich-app/immich.git
fix(mobile): Remote video playback and asset download on Android with mTLS (#16403)
* Add class to apply SSL options * Apply client certificate for native Android code * Refactor self-signed check * Allow self-signed certificates * Fix Dart analysis * Add HostnameVerifier Android explicitly does NOT check the Common Name of a certificate, only the Subject Alt Names. Chances are that someone who self-signs a certificate doesn't go through the extra steps to add a SAN, and in that case the connection would be prevented by the HostnameVerifier even thought the TrustManager was fine with the certificate itself. * Rename parameter like in Dart * Fix NPE * Catch all native errors in HttpSSLOptionsPlugin * Workaround for too early onChanged() callback * Fix formatting --------- Co-authored-by: Alex <alex.tran1502@gmail.com>pull/18142/head^2
parent
3a1e3e82e7
commit
f75d853e9a
@ -0,0 +1,146 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLEngine
|
||||
import javax.net.ssl.SSLSession
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedTrustManager
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `HttpSSLOptions`
|
||||
*/
|
||||
class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private var methodChannel: MethodChannel? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
}
|
||||
|
||||
private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
|
||||
methodChannel = MethodChannel(messenger, "immich/httpSSLOptions")
|
||||
methodChannel?.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onDetachedFromEngine()
|
||||
}
|
||||
|
||||
private fun onDetachedFromEngine() {
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
try {
|
||||
when (call.method) {
|
||||
"apply" -> {
|
||||
val args = call.arguments<ArrayList<*>>()!!
|
||||
|
||||
var tm: Array<TrustManager>? = null
|
||||
if (args[0] as Boolean) {
|
||||
tm = arrayOf(AllowSelfSignedTrustManager(args[1] as? String))
|
||||
}
|
||||
|
||||
var km: Array<KeyManager>? = null
|
||||
if (args[2] != null) {
|
||||
val cert = ByteArrayInputStream(args[2] as ByteArray)
|
||||
val password = (args[3] as String).toCharArray()
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
keyStore.load(cert, password)
|
||||
val keyManagerFactory =
|
||||
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore, null)
|
||||
km = keyManagerFactory.keyManagers
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(km, tm, null)
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(AllowSelfSignedHostnameVerifier(args[1] as? String))
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
result.error("error", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
class AllowSelfSignedTrustManager(private val serverHost: String?) : X509ExtendedTrustManager() {
|
||||
private val defaultTrustManager: X509ExtendedTrustManager = getDefaultTrustManager()
|
||||
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) =
|
||||
defaultTrustManager.checkClientTrusted(chain, authType)
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, socket: Socket?
|
||||
) = defaultTrustManager.checkClientTrusted(chain, authType, socket)
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, engine: SSLEngine?
|
||||
) = defaultTrustManager.checkClientTrusted(chain, authType, engine)
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
if (serverHost == null) return
|
||||
defaultTrustManager.checkServerTrusted(chain, authType)
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, socket: Socket?
|
||||
) {
|
||||
if (serverHost == null) return
|
||||
val socketAddress = socket?.remoteSocketAddress
|
||||
if (socketAddress is InetSocketAddress && socketAddress.hostName == serverHost) return
|
||||
defaultTrustManager.checkServerTrusted(chain, authType, socket)
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, engine: SSLEngine?
|
||||
) {
|
||||
if (serverHost == null || engine?.peerHost == serverHost) return
|
||||
defaultTrustManager.checkServerTrusted(chain, authType, engine)
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = defaultTrustManager.acceptedIssuers
|
||||
|
||||
private fun getDefaultTrustManager(): X509ExtendedTrustManager {
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
return factory.trustManagers.filterIsInstance<X509ExtendedTrustManager>().first()
|
||||
}
|
||||
}
|
||||
|
||||
class AllowSelfSignedHostnameVerifier(private val serverHost: String?) : HostnameVerifier {
|
||||
companion object {
|
||||
private val _defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
|
||||
}
|
||||
|
||||
override fun verify(hostname: String?, session: SSLSession?): Boolean {
|
||||
if (serverHost == null || hostname == serverHost) {
|
||||
return true
|
||||
} else {
|
||||
return _defaultHostnameVerifier.verify(hostname, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class HttpSSLOptions {
|
||||
static const MethodChannel _channel = MethodChannel('immich/httpSSLOptions');
|
||||
|
||||
static void apply() {
|
||||
AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert;
|
||||
bool allowSelfSignedSSLCert =
|
||||
Store.get(setting.storeKey as StoreKey<bool>, setting.defaultValue);
|
||||
_apply(allowSelfSignedSSLCert);
|
||||
}
|
||||
|
||||
static void applyFromSettings(bool newValue) {
|
||||
_apply(newValue);
|
||||
}
|
||||
|
||||
static void _apply(bool allowSelfSignedSSLCert) {
|
||||
String? serverHost;
|
||||
if (allowSelfSignedSSLCert && Store.tryGet(StoreKey.currentUser) != null) {
|
||||
serverHost = Uri.parse(Store.tryGet(StoreKey.serverEndpoint) ?? "").host;
|
||||
}
|
||||
|
||||
SSLClientCertStoreVal? clientCert = SSLClientCertStoreVal.load();
|
||||
|
||||
HttpOverrides.global =
|
||||
HttpSSLCertOverride(allowSelfSignedSSLCert, serverHost, clientCert);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
_channel.invokeMethod("apply", [
|
||||
allowSelfSignedSSLCert,
|
||||
serverHost,
|
||||
clientCert?.data,
|
||||
clientCert?.password,
|
||||
]).onError<PlatformException>((e, _) {
|
||||
final log = Logger("HttpSSLOptions");
|
||||
log.severe('Failed to set SSL options', e.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue