diff --git a/common/build.gradle.kts b/common/build.gradle.kts index bc1f3141b1..9fbc503015 100755 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -50,8 +50,4 @@ dependencies { // brotli4j implementation(libs.brotli4j) - - implementation(libs.bcprov.jdk18on) - implementation(libs.bcpkix.jdk18on) - } diff --git a/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java b/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java index 92090781a8..c810b56f9c 100755 --- a/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java +++ b/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java @@ -34,6 +34,7 @@ import com.itsaky.androidide.app.configuration.IJdkDistributionProvider; import com.itsaky.androidide.utils.Environment; import com.itsaky.androidide.utils.IoUtilsKt; +import com.itsaky.androidide.utils.SelfSignedCertUtils; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; @@ -47,20 +48,12 @@ import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.SecureRandom; -import java.security.Security; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Date; import java.util.Objects; import java.util.Properties; import java.util.concurrent.CompletableFuture; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.cert.X509v3CertificateBuilder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -196,30 +189,21 @@ private static void generateKeystore() { LOG.debug("Generating keystore at: {}", keystorePath); - String storePassword = generateRandomPassword(Environment.KEYSTORE_PWD_LEN); - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(Environment.KEYSTORE_KEY_SIZE); KeyPair keyPair = keyGen.generateKeyPair(); Date now = new Date(); Date expiry = new Date(now.getTime() + Environment.KEYSTORE_EXPIRY_5YRS); - X500Name issuer = new X500Name(generateIssuerDN()); - X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( - issuer, + X509Certificate certificate = SelfSignedCertUtils.generateSelfSignedCert( + keyPair, + generateIssuerDN(), BigInteger.valueOf(System.currentTimeMillis()), now, - expiry, - issuer, - keyPair.getPublic()); - - ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") - .build(keyPair.getPrivate()); - - X509Certificate certificate = new JcaX509CertificateConverter() - .getCertificate(certBuilder.build(signer)); + expiry); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(null, null); diff --git a/common/src/main/java/com/itsaky/androidide/utils/SelfSignedCert.kt b/common/src/main/java/com/itsaky/androidide/utils/SelfSignedCert.kt new file mode 100644 index 0000000000..b26a489198 --- /dev/null +++ b/common/src/main/java/com/itsaky/androidide/utils/SelfSignedCert.kt @@ -0,0 +1,231 @@ +@file:JvmName("SelfSignedCertUtils") + +package com.itsaky.androidide.utils + +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.security.KeyPair +import java.security.Signature +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.Calendar +import java.util.Date +import java.util.Locale + +/** + * Generates a self-signed X.509 v3 certificate using only standard Java SE / Android APIs - + * no BouncyCastle required. Encodes the DER structure manually. + * + * Supports a simple distinguished name with C, O, and CN components; empty values are encoded + * as empty strings (not omitted). Throws [IllegalArgumentException] for any other attribute + * type or a malformed "key=value" component. + */ +fun generateSelfSignedCert( + keyPair: KeyPair, + dn: String, + serial: BigInteger, + notBefore: Date, + notAfter: Date, +): X509Certificate { + val tbs = buildTbsCertificate(keyPair.public.encoded, dn, serial, notBefore, notAfter) + val sig = Signature.getInstance("SHA256WithRSA") + sig.initSign(keyPair.private) + sig.update(tbs) + val sigBytes = sig.sign() + + val certDer = derSeq(tbs + sha256WithRsaAlgId() + derBitString(sigBytes)) + return CertificateFactory + .getInstance("X.509") + .generateCertificate(certDer.inputStream()) as X509Certificate +} + +private fun buildTbsCertificate( + spkiBytes: ByteArray, + dn: String, + serial: BigInteger, + notBefore: Date, + notAfter: Date, +): ByteArray { + val version = derContextTagged(0, derInteger(BigInteger.valueOf(2))) // v3 + val serialNum = derInteger(serial) + val sigAlg = sha256WithRsaAlgId() + val issuer = encodeDn(dn) + val validity = derSeq(derTime(notBefore) + derTime(notAfter)) + val subject = issuer // self-signed + val spki = spkiBytes // already DER-encoded by JCA + + return derSeq(version + serialNum + sigAlg + issuer + validity + subject + spki) +} + +// Distinguished Name encoding +// Parses "C=XX, O=Foo, CN=Bar"-style strings. + +// OIDs for common attribute types +private val OID_COMMON_NAME = oidBytes(2, 5, 4, 3) +private val OID_ORGANIZATION = oidBytes(2, 5, 4, 10) +private val OID_COUNTRY = oidBytes(2, 5, 4, 6) + +private fun encodeDn(dn: String): ByteArray { + val rdns = mutableListOf() + for (part in splitDnParts(dn)) { + val eq = part.indexOf('=') + require(eq >= 0) { "Malformed DN component: \"$part\"" } + val key = part.substring(0, eq).trim().uppercase() + val value = part.substring(eq + 1).trim() + + // RFC 5280 App. A pins countryName to PrintableString; other attributes may be UTF8String. + val (oidBytes, encodedValue) = + when (key) { + "CN" -> OID_COMMON_NAME to derUtf8String(value) + "O" -> OID_ORGANIZATION to derUtf8String(value) + "C" -> OID_COUNTRY to derPrintableString(value) + else -> throw IllegalArgumentException("Unsupported DN attribute type: \"$key\"") + } + val attrType = derSeq(oidBytes + encodedValue) + rdns += derSet(attrType) + } + return derSeq(rdns.fold(ByteArray(0)) { acc, b -> acc + b }) +} + +// Splits on unescaped commas per RFC 4514 (a backslash escapes the following character). +private fun splitDnParts(dn: String): List { + val parts = mutableListOf() + val current = StringBuilder() + var i = 0 + while (i < dn.length) { + val c = dn[i] + when { + c == '\\' && i + 1 < dn.length -> { + current.append(dn[i + 1]) + i += 2 + } + + c == ',' -> { + parts += current.toString() + current.setLength(0) + i++ + } + + else -> { + current.append(c) + i++ + } + } + } + parts += current.toString() + return parts +} + +// Algorithm identifier: SHA256WithRSA (OID 1.2.840.113549.1.1.11) + NULL + +private val SHA256_WITH_RSA_OID = oidBytes(1, 2, 840, 113549, 1, 1, 11) + +private fun sha256WithRsaAlgId(): ByteArray = derSeq(SHA256_WITH_RSA_OID + byteArrayOf(0x05, 0x00)) // NULL + +private fun derSeq(content: ByteArray): ByteArray = tlv(0x30, content) + +private fun derSet(content: ByteArray): ByteArray = tlv(0x31, content) + +private fun derBitString(bytes: ByteArray): ByteArray = tlv(0x03, byteArrayOf(0x00) + bytes) + +private fun derUtf8String(s: String): ByteArray = tlv(0x0C, s.toByteArray(Charsets.UTF_8)) + +// X.680 Section 41: PrintableString is restricted to A-Za-z0-9 and a handful of punctuation. +private val PRINTABLE_STRING_CHARS = (('A'..'Z') + ('a'..'z') + ('0'..'9') + " '()+,-./:=?".toList()).toSet() + +private fun derPrintableString(s: String): ByteArray { + require(s.all { it in PRINTABLE_STRING_CHARS }) { "\"$s\" is not a valid PrintableString" } + return tlv(0x13, s.toByteArray(Charsets.US_ASCII)) +} + +private fun derInteger(value: BigInteger): ByteArray = + // BigInteger.toByteArray() already prepends a 0x00 sign byte for positive integers whose + // high bit would otherwise be set - exactly what DER requires. + tlv(0x02, value.toByteArray()) + +private fun derContextTagged( + tag: Int, + content: ByteArray, +): ByteArray = tlv(0xA0 or tag, content) + +private fun derTime(date: Date): ByteArray { + // Calendar.getInstance(TimeZone) without a Locale picks up the default locale's calendar + // *system* (e.g. a Thai default locale returns a Buddhist calendar, year = Gregorian + 543), + // not just digit formatting - Locale.ROOT forces the Gregorian calendar. + val cal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"), Locale.ROOT) + cal.time = date + val year = cal.get(Calendar.YEAR) + val fullYear = + String.format( + Locale.ROOT, + "%04d%02d%02d%02d%02d%02dZ", + year, + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + ) + // UTCTime's 2-digit year can only unambiguously represent 1950-2049; GeneralizedTime otherwise. + return if (year < 1950 || year >= 2050) { + tlv(0x18, fullYear.toByteArray(Charsets.US_ASCII)) + } else { + tlv(0x17, fullYear.substring(2).toByteArray(Charsets.US_ASCII)) + } +} + +private fun oidBytes(vararg arcs: Int): ByteArray { + val out = ByteArrayOutputStream() + // First two arcs are combined: first*40 + second + out.write(encodeBase128(arcs[0] * 40 + arcs[1])) + for (i in 2 until arcs.size) { + out.write(encodeBase128(arcs[i])) + } + return tlv(0x06, out.toByteArray()) +} + +private fun encodeBase128(value: Int): ByteArray { + if (value == 0) return byteArrayOf(0) + val buf = ByteArray(5) + var v = value + var pos = 4 + while (v > 0) { + buf[pos--] = (v and 0x7F).toByte() + v = v ushr 7 + } + val start = pos + 1 + val result = ByteArray(5 - start) + for (i in result.indices) { + result[i] = if (i < result.size - 1) (buf[start + i].toInt() or 0x80).toByte() else buf[start + i] + } + return result +} + +private fun tlv( + tag: Int, + content: ByteArray, +): ByteArray { + val out = ByteArrayOutputStream() + out.write(tag) + val len = content.size + if (len < 0x80) { + out.write(len) + } else { + val lenBytes = encodeLength(len) + out.write(0x80 or lenBytes.size) + out.write(lenBytes) + } + out.write(content) + return out.toByteArray() +} + +// Minimal big-endian byte encoding of a non-negative length, for DER's long-form length octets. +private fun encodeLength(len: Int): ByteArray { + val bytes = mutableListOf() + var l = len + while (l > 0) { + bytes.add(0, (l and 0xFF).toByte()) + l = l ushr 8 + } + return bytes.toByteArray() +} diff --git a/common/src/test/java/com/itsaky/androidide/utils/SelfSignedCertTest.kt b/common/src/test/java/com/itsaky/androidide/utils/SelfSignedCertTest.kt new file mode 100644 index 0000000000..acfaa24ad8 --- /dev/null +++ b/common/src/test/java/com/itsaky/androidide/utils/SelfSignedCertTest.kt @@ -0,0 +1,114 @@ +package com.itsaky.androidide.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.util.Date +import java.util.Locale + +/** Round-trip and regression coverage for the hand-rolled DER encoder in SelfSignedCert.kt. */ +class SelfSignedCertTest { + private fun rsaKeyPair() = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair() + + // DER time encoding truncates to whole seconds. + private fun assertSameInstant( + actual: Date, + expected: Date, + ) { + assertThat(actual.time / 1000).isEqualTo(expected.time / 1000) + } + + @Test + fun `generated certificate round-trips through CertificateFactory and verifies`() { + val keyPair = rsaKeyPair() + val notBefore = Date(0) + val notAfter = Date(4102444800000) // 2100-01-01, exercises the GeneralizedTime branch + val cert = generateSelfSignedCert(keyPair, "C=US, O=Test Org, CN=test", BigInteger.ONE, notBefore, notAfter) + + cert.verify(keyPair.public) + assertThat(cert.subjectX500Principal.name).contains("CN=test") + assertThat(cert.subjectX500Principal.name).contains("O=Test Org") + assertThat(cert.subjectX500Principal.name).contains("C=US") + assertSameInstant(cert.notBefore, notBefore) + assertSameInstant(cert.notAfter, notAfter) + } + + @Test + fun `certificate generation is unaffected by a non-Gregorian default locale`() { + val original = Locale.getDefault() + try { + // Thai triggers a Buddhist (non-Gregorian) Calendar from Calendar.getInstance() + // unless Locale.ROOT is passed explicitly - see derTime in SelfSignedCert.kt. + Locale.setDefault( + Locale + .Builder() + .setLanguage("th") + .setRegion("TH") + .build(), + ) + val keyPair = rsaKeyPair() + val notBefore = Date(0) + val notAfter = Date(2461449600L * 1000) + val cert = generateSelfSignedCert(keyPair, "CN=00", BigInteger.ONE, notBefore, notAfter) + + cert.verify(keyPair.public) + assertSameInstant(cert.notBefore, notBefore) + assertSameInstant(cert.notAfter, notAfter) + } finally { + Locale.setDefault(original) + } + } + + @Test + fun `dates before 1950 are encoded as GeneralizedTime`() { + val keyPair = rsaKeyPair() + val notBefore = Date(-946771200000) // 1940-01-01 + val notAfter = Date(0) + val cert = generateSelfSignedCert(keyPair, "CN=test", BigInteger.ONE, notBefore, notAfter) + + cert.verify(keyPair.public) + assertSameInstant(cert.notBefore, notBefore) + assertSameInstant(cert.notAfter, notAfter) + } + + @Test + fun `escaped comma in a DN value does not split the RDN`() { + val keyPair = rsaKeyPair() + val cert = + generateSelfSignedCert( + keyPair, + "O=Foo\\, Inc., CN=test", + BigInteger.ONE, + Date(0), + Date(4102444800000), + ) + + cert.verify(keyPair.public) + assertThat(cert.subjectX500Principal.name).contains("Foo\\, Inc.") + } + + @Test + fun `empty DN attribute values are preserved, not dropped`() { + val keyPair = rsaKeyPair() + val cert = generateSelfSignedCert(keyPair, "C=US, O=, CN=", BigInteger.ONE, Date(0), Date(4102444800000)) + + cert.verify(keyPair.public) + assertThat(cert.subjectX500Principal.name).contains("C=US") + } + + @Test(expected = IllegalArgumentException::class) + fun `unsupported DN attribute type throws`() { + generateSelfSignedCert(rsaKeyPair(), "OU=Eng, CN=test", BigInteger.ONE, Date(0), Date(4102444800000)) + } + + @Test(expected = IllegalArgumentException::class) + fun `malformed DN component throws`() { + generateSelfSignedCert(rsaKeyPair(), "CN=test,", BigInteger.ONE, Date(0), Date(4102444800000)) + } + + @Test(expected = IllegalArgumentException::class) + fun `non-PrintableString country value throws`() { + generateSelfSignedCert(rsaKeyPair(), "C=U!", BigInteger.ONE, Date(0), Date(4102444800000)) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3b799a422..b77e59bdf3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ agp = "8.8.2" agp-tooling = "8.11.0" androidx-sqlite = "2.6.2" appcompatVersion = "1.7.1" -bcprovJdk18on = "1.80" colorpickerview = "2.3.0" commonsCompress = "1.27.1" tukaaniXz = "1.9" @@ -107,8 +106,6 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi androidx-sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "androidx-sqlite" } androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "androidx-sqlite" } androidx-viewpager2-v110beta02 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } -bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcprovJdk18on" } -bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprovJdk18on" } colorpickerview = { module = "com.github.skydoves:colorpickerview", version.ref = "colorpickerview" } commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" } tukaani-xz = { module = "org.tukaani:xz", version.ref = "tukaaniXz" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0756174cc0..29fb8afcd8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -79,15 +79,6 @@ dependencyResolutionManagement { } } -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath("org.bouncycastle:bcutil-jdk18on:1.78.1") - } -} - FDroidConfig.load(rootDir) if (FDroidConfig.hasRead && FDroidConfig.isFDroidBuild) { diff --git a/subprojects/shizuku-manager/build.gradle.kts b/subprojects/shizuku-manager/build.gradle.kts index 61d9c4e8a1..71e57e0ac9 100644 --- a/subprojects/shizuku-manager/build.gradle.kts +++ b/subprojects/shizuku-manager/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { implementation(libs.libsu.core) implementation(libs.common.hiddenApiBypass) implementation(libs.boringssl) - implementation(libs.bcpkix.jdk18on) //noinspection UseTomlInstead implementation("org.lsposed.libcxx:libcxx:${BuildConfig.NDK_VERSION}") diff --git a/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbKey.kt b/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbKey.kt index ea98a93bc3..b8ce81b36a 100644 --- a/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbKey.kt +++ b/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbKey.kt @@ -10,24 +10,20 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.edit import com.itsaky.androidide.buildinfo.BuildInfo +import com.itsaky.androidide.utils.generateSelfSignedCert import moe.shizuku.manager.utils.unsafeLazy -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.ByteArrayInputStream import java.math.BigInteger import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder import java.security.Key import java.security.KeyFactory +import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore import java.security.Principal import java.security.PrivateKey import java.security.SecureRandom -import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey @@ -35,7 +31,6 @@ import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.RSAPublicKeySpec import java.util.Date -import java.util.Locale import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.spec.GCMParameterSpec @@ -313,21 +308,14 @@ class AdbKey( this.publicKey = KeyFactory.getInstance("RSA").generatePublic(RSAPublicKeySpec(privateKey.modulus, RSAKeyGenParameterSpec.F4)) as RSAPublicKey - val signer = JcaContentSignerBuilder("SHA256withRSA").build(privateKey) - val x509Certificate = - X509v3CertificateBuilder( - X500Name("CN=00"), + this.certificate = + generateSelfSignedCert( + KeyPair(publicKey, privateKey), + "CN=00", BigInteger.ONE, Date(0), - Date(2461449600 * 1000), - Locale.ROOT, - X500Name("CN=00"), - SubjectPublicKeyInfo.getInstance(publicKey.encoded), - ).build(signer) - this.certificate = - CertificateFactory - .getInstance("X.509") - .generateCertificate(ByteArrayInputStream(x509Certificate.encoded)) as X509Certificate + Date(2461449600L * 1000), + ) Log.d(TAG, privateKey.toString()) }