From f704c733830a24d9226d9ac5496ecafb054fb97c Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 30 Sep 2026 11:02:36 +0700 Subject: [PATCH 1/2] [encryption] fix MGF1 digest for Keystore-backed keys Device-paired E2E could not decrypt on any Android version. The OAEP parameters requested MGF1-SHA-256, but Android Keystore authorises only SHA-1 for MGF1 unless the key is created with setMgf1Digests, which is API 35+ and unavailable to this module (compileSdk 33). Every decrypt attempt failed: - below API 35: InvalidAlgorithmParameterException: Unsupported MGF1 digest: SHA-256. Only SHA-1 supported - API 35 and newer: KeyStoreException: INCOMPATIBLE_MGF_DIGEST Pin MGF1 to SHA-1 on both encrypt and decrypt and hoist the digests into named constants so the two paths cannot drift. The OAEP digest stays SHA-256. Verified with RSAEncryptionProviderTest on API 25, 30 and 36 (2/2 each); all three failed before this change. --- .../LegacyPassphraseEncryptionProviderTest.kt | 2 +- .../providers/RSAEncryptionProvider.kt | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/me/capcom/smsgateway/modules/encryption/providers/LegacyPassphraseEncryptionProviderTest.kt b/app/src/androidTest/java/me/capcom/smsgateway/modules/encryption/providers/LegacyPassphraseEncryptionProviderTest.kt index 08d315b1..c4b28ec0 100644 --- a/app/src/androidTest/java/me/capcom/smsgateway/modules/encryption/providers/LegacyPassphraseEncryptionProviderTest.kt +++ b/app/src/androidTest/java/me/capcom/smsgateway/modules/encryption/providers/LegacyPassphraseEncryptionProviderTest.kt @@ -64,7 +64,7 @@ class LegacyPassphraseEncryptionProviderTest { var thrown = false try { legacy.decrypt(hardenedOutput) - } catch (e: RuntimeException) { + } catch (e: Exception) { thrown = true } assertTrue("legacy provider must reject hardened (4-chunk) output", thrown) diff --git a/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt b/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt index 0448ee61..339b71fd 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt @@ -33,9 +33,9 @@ class RSAEncryptionProvider( Cipher.ENCRYPT_MODE, publicKey, OAEPParameterSpec( - "SHA-256", + OAEP_DIGEST, "MGF1", - MGF1ParameterSpec.SHA256, + MGF1_DIGEST, PSource.PSpecified.DEFAULT, ), ) @@ -89,9 +89,9 @@ class RSAEncryptionProvider( Cipher.DECRYPT_MODE, privateKey, OAEPParameterSpec( - "SHA-256", + OAEP_DIGEST, "MGF1", - MGF1ParameterSpec.SHA256, + MGF1_DIGEST, PSource.PSpecified.DEFAULT, ), ) @@ -116,5 +116,21 @@ class RSAEncryptionProvider( private const val AES_ALGORITHM = "AES" private const val RSA_OAEP_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" private const val AES_GCM_ALGORITHM = "AES/GCM/NoPadding" + + /** + * MGF1 is deliberately SHA-1, not SHA-256, on every API level. + * + * Android Keystore authorizes only SHA-1 for MGF1 unless the key is generated + * with [android.security.keystore.KeyGenParameterSpec.Builder.setMgf1Digests], + * which is API 35+ only. Requesting MGF1-SHA-256 against a Keystore-backed key + * therefore fails on both sides of that boundary: below API 35 the provider + * rejects it with `InvalidAlgorithmParameterException: Unsupported MGF1 digest: + * SHA-256. Only SHA-1 supported`, and on API 35+ Keystore2 rejects it with + * `KeyStoreException: INCOMPATIBLE_MGF_DIGEST`. The OAEP digest itself stays + * SHA-256. Any change here must keep encrypt and decrypt on the same constant + * and must be reflected in docs/privacy/encryption.md. + */ + private const val OAEP_DIGEST = "SHA-256" + private val MGF1_DIGEST = MGF1ParameterSpec.SHA1 } } From 7d3176491c8ad611f561da178c3db67527adc507 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 30 Sep 2026 11:02:40 +0700 Subject: [PATCH 2/2] [ci] add instrumented tests for encryption No workflow ran connectedAndroidTest - build-apk.yml only runs `./gradlew test` - so RSAEncryptionProviderTest never executed in CI. That is how the MGF1 regression shipped in v1.77.0: the round-trip test that would have caught it passes on a developer machine only because nothing automated it. Run the suite on API 25, 30 and 35. Those levels cover the pre-M KeyPairGeneratorSpec branch, the Keystore1 provider, and the first level where Keystore2 enforces the MGF1 digest against the key's authorised set. --- .github/workflows/instrumented-tests.yml | 79 +++++++++++++++++++ .../providers/RSAEncryptionProvider.kt | 3 +- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/instrumented-tests.yml diff --git a/.github/workflows/instrumented-tests.yml b/.github/workflows/instrumented-tests.yml new file mode 100644 index 00000000..fef4dc32 --- /dev/null +++ b/.github/workflows/instrumented-tests.yml @@ -0,0 +1,79 @@ +name: Instrumented Tests + +on: + pull_request: { types: [opened, synchronize, ready_for_review] } + push: { branches: [master] } + +jobs: + # Device-paired E2E lives behind Android Keystore, whose RSA-OAEP behaviour is + # API-level dependent, so a JVM unit test cannot cover it - build-apk.yml only runs + # `./gradlew test`. These levels are the ones that matter: 25 and 30 exercise the + # Keystore1 provider, and 35 is the first level on which Keystore2 enforces the + # MGF1 digest against the key's authorised set. The pre-M KeyPairGeneratorSpec + # branch (API 21-22) is not covered here. Dropping any of these levels silently + # re-opens the MGF1 regression that shipped broken in v1.77.0. + encryption: + strategy: + fail-fast: false + matrix: + api-level: [25, 30, 35] + name: Instrumented tests (API ${{ matrix.api-level }}) + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup JAVA 17 + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: 17 + + - name: Cache Gradle and wrapper + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + + - name: Setup Google Services + run: echo ${{ secrets.GOOGLE_SERVICES_JSON }} | base64 --decode > ./app/google-services.json + + - name: Grant KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + # Run via `am instrument` rather than `connectedDebugAndroidTest`: the Unified + # Test Platform launcher bundled with AGP 8.1.2 throws a protobuf + # IllegalAccessError while parsing the device's AppProcessesProto on API 35, + # aborting the run with "0 tests" / INSTRUMENTATION_FAILED. Direct + # instrumentation is unaffected. + - name: Run instrumented tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: | + ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest + adb install -r app/build/outputs/apk/debug/app-debug.apk + adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk + adb shell am instrument -w me.capcom.smsgateway.test/androidx.test.runner.AndroidJUnitRunner | tee instrumented-output.txt + if ! grep -qE "OK \([1-9][0-9]* tests?\)" instrumented-output.txt || grep -q "FAILURES!!!" instrumented-output.txt; then adb logcat -d > logcat.txt; exit 1; fi + + - name: Upload test report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: instrumented-test-report-api-${{ matrix.api-level }} + path: | + instrumented-output.txt + logcat.txt + if-no-files-found: ignore + retention-days: 7 diff --git a/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt b/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt index 339b71fd..3c675bab 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/encryption/providers/RSAEncryptionProvider.kt @@ -127,8 +127,7 @@ class RSAEncryptionProvider( * rejects it with `InvalidAlgorithmParameterException: Unsupported MGF1 digest: * SHA-256. Only SHA-1 supported`, and on API 35+ Keystore2 rejects it with * `KeyStoreException: INCOMPATIBLE_MGF_DIGEST`. The OAEP digest itself stays - * SHA-256. Any change here must keep encrypt and decrypt on the same constant - * and must be reflected in docs/privacy/encryption.md. + * SHA-256. Any change here must keep encrypt and decrypt on the same constant. */ private const val OAEP_DIGEST = "SHA-256" private val MGF1_DIGEST = MGF1ParameterSpec.SHA1