From 52b733a4500225a63f2a82ec10e868779eb251af Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Sun, 27 Sep 2026 01:20:07 +0800 Subject: [PATCH] fix(lotw): support more PBES2 variants, classify import errors for users - Pkcs12Reader: AES-192/AES-128/DES-EDE3-CBC ciphers, PBKDF2 SHA1/SHA256/SHA384/SHA512 PRFs, keyLength defaulting per cipher, error messages reduced to algorithm names - LoTWKeyMaterial: map BadPadding -> wrong password, IllegalState (unsupported algorithm) -> format error with algorithm name, IllegalArgumentException -> invalid file - Import dialog: user-facing messages with algorithm name embedded, no more raw OpenSSL/BoringSSL error strings - Tests: AES-192, DES-EDE3 fixtures, wrong-password BadPadding check --- .../core/data/lotw/LoTWKeyMaterial.kt | 16 ++++- .../look4sat/core/data/lotw/Pkcs12Reader.kt | 68 +++++++++++++++--- .../core/data/lotw/Pkcs12ReaderTest.kt | 61 ++++++++++++++++ .../src/test/resources/test_pbes2_aes192.p12 | Bin 0 -> 2620 bytes .../src/test/resources/test_pbes2_desede3.p12 | Bin 0 -> 2603 bytes .../src/main/res/values-zh/strings.xml | 3 +- .../src/main/res/values/strings.xml | 3 +- .../settings/LoTWUploadConfigDialog.kt | 24 ++++--- .../feature/settings/SettingsScreen.kt | 1 + .../feature/settings/SettingsState.kt | 2 + .../feature/settings/SettingsViewModel.kt | 4 +- gradle/libs.versions.toml | 4 +- 12 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 core/data/src/test/resources/test_pbes2_aes192.p12 create mode 100644 core/data/src/test/resources/test_pbes2_desede3.p12 diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWKeyMaterial.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWKeyMaterial.kt index 48e1183e..3c51ddcb 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWKeyMaterial.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/LoTWKeyMaterial.kt @@ -29,8 +29,20 @@ internal data class LoTWKeyMaterial(val key: PrivateKey, val certificate: X509Ce try { val parsed = Pkcs12Reader.read(bytes, password) parsed.first to parsed.second - } catch (_: Exception) { - fail(if (password.isNotEmpty()) LoTWProblem.CERTIFICATE_FORMAT else LoTWProblem.CERTIFICATE_PASSWORD) + } catch (e: Exception) { + // Classify by exception type so the user sees the right message: + // - BadPadding (BAD_DECRYPT) -> wrong password + // - IllegalStateException (error()) -> unsupported algorithm, + // message is the algorithm name + // - IllegalArgumentException (require()) -> structurally invalid file + fail( + when { + e is javax.crypto.BadPaddingException -> LoTWProblem.CERTIFICATE_PASSWORD + e is IllegalStateException && password.isNotEmpty() -> LoTWProblem.CERTIFICATE_FORMAT + else -> LoTWProblem.CERTIFICATE_INVALID + }, + if (e is IllegalStateException) (e.message ?: "") else "" + ) } } else { fail(LoTWProblem.CERTIFICATE_PASSWORD) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12Reader.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12Reader.kt index 87a6d9ff..be372575 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12Reader.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12Reader.kt @@ -51,9 +51,15 @@ internal object Pkcs12Reader { private val OID_CERT_BAG = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x03) private val OID_X509_CERT = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x09, 0x16, 0x01) private val OID_PBES2 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x05, 0x0d) + private val OID_PBKDF2 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x05, 0x0c) private val OID_HMAC_SHA1 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x07) + private val OID_HMAC_SHA256 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x09) + private val OID_HMAC_SHA384 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x0a) + private val OID_HMAC_SHA512 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x0b) private val OID_AES_256_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a) + private val OID_AES_192_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x16) private val OID_AES_128_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x02) + private val OID_DES_EDE3_CBC = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x03, 0x07) fun read(bytes: ByteArray, password: CharArray): Pair { val pfx = DerReader.read(bytes) @@ -123,6 +129,11 @@ internal object Pkcs12Reader { // PBES2-params ::= SEQUENCE { kdf AlgorithmIdentifier, enc AlgorithmIdentifier } val pbes2 = DerReader.children(algParts[1].content) val kdf = DerReader.children(pbes2[0].content) + // KDF must be PBKDF2 (OpenSSL 3 also supports scrypt — not available on the + // platform JCE, so report it by name instead of a generic failure). + if (kdf.isEmpty() || !kdf[0].content.contentEquals(OID_PBKDF2)) { + error(oidName(kdf.firstOrNull()?.content)) + } val kdfParams = DerReader.children(kdf[1].content) val salt = kdfParams[0].content val iterations = readInt(kdfParams[1].content) @@ -130,7 +141,7 @@ internal object Pkcs12Reader { // Optional PBKDF2-params elements: keyLength (INTEGER) and/or prf (SEQUENCE), // in either order. Distinguish by DER tag — mistaking prf for keyLength yields // an absurd key size and a PBKDF2 that runs for hours. - var keyBits = 256 + var keyBits = -1 // -1 = not written; defaulted below from the cipher var prfName = "PBKDF2WithHmacSHA1" for (i in 2 until kdfParams.size) { val param = kdfParams[i] @@ -138,26 +149,61 @@ internal object Pkcs12Reader { 0x02 -> keyBits = readInt(param.content) * 8 0x30 -> { val prf = DerReader.children(param.content) - prfName = if (prf.isNotEmpty() && prf[0].content.contentEquals(OID_HMAC_SHA1)) - "PBKDF2WithHmacSHA1" else "PBKDF2WithHmacSHA256" + prfName = when { + prf.isEmpty() || prf[0].content.contentEquals(OID_HMAC_SHA1) -> "PBKDF2WithHmacSHA1" + prf[0].content.contentEquals(OID_HMAC_SHA256) -> "PBKDF2WithHmacSHA256" + prf[0].content.contentEquals(OID_HMAC_SHA384) -> "PBKDF2WithHmacSHA384" + prf[0].content.contentEquals(OID_HMAC_SHA512) -> "PBKDF2WithHmacSHA512" + else -> error(oidName(prf[0].content)) + } } } } - require(keyBits in 128..512) { "implausible PBKDF2 key size" } val enc = DerReader.children(pbes2[1].content) val encOid = enc[0].content val iv = enc[1].content - require(encOid.contentEquals(OID_AES_256_CBC) || encOid.contentEquals(OID_AES_128_CBC)) { "unsupported cipher" } + val (cipherName, aesKeyBits) = when { + encOid.contentEquals(OID_AES_256_CBC) -> "AES/CBC/PKCS5Padding" to 256 + encOid.contentEquals(OID_AES_192_CBC) -> "AES/CBC/PKCS5Padding" to 192 + encOid.contentEquals(OID_AES_128_CBC) -> "AES/CBC/PKCS5Padding" to 128 + encOid.contentEquals(OID_DES_EDE3_CBC) -> "DESede/CBC/PKCS5Padding" to 192 + else -> error(oidName(encOid)) + } + val finalKeyBits = if (keyBits > 0) keyBits else aesKeyBits + require(finalKeyBits in 128..512) { "implausible PBKDF2 key size" } - val spec = PBEKeySpec(password, salt, iterations, keyBits) - val secretKey = SecretKeyFactory.getInstance(prfName).generateSecret(spec) - // PBKDF2 factories return a PBE key; wrap the raw bytes as an AES key. - val aesKey = SecretKeySpec(secretKey.encoded, "AES") - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv)) + val spec = PBEKeySpec(password, salt, iterations, finalKeyBits) + val secretKey = try { + SecretKeyFactory.getInstance(prfName).generateSecret(spec) + } catch (e: java.security.NoSuchAlgorithmException) { + // e.g. PBKDF2WithHmacSHA512 needs API 26+; surface the real reason. + error(prfName.replace("PBKDF2WithHmac", "PBKDF2-HMAC-") + " (needs Android 8.0+)") + } + val cipher = Cipher.getInstance(cipherName) + cipher.init( + Cipher.DECRYPT_MODE, + if (cipherName == "AES/CBC/PKCS5Padding") SecretKeySpec(secretKey.encoded, "AES") + else SecretKeySpec(secretKey.encoded, "DESede"), + IvParameterSpec(iv) + ) return cipher.doFinal(encrypted) } + /** Human-readable name for a known algorithm OID. */ + private fun oidName(oid: ByteArray?): String = when { + oid == null -> "unknown algorithm" + oid.contentEquals(OID_PBKDF2) -> "PBKDF2" + oid.contentEquals(OID_HMAC_SHA1) -> "PBKDF2-HMAC-SHA1" + oid.contentEquals(OID_HMAC_SHA256) -> "PBKDF2-HMAC-SHA256" + oid.contentEquals(OID_HMAC_SHA384) -> "PBKDF2-HMAC-SHA384" + oid.contentEquals(OID_HMAC_SHA512) -> "PBKDF2-HMAC-SHA512" + oid.contentEquals(OID_AES_256_CBC) -> "AES-256-CBC" + oid.contentEquals(OID_AES_192_CBC) -> "AES-192-CBC" + oid.contentEquals(OID_AES_128_CBC) -> "AES-128-CBC" + oid.contentEquals(OID_DES_EDE3_CBC) -> "DES-EDE3-CBC" + else -> "unknown algorithm" + } + private fun parseSafeBags( safeContentsDer: ByteArray, password: CharArray, diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt index ebaa9711..34cd997b 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt @@ -36,6 +36,67 @@ class Pkcs12ReaderTest { private fun fixture(): ByteArray = javaClass.classLoader!!.getResourceAsStream("test_pbes2.p12")!!.use { it.readBytes() } + private fun fixture(name: String): ByteArray = + javaClass.classLoader!!.getResourceAsStream(name)!!.use { it.readBytes() } + + @Test + fun readsAes192Variant() { + val (key, cert) = Pkcs12Reader.read(fixture("test_pbes2_aes192.p12"), "testpass123".toCharArray()) + assertEquals("RSA", key.algorithm) + assertEquals("RSA", cert.publicKey.algorithm) + assertTrue("key/cert pair", keyMatches(key, cert)) + } + + @Test + fun readsDesEde3CbcVariant() { + val (key, cert) = Pkcs12Reader.read(fixture("test_pbes2_desede3.p12"), "testpass123".toCharArray()) + assertEquals("RSA", key.algorithm) + assertEquals("RSA", cert.publicKey.algorithm) + assertTrue("key/cert pair", keyMatches(key, cert)) + } + + @Test + fun unsupportedCipherNamesTheAlgorithm() { + // A valid PBES2 file re-encrypted with an unsupported cipher must report the + // algorithm name (so the user can see exactly what to re-export with). + val fixtureBytes = fixture("test_pbes2.p12") + val e = assertThrows(IllegalStateException::class.java) { + // Simulate: patch the AES-256-CBC OID inside the file to an unknown OID. + val bogus = ByteArray(fixtureBytes.size) { fixtureBytes[it] } + // find AES-256-CBC OID bytes 0x60 86 48 01 65 03 04 01 2a + val oid = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a) + var idx = -1 + outer@ for (i in 0..bogus.size - oid.size) { + for (j in oid.indices) if (bogus[i + j] != oid[j]) continue@outer + idx = i; break + } + require(idx >= 0) { "AES-256-CBC OID not found in fixture" } + bogus[idx] = 0x7f.toByte() // corrupt the OID tag byte → unknown cipher + Pkcs12Reader.read(bogus, "testpass123".toCharArray()) + } + assertTrue("mentions algorithm", e.message.orEmpty().contains("algorithm")) + } + + private fun keyMatches(key: java.security.PrivateKey, cert: java.security.cert.X509Certificate): Boolean { + val challenge = "Look4Sat LoTW certificate key check".toByteArray(Charsets.US_ASCII) + val signed = Signature.getInstance("SHA1withRSA").run { + initSign(key); update(challenge); sign() + } + return Signature.getInstance("SHA1withRSA").run { + initVerify(cert); update(challenge); verify(signed) + } + } + + @Test + fun wrongPasswordFailsWithBadPadding() { + // A wrong password must surface as a decryption failure (BadPadding), which + // LoTWKeyMaterial maps to CERTIFICATE_PASSWORD — not a format error. + val e = assertThrows(Exception::class.java) { + Pkcs12Reader.read(fixture(), "definitely-wrong-password".toCharArray()) + } + assertTrue("BadPadding", e is javax.crypto.BadPaddingException) + } + @Test fun readsPbes2KeyAndCertificate() { val (key, cert) = Pkcs12Reader.read(fixture(), "testpass123".toCharArray()) diff --git a/core/data/src/test/resources/test_pbes2_aes192.p12 b/core/data/src/test/resources/test_pbes2_aes192.p12 new file mode 100644 index 0000000000000000000000000000000000000000..1608fe6fa28b308d88a474c95efedbb882050c28 GIT binary patch literal 2620 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 DZW@@> literal 0 HcmV?d00001 diff --git a/core/data/src/test/resources/test_pbes2_desede3.p12 b/core/data/src/test/resources/test_pbes2_desede3.p12 new file mode 100644 index 0000000000000000000000000000000000000000..593e0e25fc42e00e658c92a4e155ec5adb8be2a9 GIT binary patch literal 2603 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 FzW`^nsc8TJ literal 0 HcmV?d00001 diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 87d411f5..e8f0ca91 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -210,7 +210,8 @@ 证书密码错误,请检查 TQSL 导出 .p12 时设置的密码 证书已过期或尚未生效 不是有效的 LoTW 证书文件 - 该 .p12 是新版 PBES2/AES-256 格式(OpenSSL 3 / 新版 TQSL 导出),Android 无法直接读取。请用旧格式重新导出(TQSL 旧加密导出或 OpenSSL -legacy 转换)后再导入 + 不支持的 .p12 加密,请用 TQSL 默认加密重新导出后再试 + 不支持的 .p12 加密(%1$s),请用 TQSL 默认加密重新导出后再试 导入失败,请重试 %1$s · DXCC %2$d · 有效期至 %3$s 移除证书 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index e1ef78e2..a76797d4 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -241,7 +241,8 @@ Incorrect certificate password. Check the password you set when exporting the .p12 from TQSL. Certificate is expired or not yet valid. Not a valid LoTW certificate file. - This .p12 uses the new PBES2/AES-256 format (OpenSSL 3 / recent TQSL), which Android cannot read directly. Re-export it as a legacy format (TQSL "export with legacy encryption" or an OpenSSL -legacy conversion), then import again. + Unsupported .p12 encryption. Re-export from TQSL and try again. + Unsupported .p12 encryption (%1$s). Re-export from TQSL and try again. Import failed. Try again. %1$s · DXCC %2$d · expires %3$s Remove certificate diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt index a6c93b3e..29575798 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt @@ -92,6 +92,7 @@ fun LoTWUploadConfigDialog( stationMeta: LoTWStationMeta?, busy: Boolean, error: LoTWUploadError?, + errorDetail: String = "", onDismiss: () -> Unit, onImport: (ByteArray, CharArray) -> Unit, onRemove: () -> Unit, @@ -162,15 +163,20 @@ fun LoTWUploadConfigDialog( ) error?.let { Text( - text = stringResource( - when (it) { - LoTWUploadError.PASSWORD -> R.string.prefs_lotw_upload_error_password - LoTWUploadError.EXPIRED -> R.string.prefs_lotw_upload_error_expired - LoTWUploadError.INVALID_FILE -> R.string.prefs_lotw_upload_error_invalid - LoTWUploadError.FORMAT -> R.string.prefs_lotw_upload_error_format - LoTWUploadError.UNKNOWN -> R.string.prefs_lotw_upload_error_unknown - } - ), + text = when { + it == LoTWUploadError.FORMAT && errorDetail.isNotBlank() -> + stringResource(R.string.prefs_lotw_upload_error_format_alg, errorDetail) + it == LoTWUploadError.FORMAT -> stringResource(R.string.prefs_lotw_upload_error_format) + else -> stringResource( + when (it) { + LoTWUploadError.PASSWORD -> R.string.prefs_lotw_upload_error_password + LoTWUploadError.EXPIRED -> R.string.prefs_lotw_upload_error_expired + LoTWUploadError.INVALID_FILE -> R.string.prefs_lotw_upload_error_invalid + LoTWUploadError.UNKNOWN -> R.string.prefs_lotw_upload_error_unknown + LoTWUploadError.FORMAT -> R.string.prefs_lotw_upload_error_format + } + ) + }, color = MaterialTheme.colorScheme.error, fontSize = 12.sp ) diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index 6bd412a8..e5390daa 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -282,6 +282,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> stationMeta = uiState.lotwStationMeta, busy = uiState.lotwUploadBusy, error = uiState.lotwUploadError, + errorDetail = uiState.lotwUploadErrorDetail, onDismiss = { dialogs.lotwUpload = false }, onImport = { bytes, password -> onAction(SettingsAction.ImportLoTWCertificate(bytes, password)) }, onRemove = { onAction(SettingsAction.RemoveLoTWCertificate) }, diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index 5cf419f6..c19f4f3b 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -79,6 +79,8 @@ data class SettingsState( val lotwUploadBusy: Boolean = false, /** Last certificate import outcome; shown inside the upload config dialog. */ val lotwUploadError: LoTWUploadError? = null, + /** Parser detail for FORMAT errors (e.g. the unsupported algorithm name). */ + val lotwUploadErrorDetail: String = "", /** One-click logbook upload: prepared preview awaiting confirmation. */ val logbookPreview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview? = null, val logbookUploadBusy: Boolean = false, diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index 4830ea81..28fdc047 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -423,9 +423,9 @@ class SettingsViewModel( com.rtbishop.look4sat.core.domain.repository.LoTWProblem.CERTIFICATE_FORMAT -> LoTWUploadError.FORMAT else -> LoTWUploadError.INVALID_FILE } - _uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = error) } + _uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = error, lotwUploadErrorDetail = e.detail) } } catch (_: Exception) { - _uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = LoTWUploadError.UNKNOWN) } + _uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = LoTWUploadError.UNKNOWN, lotwUploadErrorDetail = "") } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c917fed3..ac1da21c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] #noinspection UnusedVersionCatalogEntry -appVersionCode = "523" +appVersionCode = "526" #noinspection UnusedVersionCatalogEntry -appVersionName = "4.4.7-ba7opf.17" +appVersionName = "4.4.7-ba7opf.17.3" #noinspection UnusedVersionCatalogEntry compileSdk = "37" #noinspection UnusedVersionCatalogEntry