feat(keystore): vendor-gate non-AEAD updateAad

Mirror Duck Detector's OperationErrorPathProbe: real Samsung and
Xiaomi-MTK TEEs return success for updateAad on a non-AEAD operation,
while other vendors reject it with INVALID_TAG. The shim reads the same
Build identity the probe reads and answers accordingly, in both the
CryptoPrimitive default (sign/verify) and CipherPrimitive paths.

Forward hardening for the #28 detector: the prior unconditional
ServiceSpecificException throw already passes the probe on every vendor,
so this guards against stricter future probes rather than fixing a
current failure.
This commit is contained in:
Enginex0
2026-06-04 13:14:43 +01:00
parent 37e9d007de
commit 40519b94ea
@@ -8,7 +8,9 @@ import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode import android.hardware.security.keymint.PaddingMode
import android.hardware.security.keymint.Tag import android.hardware.security.keymint.Tag
import android.os.Build
import android.os.ServiceSpecificException import android.os.ServiceSpecificException
import android.os.SystemProperties
import android.system.keystore2.IKeystoreOperation import android.system.keystore2.IKeystoreOperation
import android.system.keystore2.KeyParameters import android.system.keystore2.KeyParameters
import java.security.KeyPair import java.security.KeyPair
@@ -20,9 +22,39 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
/**
* Mirrors the per-vendor TEE quirk that Duck Detector's OperationErrorPathProbe checks: real
* Samsung and Xiaomi-MTK TrustZone return success for updateAad on a non-AEAD operation, while
* every other vendor rejects it with a service-specific INVALID_TAG. The module reads the same
* device-identity fields the probe reads, so a forged software operation answers exactly as that
* vendor's real TEE would.
*/
private object VendorQuirks {
private val UPDATE_AAD_ALLOWS_SUCCESS = setOf("samsung")
private val XIAOMI_BRANDS = setOf("xiaomi", "redmi", "poco")
fun nonAeadUpdateAadSucceeds(): Boolean {
val manufacturer = Build.MANUFACTURER.lowercase()
val brand = Build.BRAND.lowercase()
if (manufacturer in UPDATE_AAD_ALLOWS_SUCCESS || brand in UPDATE_AAD_ALLOWS_SUCCESS) {
return true
}
if (manufacturer != "xiaomi" && brand !in XIAOMI_BRANDS) return false
return isMediaTek()
}
private fun isMediaTek(): Boolean {
val roHardware = SystemProperties.get("ro.hardware", "")
return roHardware.startsWith("mt") || Build.HARDWARE.startsWith("mt", ignoreCase = true)
}
}
private sealed interface CryptoPrimitive { private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) { fun updateAad(aadInput: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) // Real Samsung / Xiaomi-MTK TEEs accept updateAad on non-AEAD ops; others reject it.
if (!VendorQuirks.nonAeadUpdateAadSucceeds()) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
} }
fun update(data: ByteArray?): ByteArray? fun update(data: ByteArray?): ByteArray?
@@ -158,7 +190,12 @@ private class CipherPrimitive(
} }
override fun updateAad(aadInput: ByteArray?) { override fun updateAad(aadInput: ByteArray?) {
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) if (!isAead) {
if (!VendorQuirks.nonAeadUpdateAadSucceeds()) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
return
}
if (aadInput != null) cipher.updateAAD(aadInput) if (aadInput != null) cipher.updateAAD(aadInput)
} }