Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
258a65ba59 | ||
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 | ||
|
|
ca3978888e | ||
|
|
023d7f929d | ||
|
|
bd40f4b950 | ||
|
|
23696d2f61 | ||
|
|
dfacb34cf9 | ||
|
|
06d9db443c | ||
|
|
7e87766493 | ||
|
|
f8bfa0dfd8 | ||
|
|
90ff59e0aa | ||
|
|
8bdf0d59fa | ||
|
|
6ab09f4889 | ||
|
|
f4559bcd19 | ||
|
|
3b5043a1bb | ||
|
|
8001a8678a |
+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
|
||||
|
||||
@@ -242,6 +242,9 @@ boot=device_default
|
||||
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
|
||||
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
|
||||
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
|
||||
- **[MhmRdd](https://github.com/MhmRdd)** — AOSP compliance improvements via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157), including authorize_create enforcement, attestation extension alignment, and binder transaction filtering
|
||||
- **[fatalcoder524](https://github.com/fatalcoder524)** — a real contributor and collaborator on this project
|
||||
- **[huguangares](https://github.com/huguangares)** — collaborator and tester
|
||||
|
||||
---
|
||||
|
||||
|
||||
+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.3"
|
||||
val verName = "v5.0"
|
||||
|
||||
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")
|
||||
|
||||
@@ -235,19 +235,21 @@ class BinderInterceptor : public BBinder {
|
||||
struct RegistrationEntry {
|
||||
wp<IBinder> target;
|
||||
sp<IBinder> callback_interface;
|
||||
std::vector<uint32_t> filtered_codes;
|
||||
};
|
||||
|
||||
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
||||
mutable std::shared_mutex registry_mutex_;
|
||||
std::map<wp<IBinder>, RegistrationEntry> registry_;
|
||||
|
||||
public:
|
||||
BinderInterceptor() = default;
|
||||
|
||||
// Checks if a specific Binder instance is currently registered for interception
|
||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
||||
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||
std::shared_lock lock(registry_mutex_);
|
||||
return registry_.find(target) != registry_.end();
|
||||
auto it = registry_.find(target);
|
||||
if (it == registry_.end()) return false;
|
||||
const auto &codes = it->second.filtered_codes;
|
||||
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||
}
|
||||
|
||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||
@@ -393,7 +395,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
// This is safe because we are holding a strong reference.
|
||||
wp<BBinder> wp_target = target_binder_ptr;
|
||||
|
||||
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
|
||||
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
|
||||
info.transaction_code = txn_data->code;
|
||||
info.target_binder = wp_target; // Assign the valid weak pointer
|
||||
hijack = true;
|
||||
@@ -538,18 +540,29 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
|
||||
if (data.readStrongBinder(&callback) != OK || !callback)
|
||||
return BAD_VALUE;
|
||||
|
||||
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
|
||||
if (target->localBinder() == nullptr) {
|
||||
LOGE("Cannot intercept remote binder proxies.");
|
||||
return BAD_TYPE;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> codes;
|
||||
int32_t code_count = 0;
|
||||
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||
codes.reserve(code_count);
|
||||
for (int32_t i = 0; i < code_count; i++) {
|
||||
uint32_t c = 0;
|
||||
if (data.readUint32(&c) == OK) codes.push_back(c);
|
||||
}
|
||||
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
|
||||
} else {
|
||||
LOGI("Interceptor registered for binder %p (all codes)", target.get());
|
||||
}
|
||||
|
||||
wp<IBinder> weak_target = target;
|
||||
|
||||
std::unique_lock lock(registry_mutex_);
|
||||
registry_[weak_target] = {weak_target, callback};
|
||||
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||
|
||||
LOGI("Interceptor registered for binder %p", target.get());
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import org.bouncycastle.asn1.ASN1Boolean
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1Enumerated
|
||||
@@ -127,33 +130,59 @@ object AttestationBuilder {
|
||||
return properties
|
||||
}
|
||||
|
||||
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
||||
private fun buildKeyDescription(
|
||||
params: KeyMintAttestation,
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): ASN1Sequence {
|
||||
val creationTime = System.currentTimeMillis()
|
||||
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||
|
||||
val uniqueId =
|
||||
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
|
||||
val fields =
|
||||
arrayOf(
|
||||
ASN1Integer(
|
||||
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
||||
), // attestationVersion
|
||||
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
||||
ASN1Integer(
|
||||
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
||||
), // keymasterVersion
|
||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||
DEROctetString(ByteArray(0)), // uniqueId
|
||||
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
|
||||
ASN1Enumerated(securityLevel),
|
||||
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
|
||||
ASN1Enumerated(securityLevel),
|
||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
|
||||
DEROctetString(uniqueId),
|
||||
softwareEnforced,
|
||||
teeEnforced,
|
||||
)
|
||||
return DERSequence(fields)
|
||||
}
|
||||
|
||||
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
||||
val temporalCounter = creationTimeMs / 2592000000L
|
||||
val message =
|
||||
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||
.putLong(temporalCounter)
|
||||
.put(aaidDer)
|
||||
.put(0x00)
|
||||
.array()
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||
return mac.doFinal(message).copyOf(16)
|
||||
}
|
||||
|
||||
private val hbk: ByteArray by lazy {
|
||||
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||
if (file.exists() && file.length() == 32L) {
|
||||
file.readBytes()
|
||||
} else {
|
||||
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||
private fun buildTeeEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
@@ -182,23 +211,110 @@ 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()),
|
||||
),
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.blockMode.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_BLOCK_MODE,
|
||||
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||
|
||||
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||
DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.earlyBootOnly == true && attestVersion >= 4) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.noAuthRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.allowWhileOnBody == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ORIGIN,
|
||||
ASN1Integer(0L),
|
||||
), // KeyOrigin.GENERATED
|
||||
ASN1Integer((params.origin ?: 0).toLong()),
|
||||
),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||
buildRootOfTrust(null),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||
@@ -295,20 +411,32 @@ object AttestationBuilder {
|
||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||
* Keystore.
|
||||
*/
|
||||
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
||||
val list =
|
||||
mutableListOf<ASN1Encodable>(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(System.currentTimeMillis()),
|
||||
),
|
||||
private fun buildSoftwareEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
creationTimeMs: Long = System.currentTimeMillis(),
|
||||
): DERSequence {
|
||||
val list = mutableListOf<ASN1Encodable>()
|
||||
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(creationTimeMs),
|
||||
)
|
||||
)
|
||||
|
||||
if (params.attestationChallenge != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||
createApplicationId(uid),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
@@ -318,7 +446,34 @@ object AttestationBuilder {
|
||||
)
|
||||
)
|
||||
}
|
||||
return DERSequence(list.toTypedArray())
|
||||
|
||||
params.activeDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||
)
|
||||
}
|
||||
params.originationExpireDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
|
||||
)
|
||||
}
|
||||
params.usageExpireDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
|
||||
)
|
||||
}
|
||||
params.usageCountLimit?.let {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
|
||||
)
|
||||
}
|
||||
if (params.unlockedDeviceRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,6 +501,11 @@ object AttestationBuilder {
|
||||
*/
|
||||
@Throws(Throwable::class)
|
||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||
val appUid = uid % 100000
|
||||
if (appUid == 0 || appUid == 1000) {
|
||||
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
|
||||
}
|
||||
|
||||
val pm =
|
||||
ConfigurationManager.getPackageManager()
|
||||
?: throw IllegalStateException("PackageManager not found!")
|
||||
@@ -353,12 +513,11 @@ object AttestationBuilder {
|
||||
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
val packageInfoList = mutableListOf<DERSequence>()
|
||||
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||
val signatureDigests = mutableSetOf<Digest>()
|
||||
|
||||
// Process all packages associated with the UID in a single loop.
|
||||
val userId = uid / 100000
|
||||
packages.forEach { packageName ->
|
||||
val userId = uid / 100000
|
||||
val packageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(
|
||||
@@ -371,34 +530,36 @@ object AttestationBuilder {
|
||||
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||
}
|
||||
|
||||
// Add package information (name and version code) to our list.
|
||||
packageInfoList.add(
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(packageInfo.longVersionCode),
|
||||
)
|
||||
)
|
||||
)
|
||||
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||
|
||||
// Collect unique signature digests from the signing history.
|
||||
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||
val digest = sha256.digest(signature.toByteArray())
|
||||
signatureDigests.add(Digest(digest))
|
||||
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||
}
|
||||
}
|
||||
|
||||
// The application ID is a sequence of two sets:
|
||||
// 1. A set of package information (name and version).
|
||||
// 2. A set of SHA-256 digests of the signing certificates.
|
||||
return buildApplicationIdDer(packageInfoList, signatureDigests)
|
||||
}
|
||||
|
||||
private fun buildApplicationIdDer(
|
||||
packages: List<Pair<String, Long>>,
|
||||
digests: Set<Digest>,
|
||||
): DEROctetString {
|
||||
val packageInfoList =
|
||||
packages.map { (name, version) ->
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(version),
|
||||
)
|
||||
)
|
||||
}
|
||||
val applicationIdSequence =
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DERSet(packageInfoList.toTypedArray()),
|
||||
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
|
||||
return DEROctetString(applicationIdSequence.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ object AttestationConstants {
|
||||
|
||||
// --- Key Lifetime and Usage Control ---
|
||||
const val TAG_ROLLBACK_RESISTANCE = 303
|
||||
const val TAG_EARLY_BOOT_ONLY = 305
|
||||
const val TAG_ACTIVE_DATETIME = 400
|
||||
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
||||
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
||||
const val TAG_MAX_BOOT_LEVEL = 403
|
||||
const val TAG_MAX_USES_PER_BOOT = 404
|
||||
const val TAG_USAGE_COUNT_LIMIT = 405
|
||||
|
||||
@@ -56,6 +58,10 @@ object AttestationConstants {
|
||||
const val TAG_NO_AUTH_REQUIRED = 503
|
||||
const val TAG_USER_AUTH_TYPE = 504
|
||||
const val TAG_AUTH_TIMEOUT = 505
|
||||
const val TAG_ALLOW_WHILE_ON_BODY = 506
|
||||
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
|
||||
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
|
||||
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
|
||||
|
||||
// --- Attestation and Application Info ---
|
||||
const val TAG_APPLICATION_ID = 601
|
||||
@@ -89,5 +95,5 @@ object AttestationConstants {
|
||||
|
||||
// --- Other Constants ---
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
@@ -41,19 +41,35 @@ data class KeyMintAttestation(
|
||||
val manufacturer: ByteArray?,
|
||||
val model: ByteArray?,
|
||||
val secondImei: ByteArray?,
|
||||
val activeDateTime: Date?,
|
||||
val originationExpireDateTime: Date?,
|
||||
val usageExpireDateTime: Date?,
|
||||
val usageCountLimit: Int?,
|
||||
val callerNonce: Boolean?,
|
||||
val unlockedDeviceRequired: Boolean?,
|
||||
val includeUniqueId: Boolean?,
|
||||
val rollbackResistance: Boolean?,
|
||||
val earlyBootOnly: Boolean?,
|
||||
val allowWhileOnBody: Boolean?,
|
||||
val trustedUserPresenceRequired: Boolean?,
|
||||
val trustedConfirmationRequired: Boolean?,
|
||||
val noAuthRequired: Boolean?,
|
||||
val maxUsesPerBoot: Int?,
|
||||
val maxBootLevel: Int?,
|
||||
val minMacLength: Int?,
|
||||
val rsaOaepMgfDigest: List<Int>,
|
||||
) {
|
||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||
constructor(
|
||||
params: Array<KeyParameter>
|
||||
) : this(
|
||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||
|
||||
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
||||
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)]
|
||||
@@ -100,6 +116,23 @@ data class KeyMintAttestation(
|
||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||
) {
|
||||
// Log all parsed parameters for debugging purposes.
|
||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||
@@ -156,6 +189,21 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
||||
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.digest }
|
||||
|
||||
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
|
||||
if (this.any { it.tag == tag }) true else null
|
||||
|
||||
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||
return when (curveId) {
|
||||
EcCurve.P_224 -> 224
|
||||
EcCurve.P_256 -> 256
|
||||
EcCurve.P_384 -> 384
|
||||
EcCurve.P_521 -> 521
|
||||
EcCurve.CURVE_25519 -> 256
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
||||
* to KEY_SIZE (field=Integer).
|
||||
|
||||
@@ -360,7 +360,29 @@ object ConfigurationManager {
|
||||
return iPackageManager
|
||||
}
|
||||
|
||||
/** Retrieves the package names associated with a UID. */
|
||||
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
||||
return try {
|
||||
val callerCtx =
|
||||
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||
val selfCtx =
|
||||
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||
val userId = uid / 100000
|
||||
return getPackagesForUid(uid).any { pkg ->
|
||||
try {
|
||||
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPackagesForUid(uid: Int): Array<String> {
|
||||
return uidToPackagesCache.getOrPut(uid) {
|
||||
try {
|
||||
|
||||
@@ -293,15 +293,21 @@ abstract class BinderInterceptor : Binder() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
||||
fun register(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor,
|
||||
filteredCodes: IntArray = intArrayOf(),
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
data.writeInt(filteredCodes.size)
|
||||
for (code in filteredCodes) data.writeInt(code)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
SystemLogger.info("Registered interceptor for target: $target")
|
||||
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||
} finally {
|
||||
|
||||
+3
-2
@@ -68,11 +68,12 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
||||
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||
|
||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
||||
keystoreService = service
|
||||
SystemLogger.info("Registering interceptor for service: $serviceName")
|
||||
register(backdoor, service, this)
|
||||
register(backdoor, service, this, interceptedCodes)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorReady(service, backdoor)
|
||||
}
|
||||
|
||||
+58
-1
@@ -1,11 +1,16 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import android.system.keystore2.Authorization
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
|
||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||
|
||||
@@ -18,6 +23,7 @@ object InterceptorUtils {
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeInt(EX_SERVICE_SPECIFIC)
|
||||
writeString(null)
|
||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||
writeInt(errorCode)
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
@@ -119,6 +125,57 @@ object InterceptorUtils {
|
||||
|
||||
/** Checks if a reply parcel contains an exception without consuming it. */
|
||||
fun hasException(reply: Parcel): Boolean {
|
||||
return runCatching { reply.readException() }.exceptionOrNull() != null
|
||||
val exception = runCatching { reply.readException() }.exceptionOrNull()
|
||||
if (exception != null) reply.setDataPosition(0)
|
||||
return exception != null
|
||||
}
|
||||
|
||||
fun createServiceSpecificErrorReply(
|
||||
errorCode: Int
|
||||
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||
val parcel =
|
||||
Parcel.obtain().apply {
|
||||
writeException(android.os.ServiceSpecificException(errorCode))
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
|
||||
fun patchAuthorizations(
|
||||
authorizations: Array<Authorization>?,
|
||||
callingUid: Int,
|
||||
): Array<Authorization>? {
|
||||
if (authorizations == null) return null
|
||||
|
||||
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
|
||||
return authorizations
|
||||
.map { auth ->
|
||||
val replacement =
|
||||
when (auth.keyParameter.tag) {
|
||||
Tag.OS_PATCHLEVEL ->
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||
Tag.VENDOR_PATCHLEVEL ->
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||
else null
|
||||
Tag.BOOT_PATCHLEVEL ->
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||
else -> null
|
||||
}
|
||||
if (replacement != null) {
|
||||
Authorization().apply {
|
||||
keyParameter =
|
||||
KeyParameter().apply {
|
||||
tag = auth.keyParameter.tag
|
||||
value = KeyParameterValue.integer(replacement)
|
||||
}
|
||||
securityLevel = auth.securityLevel
|
||||
}
|
||||
} else {
|
||||
auth
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
+131
-25
@@ -5,11 +5,13 @@ import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.Domain
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
@@ -44,6 +46,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (Build.VERSION.SDK_INT >= 34)
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||
else null
|
||||
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
stubBinderClass.declaredFields
|
||||
@@ -54,10 +58,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||
}
|
||||
|
||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||
|
||||
override val interceptedCodes: IntArray by lazy {
|
||||
listOfNotNull(
|
||||
GET_KEY_ENTRY_TRANSACTION,
|
||||
DELETE_KEY_TRANSACTION,
|
||||
UPDATE_SUBCOMPONENT_TRANSACTION,
|
||||
LIST_ENTRIES_TRANSACTION,
|
||||
LIST_ENTRIES_BATCHED_TRANSACTION,
|
||||
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
||||
)
|
||||
.toIntArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called once the main service is hooked. It proceeds to find and hook the
|
||||
* security level sub-services (e.g., TEE, StrongBox).
|
||||
@@ -74,7 +94,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
register(backdoor, tee.asBinder(), interceptor)
|
||||
register(
|
||||
backdoor,
|
||||
tee.asBinder(),
|
||||
interceptor,
|
||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
@@ -86,7 +111,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||
register(backdoor, strongbox.asBinder(), interceptor)
|
||||
register(
|
||||
backdoor,
|
||||
strongbox.asBinder(),
|
||||
interceptor,
|
||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
@@ -102,7 +132,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
else TransactionResult.Continue
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
|
||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||
@@ -145,30 +180,48 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
if (descriptor.alias != null) {
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
} else {
|
||||
SystemLogger.info(
|
||||
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
|
||||
)
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
||||
val keyId =
|
||||
if (descriptor.alias != null) {
|
||||
KeyIdentifier(callingUid, descriptor.alias)
|
||||
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid, descriptor.nspace
|
||||
)?.let { info ->
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||
?.key
|
||||
}
|
||||
} else null
|
||||
|
||||
if (keyId != null) {
|
||||
val isSoftwareKey =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
if (isSoftwareKey) {
|
||||
deletedSoftwareKeys.add(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
}
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val response =
|
||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
?: return TransactionResult.Continue
|
||||
if (descriptor.alias == null) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
if (response == null) {
|
||||
if (deletedSoftwareKeys.remove(keyId)) {
|
||||
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||
}
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||
@@ -206,7 +259,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||
return TransactionResult.SkipTransaction
|
||||
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
return runCatching {
|
||||
val hardwareCount = reply.readInt()
|
||||
val softwareCount =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||
it.uid == callingUid
|
||||
}
|
||||
val totalCount = hardwareCount + softwareCount
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeNoException()
|
||||
writeInt(totalCount)
|
||||
}
|
||||
TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
return runCatching {
|
||||
@@ -241,6 +313,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
|
||||
if (userUpdatedKeys.remove(keyId)) {
|
||||
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
val authorizations = response.metadata.authorizations
|
||||
val parsedParameters =
|
||||
KeyMintAttestation(
|
||||
@@ -258,6 +335,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
||||
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
if (parsedParameters.isAttestKey()) {
|
||||
SystemLogger.warning(
|
||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||
@@ -278,11 +360,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
.getOrThrow()
|
||||
|
||||
keyDescriptor.nspace = SecureRandom().nextLong()
|
||||
response.metadata.key.nspace = keyDescriptor.nspace
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||
keyData.first,
|
||||
keyDescriptor.nspace,
|
||||
response,
|
||||
parsedParameters,
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||
|
||||
@@ -294,7 +378,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,
|
||||
@@ -326,10 +410,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
finalChain =
|
||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
@@ -347,9 +437,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
val generatedKeyInfo =
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
when (descriptor.domain) {
|
||||
Domain.KEY_ID ->
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid, descriptor.nspace
|
||||
)
|
||||
Domain.APP ->
|
||||
descriptor.alias?.let {
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||
val metadata = generatedKeyInfo.response.metadata
|
||||
|
||||
+17
@@ -431,6 +431,23 @@ private data class LegacyKeygenParameters(
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
secondImei = null,
|
||||
activeDateTime = null,
|
||||
originationExpireDateTime = null,
|
||||
usageExpireDateTime = null,
|
||||
usageCountLimit = null,
|
||||
callerNonce = null,
|
||||
unlockedDeviceRequired = null,
|
||||
includeUniqueId = null,
|
||||
rollbackResistance = null,
|
||||
earlyBootOnly = null,
|
||||
allowWhileOnBody = null,
|
||||
trustedUserPresenceRequired = null,
|
||||
trustedConfirmationRequired = null,
|
||||
noAuthRequired = null,
|
||||
maxUsesPerBoot = null,
|
||||
maxBootLevel = null,
|
||||
minMacLength = null,
|
||||
rsaOaepMgfDigest = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ object ListEntriesHandler {
|
||||
startPastAlias: String?,
|
||||
): List<KeyDescriptor> {
|
||||
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
|
||||
.map { keyId ->
|
||||
KeyDescriptor().apply {
|
||||
this.domain = Domain.APP
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.Tag
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
|
||||
object AuthorizeCreate {
|
||||
|
||||
fun check(
|
||||
keyParams: KeyMintAttestation?,
|
||||
opParams: KeyMintAttestation,
|
||||
rawOpParams: Array<KeyParameter>? = null,
|
||||
): Int? {
|
||||
if (keyParams == null) return null
|
||||
return checkPurpose(keyParams, opParams)
|
||||
?: checkAlgorithmPurpose(keyParams, opParams)
|
||||
?: checkTemporalValidity(keyParams, opParams)
|
||||
?: checkCallerNonce(keyParams, rawOpParams)
|
||||
}
|
||||
|
||||
private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
|
||||
val requestedPurpose = opParams.purpose.firstOrNull() ?: return null
|
||||
if (requestedPurpose == KeyPurpose.WRAP_KEY)
|
||||
return KeystoreErrorCodes.incompatiblePurpose
|
||||
if (requestedPurpose !in keyParams.purpose)
|
||||
return KeystoreErrorCodes.incompatiblePurpose
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
|
||||
val purpose = opParams.purpose.firstOrNull() ?: return null
|
||||
return when (keyParams.algorithm) {
|
||||
Algorithm.EC -> when (purpose) {
|
||||
KeyPurpose.ENCRYPT, KeyPurpose.DECRYPT -> KeystoreErrorCodes.unsupportedPurpose
|
||||
KeyPurpose.AGREE_KEY -> null
|
||||
else -> null
|
||||
}
|
||||
Algorithm.RSA -> when (purpose) {
|
||||
KeyPurpose.AGREE_KEY -> KeystoreErrorCodes.unsupportedPurpose
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkTemporalValidity(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
|
||||
val now = System.currentTimeMillis()
|
||||
val purpose = opParams.purpose.firstOrNull()
|
||||
|
||||
keyParams.activeDateTime?.let { activeDate ->
|
||||
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
|
||||
}
|
||||
|
||||
keyParams.originationExpireDateTime?.let { expireDate ->
|
||||
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
|
||||
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||
}
|
||||
}
|
||||
|
||||
keyParams.usageExpireDateTime?.let { expireDate ->
|
||||
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
|
||||
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkCallerNonce(keyParams: KeyMintAttestation, rawOpParams: Array<KeyParameter>?): Int? {
|
||||
if (keyParams.callerNonce == true) return null
|
||||
val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true
|
||||
if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited
|
||||
return null
|
||||
}
|
||||
}
|
||||
+229
-36
@@ -1,8 +1,13 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.BlockMode
|
||||
import android.hardware.security.keymint.EcCurve
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
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
|
||||
@@ -16,6 +21,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
|
||||
@@ -32,6 +38,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,
|
||||
@@ -42,8 +49,12 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val keyPair: KeyPair,
|
||||
val nspace: Long,
|
||||
val response: KeyEntryResponse,
|
||||
val keyParams: KeyMintAttestation? = null,
|
||||
)
|
||||
|
||||
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
|
||||
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
|
||||
|
||||
override fun onPreTransact(
|
||||
txId: Long,
|
||||
target: IBinder,
|
||||
@@ -124,6 +135,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
GeneratedKeyPersistence.delete(keyId)
|
||||
}
|
||||
attestationKeys.remove(keyId)
|
||||
importedKeys.add(keyId)
|
||||
} else if (code == CREATE_OPERATION_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
@@ -150,7 +162,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val backdoor = getBackdoor(target)
|
||||
if (backdoor != null) {
|
||||
val interceptor = OperationInterceptor(operation, backdoor)
|
||||
register(backdoor, operationBinder, interceptor)
|
||||
register(backdoor, operationBinder, interceptor, OperationInterceptor.INTERCEPTED_CODES)
|
||||
interceptedOperations[operationBinder] = interceptor
|
||||
} else {
|
||||
SystemLogger.error(
|
||||
@@ -191,41 +203,113 @@ 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) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
val nspace = keyDescriptor.nspace
|
||||
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
||||
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
||||
)
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
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)
|
||||
AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode ->
|
||||
SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode")
|
||||
return InterceptorUtils.createErrorReply(errorCode)
|
||||
}
|
||||
|
||||
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 =
|
||||
CreateOperationResponse().apply {
|
||||
iOperation = operationBinder
|
||||
operationChallenge = null
|
||||
softwareOperation.iv?.let { iv ->
|
||||
parameters = KeyParameters().apply {
|
||||
keyParameter = arrayOf(
|
||||
KeyParameter().apply {
|
||||
tag = Tag.NONCE
|
||||
value = KeyParameterValue.blob(iv)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
@@ -259,13 +343,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()
|
||||
|
||||
@@ -316,6 +425,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
keyId: KeyIdentifier,
|
||||
isAttestKeyRequest: Boolean,
|
||||
): TransactionResult {
|
||||
val startNs = System.nanoTime()
|
||||
keyDescriptor.nspace = secureRandom.nextLong()
|
||||
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
||||
|
||||
@@ -331,8 +441,8 @@ class KeyMintSecurityLevelInterceptor(
|
||||
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
||||
|
||||
cleanupKeyData(keyId)
|
||||
val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
||||
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response, parsedParams)
|
||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||
|
||||
GeneratedKeyPersistence.save(
|
||||
@@ -343,12 +453,17 @@ 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 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)
|
||||
}
|
||||
|
||||
@@ -377,7 +492,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(),
|
||||
@@ -421,16 +536,25 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
|
||||
private fun buildKeyEntryResponse(
|
||||
callingUid: Int,
|
||||
chain: List<Certificate>,
|
||||
params: KeyMintAttestation,
|
||||
descriptor: KeyDescriptor,
|
||||
): KeyEntryResponse {
|
||||
val normalizedKeyDescriptor =
|
||||
KeyDescriptor().apply {
|
||||
domain = Domain.KEY_ID
|
||||
nspace = descriptor.nspace
|
||||
alias = null
|
||||
blob = null
|
||||
}
|
||||
val metadata =
|
||||
KeyMetadata().apply {
|
||||
keySecurityLevel = securityLevel
|
||||
key = descriptor
|
||||
key = normalizedKeyDescriptor
|
||||
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
|
||||
authorizations = params.toAuthorizations(securityLevel)
|
||||
authorizations = params.toAuthorizations(callingUid, securityLevel)
|
||||
modificationTimeMs = System.currentTimeMillis()
|
||||
}
|
||||
return KeyEntryResponse().apply {
|
||||
this.metadata = metadata
|
||||
@@ -505,10 +629,27 @@ class KeyMintSecurityLevelInterceptor(
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
secondImei = null,
|
||||
activeDateTime = null,
|
||||
originationExpireDateTime = null,
|
||||
usageExpireDateTime = null,
|
||||
usageCountLimit = null,
|
||||
callerNonce = null,
|
||||
unlockedDeviceRequired = null,
|
||||
includeUniqueId = null,
|
||||
rollbackResistance = null,
|
||||
earlyBootOnly = null,
|
||||
allowWhileOnBody = null,
|
||||
trustedUserPresenceRequired = null,
|
||||
trustedConfirmationRequired = null,
|
||||
noAuthRequired = null,
|
||||
maxUsesPerBoot = null,
|
||||
maxBootLevel = null,
|
||||
minMacLength = null,
|
||||
rsaOaepMgfDigest = emptyList(),
|
||||
)
|
||||
|
||||
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
||||
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response, attestation)
|
||||
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||
|
||||
SystemLogger.debug("Restored persisted key: $keyId")
|
||||
@@ -528,16 +669,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 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) }
|
||||
|
||||
@@ -571,6 +724,9 @@ class KeyMintSecurityLevelInterceptor(
|
||||
"createOperation",
|
||||
)
|
||||
|
||||
val INTERCEPTED_CODES =
|
||||
intArrayOf(GENERATE_KEY_TRANSACTION, IMPORT_KEY_TRANSACTION, CREATE_OPERATION_TRANSACTION)
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
IKeystoreSecurityLevel.Stub::class
|
||||
.java
|
||||
@@ -583,9 +739,9 @@ 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()
|
||||
val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||
|
||||
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
||||
@@ -614,6 +770,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
if (attestationKeys.remove(keyId)) {
|
||||
SystemLogger.debug("Remove cached attestaion key ${keyId}")
|
||||
}
|
||||
importedKeys.remove(keyId)
|
||||
}
|
||||
|
||||
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
|
||||
@@ -638,13 +795,17 @@ class KeyMintSecurityLevelInterceptor(
|
||||
generatedKeys.clear()
|
||||
patchedChains.clear()
|
||||
attestationKeys.clear()
|
||||
importedKeys.clear()
|
||||
GeneratedKeyPersistence.deleteAll()
|
||||
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -659,13 +820,45 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
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.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) }
|
||||
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||
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)))
|
||||
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(
|
||||
Authorization().apply {
|
||||
this.keyParameter =
|
||||
KeyParameter().apply {
|
||||
this.tag = Tag.USER_ID
|
||||
this.value = KeyParameterValue.integer(callingUid / 100000)
|
||||
}
|
||||
this.securityLevel = SecurityLevel.SOFTWARE
|
||||
}
|
||||
)
|
||||
|
||||
return authList.toTypedArray()
|
||||
}
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ class OperationInterceptor(
|
||||
private val ABORT_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
||||
|
||||
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
IKeystoreOperation.Stub::class
|
||||
.java
|
||||
|
||||
+176
-41
@@ -5,7 +5,7 @@ import android.hardware.security.keymint.BlockMode
|
||||
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
|
||||
@@ -15,16 +15,16 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
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?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||
}
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
fun getIv(): ByteArray? = null
|
||||
}
|
||||
|
||||
// Helper object to map KeyMint constants to JCA algorithm strings.
|
||||
private object JcaAlgorithmMapper {
|
||||
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
|
||||
val digest =
|
||||
@@ -34,16 +34,18 @@ private object JcaAlgorithmMapper {
|
||||
Digest.SHA_2_512 -> "SHA512"
|
||||
else -> "NONE"
|
||||
}
|
||||
val keyAlgo =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
)
|
||||
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"
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
else ->
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||
"Unsupported signature algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||
@@ -52,30 +54,32 @@ private object JcaAlgorithmMapper {
|
||||
Algorithm.RSA -> "RSA"
|
||||
Algorithm.AES -> "AES"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported cipher algorithm: ${params.algorithm}"
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||
"Unsupported cipher algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
val blockMode =
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete implementation for Signing.
|
||||
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||
private val signature: Signature =
|
||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||
@@ -95,7 +99,6 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
// Concrete implementation for Verification.
|
||||
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||
private val signature: Signature =
|
||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||
@@ -109,50 +112,60 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data != null) update(data)
|
||||
if (signature == null) throw SignatureException("Signature to verify is null")
|
||||
if (signature == null) {
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
|
||||
}
|
||||
if (!this.signature.verify(signature)) {
|
||||
// Throwing an exception is how Keystore signals verification failure.
|
||||
throw SignatureException("Signature verification failed")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
|
||||
}
|
||||
// A successful verification returns no data.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
// Concrete implementation for Encryption/Decryption.
|
||||
private class CipherPrimitive(
|
||||
keyPair: KeyPair,
|
||||
params: KeyMintAttestation,
|
||||
private val opMode: Int,
|
||||
) : CryptoPrimitive {
|
||||
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
||||
private val cipher: Cipher =
|
||||
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
|
||||
init(opMode, key)
|
||||
}
|
||||
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||
if (aadInput != null) cipher.updateAAD(aadInput)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? =
|
||||
if (data != null) cipher.update(data) else null
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
||||
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||
|
||||
override fun getIv(): ByteArray? = if (isAead) cipher.iv else null
|
||||
|
||||
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
|
||||
|
||||
val iv: ByteArray?
|
||||
get() = primitive.getIv()
|
||||
|
||||
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.")
|
||||
@@ -164,52 +177,174 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
|
||||
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
|
||||
else ->
|
||||
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.unsupportedPurpose,
|
||||
"Unsupported operation purpose: $purpose",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
throw mapToServiceSpecificException(e)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
throw mapToServiceSpecificException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
finalized = true
|
||||
primitive.abort()
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||
}
|
||||
|
||||
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
|
||||
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
|
||||
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
|
||||
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
|
||||
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
|
||||
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
internal 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)
|
||||
}
|
||||
|
||||
val invalidTag: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
|
||||
}
|
||||
|
||||
val verificationFailed: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
|
||||
}
|
||||
|
||||
val invalidArgument: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
|
||||
}
|
||||
|
||||
val invalidInputLength: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
|
||||
}
|
||||
|
||||
val incompatibleKey: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
|
||||
}
|
||||
|
||||
val incompatiblePurpose: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
|
||||
}
|
||||
|
||||
val unsupportedPurpose: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
|
||||
}
|
||||
|
||||
val incompatibleAlgorithm: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
|
||||
}
|
||||
|
||||
val keyNotYetValid: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
|
||||
}
|
||||
|
||||
val keyExpired: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
|
||||
}
|
||||
|
||||
val callerNonceProhibited: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
|
||||
}
|
||||
|
||||
val unknownError: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
|
||||
}
|
||||
|
||||
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)
|
||||
@Synchronized
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
operation.updateAad(aadInput)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
return operation.update(input)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
@Synchronized
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
@Synchronized
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import java.util.Date
|
||||
@@ -36,6 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
*/
|
||||
object CertificateGenerator {
|
||||
|
||||
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||
|
||||
/**
|
||||
* Generates a software-based cryptographic key pair.
|
||||
*
|
||||
@@ -49,7 +50,10 @@ object CertificateGenerator {
|
||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||
Algorithm.RSA ->
|
||||
"RSA" to
|
||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
||||
RSAKeyGenParameterSpec(
|
||||
params.keySize,
|
||||
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||
)
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported algorithm: ${params.algorithm}"
|
||||
@@ -88,11 +92,9 @@ object CertificateGenerator {
|
||||
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||
)
|
||||
|
||||
return runCatching {
|
||||
return try {
|
||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||
|
||||
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
||||
// Otherwise, fall back to the root key from the keybox.
|
||||
val (signingKey, issuer) =
|
||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
||||
@@ -101,20 +103,20 @@ object CertificateGenerator {
|
||||
keybox.keyPair to getIssuerFromKeybox(keybox)
|
||||
}
|
||||
|
||||
// Build the new leaf certificate with the simulated attestation.
|
||||
val leafCert =
|
||||
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||
|
||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
||||
// chain.
|
||||
if (attestKeyAlias != null) {
|
||||
listOf(leafCert)
|
||||
} else {
|
||||
listOf(leafCert) + keybox.certificates
|
||||
}
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate certificate chain.", e)
|
||||
null
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +130,7 @@ object CertificateGenerator {
|
||||
params: KeyMintAttestation,
|
||||
securityLevel: Int,
|
||||
): Pair<KeyPair, List<Certificate>>? {
|
||||
return runCatching {
|
||||
return try {
|
||||
SystemLogger.info(
|
||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
||||
)
|
||||
@@ -144,11 +146,12 @@ object CertificateGenerator {
|
||||
"Successfully generated new certificate chain for alias: '$alias'."
|
||||
)
|
||||
Pair(newKeyPair, chain)
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||
null
|
||||
}
|
||||
.onFailure {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||
@@ -163,7 +166,10 @@ object CertificateGenerator {
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||
}
|
||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
|
||||
?: throw android.os.ServiceSpecificException(
|
||||
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
||||
)
|
||||
}
|
||||
|
||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||
@@ -214,16 +220,15 @@ object CertificateGenerator {
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||
|
||||
val builder =
|
||||
JcaX509v3CertificateBuilder(
|
||||
issuer,
|
||||
params.certificateSerial ?: BigInteger.ONE,
|
||||
params.certificateNotBefore ?: Date(),
|
||||
params.certificateNotAfter ?: leafNotAfter,
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
subjectKeyPair.public,
|
||||
)
|
||||
@@ -239,10 +244,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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.locks.LockSupport
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.max
|
||||
|
||||
object TeeLatencySimulator {
|
||||
|
||||
private val rng = SecureRandom()
|
||||
|
||||
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
|
||||
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
|
||||
|
||||
@Volatile private var firstCall = true
|
||||
|
||||
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
|
||||
val elapsedMs = elapsedNanos / 1_000_000.0
|
||||
val targetMs = sampleTotalDelay(algorithm)
|
||||
val remainingMs = targetMs - elapsedMs
|
||||
|
||||
if (remainingMs > 1.0) {
|
||||
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleTotalDelay(algorithm: Int): Double {
|
||||
val base = sampleBaseCryptoDelay(algorithm)
|
||||
val transit = sampleExponential(2.5)
|
||||
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
|
||||
|
||||
var cold = 0.0
|
||||
if (firstCall) {
|
||||
firstCall = false
|
||||
cold = coldPenaltyMs
|
||||
}
|
||||
|
||||
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||
}
|
||||
|
||||
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
||||
val (mu, sigma) =
|
||||
when (algorithm) {
|
||||
Algorithm.EC -> ln(60.0) to 0.08
|
||||
Algorithm.RSA -> ln(70.0) to 0.08
|
||||
Algorithm.AES -> ln(35.0) to 0.10
|
||||
else -> ln(40.0) to 0.10
|
||||
}
|
||||
return sampleLogNormal(mu, sigma)
|
||||
}
|
||||
|
||||
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
|
||||
return exp(mu + sigma * rng.nextGaussian())
|
||||
}
|
||||
|
||||
private fun sampleExponential(mean: Double): Double {
|
||||
var u = rng.nextDouble()
|
||||
while (u == 0.0) u = rng.nextDouble()
|
||||
return -mean * ln(u)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,105 @@
|
||||
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||
|
||||
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||
|
||||
### Attestation Extension Alignment
|
||||
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||
- AAID gated on attestation challenge presence
|
||||
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||
|
||||
### Binder Infrastructure
|
||||
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||
- getNumberOfEntries includes software-generated key count
|
||||
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||
|
||||
### Software Operation AOSP Conformance
|
||||
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||
|
||||
### Security and Configuration
|
||||
- SELinux permission checks via /proc/pid/attr/current
|
||||
- Per-UID permission verification through IPackageManager.checkPermission
|
||||
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||
- nspace consistency fix in attest-key override path
|
||||
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||
|
||||
### Preserved from v4.8
|
||||
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||
- LRU operation pruning per security level
|
||||
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||
- Native Rust cert generation with BouncyCastle fallback
|
||||
- Key persistence across reboots
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||
|
||||
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||
|
||||
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||
|
||||
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
||||
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
||||
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
||||
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
||||
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
||||
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
||||
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
|
||||
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
|
||||
- **Delete cleanup scope** — `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.4: AOSP Conformance
|
||||
|
||||
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
|
||||
- **Key enumeration** — Corrected list_past_alias pagination order to match AOSP database.rs semantics.
|
||||
- **KeyMetadata fields** — Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
|
||||
- **Parcel handling** — hasException() preserves reply position for downstream consumers.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.3: Performance & Reliability
|
||||
|
||||
- **Debug log gating** — `SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
|
||||
|
||||
+8
-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 ---
|
||||
@@ -91,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
|
||||
ui_print "- Adding default target scope"
|
||||
install_file "target.txt" "$CONFIG_DIR"
|
||||
fi
|
||||
|
||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||
|
||||
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||
ui_print "- Generating device-unique hardware-bound key seed"
|
||||
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
|
||||
fi
|
||||
|
||||
+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
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "v4.3",
|
||||
"versionCode": 107,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.3/TEESimulator-v4.3-Release.zip",
|
||||
"version": "v4.5",
|
||||
"versionCode": 111,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||
}
|
||||
|
||||
+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")
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ public interface IPackageManager {
|
||||
|
||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||
|
||||
int checkPermission(String permName, String pkgName, int userId);
|
||||
|
||||
class Stub {
|
||||
public static IPackageManager asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package android.os;
|
||||
|
||||
public class SELinux {
|
||||
public static boolean checkSELinuxAccess(
|
||||
String scon, String tcon, String tclass, String perm) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static boolean isDeclared(String name) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static String[] listServices() {
|
||||
throw new UnsupportedOperationException("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