Skip to content

Commit 04c7cdf

Browse files
committed
refactor: unify Base64 encoding utilities
1 parent 4c9a1bd commit 04c7cdf

11 files changed

Lines changed: 105 additions & 62 deletions

File tree

app/src/main/kotlin/engine/xray/AndroidXrayCoreEnvironment.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ package engine.xray
66
import android.annotation.SuppressLint
77
import android.content.Context
88
import android.provider.Settings
9-
import android.util.Base64
109
import go.Seq
1110
import libv2ray.Libv2ray
11+
import utils.encodeUrlSafeBase64NoPadding
1212
import java.util.concurrent.atomic.AtomicReference
1313

1414
internal fun Context.initializeAndroidXrayCoreEnvironment(dataDir: String) {
@@ -30,10 +30,9 @@ private fun Context.xrayCoreBaseKey(): String {
3030
val deviceId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
3131
.orEmpty()
3232
.ifBlank { packageName }
33-
return Base64.encodeToString(
34-
deviceId.toByteArray(Charsets.UTF_8).copyOf(XrayCoreBaseKeyLength),
35-
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE,
36-
)
33+
return deviceId.toByteArray(Charsets.UTF_8)
34+
.copyOf(XrayCoreBaseKeyLength)
35+
.encodeUrlSafeBase64NoPadding()
3736
}
3837

3938
private const val XrayCoreBaseKeyLength = 32

app/src/main/kotlin/features/logs/AndroidLogcatRepository.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
package features.logs
55

66
import android.content.Context
7-
import android.util.Base64
87
import kotlinx.coroutines.Dispatchers
98
import kotlinx.coroutines.withContext
9+
import utils.decodeBase64OrNull
10+
import utils.encodeBase64
1011
import java.util.concurrent.atomic.AtomicBoolean
1112

1213
internal object AndroidLogcatRepository : InMemoryCoreLogRepository() {
@@ -86,11 +87,9 @@ private fun decodeLogLine(id: Long, line: String): CoreLogEntry? {
8687
}
8788

8889
private fun String.encodeBase64(): String {
89-
return Base64.encodeToString(toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
90+
return toByteArray(Charsets.UTF_8).encodeBase64()
9091
}
9192

9293
private fun String.decodeBase64(): String? {
93-
return runCatching {
94-
String(Base64.decode(this, Base64.NO_WRAP), Charsets.UTF_8)
95-
}.getOrNull()
94+
return decodeBase64OrNull()?.toString(Charsets.UTF_8)
9695
}

app/src/main/kotlin/features/proxy/server/model/HTTP.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ data class HTTP(
4242
this.server = url.host
4343
this.port = url.port.toString()
4444
url.user?.let { str ->
45-
val info = ProxyServer.base64.decode(str).decodeToString()
45+
val info = str.decodeProxyUrlBase64().decodeToString()
4646
val pos = info.indexOfFirst { it == ':' }
4747
if (pos > -1) {
4848
this.user = info.substring(0, pos)
@@ -58,7 +58,7 @@ data class HTTP(
5858
host = this@HTTP.server
5959
this@HTTP.port.toIntOrNull()?.let { port = it }
6060
if (!this@HTTP.user.isNullOrBlank())
61-
user = ProxyServer.base64.encode("${this@HTTP.user}:${this@HTTP.password.orEmpty()}".toByteArray())
61+
user = "${this@HTTP.user}:${this@HTTP.password.orEmpty()}".encodeToByteArray().encodeProxyUrlBase64()
6262
fragment = this@HTTP.remarks
6363
}.buildString()
6464
}

app/src/main/kotlin/features/proxy/server/model/ProxyServer.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import io.ktor.http.Url
99
import io.ktor.http.parsing.ParseException
1010
import kotlinx.serialization.Serializable
1111
import kotlinx.serialization.json.Json
12-
import kotlin.io.encoding.Base64
12+
import utils.decodeUrlSafeBase64OptionalPaddingOrNull
13+
import utils.encodeUrlSafeBase64OptionalPadding
1314

1415
object ProxyServerConstants {
1516
const val PROTOCOL_HTTP = "http"
@@ -35,7 +36,6 @@ data class ProxyServerInfo(
3536

3637
interface ProxyServer<T : ProxyServer<T>> {
3738
companion object {
38-
val base64 = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
3939
val json = Json {
4040
ignoreUnknownKeys = true
4141
encodeDefaults = true
@@ -49,7 +49,7 @@ interface ProxyServer<T : ProxyServer<T>> {
4949
ProxyServerConstants.PROTOCOL_SS -> Shadowsocks().parse(url)
5050
ProxyServerConstants.PROTOCOL_VMESS -> {
5151
if (url.user == null) {
52-
val originJson = base64.decode(url.host).decodeToString()
52+
val originJson = url.host.decodeProxyUrlBase64().decodeToString()
5353
json.decodeFromString<LegacyVMess>(originJson).convertToAEAD()
5454
} else {
5555
VMess().parse(url)
@@ -75,6 +75,15 @@ interface ProxyServer<T : ProxyServer<T>> {
7575
fun check()
7676
}
7777

78+
internal fun String.decodeProxyUrlBase64(): ByteArray {
79+
return decodeUrlSafeBase64OptionalPaddingOrNull()
80+
?: throw IllegalArgumentException("Bad proxy URL base64")
81+
}
82+
83+
internal fun ByteArray.encodeProxyUrlBase64(): String {
84+
return encodeUrlSafeBase64OptionalPadding()
85+
}
86+
7887
interface UrlProxyServer<T : UrlProxyServer<T>> : ProxyServer<T> {
7988
fun parse(url: Url): T
8089
fun getUrl(): String

app/src/main/kotlin/features/proxy/server/model/ProxyServerValidation.kt

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import engine.network.isPort
77
import engine.network.NetworkLimits
88
import kotlinx.serialization.json.Json
99
import kotlinx.serialization.json.JsonObject
10-
import kotlin.io.encoding.Base64
10+
import utils.decodeUrlSafeBase64NoPaddingOrNull
11+
import utils.decodeUrlSafeBase64OptionalPaddingOrNull
1112
import utils.toIntInRangeOrNull
1213

1314
private val DomainLabelRegex = Regex("[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?")
@@ -270,11 +271,7 @@ internal fun validateRealityPublicKey(value: String) {
270271
val key = value.trim()
271272
validateRequired(key, "Reality PublicKey")
272273
val decoded = if (UrlSafeBase64Regex.matches(key)) {
273-
runCatching {
274-
Base64.UrlSafe
275-
.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
276-
.decode(key)
277-
}.getOrNull()
274+
key.decodeUrlSafeBase64OptionalPaddingOrNull()
278275
} else {
279276
null
280277
}
@@ -368,11 +365,7 @@ private fun validateOptionalRealityMldsa65Verify(value: String?) {
368365

369366
private fun decodeRawUrlSafeBase64(value: String): ByteArray? {
370367
if (!RawUrlSafeBase64Regex.matches(value)) return null
371-
return runCatching {
372-
Base64.UrlSafe
373-
.withPadding(Base64.PaddingOption.ABSENT)
374-
.decode(value)
375-
}.getOrNull()
368+
return value.decodeUrlSafeBase64NoPaddingOrNull()
376369
}
377370

378371
private fun validateOptionalFingerprint(value: String?) {

app/src/main/kotlin/features/proxy/server/model/Shadowsocks.kt

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import io.ktor.http.Url
99
import kotlinx.serialization.Serializable
1010
import kotlinx.serialization.json.buildJsonObject
1111
import kotlinx.serialization.json.put
12-
import kotlin.io.encoding.Base64
12+
import utils.decodeFlexibleBase64OrNull
13+
import utils.encodeBase64
1314

1415
@Serializable
1516
data class Shadowsocks(
@@ -44,7 +45,7 @@ data class Shadowsocks(
4445
this.port = url.port.toString()
4546
if (url.port == 0) {
4647
val full =
47-
ProxyServer.base64.decode(url.host).decodeToString()
48+
url.host.decodeProxyUrlBase64().decodeToString()
4849
val infoAndServer = full.split('@')
4950
if (infoAndServer.size == 2) {
5051
val methodAndPassword = infoAndServer[0].split(':')
@@ -60,7 +61,7 @@ data class Shadowsocks(
6061
}
6162
} else {
6263
val info = url.user?.let {
63-
ProxyServer.base64.decode(it).decodeToString()
64+
it.decodeProxyUrlBase64().decodeToString()
6465
} ?: throw IllegalArgumentException("Bad Shadowsocks url")
6566
val pos = info.indexOfFirst { it == ':' }
6667
if (pos > -1) {
@@ -76,8 +77,7 @@ data class Shadowsocks(
7677
protocol = URLProtocol.createOrDefault(ProxyServerConstants.PROTOCOL_SS)
7778
host = this@Shadowsocks.server
7879
this@Shadowsocks.port.toIntOrNull()?.let { port = it }
79-
user = ProxyServer.base64
80-
.encode("${this@Shadowsocks.method}:${this@Shadowsocks.password}".toByteArray())
80+
user = "${this@Shadowsocks.method}:${this@Shadowsocks.password}".encodeToByteArray().encodeProxyUrlBase64()
8181
fragment = this@Shadowsocks.remarks
8282
}.build().toString()
8383
}
@@ -148,16 +148,14 @@ private fun normalizeShadowsocks2022Key(key: String, validLengths: Set<Int>): St
148148
validLengths.joinToString(" or "),
149149
)
150150
}
151-
return Base64.Default.encode(decodedKey)
151+
return decodedKey.encodeBase64()
152152
}
153153

154154
private fun decodeShadowsocks2022Key(key: String): ByteArray? {
155155
if (key.isBlank()) {
156156
return null
157157
}
158-
return Shadowsocks2022KeyDecoders.firstNotNullOfOrNull { decoder ->
159-
runCatching { decoder.decode(key) }.getOrNull()
160-
}
158+
return key.decodeFlexibleBase64OrNull()
161159
}
162160

163161
private val Shadowsocks2022KeyLengths = mapOf(
@@ -166,9 +164,3 @@ private val Shadowsocks2022KeyLengths = mapOf(
166164
"2022-blake3-chacha20-poly1305" to setOf(32),
167165
)
168166

169-
private val Shadowsocks2022KeyDecoders = listOf(
170-
Base64.Default,
171-
Base64.Default.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL),
172-
Base64.UrlSafe,
173-
Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL),
174-
)

app/src/main/kotlin/features/proxy/server/model/Socks.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ data class Socks(
4242
this.server = url.host
4343
this.port = url.port.toString()
4444
url.user?.let { str ->
45-
val info = ProxyServer.base64.decode(str).decodeToString()
45+
val info = str.decodeProxyUrlBase64().decodeToString()
4646
val pos = info.indexOfFirst { it == ':' }
4747
if (pos > -1) {
4848
this.user = info.substring(0, pos)
@@ -58,7 +58,7 @@ data class Socks(
5858
host = this@Socks.server
5959
this@Socks.port.toIntOrNull()?.let { port = it }
6060
if (!this@Socks.user.isNullOrBlank())
61-
user = ProxyServer.base64.encode("${this@Socks.user}:${this@Socks.password.orEmpty()}".toByteArray())
61+
user = "${this@Socks.user}:${this@Socks.password.orEmpty()}".encodeToByteArray().encodeProxyUrlBase64()
6262
fragment = this@Socks.remarks
6363
}.buildString()
6464
}

app/src/main/kotlin/features/proxy/server/model/VMess.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import kotlinx.serialization.encoding.Decoder
1717
import kotlinx.serialization.encoding.Encoder
1818
import kotlinx.serialization.json.buildJsonObject
1919
import kotlinx.serialization.json.put
20+
import utils.encodeUrlSafeBase64OptionalPadding
2021

2122
@Serializable
2223
data class LegacyVMess(
@@ -58,14 +59,14 @@ data class LegacyVMess(
5859
}
5960

6061
override fun parse(url: Url): LegacyVMess {
61-
val originJson = ProxyServer.base64.decode(url.host).decodeToString()
62+
val originJson = url.host.decodeProxyUrlBase64().decodeToString()
6263
ProxyServer.json.decodeFromString<LegacyVMess>(originJson).let { this.update(it) }
6364
return this
6465
}
6566

6667
override fun getUrl(): String {
6768
return "${ProxyServerConstants.PROTOCOL_VMESS}://${
68-
ProxyServer.base64.encode(ProxyServer.json.encodeToString(this).encodeToByteArray())
69+
ProxyServer.json.encodeToString(this).encodeToByteArray().encodeUrlSafeBase64OptionalPadding()
6970
}"
7071
}
7172

app/src/main/kotlin/features/proxy/server/usecase/importer/ProxyServerImportPayloads.kt

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package features.proxy.server.usecase.importer
55

66
import features.proxy.server.usecase.ProxyServerImportSource
7-
import kotlin.io.encoding.Base64
7+
import utils.decodeFlexibleBase64OrNull
88

99
internal fun String.importPayloads(source: ProxyServerImportSource): List<String> {
1010
return if (source.decodeBase64) {
@@ -17,20 +17,7 @@ internal fun String.importPayloads(source: ProxyServerImportSource): List<String
1717
private fun String.decodeImportBase64(): String? {
1818
val normalized = trimStart(ImportByteOrderMark).filterNot(Char::isWhitespace)
1919
if (normalized.isBlank()) return null
20-
return ImportBase64Decoders.firstNotNullOfOrNull { decoder ->
21-
runCatching { decoder.decode(normalized).decodeToString() }.getOrNull()
22-
} ?: normalized.trimEnd('=').takeIf { it.length != normalized.length }?.let { trimmed ->
23-
ImportBase64Decoders.firstNotNullOfOrNull { decoder ->
24-
runCatching { decoder.decode(trimmed).decodeToString() }.getOrNull()
25-
}
26-
}
20+
return normalized.decodeFlexibleBase64OrNull()?.decodeToString()
2721
}
2822

29-
private val ImportBase64Decoders = listOf(
30-
Base64.Default,
31-
Base64.Default.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL),
32-
Base64.UrlSafe,
33-
Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL),
34-
)
35-
3623
internal const val ImportByteOrderMark = '\uFEFF'

app/src/main/kotlin/features/subscription/runtime/AndroidSubscriptionFetcher.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
package features.subscription.runtime
55

6-
import android.util.Base64
76
import features.subscription.DefaultSubscriptionUserAgent
87
import engine.network.isPort
98
import engine.vpn.LocalProxyLoopbackAddress
109
import engine.vpn.VpnLocalProxyRuntime
1110
import kotlinx.coroutines.Dispatchers
1211
import kotlinx.coroutines.withContext
12+
import utils.encodeBase64
1313
import java.net.Authenticator
1414
import java.net.HttpURLConnection
1515
import java.net.IDN
@@ -142,7 +142,7 @@ private fun HttpURLConnection.setEmbeddedBasicAuth(rawUrl: String) {
142142
val parts = userInfo.split(":", limit = 2)
143143
val user = parts.getOrElse(0) { "" }
144144
val password = parts.getOrElse(1) { "" }
145-
val token = Base64.encodeToString("$user:$password".toByteArray(), Base64.NO_WRAP)
145+
val token = "$user:$password".encodeToByteArray().encodeBase64()
146146
setRequestProperty("Authorization", "Basic $token")
147147
}
148148

0 commit comments

Comments
 (0)