Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 | ||
|
|
ca3978888e | ||
|
|
023d7f929d | ||
|
|
bd40f4b950 | ||
|
|
23696d2f61 | ||
|
|
dfacb34cf9 | ||
|
|
06d9db443c | ||
|
|
7e87766493 | ||
|
|
f8bfa0dfd8 |
+20
-28
@@ -70,38 +70,25 @@ jobs:
|
||||
id: ver
|
||||
run: |
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "version=${ver}" >> "$GITHUB_OUTPUT"
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Rename ZIPs for release
|
||||
- name: List build artifacts
|
||||
run: |
|
||||
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
|
||||
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
|
||||
|
||||
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
|
||||
echo "::error::Could not find release or debug ZIPs in out/"
|
||||
ls -la out/ || echo "out/ does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv "$RELEASE_FILE" "out/TEESimulator-${VER}-Release.zip"
|
||||
mv "$DEBUG_FILE" "out/TEESimulator-${VER}-Debug.zip"
|
||||
|
||||
echo "Release: TEESimulator-${VER}-Release.zip ($(du -h "out/TEESimulator-${VER}-Release.zip" | cut -f1))"
|
||||
echo "Debug: TEESimulator-${VER}-Debug.zip ($(du -h "out/TEESimulator-${VER}-Debug.zip" | cut -f1))"
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
|
||||
echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-release-zip
|
||||
path: out/TEESimulator-*-Release.zip
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: out/TEESimulator-RS-*-Release.zip
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-debug-zip
|
||||
path: out/TEESimulator-*-Debug.zip
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: out/TEESimulator-RS-*-Debug.zip
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
@@ -120,27 +107,30 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read version
|
||||
id: ver
|
||||
run: |
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "version=${ver}" >> "$GITHUB_OUTPUT"
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-release-zip
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: zips
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-debug-zip
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: zips
|
||||
|
||||
- name: Extract changelog
|
||||
run: |
|
||||
ver="${VER#v}"
|
||||
awk "/^## TEESimulator v${ver}/{flag=1; next} /^## TEESimulator v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
|
||||
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
|
||||
cat /tmp/notes.md
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
@@ -148,12 +138,14 @@ jobs:
|
||||
- name: Create release
|
||||
run: |
|
||||
gh release delete "$VER" --yes 2>/dev/null || true
|
||||
RELEASE=$(ls zips/*Release*.zip | head -1)
|
||||
DEBUG=$(ls zips/*Debug*.zip | head -1)
|
||||
gh release create "$VER" \
|
||||
--title "$VER" \
|
||||
--latest \
|
||||
--notes-file /tmp/notes.md \
|
||||
"zips/TEESimulator-${VER}-Release.zip" \
|
||||
"zips/TEESimulator-${VER}-Debug.zip"
|
||||
"$RELEASE" \
|
||||
"$DEBUG"
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
out
|
||||
.gradle
|
||||
.kotlin
|
||||
app/build
|
||||
build
|
||||
native-certgen/target
|
||||
app/src/main/jniLibs
|
||||
|
||||
+9
-10
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
||||
|
||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||
val verName = "v4.5"
|
||||
val verName = "v4.8"
|
||||
|
||||
android {
|
||||
namespace = "org.matrix.TEESimulator"
|
||||
@@ -73,7 +73,7 @@ dependencies {
|
||||
|
||||
// --- Rust native cert gen build task ---
|
||||
val buildRustCertgen by tasks.registering(Exec::class) {
|
||||
group = "TEESimulator Native Build"
|
||||
group = "TEESimulator-RS Native Build"
|
||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||
|
||||
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||
@@ -108,13 +108,13 @@ androidComponents {
|
||||
// --- Define output locations and file names ---
|
||||
// Stage all files in a temporary directory inside 'build' before zipping
|
||||
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
|
||||
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
|
||||
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
|
||||
|
||||
// Task 1: Prepare all module files in the temporary build directory.
|
||||
// Using Sync ensures that stale files from previous runs are removed.
|
||||
val prepareModuleFilesTask =
|
||||
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Prepares all files for the ${variant.name} module zip."
|
||||
|
||||
if (isDebug) {
|
||||
@@ -162,8 +162,7 @@ androidComponents {
|
||||
// Use expand() for simple key-value replacement.
|
||||
expand(
|
||||
"REPLACEMEVERCODE" to gitCommitCount.toString(),
|
||||
"REPLACEMEVER" to
|
||||
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
|
||||
"REPLACEMEVER" to "$verName-$gitCommitCount",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,7 +173,7 @@ androidComponents {
|
||||
// Task 2: Zip the prepared files from the temporary directory.
|
||||
val zipTask =
|
||||
tasks.register<Zip>("zip${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Creates the flashable zip for the ${variant.name} module."
|
||||
dependsOn(prepareModuleFilesTask)
|
||||
|
||||
@@ -187,7 +186,7 @@ androidComponents {
|
||||
fun createInstallTasks(rootProvider: String, installCli: String) {
|
||||
val pushTask =
|
||||
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description =
|
||||
"Pushes the ${variant.name} module to the device for $rootProvider."
|
||||
dependsOn(zipTask)
|
||||
@@ -201,7 +200,7 @@ androidComponents {
|
||||
|
||||
val installTask =
|
||||
tasks.register<Exec>("install${rootProvider}${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider."
|
||||
dependsOn(pushTask)
|
||||
commandLine(
|
||||
@@ -214,7 +213,7 @@ androidComponents {
|
||||
}
|
||||
|
||||
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider and reboots."
|
||||
dependsOn(installTask)
|
||||
commandLine("adb", "reboot")
|
||||
|
||||
@@ -182,11 +182,40 @@ object AttestationBuilder {
|
||||
AttestationConstants.TAG_DIGEST,
|
||||
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
),
|
||||
)
|
||||
|
||||
if (params.ecCurve != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_EC_CURVE,
|
||||
ASN1Integer(params.ecCurve.toLong()),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.padding.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_PADDING,
|
||||
DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rsaPublicExponent != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_PUBLIC_EXPONENT,
|
||||
ASN1Integer(params.rsaPublicExponent.toLong()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
@@ -199,6 +228,7 @@ object AttestationBuilder {
|
||||
buildRootOfTrust(null),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
data class KeyMintAttestation(
|
||||
val keySize: Int,
|
||||
val algorithm: Int,
|
||||
val ecCurve: Int,
|
||||
val ecCurve: Int?,
|
||||
val ecCurveName: String,
|
||||
val origin: Int?,
|
||||
val blockMode: List<Int>,
|
||||
@@ -53,7 +53,7 @@ data class KeyMintAttestation(
|
||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||
ecCurveName = params.deriveEcCurveName(),
|
||||
|
||||
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||
|
||||
+2
-1
@@ -305,7 +305,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
certChain = keyData.second,
|
||||
algorithm = parsedParameters.algorithm,
|
||||
keySize = parsedParameters.keySize,
|
||||
ecCurve = parsedParameters.ecCurve,
|
||||
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||
purposes = parsedParameters.purpose,
|
||||
digests = parsedParameters.digest,
|
||||
isAttestationKey = true,
|
||||
@@ -337,6 +337,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
finalChain =
|
||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
|
||||
+155
-44
@@ -1,9 +1,11 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.EcCurve
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
@@ -17,6 +19,7 @@ import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||
@@ -33,6 +36,7 @@ import org.matrix.TEESimulator.pki.CertificateHelper
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
import org.matrix.TEESimulator.util.AndroidPermissionUtils
|
||||
|
||||
class KeyMintSecurityLevelInterceptor(
|
||||
private val original: IKeystoreSecurityLevel,
|
||||
@@ -45,6 +49,9 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val response: KeyEntryResponse,
|
||||
)
|
||||
|
||||
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
|
||||
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
|
||||
|
||||
override fun onPreTransact(
|
||||
txId: Long,
|
||||
target: IBinder,
|
||||
@@ -192,35 +199,90 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
|
||||
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
|
||||
val before = ops.size
|
||||
ops.removeIf { it.finalized }
|
||||
val afterClean = ops.size
|
||||
while (ops.size >= maxOps) {
|
||||
val oldest = ops.pollFirst() ?: break
|
||||
if (!oldest.finalized) {
|
||||
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
|
||||
oldest.abort()
|
||||
}
|
||||
}
|
||||
ops.addLast(newOp)
|
||||
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
|
||||
}
|
||||
|
||||
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
|
||||
if (securityLevel != SecurityLevel.STRONGBOX) return null
|
||||
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
|
||||
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
|
||||
timestamps.removeIf { it < cutoff }
|
||||
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
|
||||
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
|
||||
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
|
||||
}
|
||||
timestamps.addLast(System.nanoTime())
|
||||
return null
|
||||
}
|
||||
|
||||
private fun handleCreateOperation(
|
||||
txId: Long,
|
||||
callingUid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||
|
||||
// An operation must use the KEY_ID domain.
|
||||
if (keyDescriptor.domain != Domain.KEY_ID) {
|
||||
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
|
||||
|
||||
// Android framework calls createOperation with domain=APP+alias;
|
||||
// keystore2 internally resolves to KEY_ID — but software keys never
|
||||
// reach keystore2's database, so we must handle both lookup paths.
|
||||
val generatedKeyInfo = when (keyDescriptor.domain) {
|
||||
Domain.APP -> {
|
||||
val alias = keyDescriptor.alias ?: run {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val nspace = keyDescriptor.nspace
|
||||
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
||||
)
|
||||
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
}
|
||||
Domain.KEY_ID -> {
|
||||
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run {
|
||||
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
}
|
||||
|
||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
|
||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
|
||||
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val parsedParams = KeyMintAttestation(params)
|
||||
val parsedParams = KeyMintAttestation(params).let { p ->
|
||||
if (p.algorithm != 0) p
|
||||
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> Algorithm.EC
|
||||
"RSA" -> Algorithm.RSA
|
||||
else -> p.algorithm
|
||||
})
|
||||
}
|
||||
|
||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
|
||||
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
|
||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency)
|
||||
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
|
||||
pruneOpsForUid(callingUid, softwareOperation, maxOps)
|
||||
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
||||
|
||||
val response =
|
||||
@@ -260,13 +322,38 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
|
||||
}
|
||||
|
||||
if (parsedParams.serial != null || parsedParams.imei != null ||
|
||||
parsedParams.meid != null || parsedParams.secondImei != null ||
|
||||
params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION }) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting device ID attestation for uid=$callingUid")
|
||||
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||
}
|
||||
|
||||
val hasDeviceIdAttestation = params.any {
|
||||
it.tag == Tag.ATTESTATION_ID_IMEI ||
|
||||
it.tag == Tag.ATTESTATION_ID_MEID ||
|
||||
it.tag == Tag.ATTESTATION_ID_SERIAL ||
|
||||
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
|
||||
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
|
||||
}
|
||||
|
||||
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||
}
|
||||
|
||||
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
|
||||
parsedParams.algorithm == Algorithm.HMAC ||
|
||||
parsedParams.algorithm == Algorithm.TRIPLE_DES
|
||||
|
||||
if (isSymmetric) {
|
||||
SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
|
||||
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
val isAttestKeyRequest = parsedParams.isAttestKey()
|
||||
|
||||
@@ -333,7 +420,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
||||
|
||||
cleanupKeyData(keyId)
|
||||
val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
|
||||
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||
|
||||
@@ -345,14 +432,15 @@ class KeyMintSecurityLevelInterceptor(
|
||||
certChain = keyData.second.toList(),
|
||||
algorithm = parsedParams.algorithm,
|
||||
keySize = parsedParams.keySize,
|
||||
ecCurve = parsedParams.ecCurve,
|
||||
ecCurve = parsedParams.ecCurve ?: 0,
|
||||
purposes = parsedParams.purpose,
|
||||
digests = parsedParams.digest,
|
||||
isAttestationKey = isAttestKeyRequest,
|
||||
)
|
||||
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val delayMs = sampleTeeLatencyMs() - elapsedMs
|
||||
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS
|
||||
val delayMs = floor - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||
@@ -383,7 +471,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val config = CertGenConfig(
|
||||
algorithm = params.algorithm,
|
||||
keySize = params.keySize,
|
||||
ecCurve = params.ecCurve,
|
||||
ecCurve = params.ecCurve ?: 0,
|
||||
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
|
||||
attestationChallenge = params.attestationChallenge,
|
||||
purposes = params.purpose.toIntArray(),
|
||||
@@ -427,6 +515,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
|
||||
private fun buildKeyEntryResponse(
|
||||
callingUid: Int,
|
||||
chain: List<Certificate>,
|
||||
params: KeyMintAttestation,
|
||||
descriptor: KeyDescriptor,
|
||||
@@ -443,7 +532,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
keySecurityLevel = securityLevel
|
||||
key = normalizedKeyDescriptor
|
||||
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
|
||||
authorizations = params.toAuthorizations(securityLevel)
|
||||
authorizations = params.toAuthorizations(callingUid, securityLevel)
|
||||
modificationTimeMs = System.currentTimeMillis()
|
||||
}
|
||||
return KeyEntryResponse().apply {
|
||||
@@ -521,7 +610,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
secondImei = null,
|
||||
)
|
||||
|
||||
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
|
||||
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
||||
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||
|
||||
@@ -542,19 +631,28 @@ class KeyMintSecurityLevelInterceptor(
|
||||
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
||||
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
||||
private const val RESPONSE_INVALID_ARGUMENT = 20
|
||||
private const val TEE_LATENCY_FLOOR_MS = 15L
|
||||
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
|
||||
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
|
||||
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
|
||||
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
|
||||
private const val MAX_CONCURRENT_OPS_PER_UID = 15
|
||||
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
|
||||
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
|
||||
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
|
||||
// Sliding window: max hardware keygen permits per UID within the burst window
|
||||
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
|
||||
private const val BURST_WINDOW_MS = 30_000L
|
||||
private const val TEE_LATENCY_MEAN_MS = 55.0
|
||||
private const val TEE_LATENCY_STDDEV_MS = 12.0
|
||||
private const val TEE_LATENCY_FLOOR_MS = 15L
|
||||
|
||||
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
|
||||
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
||||
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
||||
|
||||
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
|
||||
Algorithm.RSA -> params.keySize <= 2048
|
||||
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
||||
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
||||
|
||||
@@ -578,11 +676,6 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleTeeLatencyMs(): Long {
|
||||
val sample = TEE_LATENCY_MEAN_MS + secureRandom.nextGaussian() * TEE_LATENCY_STDDEV_MS
|
||||
return sample.toLong().coerceAtLeast(TEE_LATENCY_FLOOR_MS)
|
||||
}
|
||||
|
||||
private val GENERATE_KEY_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
|
||||
private val IMPORT_KEY_TRANSACTION =
|
||||
@@ -605,8 +698,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
|
||||
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
||||
// Caches patched chains to prevent re-generation and signature inconsistencies
|
||||
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||
|
||||
@@ -666,7 +758,10 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
|
||||
private fun KeyMintAttestation.toAuthorizations(
|
||||
callingUid: Int,
|
||||
securityLevel: Int,
|
||||
): Array<Authorization> {
|
||||
val authList = mutableListOf<Authorization>()
|
||||
|
||||
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
|
||||
@@ -681,19 +776,35 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
|
||||
}
|
||||
}
|
||||
|
||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||
if (this.ecCurve != null) {
|
||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||
}
|
||||
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
||||
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||
|
||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
|
||||
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||
authList.add(
|
||||
createAuth(
|
||||
Tag.ORIGIN,
|
||||
KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED),
|
||||
)
|
||||
)
|
||||
if (this.rsaPublicExponent != null) {
|
||||
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
|
||||
}
|
||||
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
|
||||
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
|
||||
|
||||
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch)))
|
||||
}
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch)))
|
||||
}
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
|
||||
}
|
||||
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
|
||||
authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
|
||||
|
||||
return authList.toTypedArray()
|
||||
}
|
||||
|
||||
+85
-22
@@ -6,6 +6,7 @@ import android.hardware.security.keymint.Digest
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.os.RemoteException
|
||||
import android.os.ServiceSpecificException
|
||||
import android.system.keystore2.IKeystoreOperation
|
||||
import java.security.KeyPair
|
||||
import java.security.Signature
|
||||
@@ -17,10 +18,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||
private sealed interface CryptoPrimitive {
|
||||
fun updateAad(aadInput: ByteArray?) {}
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
}
|
||||
|
||||
@@ -34,16 +34,17 @@ private object JcaAlgorithmMapper {
|
||||
Digest.SHA_2_512 -> "SHA512"
|
||||
else -> "NONE"
|
||||
}
|
||||
val keyAlgo =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
return when (params.algorithm) {
|
||||
Algorithm.EC -> "${digest}withECDSA"
|
||||
Algorithm.RSA -> {
|
||||
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||
}
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
)
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
}
|
||||
|
||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||
@@ -60,16 +61,18 @@ private object JcaAlgorithmMapper {
|
||||
when (params.blockMode.firstOrNull()) {
|
||||
BlockMode.ECB -> "ECB"
|
||||
BlockMode.CBC -> "CBC"
|
||||
BlockMode.CTR -> "CTR"
|
||||
BlockMode.GCM -> "GCM"
|
||||
else -> "ECB" // Default for RSA
|
||||
else -> "ECB"
|
||||
}
|
||||
val padding =
|
||||
when (params.padding.firstOrNull()) {
|
||||
PaddingMode.NONE -> "NoPadding"
|
||||
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||
else -> "NoPadding" // Default for GCM
|
||||
else -> "NoPadding"
|
||||
}
|
||||
return "$keyAlgo/$blockMode/$padding"
|
||||
}
|
||||
@@ -142,17 +145,17 @@ private class CipherPrimitive(
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
||||
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
||||
*/
|
||||
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
||||
// This now holds the specific strategy object (Signer, Verifier, etc.)
|
||||
class SoftwareOperation(
|
||||
private val txId: Long,
|
||||
keyPair: KeyPair,
|
||||
params: KeyMintAttestation,
|
||||
private val latencyFloorMs: Long = 0L,
|
||||
) {
|
||||
private val primitive: CryptoPrimitive
|
||||
@Volatile var finalized = false
|
||||
private set
|
||||
|
||||
init {
|
||||
// The "Strategy" pattern: choose the implementation based on the purpose.
|
||||
// For simplicity, we only consider the first purpose listed.
|
||||
val purpose = params.purpose.firstOrNull()
|
||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||
@@ -168,9 +171,35 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkActive() {
|
||||
if (finalized) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputLength(data: ByteArray?) {
|
||||
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAad(aadInput: ByteArray?) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(aadInput)
|
||||
primitive.updateAad(aadInput)
|
||||
}
|
||||
|
||||
fun update(data: ByteArray?): ByteArray? {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
return primitive.update(data)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
throw e
|
||||
@@ -178,38 +207,72 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
}
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||
val result = primitive.finish(data, signature)
|
||||
if (latencyFloorMs > 0) {
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val delayMs = latencyFloorMs - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
}
|
||||
finalized = true
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||
return result
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||
// Re-throw the exception so the binder can report it to the client.
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
finalized = true
|
||||
primitive.abort()
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
private object KeystoreErrorCodes {
|
||||
val tooMuchData: Int by lazy {
|
||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||
}
|
||||
|
||||
val invalidOperationHandle: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||
}
|
||||
|
||||
private fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||
runCatching {
|
||||
Class.forName(className).getField(fieldName).getInt(null)
|
||||
}.getOrElse {
|
||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
/** The Binder interface for our [SoftwareOperation]. */
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
operation.updateAad(aadInput)
|
||||
}
|
||||
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
return operation.update(input)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
}
|
||||
|
||||
@@ -239,10 +239,10 @@ object CertificateGenerator {
|
||||
)
|
||||
|
||||
val signerAlgorithm =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "SHA256withECDSA"
|
||||
Algorithm.RSA -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||
when (signingKeyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||
"RSA" -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
||||
}
|
||||
val contentSigner =
|
||||
JcaContentSignerBuilder(signerAlgorithm)
|
||||
|
||||
@@ -105,7 +105,7 @@ object NativeCertGen {
|
||||
}
|
||||
|
||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||
"EC" -> "EC"
|
||||
"EC", "ECDSA" -> "EC"
|
||||
"RSA" -> "RSA"
|
||||
else -> certs[0].publicKey.algorithm
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
object AndroidPermissionUtils {
|
||||
|
||||
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||
private fun getGlobalContext(): Context? {
|
||||
return try {
|
||||
// 1. Get the hidden ActivityThread class via reflection
|
||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||
|
||||
// 2. Invoke the static currentActivityThread() method
|
||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||
currentActivityThreadMethod.isAccessible = true
|
||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||
|
||||
if (activityThread == null) {
|
||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. Try to get the application context
|
||||
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||
getApplicationMethod.isAccessible = true
|
||||
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||
|
||||
if (application != null) return application
|
||||
|
||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||
getSystemContextMethod.isAccessible = true
|
||||
getSystemContextMethod.invoke(activityThread) as? Context
|
||||
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core permission check.
|
||||
*/
|
||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||
val context = getGlobalContext() ?: run {
|
||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||
return false
|
||||
}
|
||||
|
||||
val result = context.checkPermission(permission, -1, uid)
|
||||
return result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||
}
|
||||
|
||||
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||
}
|
||||
|
||||
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||
}
|
||||
|
||||
fun hasDumpPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.DUMP")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,24 @@
|
||||
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||
|
||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
||||
|
||||
- **PADDING encoding** — Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
|
||||
- **Operation error-path conformance** — Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
|
||||
- **updateAad support** — Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
|
||||
- **Algorithm inference** — `createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.6: Rebrand & Detection Fix
|
||||
|
||||
- **RTT normalization rework** — Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
|
||||
- **Cross-algorithm attestation** — Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
|
||||
- **Device ID attestation** — Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
|
||||
- **Rebrand to TEESimulator-RS** — Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
|
||||
- **CI streamlined** — Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.5: Detection Hardening
|
||||
|
||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ fi
|
||||
|
||||
# --- Version Info ---
|
||||
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
|
||||
ui_print "- Installing TEESimulator $VERSION"
|
||||
ui_print "- Installing TEESimulator-RS $VERSION"
|
||||
ui_print ""
|
||||
|
||||
# --- Architecture Handling ---
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
id=tricky_store
|
||||
name=TEESimulator
|
||||
name=TEESimulator-RS
|
||||
version=${REPLACEMEVER}
|
||||
versionCode=${REPLACEMEVERCODE}
|
||||
author=JingMatrix, Enginex0
|
||||
description=Software simulation for Android hardware-backed key pairs with key attestation
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
|
||||
|
||||
+1
-1
@@ -235,7 +235,7 @@ print_summary() {
|
||||
|
||||
# --- Main ---
|
||||
echo ""
|
||||
bold "TEESimulator package pipeline"
|
||||
bold "TEESimulator-RS package pipeline"
|
||||
echo ""
|
||||
|
||||
[[ "$BUILD_RUST" == true ]] && build_rust
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "TEESimulator"
|
||||
rootProject.name = "TEESimulator-RS"
|
||||
|
||||
include(":stub")
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package android.os;
|
||||
|
||||
public class ServiceSpecificException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user