diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index f53e6ff..a05ba7e 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -30,7 +30,24 @@ + android:label="@string/download_wizard"> + + + + + + + + + + + + + = 0) { isIndeterminate = false @@ -289,7 +298,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { } protected fun refreshButtons() { - (requireActivity() as DownloadWizardActivity).refreshButtons() + requireWizardActivity().refreshButtons() } open fun beforeNext() {} diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index 5fa8002..1e6045d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -35,8 +35,12 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment = DownloadWizardProgressFragment() - override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = - DownloadWizardMethodSelectFragment() + override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment { + if (requireWizardActivity().getActivationCodeFromIntent() != null) { + return DownloadWizardSlotSelectFragment() + } + return DownloadWizardMethodSelectFragment() + } override fun onCreateView( inflater: LayoutInflater, diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index 2846fd7..c492f03 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -4,6 +4,7 @@ import android.app.AlertDialog import android.content.ClipboardManager import android.graphics.BitmapFactory import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -21,10 +22,16 @@ import com.journeyapps.barcodescanner.ScanOptions import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + companion object { + const val TAG = "DownloadWizardMethodSelectFragment" + } + data class DownloadMethod( val iconRes: Int, val titleRes: Int, @@ -89,6 +96,11 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = DownloadWizardSlotSelectFragment() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireWizardActivity().getActivationCodeFromIntent()?.let(::processLpaString) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -125,28 +137,30 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard } private fun processLpaString(input: String) { + if (runBlocking { preferenceRepository.verboseLoggingFlow.first() }) { + Log.i(TAG, "Processing LPA string: $input") + } try { val parsed = ActivationCode.fromString(input) state.smdp = parsed.address state.matchingId = parsed.matchingId if (parsed.confirmationCodeRequired) { - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.profile_download_required_confirmation_code) - setMessage(R.string.profile_download_required_confirmation_code_message) - setCancelable(true) - setPositiveButton(android.R.string.ok, null) - show() - } + AlertDialog.Builder(requireContext()) + .setTitle(R.string.profile_download_required_confirmation_code) + .setMessage(R.string.profile_download_required_confirmation_code_message) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() } gotoNextFragment(DownloadWizardDetailsFragment()) - } catch (e: IllegalArgumentException) { - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.profile_download_incorrect_lpa_string) - setMessage(R.string.profile_download_incorrect_lpa_string_message) - setCancelable(true) - setNegativeButton(android.R.string.cancel, null) - show() - } + } catch (e: IllegalStateException) { + Log.d(TAG, "Failed to parse LPA string", e) + AlertDialog.Builder(requireContext()) + .setTitle(R.string.profile_download_incorrect_lpa_string) + .setMessage(R.string.profile_download_incorrect_lpa_string_message) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .show() } } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt index c21e837..fbecfae 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt @@ -7,20 +7,26 @@ data class ActivationCode( val confirmationCodeRequired: Boolean = false, ) { companion object { - fun fromString(input: String): ActivationCode { - val components = input.removePrefix("LPA:").split('$') - if (components.size < 2 || components[0] != "1") { - throw IllegalArgumentException("Invalid activation code format") - } + fun fromString(token: String): ActivationCode { + val input = if (token.startsWith("LPA:", true)) token.drop(4) else token + val components = input.split('$').map { it.trim().ifEmpty { null } } + check(components.size >= 2) { "Invalid activation code format" } + check(components[0] == "1") { "Invalid activation code version" } return ActivationCode( - address = components[1].trim(), - matchingId = components.getOrNull(2)?.trim()?.ifBlank { null }, - oid = components.getOrNull(3)?.trim()?.ifBlank { null }, - confirmationCodeRequired = components.getOrNull(4)?.trim() == "1" + checkNotNull(components[1]) { "Invalid SM-DP+ address" }, + components.getOrNull(2), + components.getOrNull(3), + components.getOrNull(4) == "1", ) } } + init { + check(isFQDN(address)) { "Invalid SM-DP+ address" } + check(matchingId == null || isMatchingID(matchingId)) { "Invalid Matching ID" } + check(oid == null || isObjectIdentifier(oid)) { "Invalid OID" } + } + override fun toString(): String { val parts = arrayOf( "1", @@ -31,4 +37,40 @@ data class ActivationCode( ) return parts.joinToString("$").trimEnd('$') } -} \ No newline at end of file +} + +/** + * SGP.22 4.1 Activation Code (v2.2.2, p111) + * + * FQDN (Fully Qualified Domain Name) of the SM-DP+ (e.g., SMDP.GSMA.COM) + * restricted to the Alphanumeric mode character set defined in table 5 of ISO/IEC 18004 [15] + * excluding '$' + */ +private fun isFQDN(input: CharSequence): Boolean { + if (input.isEmpty() || input.length > 255) return false + val parts = input.split('.') + if (parts.size < 2) return false + for (part in parts) { + if (part.isEmpty() || part.length > 63) return false + if (part.all { it.isLetterOrDigit() || it == '-' }) continue + return false + } + return true +} + +/** + * SGP.22 4.1.1 Matching ID (v2.2.2, p112) + * + * Matching ID is a string of alphanumeric characters and hyphens. + */ +private fun isMatchingID(input: CharSequence) = + input.all { it.isLetterOrDigit() || it == '-' } + +/** + * SGP.22 4.1 Activation Code (v2.2.2, p111) + * + * SM-DP+ OID in the CERT.DPauth.ECDSA + */ +private fun isObjectIdentifier(input: CharSequence) = input.length < 255 && + input.count { it == '.' } > 2 && + input.split('.').all { it.isNotEmpty() && it.all(Char::isDigit) }