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.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.isdRAidFallbackFlow,
|
||||
).also {
|
||||
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
||||
it.lpa.setEs10xMss(60)
|
||||
|
@ -78,6 +79,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
),
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.isdRAidFallbackFlow,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import im.angry.openeuicc.util.UiccPortInfoCompat
|
||||
import im.angry.openeuicc.util.decodeHex
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.util.*
|
||||
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.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
||||
|
@ -14,22 +16,27 @@ class EuiccChannelImpl(
|
|||
override val intrinsicChannelName: String?,
|
||||
override val apduInterface: ApduInterface,
|
||||
verboseLoggingFlow: Flow<Boolean>,
|
||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||
ignoreTLSCertificateFlow: Flow<Boolean>,
|
||||
private val isdRAidFallback: Flow<Set<String>>,
|
||||
) : EuiccChannel {
|
||||
companion object {
|
||||
// TODO: This needs to go somewhere else.
|
||||
val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
|
||||
private const val TAG = "EuiccChannelImpl"
|
||||
|
||||
// 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 logicalSlotId = port.logicalSlotIndex
|
||||
override val portId = port.portIndex
|
||||
|
||||
override val lpa: LocalProfileAssistant =
|
||||
LocalProfileAssistantImpl(
|
||||
ISDR_AID,
|
||||
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
|
||||
isdRAid,
|
||||
apduInterface,
|
||||
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
|
||||
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow),
|
||||
)
|
||||
|
||||
override val atr: ByteArray?
|
||||
|
@ -39,4 +46,29 @@ class EuiccChannelImpl(
|
|||
get() = lpa.valid
|
||||
|
||||
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 {
|
||||
val channel = session.openLogicalChannel(aid)
|
||||
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
|
||||
val index = channels.indexOf(null)
|
||||
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 }
|
||||
return index
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -8,6 +9,7 @@ import android.provider.Settings
|
|||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
|
@ -72,6 +74,9 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
||||
.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
|
||||
|
||||
requirePreference<EditTextPreference>("pref_developer_custom_aids")
|
||||
.let(::setCustomAIDs)
|
||||
|
||||
requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
||||
.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
|
||||
|
||||
|
@ -79,6 +84,35 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
.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) =
|
||||
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.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.OpenEuiccApplication
|
||||
|
@ -31,6 +32,7 @@ internal object PreferenceKeys {
|
|||
|
||||
// ---- Developer Options ----
|
||||
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 IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||
}
|
||||
|
@ -48,6 +50,7 @@ class PreferenceRepository(private val context: Context) {
|
|||
|
||||
// ---- Developer Options ----
|
||||
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 ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||
|
||||
|
|
|
@ -171,6 +171,8 @@
|
|||
<string name="pref_advanced_logs">Logs</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_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_desc">Include non-production profiles in the list</string>
|
||||
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
|
||||
|
|
|
@ -57,6 +57,13 @@
|
|||
app:title="@string/pref_developer"
|
||||
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
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="pref_developer_unfiltered_profile_list"
|
||||
|
|
|
@ -38,6 +38,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
),
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.isdRAidFallbackFlow,
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Failed
|
||||
|
|
Loading…
Add table
Reference in a new issue