Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/instrumented-tests.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
capcom6 marked this conversation as resolved.
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ class RSAEncryptionProvider(
Cipher.ENCRYPT_MODE,
publicKey,
OAEPParameterSpec(
"SHA-256",
OAEP_DIGEST,
"MGF1",
MGF1ParameterSpec.SHA256,
MGF1_DIGEST,
PSource.PSpecified.DEFAULT,
),
)
Expand Down Expand Up @@ -89,9 +89,9 @@ class RSAEncryptionProvider(
Cipher.DECRYPT_MODE,
privateKey,
OAEPParameterSpec(
"SHA-256",
OAEP_DIGEST,
"MGF1",
MGF1ParameterSpec.SHA256,
MGF1_DIGEST,
PSource.PSpecified.DEFAULT,
),
)
Expand All @@ -116,5 +116,20 @@ 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.
*/
private const val OAEP_DIGEST = "SHA-256"
private val MGF1_DIGEST = MGF1ParameterSpec.SHA1
}
}
Loading