diff --git a/CHANGELOG.md b/CHANGELOG.md index 81ebde8b..7076d122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [1.9.1-dev.3](https://github.com/MorpheApp/morphe-cli/compare/v1.9.1-dev.2...v1.9.1-dev.3) (2026-06-02) + + +### Bug Fixes + +* signing improvements ([#160](https://github.com/MorpheApp/morphe-cli/issues/160)) ([166f940](https://github.com/MorpheApp/morphe-cli/commit/166f9409b1cbe00af7663545c41548ead2c189c5)) + +## [1.9.1-dev.2](https://github.com/MorpheApp/morphe-cli/compare/v1.9.1-dev.1...v1.9.1-dev.2) (2026-05-31) + + +### Bug Fixes + +* Update dependencies ([83d3969](https://github.com/MorpheApp/morphe-cli/commit/83d39692541ca81b7bb555dfd60a001fbb97b3f1)) + +## [1.9.1-dev.1](https://github.com/MorpheApp/morphe-cli/compare/v1.9.0...v1.9.1-dev.1) (2026-05-31) + + +### Bug Fixes + +* Update to latest ARSCLib ([f62a179](https://github.com/MorpheApp/morphe-cli/commit/f62a1793601fcfc489f54c558265115530ab6b8d)) + # [1.9.0](https://github.com/MorpheApp/morphe-cli/compare/v1.8.1...v1.9.0) (2026-05-29) diff --git a/docs/1_usage.md b/docs/1_usage.md index 930c3026..63f21b50 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -54,6 +54,12 @@ You can combine the option `-e`, `-d`, `--ei`, `--di` and `--exclusive`. Here is java -jar morphe-cli.jar patch --patches patches.mpp --exclusive -e "Patch name" --ei 123 input.apk ``` +You can also use multiple MPP files. Enable/disable and other bundle specific arguments are applied to the last `--patches` argument: + + ```bash + java -jar morphe-cli.jar patch --patches patches-a.mpp -e "patch a" --patches patches-b.mpp -e "patch b" input.apk + ``` + > [!TIP] > You can use the option `-i` to automatically install the patched app after patching. diff --git a/docs/README.md b/docs/README.md index fb4620f0..0ecbd1ee 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # 💻 Documentation and guides of Morphe CLI -This documentation contains topics around [Morphe CLI](https://github.com/MorpheApp/morphes-cli). +This documentation contains topics around [Morphe CLI](https://github.com/MorpheApp/morphe-cli). ## 📖 Table of contents diff --git a/gradle.properties b/gradle.properties index 75a6fa3b..f363fc2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.9.0 +version = 1.9.1-dev.3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24557280..e99be645 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,15 +5,15 @@ kotlin = "2.3.21" # CLI picocli = "4.7.7" -arsclib = "d78a66bcee" -morphe-patcher = "1.5.1" +arsclib = "a28c6fb2a7" +morphe-patcher = "1.5.2-dev.2" # TODO: Change to stable release morphe-library = "1.3.0" # Compose Desktop compose = "1.10.3" # Networking -ktor = "3.4.3" +ktor = "3.5.0" # DI koin-bom = "4.2.1" @@ -29,7 +29,7 @@ kotlinx-serialization = "1.11.0" jna = "5.18.1" # Testing -mockk = "1.14.9" +mockk = "1.14.11" # Logging slf4j = "2.0.18" diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index e24a1715..4134eb3a 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -15,9 +15,8 @@ import app.morphe.engine.isWindows import app.morphe.engine.PatchEngine.Config.Companion.DEFAULT_KEYSTORE_ALIAS import app.morphe.engine.PatchEngine.Config.Companion.DEFAULT_KEYSTORE_PASSWORD import app.morphe.engine.PatchEngine.Config.Companion.DEFAULT_SIGNER_NAME -import app.morphe.engine.PatchEngine.Config.Companion.LEGACY_KEYSTORE_ALIAS -import app.morphe.engine.PatchEngine.Config.Companion.LEGACY_KEYSTORE_PASSWORD import app.morphe.engine.UpdateChecker +import app.morphe.engine.util.signWithLegacyFallback import app.morphe.engine.patches.LoadedBundle import app.morphe.engine.patches.PatchBundleLoader import app.morphe.library.installation.installer.* @@ -836,33 +835,18 @@ internal object PatchCommand : Callable { patchingResult.addStepResult( PatchingStep.SIGNING, { - fun signApk(alias: String, password: String) { - ApkUtils.signApk( - patchedApkFile, - outputFilePath, - signer, - ApkUtils.KeyStoreDetails( - keystoreFilePath, - keyStorePassword, - alias, - password, - ) - ) - } - try { - signApk(keyStoreEntryAlias, keyStoreEntryPassword) - } catch (e: Exception){ - // Retry with legacy keystore defaults. - if (keyStoreEntryAlias == DEFAULT_KEYSTORE_ALIAS && - keyStoreEntryPassword == DEFAULT_KEYSTORE_PASSWORD && - keystoreFilePath.exists() - ) { - logger.info("Using legacy keystore credentials") - - signApk(LEGACY_KEYSTORE_ALIAS, LEGACY_KEYSTORE_PASSWORD) - } else { - throw e - } + signWithLegacyFallback( + primary = ApkUtils.KeyStoreDetails( + keystoreFilePath, + keyStorePassword, + keyStoreEntryAlias, + keyStoreEntryPassword, + ), + allowLegacyFallback = keyStoreEntryAlias == DEFAULT_KEYSTORE_ALIAS && + keyStoreEntryPassword == DEFAULT_KEYSTORE_PASSWORD, + logger = logger, + ) { details -> + ApkUtils.signApk(patchedApkFile, outputFilePath, signer, details) } } ) diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index cddc9cbe..26d64a2f 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -8,6 +8,7 @@ package app.morphe.engine +import app.morphe.engine.util.signWithLegacyFallback import app.morphe.patcher.Patcher import app.morphe.patcher.PatcherConfig import app.morphe.patcher.apk.ApkMerger @@ -244,7 +245,11 @@ object PatchEngine { } try { - fun signApk(details: ApkUtils.KeyStoreDetails) { + signWithLegacyFallback( + primary = keystoreDetails, + allowLegacyFallback = config.keystoreDetails == null, + logger = logger, + ) { details -> ApkUtils.signApk( rebuiltApk, tempOutput, @@ -252,25 +257,6 @@ object PatchEngine { details, ) } - - try { - signApk(keystoreDetails) - } catch (e: Exception) { - // Retry with legacy keystore defaults. - if (config.keystoreDetails == null && keystoreDetails.keyStore.exists()) { - logger.info("Using legacy keystore credentials") - - val legacyKeystoreDetails = ApkUtils.KeyStoreDetails( - keystoreDetails.keyStore, - null, - Config.LEGACY_KEYSTORE_ALIAS, - Config.LEGACY_KEYSTORE_PASSWORD, - ) - signApk(legacyKeystoreDetails) - } else { - throw e - } - } stepResults.add(StepResult(PatchStep.SIGNING, true)) } catch (e: Exception) { stepResults.add(StepResult(PatchStep.SIGNING, false, e.toString())) diff --git a/src/main/kotlin/app/morphe/engine/util/KeystoreSigner.kt b/src/main/kotlin/app/morphe/engine/util/KeystoreSigner.kt new file mode 100644 index 00000000..c859af7e --- /dev/null +++ b/src/main/kotlin/app/morphe/engine/util/KeystoreSigner.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + +package app.morphe.engine.util + +import app.morphe.engine.PatchEngine +import app.morphe.patcher.apk.ApkUtils +import java.util.logging.Logger + +/** + * Signs an APK with [primary] credentials, falling back to the legacy ("Morphe Key" / empty password) entry. + * The legacy retry only fires when [allowLegacyFallback] is true AND the keystore file already exists, + * i.e. the user is on default credentials and we're reading a pre-existing keystore that might predate the current alias. + * This preserves the exact condition both call sites (CLI + engine) used before. + * + * On double failure the PRIMARY exception is thrown (legacy attached as suppressed). + * The primary error is the meaningful one: the user expects the current Morphe key, + * so "no 'Morphe' entry" is more actionable than whatever the legacy retry hit. + * The old behavior threw the *legacy* failure, which surfaced confusing errors. + * + * [sign] performs the actual signing; callers wrap this call with their own progress / step-result reporting. + */ +fun signWithLegacyFallback( + primary: ApkUtils.KeyStoreDetails, + allowLegacyFallback: Boolean, + logger: Logger, + sign: (ApkUtils.KeyStoreDetails) -> Unit, +) { + try { + sign(primary) + } catch (primaryError: Exception) { + if (!allowLegacyFallback || !primary.keyStore.exists()) throw primaryError + + // Never silently swallow the real cause. Always log it before the back-compat path. + logger.info( + "Default keystore credentials failed (${primaryError.message}). Retrying with legacy credentials" + ) + + val legacy = ApkUtils.KeyStoreDetails( + primary.keyStore, + primary.keyStorePassword, + PatchEngine.Config.LEGACY_KEYSTORE_ALIAS, + PatchEngine.Config.LEGACY_KEYSTORE_PASSWORD, + ) + try { + sign(legacy) + } catch (legacyError: Exception) { + primaryError.addSuppressed(legacyError) + throw primaryError + } + } +}