Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,26 @@ kotlin {
}

sourceSets {
androidMain.dependencies {
implementation(libs.core.ktx)
implementation(libs.libsu.nio)
implementation(libs.libsu.service)
val androidMain by getting {
dependencies {
implementation(libs.core.ktx)
implementation(libs.libsu.nio)
implementation(libs.libsu.service)

// Shizuku & ADB
val libadb = libs.libadb.android.get()
api("${libadb.group}:${libadb.name}:${libadb.version}") {
exclude(group = "org.bouncycastle")
}
val sunSecurity = libs.sun.security.android.get()
api("${sunSecurity.group}:${sunSecurity.name}:${sunSecurity.version}") {
exclude(group = "org.bouncycastle")
}
implementation(libs.conscrypt.android)
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.work.runtime.ktx)
}
}

commonMain.dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package app.revanced.library.installation.installer

import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import app.revanced.shizukulibrary.adb.AdbConnectionManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException

/**
* [AdbInstaller] for installing and uninstalling [Installer.Apk] files via ADB.
*
* @param context The [Context] to use for string resources.
* @param adbConnectionManager The [AdbConnectionManager] to use for ADB communication.
* @param mapErrorMessage The function to map ADB output to a localized error message.
* @param mapUninstallErrorMessage The function to map ADB uninstall output to a localized error message.
*/
class ShizukuAdbInstaller(
private val context: Context,
private val adbConnectionManager: AdbConnectionManager,
private val mapErrorMessage: (String) -> String = { it },
private val mapUninstallErrorMessage: (String) -> String = { it }
) : Installer<AdbInstallerResult, Installation>() {

/**
* Checks if the ADB connection is active.
*/
fun isConnected(): Boolean = adbConnectionManager.isConnected

override suspend fun install(apk: Apk): AdbInstallerResult = withContext(Dispatchers.IO) {
val size = apk.file.length()
Log.i("ShizukuAdbInstaller", "Installing ${apk.file.name} via ADB (size: $size bytes)")

// Use exec:cmd package install for streaming
try {
adbConnectionManager.openStream("exec:cmd package install -r -t -S $size").use { stream ->
Log.i("ShizukuAdbInstaller", "ADB installation stream opened")
// Write APK bytes
try {
stream.openOutputStream().use { os ->
Log.i("ShizukuAdbInstaller", "Writing APK bytes to ADB stream...")
apk.file.inputStream().use { fis ->
val bytesCopied = fis.copyTo(os)
Log.i("ShizukuAdbInstaller", "Successfully wrote $bytesCopied bytes to ADB stream")
}
os.flush()
Log.i("ShizukuAdbInstaller", "ADB stream flushed")
}
} catch (e: Exception) {
// Log and continue, as the server might have already started processing
Log.w("ShizukuAdbInstaller", "Output stream closed during write: ${e.message}")
}

Log.i("ShizukuAdbInstaller", "Waiting for ADB installation response...")
// Read response
val output = StringBuilder()
try {
stream.openInputStream().bufferedReader().use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
Log.i("ShizukuAdbInstaller", "ADB Output: $line")
output.append(line).append("\n")
if (line?.contains("Success", ignoreCase = true) == true ||
line?.contains("Failure", ignoreCase = true) == true) {
break
}
}
}
} catch (e: IOException) {
// "Stream closed" is common if the server finishes abruptly after sending Success
Log.w("ShizukuAdbInstaller", "ADB input stream closed: ${e.message}")
if (output.isEmpty()) return@withContext AdbInstallerResult.Failure(e)
}

val result = output.toString().trim()
Log.i("ShizukuAdbInstaller", "ADB Installation summary: $result")
if (!result.contains("Success")) {
AdbInstallerResult.Failure(AdbInstallationException(mapErrorMessage(result), result))
} else {
AdbInstallerResult.Success
}
}
} catch (e: Exception) {
Log.e("ShizukuAdbInstaller", "Failed to open ADB installation stream: ${e.message}", e)
AdbInstallerResult.Failure(e)
}
}

override suspend fun uninstall(packageName: String): AdbInstallerResult = withContext(Dispatchers.IO) {
Log.i("ShizukuAdbInstaller", "Uninstalling $packageName via ADB")

adbConnectionManager.openStream("shell:pm uninstall $packageName").use { stream ->
val output = StringBuilder()
try {
stream.openInputStream().bufferedReader().use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
output.append(line).append("\n")
if (line?.contains("Success", ignoreCase = true) == true ||
line?.contains("Failure", ignoreCase = true) == true) {
break
}
}
}
} catch (e: IOException) {
// Ignore "Stream closed" if we already have some output
Log.w("ShizukuAdbInstaller", "ADB uninstall stream error: ${e.message}")
if (output.isEmpty()) return@withContext AdbInstallerResult.Failure(e)
}

val result = output.toString().trim()
Log.i("ShizukuAdbInstaller", "ADB Uninstall summary: $result")
if (!result.contains("Success") && result.isNotEmpty()) {
val message = mapUninstallErrorMessage(result)
AdbInstallerResult.Failure(AdbInstallationException(message, result))
} else {
AdbInstallerResult.Success
}
}
}

override suspend fun getInstallation(packageName: String): Installation? = try {
val packageInfo = context.packageManager.getPackageInfo(packageName, 0)
Installation(packageInfo.applicationInfo!!.sourceDir)
} catch (e: PackageManager.NameNotFoundException) {
null
}

class AdbInstallationException(message: String, val output: String) : Exception(message)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package app.revanced.shizukulibrary.adb

