feat: Extend signing API

This commit allows setting the keystore as well as the keystore entry password, alias and signer.

BREAKING CHANGE: This changes many signatures of existing APIs and adds new functions for signing
This commit is contained in:
oSumAtrIX 2023-09-21 06:21:40 +02:00
parent 8da0c2bdfe
commit 592dc1c64a
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
5 changed files with 339 additions and 108 deletions

View File

@ -4,7 +4,6 @@ import app.revanced.lib.ApkUtils
import app.revanced.lib.Options import app.revanced.lib.Options
import app.revanced.lib.Options.setOptions import app.revanced.lib.Options.setOptions
import app.revanced.lib.adb.AdbManager import app.revanced.lib.adb.AdbManager
import app.revanced.lib.signing.SigningOptions
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
@ -80,22 +79,34 @@ internal object PatchCommand : Runnable {
private var mount: Boolean = false private var mount: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["--common-name"], names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"],
description = ["The common name of the signer of the patched APK file"],
showDefaultValue = ALWAYS
)
private var commonName = "ReVanced"
@CommandLine.Option(
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
) )
private var keystoreFilePath: File? = null private var keystoreFilePath: File? = null
// key store password
@CommandLine.Option( @CommandLine.Option(
names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"] names = ["--keystore-password"],
description = ["The password of the keystore to sign the patched APK file with"],
) )
private var password = "ReVanced" private var keyStorePassword: String? = null // Empty password by default
@CommandLine.Option(
names = ["--alias"], description = ["The alias of the key from the keystore to sign the patched APK file with"],
showDefaultValue = ALWAYS
)
private var alias = "ReVanced Key"
@CommandLine.Option(
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 = "" // 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( @CommandLine.Option(
names = ["-r", "--resource-cache"], names = ["-r", "--resource-cache"],
@ -208,16 +219,22 @@ internal object PatchCommand : Runnable {
// region Save // region Save
val tempFile = resourceCachePath.resolve(apk.name) val tempFile = resourceCachePath.resolve(apk.name).apply {
ApkUtils.copyAligned(apk, tempFile, patcherResult) ApkUtils.copyAligned(apk, this, patcherResult)
}
val keystoreFilePath = keystoreFilePath ?: outputFilePath.absoluteFile.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore")
if (!mount) ApkUtils.sign( if (!mount) ApkUtils.sign(
tempFile, tempFile,
outputFilePath, outputFilePath,
SigningOptions( ApkUtils.SigningOptions(
commonName, keystoreFilePath,
keyStorePassword,
alias,
password, password,
keystoreFilePath ?: outputFilePath.absoluteFile.parentFile signer
.resolve("${outputFilePath.nameWithoutExtension}.keystore"),
) )
) )

View File

@ -1,7 +1,17 @@
public final class app/revanced/lib/ApkUtils { public final class app/revanced/lib/ApkUtils {
public static final field INSTANCE Lapp/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 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 <init> (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (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 { 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 final class app/revanced/lib/signing/ApkSigner {
public fun <init> (Lapp/revanced/lib/signing/SigningOptions;)V public static final field INSTANCE Lapp/revanced/lib/signing/ApkSigner;
public final fun signApk (Ljava/io/File;Ljava/io/File;)V 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/SigningOptions { public final class app/revanced/lib/signing/ApkSigner$KeyStoreEntry {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)V public fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;)V
public final fun component1 ()Ljava/lang/String; public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component2 ()Ljava/lang/String; public final fun getAlias ()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 final fun getPassword ()Ljava/lang/String; public final fun getPassword ()Ljava/lang/String;
public fun hashCode ()I public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;
public fun toString ()Ljava/lang/String; }
public final class app/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair {
public fun <init> (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 { public final class app/revanced/lib/zip/ZipFile : java/io/Closeable {

View File

@ -1,7 +1,7 @@
package app.revanced.lib package app.revanced.lib
import app.revanced.lib.signing.ApkSigner 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.ZipFile
import app.revanced.lib.zip.structures.ZipEntry import app.revanced.lib.zip.structures.ZipEntry
import app.revanced.patcher.PatcherResult 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 apk The apk to sign.
* @param output The apk to write the signed apk to. * @param output The apk to write the signed apk to.
@ -60,8 +59,44 @@ object ApkUtils {
output: File, output: File,
signingOptions: SigningOptions, 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)
ApkSigner(signingOptions).signApk(apk, output) // 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.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",
)
}

View File

@ -6,85 +6,256 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.IOException
import java.io.FileOutputStream import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger import java.math.BigInteger
import java.security.* import java.security.*
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.time.Duration.Companion.days
class ApkSigner( @Suppress("unused", "MemberVisibilityCanBePrivate")
private val signingOptions: SigningOptions object ApkSigner {
) {
private val logger = Logger.getLogger(app.revanced.lib.signing.ApkSigner::class.java.name) 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 { init {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
Security.addProvider(BouncyCastleProvider()) 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()
} }
with( /**
ApkSigner.SignerConfig.Builder( * Create a new [PrivateKeyCertificatePair].
signingOptions.commonName, *
keyStore.getKey(alias, passwordCharArray) as PrivateKey, * @param commonName The common name of the certificate.
listOf(keyStore.getCertificate(alias) as X509Certificate) * @param validUntil The date until the certificate is valid.
).build() * @return The created [PrivateKeyCertificatePair].
) { */
this@ApkSigner.signer = ApkSigner.Builder(listOf(this)) fun newPrivateKeyCertificatePair(
signer.setCreatedBy(signingOptions.commonName) commonName: String = "ReVanced",
} validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24)
} ): PrivateKeyCertificatePair {
logger.fine("Creating certificate for $commonName")
private fun newKeystore(out: File) { // Generate a new key pair.
val (publicKey, privateKey) = createKey() val keyPair = KeyPairGenerator.getInstance("RSA").apply {
val privateKS = KeyStore.getInstance("BKS", "BC") initialize(2048)
privateKS.load(null, passwordCharArray) }.generateKeyPair()
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
privateKS.store(FileOutputStream(out), passwordCharArray)
}
private fun createKey(): Pair<X509Certificate, PrivateKey> {
val gen = KeyPairGenerator.getInstance("RSA")
gen.initialize(2048)
val pair = gen.generateKeyPair()
var serialNumber: BigInteger var serialNumber: BigInteger
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO) do serialNumber = BigInteger.valueOf(SecureRandom().nextLong())
val x500Name = X500Name("CN=${signingOptions.commonName}") while (serialNumber < BigInteger.ZERO)
val builder = X509v3CertificateBuilder(
x500Name, val name = X500Name("CN=$commonName")
// Create a new certificate.
val certificate = JcaX509CertificateConverter().getCertificate(
X509v3CertificateBuilder(
name,
serialNumber, serialNumber,
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), Date(System.currentTimeMillis()),
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), validUntil,
Locale.ENGLISH, Locale.ENGLISH,
x500Name, name,
SubjectPublicKeyInfo.getInstance(pair.public.encoded) SubjectPublicKeyInfo.getInstance(keyPair.public.encoded)
).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private))
) )
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private return PrivateKeyCertificatePair(keyPair.private, certificate)
} }
fun signApk(input: File, output: File) { /**
signer.setInputApk(input) * Create a new keystore with a new keypair.
signer.setOutputApk(output) *
* @param entries The entries to add to the keystore.
* @return The created keystore.
* @see KeyStoreEntry
*/
fun newKeyStore(
entries: List<KeyStoreEntry>
): KeyStore {
logger.fine("Creating keystore")
signer.build().sign() 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)
)
} }
} }
}
/**
* 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<KeyStoreEntry>
) = 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
}
}
}
/**
* 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"
)
// 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)
}
/**
* 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
)
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,
)
}

View File

@ -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
)