From 325d9a0b8670d37d7f6bb49d496fbbbe0c897657 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Sun, 20 Oct 2019 06:56:33 -0400 Subject: [PATCH] Generate keys for signing hidden Magisk Manager --- .../main/java/com/topjohnwu/magisk/Config.kt | 6 +- .../java/com/topjohnwu/magisk/utils/Keygen.kt | 133 ++++++++++++++++++ .../com/topjohnwu/magisk/utils/PatchAPK.kt | 2 +- .../com/topjohnwu/signing/CryptoUtils.java | 6 +- .../java/com/topjohnwu/signing/SignAPK.java | 41 ++---- .../java/com/topjohnwu/signing/ZipSigner.java | 50 ++++++- 6 files changed, 196 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt diff --git a/app/src/main/java/com/topjohnwu/magisk/Config.kt b/app/src/main/java/com/topjohnwu/magisk/Config.kt index e6dddcbf9..54ab1b499 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Config.kt +++ b/app/src/main/java/com/topjohnwu/magisk/Config.kt @@ -32,8 +32,9 @@ object Config : PreferenceModel, DBConfig { const val ROOT_ACCESS = "root_access" const val SU_MULTIUSER_MODE = "multiuser_mode" const val SU_MNT_NS = "mnt_ns" - const val SU_MANAGER = "requester" const val SU_FINGERPRINT = "su_fingerprint" + const val SU_MANAGER = "requester" + const val KEYSTORE = "keystore" // prefs const val SU_REQUEST_TIMEOUT = "su_request_timeout" @@ -123,6 +124,7 @@ object Config : PreferenceModel, DBConfig { var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY) var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false) var suManager by dbStrings(Key.SU_MANAGER, "", true) + var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true) // Always return a path in external storage where we can write val downloadDirectory get() = @@ -205,4 +207,4 @@ object Config : PreferenceModel, DBConfig { Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt b/app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt new file mode 100644 index 000000000..6620f7a14 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt @@ -0,0 +1,133 @@ +package com.topjohnwu.magisk.utils + +import android.content.pm.PackageManager +import android.util.Base64 +import android.util.Base64OutputStream +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.di.koinModules +import com.topjohnwu.signing.CryptoUtils.readCertificate +import com.topjohnwu.signing.CryptoUtils.readPrivateKey +import com.topjohnwu.superuser.internal.InternalUtils +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.koin.core.context.GlobalContext +import org.koin.core.context.startKoin +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.cert.X509Certificate +import java.util.* +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +private interface CertKeyProvider { + val cert: X509Certificate + val key: PrivateKey +} + +@Suppress("DEPRECATION") +object Keygen: CertKeyProvider { + private const val ALIAS = "magisk" + private val PASSWORD = "magisk".toCharArray() + private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81" + + private val start get() = Calendar.getInstance() + private val end get() = Calendar.getInstance().apply { + add(Calendar.YEAR, 20) + } + + override val cert get() = provider.cert + override val key get() = provider.key + + private val provider: CertKeyProvider + + class KeyStoreProvider : CertKeyProvider { + private val ks by lazy { init() } + override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate } + override val key by lazy { ks.getKey(ALIAS, PASSWORD) as PrivateKey } + } + + class TestProvider : CertKeyProvider { + override val cert by lazy { + readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem")) + } + override val key by lazy { + readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8")) + } + } + + init { + // This object could possibly be accessed from an external app + // Get context from reflection into Android's framework + val context = InternalUtils.getContext() + val pm = context.packageManager + val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) + val sig = info.signatures[0] + val digest = MessageDigest.getInstance("SHA1") + val chksum = digest.digest(sig.toByteArray()) + + val sb = StringBuilder() + for (b in chksum) { + sb.append("%02x".format(0xFF and b.toInt())) + } + + provider = if (sb.toString() == TESTKEY_CERT) { + // The app was signed by the test key, continue to use it (legacy mode) + TestProvider() + } else { + KeyStoreProvider() + } + } + + private fun init(): KeyStore { + GlobalContext.getOrNull() ?: { + // Invoked externally, do some basic initialization + startKoin { + modules(koinModules) + } + Timber.plant(Timber.DebugTree()) + }() + + val raw = Config.keyStoreRaw + val ks = KeyStore.getInstance("PKCS12") + if (raw.isEmpty()) { + ks.load(null) + } else { + GZIPInputStream(ByteArrayInputStream( + Base64.decode(raw, Base64.NO_PADDING or Base64.NO_WRAP) + )).use { + ks.load(it, PASSWORD) + } + } + + // Keys already exist + if (ks.containsAlias(ALIAS)) + return ks + + // Generate new private key and certificate + val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.genKeyPair() + val dn = X500Name("CN=Magisk") + val builder = JcaX509v3CertificateBuilder(dn, + BigInteger.valueOf(start.timeInMillis), start.time, end.time, dn, kp.public) + val signer = JcaContentSignerBuilder("SHA256WithRSA").build(kp.private) + val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer)) + + // Store them into keystore + ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert)) + val bytes = ByteArrayOutputStream() + GZIPOutputStream(Base64OutputStream(bytes, Base64.NO_PADDING or Base64.NO_WRAP)).use { + ks.store(it, PASSWORD) + } + Config.keyStoreRaw = bytes.toString() + + return ks + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt b/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt index edd1cff66..9ab490787 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt @@ -107,7 +107,7 @@ object PatchAPK { // Write apk changes jar.getOutputStream(je).write(xml) - SignAPK.sign(jar, FileOutputStream(out).buffered()) + SignAPK.sign(Keygen.cert, Keygen.key, jar, FileOutputStream(out).buffered()) } catch (e: Exception) { Timber.e(e) return false diff --git a/signing/src/main/java/com/topjohnwu/signing/CryptoUtils.java b/signing/src/main/java/com/topjohnwu/signing/CryptoUtils.java index a80963f06..2bbc22a16 100644 --- a/signing/src/main/java/com/topjohnwu/signing/CryptoUtils.java +++ b/signing/src/main/java/com/topjohnwu/signing/CryptoUtils.java @@ -24,7 +24,7 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.util.HashMap; import java.util.Map; -class CryptoUtils { +public class CryptoUtils { static final Map ID_TO_ALG; static final Map ALG_TO_ID; @@ -81,7 +81,7 @@ class CryptoUtils { return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id)); } - static X509Certificate readCertificate(InputStream input) + public static X509Certificate readCertificate(InputStream input) throws IOException, GeneralSecurityException { try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); @@ -92,7 +92,7 @@ class CryptoUtils { } /** Read a PKCS#8 format private key. */ - static PrivateKey readPrivateKey(InputStream input) + public static PrivateKey readPrivateKey(InputStream input) throws IOException, GeneralSecurityException { try { ByteArrayStream buf = new ByteArrayStream(); diff --git a/signing/src/main/java/com/topjohnwu/signing/SignAPK.java b/signing/src/main/java/com/topjohnwu/signing/SignAPK.java index f965fdfa8..1c84f33de 100644 --- a/signing/src/main/java/com/topjohnwu/signing/SignAPK.java +++ b/signing/src/main/java/com/topjohnwu/signing/SignAPK.java @@ -30,7 +30,6 @@ import java.io.PrintStream; import java.io.RandomAccessFile; import java.security.DigestOutputStream; import java.security.GeneralSecurityException; -import java.security.KeyStore; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; @@ -61,29 +60,8 @@ public class SignAPK { private static final int USE_SHA1 = 1; private static final int USE_SHA256 = 2; - public static void sign(JarMap input, OutputStream output) throws Exception { - sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"), - SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output); - } - - public static void sign(InputStream certIs, InputStream keyIs, - JarMap input, OutputStream output) throws Exception { - X509Certificate cert = CryptoUtils.readCertificate(certIs); - PrivateKey key = CryptoUtils.readPrivateKey(keyIs); - sign(cert, key, input, output); - } - - public static void sign(InputStream jks, String keyStorePass, String alias, String keyPass, - JarMap input, OutputStream output) throws Exception { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(jks, keyStorePass.toCharArray()); - X509Certificate cert = (X509Certificate) ks.getCertificate(alias); - PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray()); - sign(cert, key, input, output); - } - - private static void sign(X509Certificate cert, PrivateKey key, - JarMap input, OutputStream output) throws Exception { + public static void signAndAdjust(X509Certificate cert, PrivateKey key, + JarMap input, OutputStream output) throws Exception { File temp1 = File.createTempFile("signAPK", null); File temp2 = File.createTempFile("signAPK", null); @@ -103,6 +81,11 @@ public class SignAPK { } } + public static void sign(X509Certificate cert, PrivateKey key, + JarMap input, OutputStream output) throws Exception { + sign(cert, key, input, output, false); + } + private static void sign(X509Certificate cert, PrivateKey key, JarMap input, OutputStream output, boolean minSign) throws Exception { int hashes = 0; @@ -498,11 +481,11 @@ public class SignAPK { outputStream.close(); } private static void signFile(Manifest manifest, JarMap inputJar, - X509Certificate publicKey, PrivateKey privateKey, + X509Certificate cert, PrivateKey privateKey, JarOutputStream outputJar) throws Exception { // Assume the certificate is valid for at least an hour. - long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; + long timestamp = cert.getNotBefore().getTime() + 3600L * 1000; // MANIFEST.MF JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); je.setTime(timestamp); @@ -512,15 +495,15 @@ public class SignAPK { je.setTime(timestamp); outputJar.putNextEntry(je); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey)); + writeSignatureFile(manifest, baos, getDigestAlgorithm(cert)); byte[] signedData = baos.toByteArray(); outputJar.write(signedData); // CERT.{EC,RSA} / CERT#.{EC,RSA} - final String keyType = publicKey.getPublicKey().getAlgorithm(); + final String keyType = cert.getPublicKey().getAlgorithm(); je = new JarEntry(String.format(CERT_SIG_NAME, keyType)); je.setTime(timestamp); outputJar.putNextEntry(je); writeSignatureBlock(new CMSProcessableByteArray(signedData), - publicKey, privateKey, outputJar); + cert, privateKey, outputJar); } } diff --git a/signing/src/main/java/com/topjohnwu/signing/ZipSigner.java b/signing/src/main/java/com/topjohnwu/signing/ZipSigner.java index 8e68de715..9e3fce6ed 100644 --- a/signing/src/main/java/com/topjohnwu/signing/ZipSigner.java +++ b/signing/src/main/java/com/topjohnwu/signing/ZipSigner.java @@ -4,23 +4,61 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; public class ZipSigner { - public static void usage() { + private static void usage() { System.err.println("ZipSigner usage:"); System.err.println(" zipsigner.jar input.jar output.jar"); System.err.println(" sign jar with AOSP test keys"); System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar"); System.err.println(" sign jar with certificate / private key pair"); - System.err.println(" zipsigner.jar jks keyStorePass keyAlias keyPass input.jar output.jar"); + System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar"); System.err.println(" sign jar with Java KeyStore"); System.exit(2); } + private static void sign(JarMap input, OutputStream output) throws Exception { + sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"), + SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output); + } + + private static void sign(InputStream certIs, InputStream keyIs, + JarMap input, OutputStream output) throws Exception { + X509Certificate cert = CryptoUtils.readCertificate(certIs); + PrivateKey key = CryptoUtils.readPrivateKey(keyIs); + SignAPK.signAndAdjust(cert, key, input, output); + } + + private static void sign(String keyStore, String keyStorePass, String alias, String keyPass, + JarMap in, OutputStream out) throws Exception { + KeyStore ks; + try { + ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(keyStore)) { + ks.load(is, keyStorePass.toCharArray()); + } + } catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) { + ks = KeyStore.getInstance("PKCS12"); + try (InputStream is = new FileInputStream(keyStore)) { + ks.load(is, keyStorePass.toCharArray()); + } + } + X509Certificate cert = (X509Certificate) ks.getCertificate(alias); + PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray()); + SignAPK.signAndAdjust(cert, key, in, out); + } + public static void main(String[] args) throws Exception { if (args.length != 2 && args.length != 4 && args.length != 6) usage(); @@ -30,16 +68,14 @@ public class ZipSigner { try (JarMap in = new JarMap(args[args.length - 2], false); OutputStream out = new FileOutputStream(args[args.length - 1])) { if (args.length == 2) { - SignAPK.sign(in, out); + sign(in, out); } else if (args.length == 4) { try (InputStream cert = new FileInputStream(args[0]); InputStream key = new FileInputStream(args[1])) { - SignAPK.sign(cert, key, in, out); + sign(cert, key, in, out); } } else if (args.length == 6) { - try (InputStream jks = new FileInputStream(args[0])) { - SignAPK.sign(jks, args[1], args[2], args[3], in, out); - } + sign(args[0], args[1], args[2], args[3], in, out); } } }