From 2df3484b68ed72338a52e76fb4b7ceb9c9c644ed Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 13 Mar 2024 23:04:35 +0100 Subject: [PATCH] feat: Add utility function around key certificate pairs --- api/revanced-library.api | 23 +- .../kotlin/app/revanced/library/ApkSigner.kt | 219 ++++++++++-------- .../kotlin/app/revanced/library/ApkUtils.kt | 156 ++++++++++--- 3 files changed, 259 insertions(+), 139 deletions(-) diff --git a/api/revanced-library.api b/api/revanced-library.api index 91e2553..8c40200 100644 --- a/api/revanced-library.api +++ b/api/revanced-library.api @@ -7,14 +7,13 @@ public final class app/revanced/library/ApkSigner { public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Set;)V public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore; public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; - public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/library/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; + public final fun readPrivateKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; } public final class app/revanced/library/ApkSigner$KeyStoreEntry { public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getAlias ()Ljava/lang/String; public final fun getPassword ()Ljava/lang/String; public final fun getPrivateKeyCertificatePair ()Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; @@ -35,8 +34,28 @@ public final class app/revanced/library/ApkSigner$Signer { public final class app/revanced/library/ApkUtils { public static final field INSTANCE Lapp/revanced/library/ApkUtils; public final fun applyTo (Lapp/revanced/patcher/PatcherResult;Ljava/io/File;)V + public final fun newPrivateKeyCertificatePair (Lapp/revanced/library/ApkUtils$PrivateKeyCertificatePairDetails;Lapp/revanced/library/ApkUtils$KeyStoreDetails;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; + public final fun readPrivateKeyCertificatePairFromKeyStore (Lapp/revanced/library/ApkUtils$KeyStoreDetails;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun sign (Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V + public final fun sign (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)V +} + +public final class app/revanced/library/ApkUtils$KeyStoreDetails { + public fun (Ljava/io/File;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;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 class app/revanced/library/ApkUtils$PrivateKeyCertificatePairDetails { + public fun ()V + public fun (Ljava/lang/String;Ljava/util/Date;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Date;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCommonName ()Ljava/lang/String; + public final fun getValidUntil ()Ljava/util/Date; } public final class app/revanced/library/ApkUtils$SigningOptions { diff --git a/src/main/kotlin/app/revanced/library/ApkSigner.kt b/src/main/kotlin/app/revanced/library/ApkSigner.kt index 7faea77..35d2091 100644 --- a/src/main/kotlin/app/revanced/library/ApkSigner.kt +++ b/src/main/kotlin/app/revanced/library/ApkSigner.kt @@ -19,7 +19,6 @@ import java.security.* import java.security.cert.X509Certificate import java.util.* import java.util.logging.Logger -import kotlin.time.Duration.Companion.days /** * Utility class for reading or writing keystore files and entries as well as signing APK files. @@ -34,17 +33,80 @@ object ApkSigner { } } + private fun newKeyStoreInstance() = KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME) + /** - * Create a new [PrivateKeyCertificatePair]. + * Create a new keystore with a new keypair. + * + * @param entries The entries to add to the keystore. + * + * @return The created keystore. + * + * @see KeyStoreEntry + * @see KeyStore + */ + fun newKeyStore(entries: Set): KeyStore { + logger.fine("Creating keystore") + + return newKeyStoreInstance().apply { + load(null) + + entries.forEach { entry -> + // Add all entries to the keystore. + setKeyEntry( + entry.alias, + entry.privateKeyCertificatePair.privateKey, + entry.password.toCharArray(), + arrayOf(entry.privateKeyCertificatePair.certificate), + ) + } + } + } + + /** + * 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. + * + * @see KeyStore + */ + fun readKeyStore( + keyStoreInputStream: InputStream, + keyStorePassword: String?, + ): KeyStore { + logger.fine("Reading keystore") + + return newKeyStoreInstance().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 private key and certificate pair. * * @param commonName The common name of the certificate. - * @param validUntil The date until the certificate is valid. + * @param validUntil The date until which the certificate is valid. * - * @return The created [PrivateKeyCertificatePair]. + * @return The newly created private key and certificate pair. + * + * @see PrivateKeyCertificatePair */ fun newPrivateKeyCertificatePair( - commonName: String = "ReVanced", - validUntil: Date = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24), + commonName: String, + validUntil: Date, ): PrivateKeyCertificatePair { logger.fine("Creating certificate for $commonName") @@ -80,8 +142,11 @@ object ApkSigner { * @return The read [PrivateKeyCertificatePair]. * * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. + * + * @see PrivateKeyCertificatePair + * @see KeyStore */ - fun readKeyCertificatePair( + fun readPrivateKeyCertificatePair( keyStore: KeyStore, keyStoreEntryAlias: String, keyStoreEntryPassword: String, @@ -106,80 +171,6 @@ object ApkSigner { return PrivateKeyCertificatePair(privateKey, 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: Set): KeyStore { - logger.fine("Creating keystore") - - return newKeyStoreInstance().apply { - load(null) - - entries.forEach { entry -> - // Add all entries to the keystore. - setKeyEntry( - entry.alias, - entry.privateKeyCertificatePair.privateKey, - entry.password.toCharArray(), - arrayOf(entry.privateKeyCertificatePair.certificate), - ) - } - } - } - - private fun newKeyStoreInstance() = KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME) - - /** - * 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: Set, - ) = newKeyStore(entries).store( - keyStoreOutputStream, - keyStorePassword.toCharArray(), - ) - - /** - * 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 newKeyStoreInstance().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 [Signer]. * @@ -206,6 +197,41 @@ object ApkSigner { ), ) + /** + * 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. + */ + @Deprecated("This method will be removed in the future.") + fun readKeyCertificatePair( + keyStore: KeyStore, + keyStoreEntryAlias: String, + keyStoreEntryPassword: String, + ) = readPrivateKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword) + + /** + * 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. + */ + @Deprecated("This method will be removed in the future.") + fun newKeyStore( + keyStoreOutputStream: OutputStream, + keyStorePassword: String?, + entries: Set, + ) = newKeyStore(entries).store( + keyStoreOutputStream, + keyStorePassword?.toCharArray(), + ) + /** * Create a new [Signer]. * @@ -216,13 +242,7 @@ object ApkSigner { * @see PrivateKeyCertificatePair * @see Signer */ - @Suppress("DEPRECATION") - @Deprecated( - "This method will be removed in the future.", - ReplaceWith( - "newApkSigner(\"ReVanced\", privateKeyCertificatePair)", - ), - ) + @Deprecated("This method will be removed in the future.") fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) = Signer( SigningExtension( @@ -249,6 +269,7 @@ object ApkSigner { * @see KeyStore * @see Signer */ + @Deprecated("This method will be removed in the future.") fun newApkSigner( signer: String, keyStore: KeyStore, @@ -268,13 +289,7 @@ object ApkSigner { * @see KeyStore * @see Signer */ - @Deprecated( - "This method will be removed in the future.", - ReplaceWith( - "newApkSigner(\"ReVanced\", readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword))", - "app.revanced.library.ApkSigner.newApkSigner", - ), - ) + @Deprecated("This method will be removed in the future.") fun newApkSigner( keyStore: KeyStore, keyStoreEntryAlias: String, @@ -293,7 +308,7 @@ object ApkSigner { class KeyStoreEntry( val alias: String, val password: String, - val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair(), + val privateKeyCertificatePair: PrivateKeyCertificatePair, ) /** @@ -316,6 +331,12 @@ object ApkSigner { signingExtension = null } + fun signApk(inputApkFile: File, outputApkFile: File) { + logger.info("Signing APK") + + signerBuilder?.setInputApk(inputApkFile)?.setOutputApk(outputApkFile)?.build()?.sign() + } + @Deprecated("This constructor will be removed in the future.") internal constructor(signingExtension: SigningExtension) { signerBuilder = null @@ -344,11 +365,5 @@ object ApkSigner { signingExtension?.register(apkZFile) } - - fun signApk(inputApkFile: File, outputApkFile: File) { - logger.info("Signing APK") - - signerBuilder?.setInputApk(inputApkFile)?.setOutputApk(outputApkFile)?.build()?.sign() - } } } diff --git a/src/main/kotlin/app/revanced/library/ApkUtils.kt b/src/main/kotlin/app/revanced/library/ApkUtils.kt index ab73bc3..d098779 100644 --- a/src/main/kotlin/app/revanced/library/ApkUtils.kt +++ b/src/main/kotlin/app/revanced/library/ApkUtils.kt @@ -1,12 +1,15 @@ package app.revanced.library +import app.revanced.library.ApkSigner.newPrivateKeyCertificatePair import app.revanced.patcher.PatcherResult import com.android.tools.build.apkzlib.zip.AlignmentRules import com.android.tools.build.apkzlib.zip.StoredEntry import com.android.tools.build.apkzlib.zip.ZFile import com.android.tools.build.apkzlib.zip.ZFileOptions import java.io.File +import java.util.* import java.util.logging.Logger +import kotlin.time.Duration.Companion.days /** * Utility functions to work with APK files. @@ -93,24 +96,89 @@ object ApkUtils { } /** - * Reads an existing or creates a new keystore. + * Creates a new private key and certificate pair and saves it to the keystore in [keyStoreDetails]. * - * @param signingOptions The options to use for signing. + * @param privateKeyCertificatePairDetails The details for the private key and certificate pair. + * @param keyStoreDetails The details for the keystore. + * + * @return The newly created private key and certificate pair. */ - private fun readOrNewKeyStore(signingOptions: SigningOptions) = if (signingOptions.keyStore.exists()) { - ApkSigner.readKeyStore( - signingOptions.keyStore.inputStream(), - signingOptions.keyStorePassword ?: "", + fun newPrivateKeyCertificatePair( + privateKeyCertificatePairDetails: PrivateKeyCertificatePairDetails, + keyStoreDetails: KeyStoreDetails, + ) = newPrivateKeyCertificatePair( + privateKeyCertificatePairDetails.commonName, + privateKeyCertificatePairDetails.validUntil, + ).also { privateKeyCertificatePair -> + ApkSigner.newKeyStore( + setOf( + ApkSigner.KeyStoreEntry( + keyStoreDetails.alias, + keyStoreDetails.password, + privateKeyCertificatePair, + ), + ), + ).store( + keyStoreDetails.keyStore.outputStream(), + keyStoreDetails.keyStorePassword?.toCharArray(), ) - } else { - val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password) + } - // Create a new keystore with a new keypair and saves it. - ApkSigner.newKeyStore(setOf(entry)).apply { - store( - signingOptions.keyStore.outputStream(), - signingOptions.keyStorePassword?.toCharArray(), - ) + /** + * Reads the private key and certificate pair from an existing keystore. + * + * @param keyStoreDetails The details for the keystore. + * + * @return The private key and certificate pair. + */ + fun readPrivateKeyCertificatePairFromKeyStore( + keyStoreDetails: KeyStoreDetails, + ) = ApkSigner.readKeyCertificatePair( + ApkSigner.readKeyStore( + keyStoreDetails.keyStore.inputStream(), + keyStoreDetails.keyStorePassword, + ), + keyStoreDetails.alias, + keyStoreDetails.password, + ) + + /** + * Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile]. + * + * @param inputApkFile The apk file to sign. + * @param outputApkFile The file to save the signed apk to. + * @param signer The name of the signer. + * @param privateKeyCertificatePair The private key and certificate pair to use for signing. + */ + fun sign( + inputApkFile: File, + outputApkFile: File, + signer: String, + privateKeyCertificatePair: ApkSigner.PrivateKeyCertificatePair, + ) = ApkSigner.newApkSigner( + signer, + privateKeyCertificatePair, + ).signApk(inputApkFile, outputApkFile) + + @Deprecated("This method will be removed in the future.") + private fun readOrNewPrivateKeyCertificatePair( + signingOptions: SigningOptions, + ): ApkSigner.PrivateKeyCertificatePair { + val privateKeyCertificatePairDetails = PrivateKeyCertificatePairDetails( + signingOptions.alias, + PrivateKeyCertificatePairDetails().validUntil, + ) + val keyStoreDetails = KeyStoreDetails( + signingOptions.keyStore, + signingOptions.keyStorePassword, + signingOptions.alias, + signingOptions.password, + ) + + return if (keyStoreDetails.keyStore.exists()) { + readPrivateKeyCertificatePairFromKeyStore(keyStoreDetails) + } else { + newPrivateKeyCertificatePair(privateKeyCertificatePairDetails, keyStoreDetails) } } @@ -119,17 +187,11 @@ object ApkUtils { * * @param signingOptions The options to use for signing. */ - @Deprecated("Use sign(File, File, SigningOptions) instead.") - fun File.sign(signingOptions: SigningOptions) { - val keyStore = readOrNewKeyStore(signingOptions) - - @Suppress("DEPRECATION") - ApkSigner.newApkSigner( - keyStore, - signingOptions.alias, - signingOptions.password, - ).signApk(this) - } + @Deprecated("This method will be removed in the future.") + fun File.sign(signingOptions: SigningOptions) = ApkSigner.newApkSigner( + signingOptions.signer, + readOrNewPrivateKeyCertificatePair(signingOptions), + ).signApk(this) /** * Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile]. @@ -138,16 +200,13 @@ object ApkUtils { * @param outputApkFile The file to save the signed apk to. * @param signingOptions The options to use for signing. */ - fun sign(inputApkFile: File, outputApkFile: File, signingOptions: SigningOptions) { - val keyStore = readOrNewKeyStore(signingOptions) - - ApkSigner.newApkSigner( - signingOptions.signer, - keyStore, - signingOptions.alias, - signingOptions.password, - ).signApk(inputApkFile, outputApkFile) - } + @Deprecated("This method will be removed in the future.") + fun sign(inputApkFile: File, outputApkFile: File, signingOptions: SigningOptions) = sign( + inputApkFile, + outputApkFile, + signingOptions.signer, + readOrNewPrivateKeyCertificatePair(signingOptions), + ) /** * Options for signing an apk. @@ -158,6 +217,7 @@ object ApkUtils { * @param password The password for recovering the signing key. * @param signer The name of the signer. */ + @Deprecated("This class will be removed in the future.") class SigningOptions( val keyStore: File, val keyStorePassword: String?, @@ -165,4 +225,30 @@ object ApkUtils { val password: String = "", val signer: String = "ReVanced", ) + + /** + * Details for a keystore. + * + * @param keyStore The file to save the keystore to. + * @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. + */ + class KeyStoreDetails( + val keyStore: File, + val keyStorePassword: String? = null, + val alias: String = "ReVanced Key", + val password: String = "", + ) + + /** + * Details for a private key and certificate pair. + * + * @param commonName The common name for the certificate saved in the keystore. + * @param validUntil The date until which the certificate is valid. + */ + class PrivateKeyCertificatePairDetails( + val commonName: String = "ReVanced", + val validUntil: Date = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24), + ) }