Skip to content

Commit 69ce29b

Browse files
feat(security): implement SHA-256 verification for OTA updates and remove insecure proxy script
1 parent 9812d2f commit 69ce29b

6 files changed

Lines changed: 81 additions & 1158 deletions

File tree

‎.github/workflows/android_build.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ jobs:
7272
-Pandroid.injected.signing.key.password=${{ secrets.RELEASE_KEY_PASSWORD }}
7373
working-directory: V2rayNG
7474

75+
- name: Prepare Release Assets
76+
run: |
77+
mkdir -p release-apks
78+
find V2rayNG/app/build/outputs/apk/ -name "*.apk" -type f -exec cp {} release-apks/ \;
79+
cd release-apks
80+
sha256sum *.apk > sha256sum.txt
81+
7582
- name: Create Release and Upload APK
7683
uses: softprops/action-gh-release@v2
7784
with:
7885
prerelease: true
7986
files: |
80-
V2rayNG/app/build/outputs/apk/*/release/*.apk
87+
release-apks/*
8188
env:
8289
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎V2rayNG/app/src/main/java/com/kiktor/v2whitelist/dto/CheckUpdateResult.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ data class CheckUpdateResult(
55
val latestVersion: String? = null,
66
val releaseNotes: String? = null,
77
val downloadUrl: String? = null,
8+
val checksumUrl: String? = null,
9+
val apkFileName: String? = null,
810
val error: String? = null,
911
val isPreRelease: Boolean = false
1012
)

‎V2rayNG/app/src/main/java/com/kiktor/v2whitelist/handler/UpdateCheckerManager.kt‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,16 @@ object UpdateCheckerManager {
5050

5151
return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
5252
val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
53+
val apkFileName = downloadUrl.substringAfterLast("/")
54+
val checksumUrl = latestRelease.assets.firstOrNull { it.name == "sha256sum.txt" }?.browserDownloadUrl
55+
5356
CheckUpdateResult(
5457
hasUpdate = true,
5558
latestVersion = latestVersion,
5659
releaseNotes = latestRelease.body,
5760
downloadUrl = downloadUrl,
61+
checksumUrl = checksumUrl,
62+
apkFileName = apkFileName,
5863
isPreRelease = latestRelease.prerelease
5964
)
6065
} else {
@@ -193,4 +198,43 @@ object UpdateCheckerManager {
193198
return asset?.browserDownloadUrl
194199
?: throw IllegalStateException("No compatible APK found")
195200
}
201+
202+
suspend fun verifyChecksum(apkFile: File, checksumUrl: String, apkFileName: String): Boolean = withContext(Dispatchers.IO) {
203+
try {
204+
val isRunning = com.kiktor.v2whitelist.handler.V2RayServiceManager.isRunning()
205+
val httpPort = if (isRunning) SettingsManager.getHttpPort() else 0
206+
val response = HttpUtil.getUrlContent(checksumUrl, 5000, httpPort) ?: return@withContext false
207+
208+
var expectedHash: String? = null
209+
for (line in response.lines()) {
210+
if (line.contains(apkFileName)) {
211+
expectedHash = line.substringBefore(" ").trim()
212+
break
213+
}
214+
}
215+
216+
if (expectedHash == null) {
217+
Log.e(AppConfig.TAG, "Checksum for $apkFileName not found in sha256sum.txt")
218+
return@withContext false
219+
}
220+
221+
val digest = java.security.MessageDigest.getInstance("SHA-256")
222+
val fis = java.io.FileInputStream(apkFile)
223+
val buffer = ByteArray(8192)
224+
var read: Int
225+
while (fis.read(buffer).also { read = it } != -1) {
226+
digest.update(buffer, 0, read)
227+
}
228+
fis.close()
229+
230+
val hashBytes = digest.digest()
231+
val actualHash = hashBytes.joinToString("") { "%02x".format(it) }
232+
233+
Log.i(AppConfig.TAG, "Expected SHA-256: $expectedHash, Actual: $actualHash")
234+
return@withContext expectedHash.equals(actualHash, ignoreCase = true)
235+
} catch (e: Exception) {
236+
Log.e(AppConfig.TAG, "Failed to verify checksum: ${e.message}", e)
237+
return@withContext false
238+
}
239+
}
196240
}

‎V2rayNG/app/src/main/java/com/kiktor/v2whitelist/ui/CheckUpdateActivity.kt‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,31 @@ class CheckUpdateActivity : BaseActivity() {
9090
AlertDialog.Builder(this)
9191
.setView(dialogView)
9292
.setPositiveButton(R.string.update_now) { _, _ ->
93-
result.downloadUrl?.let { url ->
94-
downloadAndInstall(url)
93+
if (result.downloadUrl != null) {
94+
downloadAndInstall(result)
9595
}
9696
}
9797
.setNegativeButton(android.R.string.cancel, null)
9898
.show()
9999
}
100100

101-
private fun downloadAndInstall(url: String) {
101+
private fun downloadAndInstall(result: CheckUpdateResult) {
102102
showLoading()
103103
toast("Downloading update...")
104104
lifecycleScope.launch {
105105
try {
106-
val apkFile = UpdateCheckerManager.downloadApk(this@CheckUpdateActivity, url)
106+
val apkFile = UpdateCheckerManager.downloadApk(this@CheckUpdateActivity, result.downloadUrl!!)
107107
if (apkFile != null && apkFile.exists()) {
108+
if (result.checksumUrl != null && result.apkFileName != null) {
109+
toast("Verifying security signature...")
110+
val isValid = UpdateCheckerManager.verifyChecksum(apkFile, result.checksumUrl, result.apkFileName)
111+
if (!isValid) {
112+
apkFile.delete()
113+
toastError("Security check failed! APK might be corrupted or compromised.")
114+
hideLoading()
115+
return@launch
116+
}
117+
}
108118
installApk(apkFile)
109119
} else {
110120
toastError("Failed to download APK")

‎V2rayNG/app/src/main/java/com/kiktor/v2whitelist/ui/MainActivity.kt‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,15 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
214214
androidx.appcompat.app.AlertDialog.Builder(this)
215215
.setView(dialogView)
216216
.setPositiveButton(R.string.update_now) { _, _ ->
217-
result.downloadUrl?.let { url ->
218-
downloadAndInstall(url)
217+
if (result.downloadUrl != null) {
218+
downloadAndInstall(result)
219219
}
220220
}
221221
.setNegativeButton(android.R.string.cancel, null)
222222
.show()
223223
}
224224

225-
private fun downloadAndInstall(url: String) {
225+
private fun downloadAndInstall(result: com.kiktor.v2whitelist.dto.CheckUpdateResult) {
226226
val progressBar = android.widget.ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
227227
max = 100
228228
val pad = (24 * resources.displayMetrics.density).toInt()
@@ -237,13 +237,22 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
237237

238238
lifecycleScope.launch {
239239
try {
240-
val apkFile = com.kiktor.v2whitelist.handler.UpdateCheckerManager.downloadApk(this@MainActivity, url) { progress ->
240+
val apkFile = com.kiktor.v2whitelist.handler.UpdateCheckerManager.downloadApk(this@MainActivity, result.downloadUrl!!) { progress ->
241241
runOnUiThread {
242242
progressBar.progress = progress
243243
}
244244
}
245245
progressDialog.dismiss()
246246
if (apkFile != null && apkFile.exists()) {
247+
if (result.checksumUrl != null && result.apkFileName != null) {
248+
toast("Verifying security signature...")
249+
val isValid = com.kiktor.v2whitelist.handler.UpdateCheckerManager.verifyChecksum(apkFile, result.checksumUrl, result.apkFileName)
250+
if (!isValid) {
251+
apkFile.delete()
252+
toast("Security check failed! APK might be corrupted or compromised.")
253+
return@launch
254+
}
255+
}
247256
installApk(apkFile)
248257
} else {
249258
toast(R.string.toast_failure)

0 commit comments

Comments
 (0)