import android.content.Context
import android.os.Build
import android.sun.misc.BASE64Encoder
import android.sun.security.provider.X509Factory
import android.sun.security.x509.*
import io.github.muntashirakon.adb.AbsAdbConnectionManager
import java.io.*
import java.nio.charset.StandardCharsets
import java.security.*
import java.security.cert.Certificate
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*

class AdbConnectionManager private constructor(context: Context) : AbsAdbConnectionManager() {
private var mPrivateKey: PrivateKey? = null
private var mCertificate: Certificate? = null

init {
setApi(Build.VERSION.SDK_INT)
try {
mPrivateKey = readPrivateKeyFromFile(context)
mCertificate = readCertificateFromFile(context)
} catch (e: Exception) {
// Log or handle initial read failure
}

// Regenerate if key is missing or certificate is expired
var needsRegeneration = mPrivateKey == null || mCertificate == null
if (!needsRegeneration && mCertificate is X509Certificate) {
try {
(mCertificate as X509Certificate).checkValidity()
} catch (e: Exception) {
needsRegeneration = true
}
}

if (needsRegeneration) {
try {
// Generate a new key pair
val keySize = 2048
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(keySize, SecureRandom.getInstance("SHA1PRNG"))
val generateKeyPair = keyPairGenerator.generateKeyPair()
val publicKey = generateKeyPair.public
mPrivateKey = generateKeyPair.private

// Generate a new certificate
val subject = "CN=Revanced Library"
val algorithmName = "SHA512withRSA"
val expiryDate = System.currentTimeMillis() + 10L * 365 * 86400000

val certificateExtensions = CertificateExtensions()
certificateExtensions.set(
"SubjectKeyIdentifier", SubjectKeyIdentifierExtension(
KeyIdentifier(publicKey).identifier
)
)
val x500Name = X500Name(subject)
val notBefore = Date()
val notAfter = Date(expiryDate)
certificateExtensions.set("PrivateKeyUsage", PrivateKeyUsageExtension(notBefore, notAfter))
val certificateValidity = CertificateValidity(notBefore, notAfter)
val x509CertInfo = X509CertInfo()
x509CertInfo.set("version", CertificateVersion(2))
x509CertInfo.set("serialNumber", CertificateSerialNumber(Random().nextInt() and Int.MAX_VALUE))
x509CertInfo.set("algorithmID", CertificateAlgorithmId(AlgorithmId.get(algorithmName)))
x509CertInfo.set("subject", CertificateSubjectName(x500Name))
x509CertInfo.set("key", CertificateX509Key(publicKey))
x509CertInfo.set("validity", certificateValidity)
x509CertInfo.set("issuer", CertificateIssuerName(x500Name))
x509CertInfo.set("extensions", certificateExtensions)

val x509CertImpl = X509CertImpl(x509CertInfo)
x509CertImpl.sign(mPrivateKey, algorithmName)
mCertificate = x509CertImpl

// Write files
writePrivateKeyToFile(context, mPrivateKey!!)
writeCertificateToFile(context, mCertificate!!)
} catch (e: Exception) {
throw RuntimeException("Failed to generate ADB credentials", e)
}
}
}

public override fun getPrivateKey(): PrivateKey {
return mPrivateKey ?: throw IllegalStateException("Private key not initialized")
}

public override fun getCertificate(): Certificate {
return mCertificate ?: throw IllegalStateException("Certificate not initialized")
}

override fun getDeviceName(): String {
return "MyAwesomeApp"
}

companion object {
private var INSTANCE: AdbConnectionManager? = null

@JvmStatic
@Synchronized
fun getInstance(context: Context): AdbConnectionManager {
if (INSTANCE == null) {
INSTANCE = AdbConnectionManager(context)
}
return INSTANCE!!
}

@Throws(IOException::class, CertificateException::class)
private fun readCertificateFromFile(context: Context): Certificate? {
val certFile = File(context.filesDir, "cert.pem")
if (!certFile.exists()) return null
return FileInputStream(certFile).use { cert ->
CertificateFactory.getInstance("X.509").generateCertificate(cert)
}
}

@Throws(CertificateEncodingException::class, IOException::class)
private fun writeCertificateToFile(context: Context, certificate: Certificate) {
val certFile = File(context.filesDir, "cert.pem")
val encoder = BASE64Encoder()
FileOutputStream(certFile).use { os ->
os.write(X509Factory.BEGIN_CERT.toByteArray(StandardCharsets.UTF_8))
os.write('\n'.toInt())
encoder.encode(certificate.encoded, os)
os.write('\n'.toInt())
os.write(X509Factory.END_CERT.toByteArray(StandardCharsets.UTF_8))
}
}

@Throws(IOException::class, NoSuchAlgorithmException::class, InvalidKeySpecException::class)
private fun readPrivateKeyFromFile(context: Context): PrivateKey? {
val privateKeyFile = File(context.filesDir, "private.key")
if (!privateKeyFile.exists()) return null
val privKeyBytes = ByteArray(privateKeyFile.length().toInt())
DataInputStream(FileInputStream(privateKeyFile)).use { dis ->
dis.readFully(privKeyBytes)
}
val keyFactory = KeyFactory.getInstance("RSA")
val privateKeySpec = PKCS8EncodedKeySpec(privKeyBytes)
return keyFactory.generatePrivate(privateKeySpec)
}

@Throws(IOException::class)
private fun writePrivateKeyToFile(context: Context, privateKey: PrivateKey) {
val privateKeyFile = File(context.filesDir, "private.key")
FileOutputStream(privateKeyFile).use { os ->
os.write(privateKey.encoded)
}
// Restrict file permissions to owner only
privateKeyFile.setReadable(false, false)
privateKeyFile.setReadable(true, true)
privateKeyFile.setWritable(false, false)
privateKeyFile.setWritable(true, true)
}
}
}
Loading