mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-11-19 01:59:25 +01:00
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:
parent
8da0c2bdfe
commit
592dc1c64a
@ -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"),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
|
||||||
)
|
|
Loading…
Reference in New Issue
Block a user