feat: custom ISD-R AIDs for developer #146
9 changed files with 104 additions and 13 deletions
|
@ -46,6 +46,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
),
|
),
|
||||||
context.preferenceRepository.verboseLoggingFlow,
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
|
context.preferenceRepository.isdRAidFallbackFlow,
|
||||||
).also {
|
).also {
|
||||||
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
||||||
it.lpa.setEs10xMss(60)
|
it.lpa.setEs10xMss(60)
|
||||||
|
@ -78,6 +79,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
),
|
),
|
||||||
context.preferenceRepository.verboseLoggingFlow,
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
|
context.preferenceRepository.isdRAidFallbackFlow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import im.angry.openeuicc.util.UiccPortInfoCompat
|
import android.util.Log
|
||||||
import im.angry.openeuicc.util.decodeHex
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.typeblog.lpac_jni.ApduInterface
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
||||||
|
@ -14,23 +16,28 @@ class EuiccChannelImpl(
|
||||||
override val intrinsicChannelName: String?,
|
override val intrinsicChannelName: String?,
|
||||||
override val apduInterface: ApduInterface,
|
override val apduInterface: ApduInterface,
|
||||||
verboseLoggingFlow: Flow<Boolean>,
|
verboseLoggingFlow: Flow<Boolean>,
|
||||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
ignoreTLSCertificateFlow: Flow<Boolean>,
|
||||||
|
private val isdRAidFallback: Flow<Set<String>>,
|
||||||
) : EuiccChannel {
|
) : EuiccChannel {
|
||||||
companion object {
|
companion object {
|
||||||
// TODO: This needs to go somewhere else.
|
private const val TAG = "EuiccChannelImpl"
|
||||||
val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
|
|
||||||
|
// Specs: SGP.02 v4.3 (Section 2.2.3)
|
||||||
|
// https://www.gsma.com/esim/wp-content/uploads/2023/02/SGP.02-v4.3.pdf#page=27
|
||||||
|
val STANDARD_ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val isdRAid = findISDRAID()
|
||||||
|
|
||||||
override val slotId = port.card.physicalSlotIndex
|
override val slotId = port.card.physicalSlotIndex
|
||||||
override val logicalSlotId = port.logicalSlotIndex
|
override val logicalSlotId = port.logicalSlotIndex
|
||||||
override val portId = port.portIndex
|
override val portId = port.portIndex
|
||||||
|
|
||||||
override val lpa: LocalProfileAssistant =
|
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
|
||||||
LocalProfileAssistantImpl(
|
isdRAid,
|
||||||
ISDR_AID,
|
apduInterface,
|
||||||
apduInterface,
|
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow),
|
||||||
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
override val atr: ByteArray?
|
override val atr: ByteArray?
|
||||||
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
|
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
|
||||||
|
@ -39,4 +46,29 @@ class EuiccChannelImpl(
|
||||||
get() = lpa.valid
|
get() = lpa.valid
|
||||||
|
|
||||||
override fun close() = lpa.close()
|
override fun close() = lpa.close()
|
||||||
|
|
||||||
|
private fun findISDRAID(): ByteArray {
|
||||||
|
val aids = buildList {
|
||||||
|
add(STANDARD_ISDR_AID.encodeHex())
|
||||||
|
addAll(runBlocking { isdRAidFallback.first() })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
apduInterface.connect()
|
||||||
|
for (aid in aids) {
|
||||||
|
val channel = aid.decodeHex()
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Trying ISD-R AID: $aid")
|
||||||
|
apduInterface.withLogicalChannel(channel) { true }
|
||||||
|
Log.d(TAG, "Selected ISD-R AID: $aid")
|
||||||
|
return channel
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "Failed to select ISD-R AID: $aid")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
apduInterface.disconnect()
|
||||||
|
}
|
||||||
|
return STANDARD_ISDR_AID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,10 @@ class OmapiApduInterface(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||||
val channel = session.openLogicalChannel(aid)
|
|
||||||
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
|
|
||||||
val index = channels.indexOf(null)
|
val index = channels.indexOf(null)
|
||||||
check(index != -1) { "No free logical channel slots" }
|
check(index != -1) { "No free logical channel slots" }
|
||||||
|
val channel = session.openLogicalChannel(aid)
|
||||||
|
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
|
||||||
synchronized(channels) { channels[index] = channel }
|
synchronized(channels) { channels[index] = channel }
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -8,6 +9,7 @@ import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
@ -72,6 +74,9 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
||||||
.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
|
.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
|
||||||
|
|
||||||
|
requirePreference<EditTextPreference>("pref_developer_custom_aids")
|
||||||
|
.let(::setCustomAIDs)
|
||||||
|
|
||||||
requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
||||||
.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
|
.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
|
||||||
|
|
||||||
|
@ -79,6 +84,35 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setCustomAIDs(preference: EditTextPreference) {
|
||||||
|
val flow = preferenceRepository.isdRAidFallbackFlow
|
||||||
|
val separator = "\n"
|
||||||
|
|
||||||
|
fun prepare(text: String) = text.uppercase().lines()
|
||||||
|
.map(String::trim).toSet()
|
||||||
|
.filter { it.length % 2 == 0 && it.length in 10..32 && it.all(Char::isHex) }
|
||||||
|
.take(10) // limit to 10 AIDs
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
flow.collect { preference.text = it.joinToString(separator) }
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setOnBindEditTextListener {
|
||||||
|
it.filters += allowedInputFilter { c -> c.isHex() || c == '\n' }
|
||||||
|
it.typeface = Typeface.MONOSPACE
|
||||||
|
it.setText(prepare(it.text.toString()).joinToString(separator))
|
||||||
|
it.setSelection(it.text.length)
|
||||||
|
it.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
runBlocking {
|
||||||
|
flow.updatePreference(prepare(newValue as String).toSet())
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
||||||
findPreference<T>(key)!!
|
findPreference<T>(key)!!
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.text.InputFilter
|
||||||
|
|
||||||
|
fun allowedInputFilter(predicate: (Char) -> Boolean) =
|
||||||
|
InputFilter { source, start, end, _, _, _ ->
|
||||||
|
source.substring(start until end).filter(predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Char.isHex() = isDigit() || uppercaseChar() in 'A'..'F'
|
|
@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
|
@ -31,6 +32,7 @@ internal object PreferenceKeys {
|
||||||
|
|
||||||
// ---- Developer Options ----
|
// ---- Developer Options ----
|
||||||
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
||||||
|
val ISD_R_AID_FALLBACK = stringSetPreferencesKey("isd_r_aid_fallback")
|
||||||
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
||||||
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||||
}
|
}
|
||||||
|
@ -48,6 +50,7 @@ class PreferenceRepository(private val context: Context) {
|
||||||
|
|
||||||
// ---- Developer Options ----
|
// ---- Developer Options ----
|
||||||
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
||||||
|
val isdRAidFallbackFlow = bindFlow(PreferenceKeys.ISD_R_AID_FALLBACK, emptySet())
|
||||||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||||
|
|
||||||
|
|
|
@ -171,6 +171,8 @@
|
||||||
<string name="pref_advanced_logs">Logs</string>
|
<string name="pref_advanced_logs">Logs</string>
|
||||||
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
||||||
<string name="pref_developer">Developer Options</string>
|
<string name="pref_developer">Developer Options</string>
|
||||||
|
<string name="pref_developer_custom_aids">ISD-R AIDs Fallback</string>
|
||||||
|
<string name="pref_developer_custom_aids_desc">Prefer to use standard ISD-R AID access, one ISD-R AID per line (hexadecimal string)</string>
|
||||||
<string name="pref_developer_unfiltered_profile_list">Show unfiltered profile list</string>
|
<string name="pref_developer_unfiltered_profile_list">Show unfiltered profile list</string>
|
||||||
<string name="pref_developer_unfiltered_profile_list_desc">Include non-production profiles in the list</string>
|
<string name="pref_developer_unfiltered_profile_list_desc">Include non-production profiles in the list</string>
|
||||||
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
|
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
|
||||||
|
|
|
@ -57,6 +57,13 @@
|
||||||
app:title="@string/pref_developer"
|
app:title="@string/pref_developer"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false">
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:key="pref_developer_custom_aids"
|
||||||
|
app:summary="@string/pref_developer_custom_aids_desc"
|
||||||
|
app:dialogMessage="@string/pref_developer_custom_aids_desc"
|
||||||
|
app:title="@string/pref_developer_custom_aids" />
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:key="pref_developer_unfiltered_profile_list"
|
app:key="pref_developer_unfiltered_profile_list"
|
||||||
|
|
|
@ -38,6 +38,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
||||||
),
|
),
|
||||||
context.preferenceRepository.verboseLoggingFlow,
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
|
context.preferenceRepository.isdRAidFallbackFlow,
|
||||||
)
|
)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// Failed
|
// Failed
|
||||||
|
|
Loading…
Add table
Reference in a new issue