feat: custom ISD-R AIDs for developer #146

Closed
septs wants to merge 3 commits from septs:custom-aids into master
9 changed files with 104 additions and 13 deletions

View file

@ -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,
) )
} }

View file

@ -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
}
} }

View file

@ -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
} }

View file

@ -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)!!

View file

@ -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'

View file

@ -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)

View file

@ -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>

View file

@ -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"

View file

@ -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