From 71a7ac508cf386113b85f7d16a98a84aca7153ac Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 11 Feb 2023 23:09:49 -0500 Subject: [PATCH] Support hex-encoded SSID --- README.md | 3 +- .../be/mygod/vpnhotspot/RepeaterService.kt | 21 ++++-- .../manage/LocalOnlyHotspotTileService.kt | 2 +- .../vpnhotspot/manage/RepeaterManager.kt | 11 ++- .../net/wifi/P2pSupplicantConfiguration.kt | 5 +- .../net/wifi/SoftApConfigurationCompat.kt | 27 +++++--- .../net/wifi/WifiApDialogFragment.kt | 65 ++++++++++++++--- .../vpnhotspot/net/wifi/WifiSsidCompat.kt | 69 +++++++++++++++++++ .../res/drawable/ic_av_closed_caption.xml | 5 ++ .../res/drawable/ic_av_closed_caption_off.xml | 5 ++ mobile/src/main/res/drawable/toggle_hex.xml | 5 ++ mobile/src/main/res/layout/dialog_wifi_ap.xml | 2 + mobile/src/main/res/values-zh-rCN/strings.xml | 1 + mobile/src/main/res/values/strings.xml | 1 + 14 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiSsidCompat.kt create mode 100644 mobile/src/main/res/drawable/ic_av_closed_caption.xml create mode 100644 mobile/src/main/res/drawable/ic_av_closed_caption_off.xml create mode 100644 mobile/src/main/res/drawable/toggle_hex.xml diff --git a/README.md b/README.md index d258ac60..64554814 100644 --- a/README.md +++ b/README.md @@ -261,8 +261,9 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` -* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30, prior to API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setVendorElements(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setWifiSsid(Landroid/net/wifi/WifiSsid;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api` diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index 85b37aa0..c02c0945 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -34,6 +34,7 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupI import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setVendorElements import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps +import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat import be.mygod.vpnhotspot.root.RepeaterCommands import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.util.* @@ -53,6 +54,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene const val KEY_SAFE_MODE = "service.repeater.safeMode" private const val KEY_NETWORK_NAME = "service.repeater.networkName" + private const val KEY_NETWORK_NAME_HEX = "service.repeater.networkNameHex" private const val KEY_PASSPHRASE = "service.repeater.passphrase" private const val KEY_OPERATING_BAND = "service.repeater.band.v4" private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3" @@ -76,9 +78,16 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene @get:RequiresApi(29) private val mNetworkName by lazy @TargetApi(29) { UnblockCentral.WifiP2pConfig_Builder_mNetworkName } - var networkName: String? - get() = app.pref.getString(KEY_NETWORK_NAME, null) - set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) } + var networkName: WifiSsidCompat? + get() = app.pref.getString(KEY_NETWORK_NAME, null).let { legacy -> + if (legacy != null) WifiSsidCompat.fromUtf8Text(legacy).also { + app.pref.edit { + putString(KEY_NETWORK_NAME_HEX, it!!.hex) + remove(KEY_NETWORK_NAME) + } + } else WifiSsidCompat.fromHex(app.pref.getString(KEY_NETWORK_NAME_HEX, null)) + } + set(value) = app.pref.edit { putString(KEY_NETWORK_NAME_HEX, value?.hex) } var passphrase: String? get() = app.pref.getString(KEY_PASSPHRASE, null) set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) } @@ -447,10 +456,10 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene setOperatingChannel() } if (Build.VERSION.SDK_INT >= 33) setVendorElements() - val networkName = networkName + val networkName = networkName?.toString() val passphrase = passphrase @SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR - if (!safeMode || networkName.isNullOrEmpty() || passphrase.isNullOrEmpty()) { + if (!safeMode || networkName == null || passphrase.isNullOrEmpty()) { persistNextGroup = true p2pManager.createGroup(channel, listener) } else @TargetApi(29) { @@ -510,7 +519,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene } binder.group = group if (persistNextGroup) { - networkName = group.networkName + networkName = WifiSsidCompat.fromUtf8Text(group.networkName) passphrase = group.passphrase persistNextGroup = false } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt index 2983e40c..2a7b7c3e 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt @@ -36,7 +36,7 @@ class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() { label = getText(R.string.tethering_temp_hotspot) } else { state = Tile.STATE_ACTIVE - label = binder.configuration?.ssid ?: getText(R.string.tethering_temp_hotspot) + label = binder.configuration?.ssid?.toString() ?: getText(R.string.tethering_temp_hotspot) subtitleDevices { it == iface } } updateTile() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt index 4f1f252a..c89e5dc7 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt @@ -33,6 +33,7 @@ import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment import be.mygod.vpnhotspot.net.wifi.WifiApManager +import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.showAllowingStateLoss @@ -215,7 +216,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic } else binder?.let { binder -> val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group } if (group != null) return SoftApConfigurationCompat( - ssid = group.networkName, + ssid = WifiSsidCompat.fromUtf8Text(group.networkName), securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled, shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis, @@ -253,8 +254,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic RepeaterService.passphrase = config.passphrase } else holder.config?.let { master -> val binder = binder - if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase || - master.bssid != config.bssid) try { + val mayBeModified = master.psk != config.passphrase || master.bssid != config.bssid || config.ssid.run { + if (this != null) decode().let { + it == null || binder?.group?.networkName != it + } else binder?.group?.networkName != null + } + if (mayBeModified) try { withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!, config.bssid) } (this.binder ?: binder)?.group = null } catch (e: Exception) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt index a0afe16f..8f178127 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt @@ -145,10 +145,9 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) { content.target.bssid?.let { MacAddress.fromString(it) } } - suspend fun update(ssid: String, psk: String, bssid: MacAddress?) { + suspend fun update(ssid: WifiSsidCompat, psk: String, bssid: MacAddress?) { val (lines, block, persistentMacLine, legacy) = content - block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray() - .joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') } + block[block.ssidLine!!] = "\tssid=${ssid.hex}" block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff if (bssid != null) { persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt index 08d94eec..f82b4d8a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt @@ -5,6 +5,7 @@ import android.annotation.TargetApi import android.net.MacAddress import android.net.wifi.ScanResult import android.net.wifi.SoftApConfiguration +import android.net.wifi.WifiSsid import android.os.Build import android.os.Parcelable import android.util.SparseIntArray @@ -12,6 +13,7 @@ import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.requireSingleBand import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.setChannel +import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat.Companion.toCompat import be.mygod.vpnhotspot.util.ConstantLookup import be.mygod.vpnhotspot.util.UnblockCentral import kotlinx.parcelize.Parcelize @@ -20,7 +22,7 @@ import java.lang.reflect.InvocationTargetException @Parcelize data class SoftApConfigurationCompat( - var ssid: String? = null, + var ssid: WifiSsidCompat? = null, var bssid: MacAddress? = null, var passphrase: String? = null, var isHiddenSsid: Boolean = false, @@ -116,8 +118,6 @@ data class SoftApConfigurationCompat( "WPA3-OWE", ) - private val qrSanitizer = Regex("([\\\\\":;,])") - /** * Based on: * https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75 @@ -344,11 +344,15 @@ data class SoftApConfigurationCompat( private val setVendorElements by lazy @TargetApi(33) { classBuilder.getDeclaredMethod("setVendorElements", java.util.List::class.java) } + @get:RequiresApi(33) + private val setWifiSsid by lazy @TargetApi(33) { + classBuilder.getDeclaredMethod("setWifiSsid", WifiSsid::class.java) + } @Deprecated("Class deprecated in framework") @Suppress("DEPRECATION") fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat( - SSID, + WifiSsidCompat.fromUtf8Text(SSID), BSSID?.let { MacAddress.fromString(it) }, preSharedKey, hiddenSSID, @@ -388,7 +392,9 @@ data class SoftApConfigurationCompat( @RequiresApi(30) @Suppress("UNCHECKED_CAST") fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat( - ssid, + if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") { + WifiSsidCompat.fromUtf8Text(ssid) + }, bssid, passphrase, isHiddenSsid, @@ -483,7 +489,7 @@ data class SoftApConfigurationCompat( val wc = underlying as? android.net.wifi.WifiConfiguration val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc) val original = wc?.toCompat() - result.SSID = ssid + result.SSID = ssid?.toString() result.preSharedKey = passphrase result.hiddenSSID = isHiddenSsid apBand.setInt(result, when (band) { @@ -520,7 +526,9 @@ data class SoftApConfigurationCompat( fun toPlatform(): SoftApConfiguration { val sac = underlying as? SoftApConfiguration val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac) - setSsid(builder, ssid) + if (Build.VERSION.SDK_INT >= 33) { + setWifiSsid(builder, ssid?.toPlatform()) + } else setSsid(builder, ssid?.toString()) setPassphrase(builder, when (securityType) { SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION, @@ -581,7 +589,6 @@ data class SoftApConfigurationCompat( * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161 */ fun toQrCode() = StringBuilder("WIFI:").apply { - fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" } when (securityType) { SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { } @@ -592,11 +599,11 @@ data class SoftApConfigurationCompat( else -> throw IllegalArgumentException("Unsupported authentication type") } append("S:") - append(ssid!!.sanitize()) + append(ssid!!.toMeCard()) append(';') passphrase?.let { passphrase -> append("P:") - append(passphrase.sanitize()) + append(WifiSsidCompat.toMeCard(passphrase)) append(';') } if (isHiddenSsid) append("H:true;") diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt index ec9186e9..cbb249c9 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt @@ -36,6 +36,7 @@ import be.mygod.vpnhotspot.util.QRCodeDialog import be.mygod.vpnhotspot.util.RangeInput import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.showAllowingStateLoss +import com.google.android.material.textfield.TextInputLayout import kotlinx.parcelize.Parcelize import timber.log.Timber import java.text.DecimalFormat @@ -129,6 +130,14 @@ class WifiApDialogFragment : AlertDialogFragment= 33 + private var hexSsid = false + set(value) { + field = value + dialogView.ssidWrapper.setEndIconActivated(value) + } + private val ssid get() = + if (hexSsid) WifiSsidCompat.fromHex(dialogView.ssid.text) else WifiSsidCompat.fromUtf8Text(dialogView.ssid.text) private fun generateChannels() = SparseIntArray(2).apply { if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31) { @@ -137,7 +146,7 @@ class WifiApDialogFragment : AlertDialogFragment(com.google.android.material.R.id.text_input_end_icon).apply { + tooltipText = contentDescription + } + } if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment) if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply { adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, @@ -293,7 +326,14 @@ class WifiApDialogFragment : AlertDialogFragment + when { + ssid == null -> null + hexSsid -> ssid.hex + hexToggleable -> ssid.decode() ?: ssid.hex.also { hexSsid = true } + else -> ssid.toString() + } + }) if (!arg.p2pMode) dialogView.security.setSelection(base.securityType) dialogView.password.setText(base.passphrase) dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled @@ -334,11 +374,18 @@ class WifiApDialogFragment : AlertDialogFragment null; !null -> !null") + fun fromUtf8Text(text: CharSequence?) = text?.toString()?.toByteArray()?.let { WifiSsidCompat(it) } + + fun toMeCard(text: String) = qrSanitizer.replace(text) { "\\${it.groupValues[1]}" } + + @RequiresApi(33) + fun WifiSsid.toCompat() = WifiSsidCompat(bytes) + } + + init { + require(bytes.size <= 32) + } + + @RequiresApi(31) + fun toPlatform() = WifiSsid.fromBytes(bytes) + + fun decode(charset: Charset = Charsets.UTF_8) = CharBuffer.allocate(32).run { + val result = charset.newDecoder().apply { + onMalformedInput(CodingErrorAction.REPORT) + onUnmappableCharacter(CodingErrorAction.REPORT) + }.decode(ByteBuffer.wrap(bytes), this, true) + if (result.isError) null else flip().toString() + } + val hex get() = bytes.joinToString("") { "%02x".format(it.toUByte().toInt()) } + + fun toMeCard(): String { + val utf8 = decode() ?: return hex + return if (hexTester.matches(utf8)) "\"$utf8\"" else toMeCard(utf8) + } + + override fun toString() = String(bytes) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as WifiSsidCompat + if (!bytes.contentEquals(other.bytes)) return false + return true + } + + override fun hashCode(): Int { + return bytes.contentHashCode() + } +} diff --git a/mobile/src/main/res/drawable/ic_av_closed_caption.xml b/mobile/src/main/res/drawable/ic_av_closed_caption.xml new file mode 100644 index 00000000..359f268b --- /dev/null +++ b/mobile/src/main/res/drawable/ic_av_closed_caption.xml @@ -0,0 +1,5 @@ + + + diff --git a/mobile/src/main/res/drawable/ic_av_closed_caption_off.xml b/mobile/src/main/res/drawable/ic_av_closed_caption_off.xml new file mode 100644 index 00000000..a21d5963 --- /dev/null +++ b/mobile/src/main/res/drawable/ic_av_closed_caption_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/mobile/src/main/res/drawable/toggle_hex.xml b/mobile/src/main/res/drawable/toggle_hex.xml new file mode 100644 index 00000000..fa5ce92c --- /dev/null +++ b/mobile/src/main/res/drawable/toggle_hex.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile/src/main/res/layout/dialog_wifi_ap.xml b/mobile/src/main/res/layout/dialog_wifi_ap.xml index 41ec41e3..94604f2b 100644 --- a/mobile/src/main/res/layout/dialog_wifi_ap.xml +++ b/mobile/src/main/res/layout/dialog_wifi_ap.xml @@ -49,6 +49,8 @@ android:hint="@string/wifi_ssid" app:counterEnabled="true" app:counterMaxLength="32" + app:endIconContentDescription="@string/wifi_ssid_toggle_hex" + app:endIconDrawable="@drawable/toggle_hex" app:errorEnabled="true"> 使用 QR 码分享 Android 系统拒绝使用此配置。(详情参见日志) "网络名称" + 切换十六进制显示 "安全性" "密码" 未连接任何设备时自动关闭 WLAN 热点 diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 765ab197..5be804f9 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -199,6 +199,7 @@ Share via QR code Android system refuses such configuration. (see logcat) Network name + Toggle hex display Security Password Turn off hotspot automatically when no devices are connected