diff --git a/revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 97dc6a13..0ed45529 100644 --- a/revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -4,7 +4,6 @@ import app.revanced.lib.ApkUtils import app.revanced.lib.Options import app.revanced.lib.Options.setOptions import app.revanced.lib.adb.AdbManager -import app.revanced.lib.signing.SigningOptions import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchSet import app.revanced.patcher.Patcher @@ -80,22 +79,34 @@ internal object PatchCommand : Runnable { private var mount: Boolean = false @CommandLine.Option( - names = ["--common-name"], - description = ["The common name of the signer of the patched APK file"], - showDefaultValue = ALWAYS + names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"], + ) + private var keystoreFilePath: File? = null + // key store password + @CommandLine.Option( + names = ["--keystore-password"], + description = ["The password of the keystore to sign the patched APK file with"], ) - private var commonName = "ReVanced" + private var keyStorePassword: String? = null // Empty password by default @CommandLine.Option( - names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"] + names = ["--alias"], description = ["The alias of the key from the keystore to sign the patched APK file with"], + showDefaultValue = ALWAYS ) - private var keystoreFilePath: File? = null + private var alias = "ReVanced Key" @CommandLine.Option( - names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"] + names = ["--keystore-entry-password"], + description = ["The password of the entry from the keystore for the key to sign the patched APK file with"] ) - private var password = "ReVanced" + private var password = "" // Empty password by default + + @CommandLine.Option( + names = ["--signer"], description = ["The name of the signer to sign the patched APK file with"], + showDefaultValue = ALWAYS + ) + private var signer = "ReVanced" @CommandLine.Option( names = ["-r", "--resource-cache"], @@ -208,16 +219,22 @@ internal object PatchCommand : Runnable { // region Save - val tempFile = resourceCachePath.resolve(apk.name) - ApkUtils.copyAligned(apk, tempFile, patcherResult) + val tempFile = resourceCachePath.resolve(apk.name).apply { + ApkUtils.copyAligned(apk, this, patcherResult) + } + + val keystoreFilePath = keystoreFilePath ?: outputFilePath.absoluteFile.parentFile + .resolve("${outputFilePath.nameWithoutExtension}.keystore") + if (!mount) ApkUtils.sign( tempFile, outputFilePath, - SigningOptions( - commonName, + ApkUtils.SigningOptions( + keystoreFilePath, + keyStorePassword, + alias, password, - keystoreFilePath ?: outputFilePath.absoluteFile.parentFile - .resolve("${outputFilePath.nameWithoutExtension}.keystore"), + signer ) ) diff --git a/revanced-lib/api/revanced-lib.api b/revanced-lib/api/revanced-lib.api index dd646c9f..685ab41b 100644 --- a/revanced-lib/api/revanced-lib.api +++ b/revanced-lib/api/revanced-lib.api @@ -1,7 +1,17 @@ public final class app/revanced/lib/ApkUtils { public static final field INSTANCE Lapp/revanced/lib/ApkUtils; public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V - public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/lib/signing/SigningOptions;)V + public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/lib/ApkUtils$SigningOptions;)V +} + +public final class app/revanced/lib/ApkUtils$SigningOptions { + public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAlias ()Ljava/lang/String; + public final fun getKeyStore ()Ljava/io/File; + public final fun getKeyStorePassword ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; + public final fun getSigner ()Ljava/lang/String; } public final class app/revanced/lib/Options { @@ -77,23 +87,30 @@ public final class app/revanced/lib/logging/Logger { } public final class app/revanced/lib/signing/ApkSigner { - public fun (Lapp/revanced/lib/signing/SigningOptions;)V - public final fun signApk (Ljava/io/File;Ljava/io/File;)V -} - -public final class app/revanced/lib/signing/SigningOptions { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/io/File; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)Lapp/revanced/lib/signing/SigningOptions; - public static synthetic fun copy$default (Lapp/revanced/lib/signing/SigningOptions;Ljava/lang/String;Ljava/lang/String;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/lib/signing/SigningOptions; - public fun equals (Ljava/lang/Object;)Z - public final fun getCommonName ()Ljava/lang/String; - public final fun getKeyStoreOutputFilePath ()Ljava/io/File; + public static final field INSTANCE Lapp/revanced/lib/signing/ApkSigner; + public final fun newApkSignerBuilder (Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; + public final fun newApkSignerBuilder (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; + public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore; + public final fun newKeystore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V + public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; + public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/lib/signing/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; + public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; + public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; + public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V +} + +public final class app/revanced/lib/signing/ApkSigner$KeyStoreEntry { + public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAlias ()Ljava/lang/String; public final fun getPassword ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; +} + +public final class app/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair { + public fun (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V + public final fun getCertificate ()Ljava/security/cert/X509Certificate; + public final fun getPrivateKey ()Ljava/security/PrivateKey; } public final class app/revanced/lib/zip/ZipFile : java/io/Closeable { diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt index 9a8ebcc4..06b73342 100644 --- a/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt @@ -1,7 +1,7 @@ package app.revanced.lib import app.revanced.lib.signing.ApkSigner -import app.revanced.lib.signing.SigningOptions +import app.revanced.lib.signing.ApkSigner.signApk import app.revanced.lib.zip.ZipFile import app.revanced.lib.zip.structures.ZipEntry import app.revanced.patcher.PatcherResult @@ -47,9 +47,8 @@ object ApkUtils { } } - /** - * Signs the apk at [apk] and writes it to [output]. + * Signs the [apk] file and writes it to [output]. * * @param apk The apk to sign. * @param output The apk to write the signed apk to. @@ -60,8 +59,44 @@ object ApkUtils { output: File, signingOptions: SigningOptions, ) { - logger.info("Signing ${apk.name}") + // Get the keystore from the file or create a new one. + val keyStore = if (signingOptions.keyStore.exists()) { + ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword) + } else { + val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password) + + // Create a new keystore with a new keypair and saves it. + ApkSigner.newKeyStore(listOf(entry)).also { keyStore -> + keyStore.store( + signingOptions.keyStore.outputStream(), + signingOptions.keyStorePassword?.toCharArray() + ) + } + } - ApkSigner(signingOptions).signApk(apk, output) + ApkSigner.newApkSignerBuilder( + keyStore, + signingOptions.alias, + signingOptions.password, + signingOptions.signer, + signingOptions.signer + ).signApk(apk, output) } + + /** + * Options for signing an apk. + * + * @param keyStore The keystore to use for signing. + * @param keyStorePassword The password for the keystore. + * @param alias The alias of the key store entry to use for signing. + * @param password The password for recovering the signing key. + * @param signer The name of the signer. + */ + class SigningOptions( + val keyStore: File, + val keyStorePassword: String?, + val alias: String = "ReVanced Key", + val password: String = "", + val signer: String = "ReVanced", + ) } \ No newline at end of file diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt index 15c2522a..4bc0033b 100644 --- a/revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt @@ -6,85 +6,256 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream import java.math.BigInteger import java.security.* import java.security.cert.X509Certificate import java.util.* import java.util.logging.Logger +import kotlin.time.Duration.Companion.days -class ApkSigner( - private val signingOptions: SigningOptions -) { +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ApkSigner { private val logger = Logger.getLogger(app.revanced.lib.signing.ApkSigner::class.java.name) - private val signer: ApkSigner.Builder - private val passwordCharArray = signingOptions.password.toCharArray() - init { - Security.addProvider(BouncyCastleProvider()) - - val keyStore = KeyStore.getInstance("BKS", "BC") - val alias = keyStore.let { store -> - FileInputStream(signingOptions.keyStoreOutputFilePath.also { - if (!it.exists()) { - logger.info("Creating keystore at ${it.absolutePath}") - newKeystore(it) - } else { - logger.info("Using keystore ${it.absolutePath}") - } - }).use { fis -> store.load(fis, null) } - store.aliases().nextElement() - } + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) + Security.addProvider(BouncyCastleProvider()) + } + + /** + * Create a new [PrivateKeyCertificatePair]. + * + * @param commonName The common name of the certificate. + * @param validUntil The date until the certificate is valid. + * @return The created [PrivateKeyCertificatePair]. + */ + fun newPrivateKeyCertificatePair( + commonName: String = "ReVanced", + validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24) + ): PrivateKeyCertificatePair { + logger.fine("Creating certificate for $commonName") + + // Generate a new key pair. + val keyPair = KeyPairGenerator.getInstance("RSA").apply { + initialize(2048) + }.generateKeyPair() + + var serialNumber: BigInteger + do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) + while (serialNumber < BigInteger.ZERO) + + val name = X500Name("CN=$commonName") - with( - ApkSigner.SignerConfig.Builder( - signingOptions.commonName, - keyStore.getKey(alias, passwordCharArray) as PrivateKey, - listOf(keyStore.getCertificate(alias) as X509Certificate) - ).build() - ) { - this@ApkSigner.signer = ApkSigner.Builder(listOf(this)) - signer.setCreatedBy(signingOptions.commonName) + // Create a new certificate. + val certificate = JcaX509CertificateConverter().getCertificate( + X509v3CertificateBuilder( + name, + serialNumber, + Date(System.currentTimeMillis()), + validUntil, + Locale.ENGLISH, + name, + SubjectPublicKeyInfo.getInstance(keyPair.public.encoded) + ).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private)) + ) + + return PrivateKeyCertificatePair(keyPair.private, certificate) + } + + /** + * Create a new keystore with a new keypair. + * + * @param entries The entries to add to the keystore. + * @return The created keystore. + * @see KeyStoreEntry + */ + fun newKeyStore( + entries: List + ): KeyStore { + logger.fine("Creating keystore") + + return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { + entries.forEach { entry -> + load(null) + // Add all entries to the keystore. + setKeyEntry( + entry.alias, + entry.privateKeyCertificatePair.privateKey, + entry.password.toCharArray(), + arrayOf(entry.privateKeyCertificatePair.certificate) + ) + } } } - private fun newKeystore(out: File) { - val (publicKey, privateKey) = createKey() - val privateKS = KeyStore.getInstance("BKS", "BC") - privateKS.load(null, passwordCharArray) - privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey)) - privateKS.store(FileOutputStream(out), passwordCharArray) + /** + * Create a new keystore with a new keypair and saves it to the given [keyStoreOutputStream]. + * + * @param keyStoreOutputStream The stream to write the keystore to. + * @param keyStorePassword The password for the keystore. + * @param entries The entries to add to the keystore. + */ + fun newKeystore( + keyStoreOutputStream: OutputStream, + keyStorePassword: String, + entries: List + ) = newKeyStore(entries).store( + keyStoreOutputStream, + keyStorePassword.toCharArray() + ) // Save the keystore. + + /** + * Read a keystore from the given [keyStoreInputStream]. + * + * @param keyStoreInputStream The stream to read the keystore from. + * @param keyStorePassword The password for the keystore. + * @return The keystore. + * @throws IllegalArgumentException If the keystore password is invalid. + */ + fun readKeyStore( + keyStoreInputStream: InputStream, + keyStorePassword: String? + ): KeyStore { + logger.fine("Reading keystore") + + return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { + try { + load(keyStoreInputStream, keyStorePassword?.toCharArray()) + } catch (exception: IOException) { + if (exception.cause is UnrecoverableKeyException) + throw IllegalArgumentException("Invalid keystore password") + else + throw exception + } + } } - private fun createKey(): Pair { - val gen = KeyPairGenerator.getInstance("RSA") - gen.initialize(2048) - val pair = gen.generateKeyPair() - var serialNumber: BigInteger - do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO) - val x500Name = X500Name("CN=${signingOptions.commonName}") - val builder = X509v3CertificateBuilder( - x500Name, - serialNumber, - Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), - Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), - Locale.ENGLISH, - x500Name, - SubjectPublicKeyInfo.getInstance(pair.public.encoded) + /** + * Create a new [ApkSigner.Builder]. + * + * @param privateKeyCertificatePair The private key and certificate pair to use for signing. + * @param signer The name of the signer. + * @param createdBy The value for the `Created-By` attribute in the APK's manifest. + * @return The created [ApkSigner.Builder] instance. + */ + fun newApkSignerBuilder( + privateKeyCertificatePair: PrivateKeyCertificatePair, + signer: String, + createdBy: String + ): ApkSigner.Builder { + logger.fine( + "Creating new ApkSigner " + + "with $signer as signer and " + + "$createdBy as Created-By attribute in the APK's manifest" ) - val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private) - return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private + + // Create the signer config. + val signerConfig = ApkSigner.SignerConfig.Builder( + signer, + privateKeyCertificatePair.privateKey, + listOf(privateKeyCertificatePair.certificate) + ).build() + + // Create the signer. + return ApkSigner.Builder(listOf(signerConfig)).apply { + setCreatedBy(createdBy) + } + } + + /** + * Read a [PrivateKeyCertificatePair] from a keystore entry. + * + * @param keyStore The keystore to read the entry from. + * @param keyStoreEntryAlias The alias of the key store entry to read. + * @param keyStoreEntryPassword The password for recovering the signing key. + * @return The read [PrivateKeyCertificatePair]. + * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. + */ + fun readKeyCertificatePair( + keyStore: KeyStore, + keyStoreEntryAlias: String, + keyStoreEntryPassword: String, + ): PrivateKeyCertificatePair { + logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias") + + if (!keyStore.containsAlias(keyStoreEntryAlias)) + throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias") + + // Read the private key and certificate from the keystore. + + val privateKey = try { + keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey + } catch (exception: UnrecoverableKeyException) { + throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias") + } + + val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate + + return PrivateKeyCertificatePair(privateKey, certificate) } - fun signApk(input: File, output: File) { - signer.setInputApk(input) - signer.setOutputApk(output) + /** + * Create a new [ApkSigner.Builder]. + * + * @param keyStore The keystore to use for signing. + * @param keyStoreEntryAlias The alias of the key store entry to use for signing. + * @param keyStoreEntryPassword The password for recovering the signing key. + * @param signer The name of the signer. + * @param createdBy The value for the `Created-By` attribute in the APK's manifest. + * @return The created [ApkSigner.Builder] instance. + * @see KeyStoreEntry + * @see PrivateKeyCertificatePair + * @see ApkSigner.Builder.setCreatedBy + * @see ApkSigner.Builder.signApk + */ + fun newApkSignerBuilder( + keyStore: KeyStore, + keyStoreEntryAlias: String, + keyStoreEntryPassword: String, + signer: String, + createdBy: String, + ) = newApkSignerBuilder( + readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword), + signer, + createdBy + ) - signer.build().sign() + fun ApkSigner.Builder.signApk(input: File, output: File) { + logger.info("Signing ${input.name}") + + setInputApk(input) + setOutputApk(output) + + build().sign() } + + /** + * An entry in a keystore. + * + * @param alias The alias of the entry. + * @param password The password for recovering the signing key. + * @param privateKeyCertificatePair The private key and certificate pair. + * @see PrivateKeyCertificatePair + */ + class KeyStoreEntry( + val alias: String, + val password: String, + val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair() + ) + + /** + * A private key and certificate pair. + * + * @param privateKey The private key. + * @param certificate The certificate. + */ + class PrivateKeyCertificatePair( + val privateKey: PrivateKey, + val certificate: X509Certificate, + ) } \ No newline at end of file diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/signing/SigningOptions.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/signing/SigningOptions.kt deleted file mode 100644 index a4ed2188..00000000 --- a/revanced-lib/src/main/kotlin/app/revanced/lib/signing/SigningOptions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.revanced.lib.signing - -import java.io.File - -data class SigningOptions( - val commonName: String, - val password: String, - val keyStoreOutputFilePath: File -) \ No newline at end of file