From 4989b1eec7434a3f12290e94b5965435b9fae666 Mon Sep 17 00:00:00 2001 From: septs Date: Sat, 8 Mar 2025 21:05:12 +0800 Subject: [PATCH 1/2] feat: discovery --- .../core/LocalProfileAssistantWrapper.kt | 8 ++ .../lpac_jni/EuiccConfiguredAddresses.kt | 29 +++++ .../lpac_jni/LocalProfileAssistant.kt | 13 ++ .../java/net/typeblog/lpac_jni/LpacJni.kt | 6 + .../lpac_jni/ProfileDiscoveryCallback.kt | 5 + .../impl/LocalProfileAssistantImpl.kt | 41 ++++-- .../src/main/jni/lpac-jni/lpac-discovery.c | 121 ++++++++++++++++++ .../src/main/jni/lpac-jni/lpac-discovery.h | 6 + .../src/main/jni/lpac-jni/lpac-download.c | 16 +-- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 2 + 10 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt create mode 100644 libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt create mode 100644 libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c create mode 100644 libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index b715ca0..f19616d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -1,9 +1,11 @@ package im.angry.openeuicc.core +import net.typeblog.lpac_jni.EuiccConfiguredAddresses import net.typeblog.lpac_jni.EuiccInfo2 import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification +import net.typeblog.lpac_jni.ProfileDiscoveryCallback import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : @@ -32,6 +34,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss) + override fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses = + lpa.getEuiccConfiguredAddresses() + override fun enableProfile(iccid: String, refresh: Boolean): Boolean = lpa.enableProfile(iccid, refresh) @@ -48,6 +53,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : callback: ProfileDownloadCallback ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) + override fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) = + lpa.discoveryProfile(smds, imei, callback) + override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber) override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt new file mode 100644 index 0000000..e7f090a --- /dev/null +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt @@ -0,0 +1,29 @@ +package net.typeblog.lpac_jni + +import android.util.Patterns + +// example address in GSMA SGP.26, some chips use addresses like this +@Suppress("SpellCheckingInspection") +val invalidDPAddresses = setOf( + "testrootsmds.gsma.com", + "testrootsmds.example.com", +) + +class EuiccConfiguredAddresses(defaultDPAddress: String?, rootDSAddress: String?) { + val defaultDPAddress: String? = defaultDPAddress.takeUnless(::isInvalidDPAddress) + val rootDSAddress = rootDSAddress.takeUnless(::isInvalidDSAddress) + + val discoverable: Boolean + get() = !defaultDPAddress.isNullOrBlank() || !rootDSAddress.isNullOrBlank() +} + +private fun isInvalidDPAddress(address: String?): Boolean { + if (address.isNullOrBlank()) return true + return !Patterns.DOMAIN_NAME.matcher(address).matches() +} + +private fun isInvalidDSAddress(address: String?): Boolean { + if (address.isNullOrBlank()) return true + if (address in invalidDPAddresses) return true + return !Patterns.DOMAIN_NAME.matcher(address).matches() +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index 48ab1c5..d98c241 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -12,6 +12,15 @@ interface LocalProfileAssistant { val lastApduException: Exception?, ) : Exception("Failed to download profile") + @Suppress("ArrayInDataClass") + data class ProfileDiscoveryException( + val lpaErrorReason: String, + val lastHttpResponse: HttpResponse?, + val lastHttpException: Exception?, + val lastApduResponse: ByteArray?, + val lastApduException: Exception?, + ) : Exception("Failed to discovery profile") + class ProfileRenameException() : Exception("Failed to rename profile") class ProfileNameTooLongException() : Exception("Profile name too long") class ProfileNameIsInvalidUTF8Exception() : Exception("Profile name is invalid UTF-8") @@ -30,6 +39,9 @@ interface LocalProfileAssistant { */ fun setEs10xMss(mss: Byte) + // es10a + fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses + // All blocking functions in this class assume that they are executed on non-Main threads // The IO context in Kotlin's coroutine library is recommended. fun enableProfile(iccid: String, refresh: Boolean = true): Boolean @@ -38,6 +50,7 @@ interface LocalProfileAssistant { fun downloadProfile(smdp: String, matchingId: String?, imei: String?, confirmationCode: String?, callback: ProfileDownloadCallback) + fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) fun deleteNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): Boolean diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index fa9474f..bf57f63 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -29,6 +29,12 @@ internal object LpacJni { external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null) external fun es10bDeleteNotification(handle: Long, seqNumber: Long): Int + // es10a + external fun es10aGetEuiccConfiguredAddresses(handle: Long): EuiccConfiguredAddresses + + // es9p + es11 + external fun discoveryProfile(handle: Long, address: String, imei: String?, callback: ProfileDiscoveryCallback): Int + // es9p + es10b // We do not expose all of the functions because of tediousness :) external fun downloadProfile(handle: Long, smdp: String, matchingId: String?, imei: String?, diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt new file mode 100644 index 0000000..e7b7196 --- /dev/null +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt @@ -0,0 +1,5 @@ +package net.typeblog.lpac_jni + +interface ProfileDiscoveryCallback { + fun onDiscovered(hosts: Array) +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 3674f4f..11a9931 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -9,6 +9,7 @@ import net.typeblog.lpac_jni.HttpInterface.HttpResponse import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification +import net.typeblog.lpac_jni.ProfileDiscoveryCallback import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.Version @@ -93,6 +94,9 @@ class LocalProfileAssistantImpl( LpacJni.euiccSetMss(contextHandle, mss) } + override fun getEuiccConfiguredAddresses() = + LpacJni.es10aGetEuiccConfiguredAddresses(contextHandle) + override val valid: Boolean get() = !finalized && apduInterface.valid && try { // If we can read both eID and euiccInfo2 properly, we are likely looking at @@ -212,21 +216,32 @@ class LocalProfileAssistantImpl( callback ) - if (res != 0) { - // Construct the error now to store any error information we _can_ access - val err = LocalProfileAssistant.ProfileDownloadException( - lpaErrorReason = LpacJni.downloadErrCodeToString(-res), - httpInterface.lastHttpResponse, - httpInterface.lastHttpException, - apduInterface.lastApduResponse, - apduInterface.lastApduException, - ) + if (res == 0) return + // Construct the error now to store any error information we _can_ access + val err = LocalProfileAssistant.ProfileDownloadException( + lpaErrorReason = LpacJni.downloadErrCodeToString(-res), + httpInterface.lastHttpResponse, + httpInterface.lastHttpException, + apduInterface.lastApduResponse, + apduInterface.lastApduException, + ) - // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. - LpacJni.cancelSessions(contextHandle) + // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. + LpacJni.cancelSessions(contextHandle) - throw err - } + throw err + } + + override fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) { + val res = LpacJni.discoveryProfile(contextHandle, smds, imei, callback) + if (res == 0) return + throw LocalProfileAssistant.ProfileDiscoveryException( + lpaErrorReason = LpacJni.downloadErrCodeToString(-res), + httpInterface.lastHttpResponse, + httpInterface.lastHttpException, + apduInterface.lastApduResponse, + apduInterface.lastApduException, + ) } @Synchronized diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c new file mode 100644 index 0000000..85d56ab --- /dev/null +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c @@ -0,0 +1,121 @@ +#include "lpac-notifications.h" +#include +#include +#include +#include +#include +#include + +#define EUICC_CONFIGURED_ADDRESSES_CLASS "net/typeblog/lpac_jni/EuiccConfiguredAddresses" + +jclass euicc_configured_addresses_class; +jmethodID euicc_configured_addresses_constructor; + +jmethodID on_discovered; + +#define DISCOVERY_CALLBACK_CLASS "net/typeblog/lpac_jni/ProfileDiscoveryCallback" +#define STRING_CLASS "java/lang/String" + +void lpac_discovery_init() { + LPAC_JNI_SETUP_ENV; + + jclass download_callback_class = (*env)->FindClass(env, DISCOVERY_CALLBACK_CLASS); + on_discovered = (*env)->GetMethodID(env, download_callback_class, "onDiscovered", + "([L" STRING_CLASS ";)V"); + + euicc_configured_addresses_class = (*env)->FindClass(env, EUICC_CONFIGURED_ADDRESSES_CLASS); + euicc_configured_addresses_class = (*env)->NewGlobalRef(env, euicc_configured_addresses_class); + euicc_configured_addresses_constructor = (*env)->GetMethodID( + env, euicc_configured_addresses_class, "", + "(L" STRING_CLASS ";L" STRING_CLASS ";)V"); +} + +JNIEXPORT jobject JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_es10aGetEuiccConfiguredAddresses( + JNIEnv *env, + __attribute__((unused)) jobject thiz, + jlong handle +) { + struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + struct es10a_euicc_configured_addresses addresses; + jobject ret = NULL; + if (es10a_get_euicc_configured_addresses(ctx, &addresses) == 0) { + jstring default_dp_address = toJString(env, addresses.defaultDpAddress); + jstring root_ds_address = toJString(env, addresses.rootDsAddress); + ret = (*env)->NewObject(env, euicc_configured_addresses_class, + euicc_configured_addresses_constructor, + default_dp_address, root_ds_address); + } + es10a_euicc_configured_addresses_free(&addresses); + return ret; +} + +JNIEXPORT jint JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_discoveryProfile( + JNIEnv *env, + __attribute__((unused)) jobject thiz, + jlong handle, + jstring address, + jstring imei, + jobject callback +) { + struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + + const char *_address = (*env)->GetStringUTFChars(env, address, NULL); + const char *_imei = NULL; + + if (imei != NULL) { + _imei = (*env)->GetStringUTFChars(env, address, NULL); + } + + ctx->http.server_address = _address; + + char **smdp_list = NULL; + jobjectArray addresses = NULL; + int ret = -1; + + ret = es10b_get_euicc_challenge_and_info(ctx); + syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + ret = es9p_initiate_authentication(ctx); + syslog(LOG_INFO, "es9p_initiate_authentication %d", ret); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + ret = es10b_authenticate_server(ctx, NULL, _imei); + syslog(LOG_INFO, "es10b_authenticate_server %d", ret); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + ret = es11_authenticate_client(ctx, &smdp_list); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + jsize n = 0; + for (n = 0; smdp_list[n] != NULL; n++) continue; + + addresses = (*env)->NewObjectArray(env, n, string_class, NULL); + + for (jsize index = 0; index < n; index++) { + jstring element = toJString(env, smdp_list[index]); + (*env)->SetObjectArrayElement(env, addresses, index, element); + } + + (*env)->CallVoidMethod(env, callback, on_discovered, addresses); + + out: + if (_imei != NULL) (*env)->ReleaseStringUTFChars(env, imei, _imei); + (*env)->ReleaseStringUTFChars(env, address, _address); + es11_smdp_list_free_all(smdp_list); + return ret; +} \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h new file mode 100644 index 0000000..cee05e1 --- /dev/null +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "lpac-jni.h" + +void lpac_discovery_init(); diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c index bae2ee8..61789c0 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c @@ -11,7 +11,7 @@ jobject download_state_authenticating; jobject download_state_downloading; jobject download_state_finalizing; -jmethodID on_state_update; +jmethodID on_discovered; void lpac_download_init() { LPAC_JNI_SETUP_ENV; @@ -52,8 +52,8 @@ void lpac_download_init() { jclass download_callback_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/ProfileDownloadCallback"); - on_state_update = (*env)->GetMethodID(env, download_callback_class, "onStateUpdate", - "(Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;)V"); + on_discovered = (*env)->GetMethodID(env, download_callback_class, "onStateUpdate", + "(Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;)V"); } JNIEXPORT jint JNICALL @@ -79,7 +79,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j ctx->http.server_address = _smdp; - (*env)->CallVoidMethod(env, callback, on_state_update, download_state_preparing); + (*env)->CallVoidMethod(env, callback, on_discovered, download_state_preparing); ret = es10b_get_euicc_challenge_and_info(ctx); syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret); if (ret < 0) { @@ -87,7 +87,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j goto out; } - (*env)->CallVoidMethod(env, callback, on_state_update, download_state_connecting); + (*env)->CallVoidMethod(env, callback, on_discovered, download_state_connecting); ret = es9p_initiate_authentication(ctx); syslog(LOG_INFO, "es9p_initiate_authentication %d", ret); if (ret < 0) { @@ -95,7 +95,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j goto out; } - (*env)->CallVoidMethod(env, callback, on_state_update, download_state_authenticating); + (*env)->CallVoidMethod(env, callback, on_discovered, download_state_authenticating); ret = es10b_authenticate_server(ctx, _matching_id, _imei); syslog(LOG_INFO, "es10b_authenticate_server %d", ret); if (ret < 0) { @@ -109,7 +109,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j goto out; } - (*env)->CallVoidMethod(env, callback, on_state_update, download_state_downloading); + (*env)->CallVoidMethod(env, callback, on_discovered, download_state_downloading); ret = es10b_prepare_download(ctx, _confirmation_code); syslog(LOG_INFO, "es10b_prepare_download %d", ret); if (ret < 0) { @@ -121,7 +121,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j if (ret < 0) goto out; - (*env)->CallVoidMethod(env, callback, on_state_update, download_state_finalizing); + (*env)->CallVoidMethod(env, callback, on_discovered, download_state_finalizing); ret = es10b_load_bound_profile_package(ctx, &es10b_load_bound_profile_package_result); syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason); if (ret < 0) { diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index ca319db..b144484 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -8,6 +8,7 @@ #include "lpac-jni.h" #include "lpac-download.h" #include "lpac-notifications.h" +#include "lpac-discovery.h" #include "interface-wrapper.h" JavaVM *jvm = NULL; @@ -21,6 +22,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { jvm = vm; interface_wrapper_init(); lpac_download_init(); + lpac_discovery_init(); LPAC_JNI_SETUP_ENV; string_class = (*env)->FindClass(env, "java/lang/String"); From 956280affe60c965ed1e1af0f086cc2b650dfaa8 Mon Sep 17 00:00:00 2001 From: septs Date: Sat, 8 Mar 2025 21:05:12 +0800 Subject: [PATCH 2/2] feat: discovery --- .../core/LocalProfileAssistantWrapper.kt | 8 ++ .../lpac_jni/EuiccConfiguredAddresses.kt | 29 +++++ .../lpac_jni/LocalProfileAssistant.kt | 13 ++ .../java/net/typeblog/lpac_jni/LpacJni.kt | 6 + .../lpac_jni/ProfileDiscoveryCallback.kt | 5 + .../impl/LocalProfileAssistantImpl.kt | 41 ++++-- .../src/main/jni/lpac-jni/lpac-discovery.c | 121 ++++++++++++++++++ .../src/main/jni/lpac-jni/lpac-discovery.h | 6 + .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 2 + 9 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt create mode 100644 libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt create mode 100644 libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c create mode 100644 libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index b715ca0..f19616d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -1,9 +1,11 @@ package im.angry.openeuicc.core +import net.typeblog.lpac_jni.EuiccConfiguredAddresses import net.typeblog.lpac_jni.EuiccInfo2 import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification +import net.typeblog.lpac_jni.ProfileDiscoveryCallback import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : @@ -32,6 +34,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss) + override fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses = + lpa.getEuiccConfiguredAddresses() + override fun enableProfile(iccid: String, refresh: Boolean): Boolean = lpa.enableProfile(iccid, refresh) @@ -48,6 +53,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : callback: ProfileDownloadCallback ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) + override fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) = + lpa.discoveryProfile(smds, imei, callback) + override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber) override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt new file mode 100644 index 0000000..e7f090a --- /dev/null +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt @@ -0,0 +1,29 @@ +package net.typeblog.lpac_jni + +import android.util.Patterns + +// example address in GSMA SGP.26, some chips use addresses like this +@Suppress("SpellCheckingInspection") +val invalidDPAddresses = setOf( + "testrootsmds.gsma.com", + "testrootsmds.example.com", +) + +class EuiccConfiguredAddresses(defaultDPAddress: String?, rootDSAddress: String?) { + val defaultDPAddress: String? = defaultDPAddress.takeUnless(::isInvalidDPAddress) + val rootDSAddress = rootDSAddress.takeUnless(::isInvalidDSAddress) + + val discoverable: Boolean + get() = !defaultDPAddress.isNullOrBlank() || !rootDSAddress.isNullOrBlank() +} + +private fun isInvalidDPAddress(address: String?): Boolean { + if (address.isNullOrBlank()) return true + return !Patterns.DOMAIN_NAME.matcher(address).matches() +} + +private fun isInvalidDSAddress(address: String?): Boolean { + if (address.isNullOrBlank()) return true + if (address in invalidDPAddresses) return true + return !Patterns.DOMAIN_NAME.matcher(address).matches() +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index 48ab1c5..d98c241 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -12,6 +12,15 @@ interface LocalProfileAssistant { val lastApduException: Exception?, ) : Exception("Failed to download profile") + @Suppress("ArrayInDataClass") + data class ProfileDiscoveryException( + val lpaErrorReason: String, + val lastHttpResponse: HttpResponse?, + val lastHttpException: Exception?, + val lastApduResponse: ByteArray?, + val lastApduException: Exception?, + ) : Exception("Failed to discovery profile") + class ProfileRenameException() : Exception("Failed to rename profile") class ProfileNameTooLongException() : Exception("Profile name too long") class ProfileNameIsInvalidUTF8Exception() : Exception("Profile name is invalid UTF-8") @@ -30,6 +39,9 @@ interface LocalProfileAssistant { */ fun setEs10xMss(mss: Byte) + // es10a + fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses + // All blocking functions in this class assume that they are executed on non-Main threads // The IO context in Kotlin's coroutine library is recommended. fun enableProfile(iccid: String, refresh: Boolean = true): Boolean @@ -38,6 +50,7 @@ interface LocalProfileAssistant { fun downloadProfile(smdp: String, matchingId: String?, imei: String?, confirmationCode: String?, callback: ProfileDownloadCallback) + fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) fun deleteNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): Boolean diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index fa9474f..bf57f63 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -29,6 +29,12 @@ internal object LpacJni { external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null) external fun es10bDeleteNotification(handle: Long, seqNumber: Long): Int + // es10a + external fun es10aGetEuiccConfiguredAddresses(handle: Long): EuiccConfiguredAddresses + + // es9p + es11 + external fun discoveryProfile(handle: Long, address: String, imei: String?, callback: ProfileDiscoveryCallback): Int + // es9p + es10b // We do not expose all of the functions because of tediousness :) external fun downloadProfile(handle: Long, smdp: String, matchingId: String?, imei: String?, diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt new file mode 100644 index 0000000..e7b7196 --- /dev/null +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt @@ -0,0 +1,5 @@ +package net.typeblog.lpac_jni + +interface ProfileDiscoveryCallback { + fun onDiscovered(hosts: Array) +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 3674f4f..11a9931 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -9,6 +9,7 @@ import net.typeblog.lpac_jni.HttpInterface.HttpResponse import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification +import net.typeblog.lpac_jni.ProfileDiscoveryCallback import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.Version @@ -93,6 +94,9 @@ class LocalProfileAssistantImpl( LpacJni.euiccSetMss(contextHandle, mss) } + override fun getEuiccConfiguredAddresses() = + LpacJni.es10aGetEuiccConfiguredAddresses(contextHandle) + override val valid: Boolean get() = !finalized && apduInterface.valid && try { // If we can read both eID and euiccInfo2 properly, we are likely looking at @@ -212,21 +216,32 @@ class LocalProfileAssistantImpl( callback ) - if (res != 0) { - // Construct the error now to store any error information we _can_ access - val err = LocalProfileAssistant.ProfileDownloadException( - lpaErrorReason = LpacJni.downloadErrCodeToString(-res), - httpInterface.lastHttpResponse, - httpInterface.lastHttpException, - apduInterface.lastApduResponse, - apduInterface.lastApduException, - ) + if (res == 0) return + // Construct the error now to store any error information we _can_ access + val err = LocalProfileAssistant.ProfileDownloadException( + lpaErrorReason = LpacJni.downloadErrCodeToString(-res), + httpInterface.lastHttpResponse, + httpInterface.lastHttpException, + apduInterface.lastApduResponse, + apduInterface.lastApduException, + ) - // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. - LpacJni.cancelSessions(contextHandle) + // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. + LpacJni.cancelSessions(contextHandle) - throw err - } + throw err + } + + override fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) { + val res = LpacJni.discoveryProfile(contextHandle, smds, imei, callback) + if (res == 0) return + throw LocalProfileAssistant.ProfileDiscoveryException( + lpaErrorReason = LpacJni.downloadErrCodeToString(-res), + httpInterface.lastHttpResponse, + httpInterface.lastHttpException, + apduInterface.lastApduResponse, + apduInterface.lastApduException, + ) } @Synchronized diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c new file mode 100644 index 0000000..85d56ab --- /dev/null +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c @@ -0,0 +1,121 @@ +#include "lpac-notifications.h" +#include +#include +#include +#include +#include +#include + +#define EUICC_CONFIGURED_ADDRESSES_CLASS "net/typeblog/lpac_jni/EuiccConfiguredAddresses" + +jclass euicc_configured_addresses_class; +jmethodID euicc_configured_addresses_constructor; + +jmethodID on_discovered; + +#define DISCOVERY_CALLBACK_CLASS "net/typeblog/lpac_jni/ProfileDiscoveryCallback" +#define STRING_CLASS "java/lang/String" + +void lpac_discovery_init() { + LPAC_JNI_SETUP_ENV; + + jclass download_callback_class = (*env)->FindClass(env, DISCOVERY_CALLBACK_CLASS); + on_discovered = (*env)->GetMethodID(env, download_callback_class, "onDiscovered", + "([L" STRING_CLASS ";)V"); + + euicc_configured_addresses_class = (*env)->FindClass(env, EUICC_CONFIGURED_ADDRESSES_CLASS); + euicc_configured_addresses_class = (*env)->NewGlobalRef(env, euicc_configured_addresses_class); + euicc_configured_addresses_constructor = (*env)->GetMethodID( + env, euicc_configured_addresses_class, "", + "(L" STRING_CLASS ";L" STRING_CLASS ";)V"); +} + +JNIEXPORT jobject JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_es10aGetEuiccConfiguredAddresses( + JNIEnv *env, + __attribute__((unused)) jobject thiz, + jlong handle +) { + struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + struct es10a_euicc_configured_addresses addresses; + jobject ret = NULL; + if (es10a_get_euicc_configured_addresses(ctx, &addresses) == 0) { + jstring default_dp_address = toJString(env, addresses.defaultDpAddress); + jstring root_ds_address = toJString(env, addresses.rootDsAddress); + ret = (*env)->NewObject(env, euicc_configured_addresses_class, + euicc_configured_addresses_constructor, + default_dp_address, root_ds_address); + } + es10a_euicc_configured_addresses_free(&addresses); + return ret; +} + +JNIEXPORT jint JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_discoveryProfile( + JNIEnv *env, + __attribute__((unused)) jobject thiz, + jlong handle, + jstring address, + jstring imei, + jobject callback +) { + struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + + const char *_address = (*env)->GetStringUTFChars(env, address, NULL); + const char *_imei = NULL; + + if (imei != NULL) { + _imei = (*env)->GetStringUTFChars(env, address, NULL); + } + + ctx->http.server_address = _address; + + char **smdp_list = NULL; + jobjectArray addresses = NULL; + int ret = -1; + + ret = es10b_get_euicc_challenge_and_info(ctx); + syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + ret = es9p_initiate_authentication(ctx); + syslog(LOG_INFO, "es9p_initiate_authentication %d", ret); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + ret = es10b_authenticate_server(ctx, NULL, _imei); + syslog(LOG_INFO, "es10b_authenticate_server %d", ret); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + ret = es11_authenticate_client(ctx, &smdp_list); + if (ret < 0) { + ret = -ES10B_ERROR_REASON_UNDEFINED; + goto out; + } + + jsize n = 0; + for (n = 0; smdp_list[n] != NULL; n++) continue; + + addresses = (*env)->NewObjectArray(env, n, string_class, NULL); + + for (jsize index = 0; index < n; index++) { + jstring element = toJString(env, smdp_list[index]); + (*env)->SetObjectArrayElement(env, addresses, index, element); + } + + (*env)->CallVoidMethod(env, callback, on_discovered, addresses); + + out: + if (_imei != NULL) (*env)->ReleaseStringUTFChars(env, imei, _imei); + (*env)->ReleaseStringUTFChars(env, address, _address); + es11_smdp_list_free_all(smdp_list); + return ret; +} \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h new file mode 100644 index 0000000..cee05e1 --- /dev/null +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "lpac-jni.h" + +void lpac_discovery_init(); diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index ca319db..b144484 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -8,6 +8,7 @@ #include "lpac-jni.h" #include "lpac-download.h" #include "lpac-notifications.h" +#include "lpac-discovery.h" #include "interface-wrapper.h" JavaVM *jvm = NULL; @@ -21,6 +22,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { jvm = vm; interface_wrapper_init(); lpac_download_init(); + lpac_discovery_init(); LPAC_JNI_SETUP_ENV; string_class = (*env)->FindClass(env, "java/lang/String");