diff --git a/.gitignore b/.gitignore index f5ca0232..020d8110 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ .externalNativeBuild .cxx /*.jks +/.kotlin/sessions/kotlin-compiler-*.salive diff --git a/app/src/main/aidl/io/github/vvb2060/keyattestation/keystore/IAndroidKeyStore.aidl b/app/src/main/aidl/io/github/vvb2060/keyattestation/keystore/IAndroidKeyStore.aidl index e9d9c3d4..790e01a6 100644 --- a/app/src/main/aidl/io/github/vvb2060/keyattestation/keystore/IAndroidKeyStore.aidl +++ b/app/src/main/aidl/io/github/vvb2060/keyattestation/keystore/IAndroidKeyStore.aidl @@ -17,5 +17,7 @@ interface IAndroidKeyStore { String getRkpHostname(); boolean canRemoteProvisioning(boolean useStrongBox); RpcHardwareInfo getHardwareInfo(boolean useStrongBox, out DeviceInfo deviceInfo); + byte[] getDiceChain(boolean useStrongBox); byte[] checkRemoteProvisioning(boolean useStrongBox); + String getVbmetaDigest(); } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationApplicationId.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationApplicationId.java index de865f9f..8656d4d8 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationApplicationId.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationApplicationId.java @@ -30,6 +30,7 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateParsingException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class AttestationApplicationId implements java.lang.Comparable { @@ -137,6 +138,16 @@ public boolean equals(Object o) { && (0 == compareTo((AttestationApplicationId) o)); } + @Override + public int hashCode() { + // byte[] has identity hashCode, so combine by content to stay consistent with equals(). + int sigHash = 0; + for (byte[] digest : signatureDigests) { + sigHash = 31 * sigHash + Arrays.hashCode(digest); + } + return 31 * packageInfos.hashCode() + sigHash; + } + private List parseAttestationPackageInfos(ASN1Encodable asn1Encodable) throws CertificateParsingException { if (!(asn1Encodable instanceof ASN1Set set)) { diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationPackageInfo.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationPackageInfo.java index b2a1b24d..d97ab436 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationPackageInfo.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationPackageInfo.java @@ -18,6 +18,7 @@ import org.bouncycastle.asn1.ASN1Sequence; import java.security.cert.CertificateParsingException; +import java.util.Objects; public class AttestationPackageInfo implements java.lang.Comparable { private static final int PACKAGE_NAME_INDEX = 0; @@ -70,4 +71,9 @@ public boolean equals(Object o) { return (o instanceof AttestationPackageInfo) && (0 == compareTo((AttestationPackageInfo) o)); } + + @Override + public int hashCode() { + return Objects.hash(packageName, version); + } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java index 10b66de5..31c2786e 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java @@ -19,6 +19,7 @@ import static com.google.common.base.Functions.forMap; import static com.google.common.collect.Collections2.transform; +import android.os.Build; import android.security.keystore.KeyProperties; import android.util.Log; @@ -32,7 +33,6 @@ import org.bouncycastle.asn1.ASN1TaggedObject; import java.security.cert.CertificateParsingException; -import java.text.DateFormat; import java.util.Collection; import java.util.Date; import java.util.List; @@ -591,6 +591,22 @@ public static String ecCurveAsString(Integer ecCurve) { }; } + public static String osVersionToString(int osVersion) { + int major = osVersion / 10000; + int minor = (osVersion / 100) % 100; + int subMinor = osVersion % 100; + return "Android " + major + "." + minor + "." + subMinor + " (" + osVersion + ")"; + } + + public static String patchLevelToString(int patchLevel) { + var s = Integer.toString(patchLevel); + return switch (s.length()) { + case 6 -> s.substring(0, 4) + "-" + s.substring(4, 6); + case 8 -> s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6, 8); + default -> s; + }; + } + public Integer getSecurityLevel() { return securityLevel; } @@ -711,6 +727,39 @@ public Integer getOsPatchLevel() { return osPatchLevel; } + public boolean isPatchLevelOutdated() { + try { + int device = Integer.parseInt( + Build.VERSION.SECURITY_PATCH.substring(0, 7).replace("-", "")); + return isOlder(osPatchLevel, device) + || isOlder(vendorPatchLevel, device) + || isOlder(bootPatchLevel, device); + } catch (RuntimeException e) { + return false; + } + } + + // Normalize YYYYMMDD to YYYYMM + private static int toMonth(int patchLevel) { + return patchLevel > 999999 ? patchLevel / 100 : patchLevel; + } + + private static boolean isOlder(Integer patchLevel, int device) { + return patchLevel != null && toMonth(patchLevel) < device; + } + + public String getOldestPatchLevel() { + Integer oldest = null; + for (var patchLevel : new Integer[]{osPatchLevel, vendorPatchLevel, bootPatchLevel}) { + if (patchLevel == null) continue; + int month = toMonth(patchLevel); + if (oldest == null || month < oldest) oldest = month; + } + if (oldest == null) return null; + var s = oldest.toString(); + return s.length() == 6 ? s.substring(0, 4) + "-" + s.substring(4) : s; + } + public AttestationApplicationId getAttestationApplicationId() { return attestationApplicationId; } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CborUtils.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CborUtils.java index 83867805..36aa4e3e 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CborUtils.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CborUtils.java @@ -16,8 +16,14 @@ package io.github.vvb2060.keyattestation.attestation; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import co.nstant.in.cbor.CborDecoder; -import co.nstant.in.cbor.CborEncoder; import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.Array; import co.nstant.in.cbor.model.ByteString; @@ -30,15 +36,6 @@ import co.nstant.in.cbor.model.UnicodeString; import co.nstant.in.cbor.model.UnsignedInteger; -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - class CborUtils { public static Number toNumber(long l) { diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatAttestation.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatAttestation.java index 7ec68ebd..e2bffd4e 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatAttestation.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatAttestation.java @@ -18,13 +18,6 @@ import android.util.Log; -import co.nstant.in.cbor.CborDecoder; -import co.nstant.in.cbor.CborException; -import co.nstant.in.cbor.model.DataItem; -import co.nstant.in.cbor.model.Map; -import co.nstant.in.cbor.model.Number; -import co.nstant.in.cbor.model.UnicodeString; - import org.bouncycastle.asn1.ASN1Encodable; import java.security.cert.CertificateParsingException; @@ -32,6 +25,12 @@ import java.util.Arrays; import java.util.List; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.Number; +import co.nstant.in.cbor.model.UnicodeString; + public class EatAttestation extends Attestation { static final String TAG = "EatAttestation"; final Map extension; diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java index 72ac0b24..98d5a799 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java @@ -67,11 +67,13 @@ public byte[] getRecordHash() { @Override public String toString() { + String hash = recordHash == null ? "unknown" : BaseEncoding.base16().lowerCase().encode(recordHash); + return super.toString() + "\n\nExtension type: " + getClass().getSimpleName() + "\nID attestation: " + idAttest + "\nChallenge: " + challenge + "\nIntegrity status: " + knoxIntegrity + - "\nAttestation record hash: " + BaseEncoding.base16().lowerCase().encode(recordHash); + "\nAttestation record hash: " + hash; } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java index 80aeb42e..91528069 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java @@ -22,6 +22,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import io.github.vvb2060.keyattestation.AppApplication; import io.github.vvb2060.keyattestation.R; @@ -40,9 +41,9 @@ public enum DataSource { private static final String PREFS_NAME = "revocation_prefs"; private static final String KEY_PUBLISH_TIME = "last_publish_time"; - private static JSONObject data = null; - private static Date publishTime = null; - private static DataSource currentSource = DataSource.BUNDLED; + private static volatile JSONObject data = null; + private static volatile Date publishTime = null; + private static volatile DataSource currentSource = DataSource.BUNDLED; private static final ExecutorService asyncExecutor = Executors.newSingleThreadExecutor(); private static final ExecutorService networkExecutor = Executors.newSingleThreadExecutor(); @@ -89,14 +90,15 @@ private static void saveToCache(JSONObject fullJson) { } } - private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime) { + private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime, AtomicReference connRef) { HttpURLConnection connection = null; try { URL url = new URL(statusUrl); connection = (HttpURLConnection) url.openConnection(); + connRef.set(connection); connection.setRequestMethod("GET"); - connection.setConnectTimeout(3000); - connection.setReadTimeout(3000); + connection.setConnectTimeout(10_000); + connection.setReadTimeout(20_000); connection.setRequestProperty("User-Agent", "KeyAttestation"); double rand = Math.round(Math.random() * 1000.0) / 1000.0; @@ -132,12 +134,15 @@ private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime) } private static NetworkResult fetchNetworkWithTimeout(String url, long cachedTime) { - Future future = networkExecutor.submit(() -> fetchFromNetwork(url, cachedTime)); + var connRef = new AtomicReference(); + Future future = networkExecutor.submit(() -> fetchFromNetwork(url, cachedTime, connRef)); try { return future.get(3, TimeUnit.SECONDS); } catch (Exception e) { Log.w(TAG, "Network fetch dropped gracefully (Hard 3-second DNS/Connection Timeout)"); future.cancel(true); + var connection = connRef.get(); + if (connection != null) connection.disconnect(); return null; } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootOfTrust.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootOfTrust.java index 43331f58..87467d12 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootOfTrust.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootOfTrust.java @@ -50,9 +50,12 @@ public RootOfTrust(ASN1Encodable asn1Encodable) throws CertificateParsingExcepti deviceLocked = Asn1Utils.getBooleanFromAsn1(sequence.getObjectAt(DEVICE_LOCKED_INDEX)); verifiedBootState = Asn1Utils.getIntegerFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_STATE_INDEX)); - if (sequence.size() == 3) verifiedBootHash = null; - else verifiedBootHash = - Asn1Utils.getByteArrayFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_HASH_INDEX)); + if (sequence.size() == 3) { + verifiedBootHash = null; + } else { + var hash = Asn1Utils.getByteArrayFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_HASH_INDEX)); + verifiedBootHash = hash.length == 0 ? null : hash; + } } RootOfTrust(byte[] verifiedBootKey, boolean deviceLocked, @@ -94,6 +97,20 @@ public byte[] getVerifiedBootHash() { return verifiedBootHash; } + private static boolean isAllZero(byte[] data) { + for (var b : data) { + if (b != 0) return false; + } + return true; + } + + public boolean isVerifiedBootKeySuspicious() { + if (verifiedBootKey == null || !isAllZero(verifiedBootKey)) { + return false; + } + return deviceLocked || verifiedBootState == KM_VERIFIED_BOOT_VERIFIED; + } + @Override public String toString() { StringBuilder sb = new StringBuilder() diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootPublicKey.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootPublicKey.java index 2661b2af..6eb96495 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootPublicKey.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootPublicKey.java @@ -40,6 +40,9 @@ public enum Status { ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+\ NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ=="""; + // New P-384 KeyMint attestation root (CN=Key Attestation CA1) + // RKP devices use it exclusively + // https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate_rotation private static final String GOOGLE_RKP_ROOT_PUBLIC_KEY = """ MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEI9ojcU7fPlsFCjxy6IRqzgeOoK0b+YsV\ 9FPQywiyw8EQRTkJ9u3qwfnI4DGoSLlBqClTXJfgfCcZvs60FikNMHnu4fkRzObf\ diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/BootStateViewHolder.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/BootStateViewHolder.kt index 5b9203d6..ff6f0cd1 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/BootStateViewHolder.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/BootStateViewHolder.kt @@ -4,9 +4,9 @@ import android.content.res.ColorStateList import android.view.View import androidx.core.view.isVisible import io.github.vvb2060.keyattestation.R -import io.github.vvb2060.keyattestation.repository.AttestationData import io.github.vvb2060.keyattestation.attestation.RootOfTrust import io.github.vvb2060.keyattestation.databinding.HomeHeaderBinding +import io.github.vvb2060.keyattestation.repository.AttestationData import rikka.core.res.resolveColor class BootStateViewHolder(itemView: View, binding: HomeHeaderBinding) : diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt index c0bff9b7..d950c8fb 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt @@ -16,7 +16,6 @@ import io.github.vvb2060.keyattestation.attestation.CertificateInfo import io.github.vvb2060.keyattestation.attestation.RootPublicKey import io.github.vvb2060.keyattestation.databinding.HomeCommonItemBinding import rikka.core.res.resolveColorStateList -import rikka.recyclerview.BaseViewHolder.Creator import java.util.Date import javax.security.auth.x500.X500Principal @@ -24,6 +23,18 @@ open class CommonItemViewHolder(itemView: View, binding: HomeCommonItemBindin HomeViewHolder(itemView, binding) { companion object { + private fun copyRootOfTrustHex(context: Context, rootOfTrust: String): Boolean { + if (!rootOfTrust.contains("verifiedBootKey:")) return false + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboardManager.setPrimaryClip(ClipData.newPlainText("rootOfTrust", rootOfTrust)) + Toast.makeText( + context, + context.getString(R.string.copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + return true + } + val SIMPLE_CREATOR = Creator> { inflater, parent -> val binding = HomeCommonItemBinding.inflate(inflater, parent, false) object : CommonItemViewHolder>(binding.root, binding) { @@ -37,7 +48,11 @@ open class CommonItemViewHolder(itemView: View, binding: HomeCommonItemBindin override fun onBind() { binding.title.text = data.first - binding.summary.text = data.second + if (data.second.isEmpty()) { + binding.summary.setText(R.string.empty) + } else { + binding.summary.text = data.second + } } } } @@ -97,29 +112,11 @@ open class CommonItemViewHolder(itemView: View, binding: HomeCommonItemBindin init { this.binding.apply { root.setOnClickListener { - if (data.data.contains("verifiedBootHash:")) { - val lines = data.data.lines() - for (line in lines) { - if (line.startsWith("verifiedBootHash:")) { - val verifiedBootHash = line.substringAfter(":").trim() - val clipboardManager = - context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboardManager.setPrimaryClip( - ClipData.newPlainText( - "verifiedBootHash", - verifiedBootHash - ) - ) - Toast.makeText( - context, - context.getString(R.string.copied_verifiedBootHash_to_clipboard), - Toast.LENGTH_SHORT - ).show() - } - } - } listener.onCommonDataClick(data) } + root.setOnLongClickListener { + copyRootOfTrustHex(context, data.data) + } icon.isVisible = false } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/Data.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/Data.kt index 8696b27b..f94e8e9a 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/Data.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/Data.kt @@ -30,7 +30,8 @@ class HeaderData( override val title: Int, override val description: Int, val icon: Int, - val color: Int + val color: Int, + vararg val formatArgs: Any ) : Data() class AuthorizationItemData( diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/ErrorViewHolder.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/ErrorViewHolder.kt index ab9e3ab0..7b14396e 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/ErrorViewHolder.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/ErrorViewHolder.kt @@ -23,14 +23,16 @@ class ErrorViewHolder(itemView: View, binding: HomeErrorBinding) : HomeViewHolde val sb = StringBuilder() sb.append(context.getString(data.descriptionResId)).append("

") - sb.append(context.getString(R.string.error_message_subtitle)).append("
") - sb.append("") - var tr = data.cause - while (tr != null) { - sb.append(tr).append("
") - tr = tr.cause - } - sb.append("
") + if (data.hasDetailedMessage) { + sb.append(context.getString(R.string.error_message_subtitle)).append("
") + sb.append("") + var tr = data.cause + while (tr != null) { + sb.append(tr).append("
") + tr = tr.cause + } + sb.append("
") + } text1.text = sb.toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HeaderViewHolder.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HeaderViewHolder.kt index b48ed86f..2a17ff0e 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HeaderViewHolder.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HeaderViewHolder.kt @@ -4,7 +4,6 @@ import android.view.View import androidx.core.view.isVisible import io.github.vvb2060.keyattestation.databinding.HomeHeaderBinding import rikka.core.res.resolveColorStateList -import rikka.recyclerview.BaseViewHolder.Creator class HeaderViewHolder(itemView: View, binding: HomeHeaderBinding) : HomeViewHolder(itemView, binding) { @@ -23,7 +22,7 @@ class HeaderViewHolder(itemView: View, binding: HomeHeaderBinding) : HomeViewHol icon.setImageDrawable(context.getDrawable(data.icon)) title.setText(data.title) if (data.description != 0) { - summary.setText(data.description) + summary.text = context.getString(data.description, *data.formatArgs) summary.isVisible = true } else { summary.isVisible = false diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt index 0c961898..667b3ffb 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt @@ -1,5 +1,7 @@ package io.github.vvb2060.keyattestation.home +import android.hardware.security.keymint.RpcHardwareInfo +import android.os.Build import android.util.Base64 import android.util.Pair import com.google.common.io.BaseEncoding @@ -9,6 +11,7 @@ import io.github.vvb2060.keyattestation.lang.AttestationException import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE_RKP import io.github.vvb2060.keyattestation.repository.AttestationData import io.github.vvb2060.keyattestation.repository.BaseData +import io.github.vvb2060.keyattestation.repository.KeyboxData import io.github.vvb2060.keyattestation.repository.RemoteProvisioningData import rikka.recyclerview.IdBasedRecyclerViewAdapter @@ -30,68 +33,105 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { when (baseData.status) { RootPublicKey.Status.NULL -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.error_remote_key_provisioning, - 0, - R.drawable.ic_error_outline_24, - rikka.material.R.attr.colorInactive), ID_CERT_STATUS) + R.string.error_remote_key_provisioning, + 0, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorInactive), ID_CERT_STATUS) } RootPublicKey.Status.FAILED -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.cert_chain_not_trusted, - R.string.cert_chain_not_trusted_summary, - R.drawable.ic_error_outline_24, - rikka.material.R.attr.colorAlert), ID_CERT_STATUS) + R.string.cert_chain_not_trusted, + R.string.cert_chain_not_trusted_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorAlert), ID_CERT_STATUS) } RootPublicKey.Status.UNKNOWN -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.unknown_root_cert, - R.string.unknown_root_cert_summary, - R.drawable.ic_error_outline_24, - rikka.material.R.attr.colorWarning), ID_CERT_STATUS) + R.string.unknown_root_cert, + R.string.unknown_root_cert_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorWarning), ID_CERT_STATUS) } RootPublicKey.Status.AOSP -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.aosp_root_cert, - R.string.aosp_root_cert_summary, - R.drawable.ic_error_outline_24, - rikka.material.R.attr.colorWarning), ID_CERT_STATUS) + R.string.aosp_root_cert, + R.string.aosp_root_cert_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorWarning), ID_CERT_STATUS) } RootPublicKey.Status.GOOGLE -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.google_root_cert, - R.string.google_root_cert_summary, - R.drawable.ic_trustworthy_24, - rikka.material.R.attr.colorSafe), ID_CERT_STATUS) + R.string.google_root_cert, + R.string.google_root_cert_summary, + R.drawable.ic_trustworthy_24, + rikka.material.R.attr.colorSafe), ID_CERT_STATUS) } RootPublicKey.Status.GOOGLE_RKP -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.google_root_cert_rkp, - R.string.google_root_cert_rkp_summary, - R.drawable.ic_trustworthy_24, - rikka.material.R.attr.colorSafe), ID_CERT_STATUS) + R.string.google_root_cert_rkp, + R.string.google_root_cert_rkp_summary, + R.drawable.ic_trustworthy_24, + rikka.material.R.attr.colorSafe), ID_CERT_STATUS) } RootPublicKey.Status.KNOX -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.knox_root_cert, - R.string.knox_root_cert_summary, - R.drawable.ic_trustworthy_24, - rikka.material.R.attr.colorSafe), ID_CERT_STATUS) + R.string.knox_root_cert, + R.string.knox_root_cert_summary, + R.drawable.ic_trustworthy_24, + rikka.material.R.attr.colorSafe), ID_CERT_STATUS) } RootPublicKey.Status.OEM -> { addItem(HeaderViewHolder.CREATOR, HeaderData( - R.string.oem_root_cert, - R.string.oem_root_cert_summary, - R.drawable.ic_trustworthy_24, - rikka.material.R.attr.colorSafe), ID_CERT_STATUS) + R.string.oem_root_cert, + R.string.oem_root_cert_summary, + R.drawable.ic_trustworthy_24, + rikka.material.R.attr.colorSafe), ID_CERT_STATUS) } } var id = ID_CERT_INFO_START addItem(SubtitleViewHolder.CREATOR, CommonData( - R.string.cert_chain, - R.string.cert_chain_description), id++) - baseData.certs.forEach { certInfo -> - addItem(CommonItemViewHolder.CERT_INFO_CREATOR, certInfo, id++) + R.string.cert_chain, + R.string.cert_chain_description), id++) + + val certsList = baseData.certs + if (!certsList.isNullOrEmpty()) { + val containsEc = certsList.any { it.cert?.publicKey?.algorithm?.equals("EC", ignoreCase = true) == true || it.cert?.publicKey?.algorithm?.equals("ECDSA", ignoreCase = true) == true } + + // Track unique serial numbers to block duplicates + val seenSerials = mutableSetOf() + + certsList.forEach { certInfo -> + val currentAlgo = certInfo.cert?.publicKey?.algorithm + val serialNumber = certInfo.cert?.serialNumber?.toString() + + // Identify the Root Certificate (A root is always self-signed: Subject == Issuer) + val subjectStr = certInfo.cert?.subjectDN?.toString() + val issuerStr = certInfo.cert?.issuerDN?.toString() + val isRoot = (subjectStr != null && subjectStr == issuerStr) + + // Display only if serial number hasn't been seen before + if (serialNumber == null || !seenSerials.contains(serialNumber)) { + + if (serialNumber != null) { + seenSerials.add(serialNumber) + } + + if (containsEc) { + // 1. Show EC certificates and Root certificate (even if Root is RSA) + if (currentAlgo.equals("EC", ignoreCase = true) || currentAlgo.equals("ECDSA", ignoreCase = true) || isRoot) { + addItem(CommonItemViewHolder.CERT_INFO_CREATOR, certInfo, id++) + } + } else { + // 2. Show RSA certificates (duplicates are blocked) + addItem(CommonItemViewHolder.CERT_INFO_CREATOR, certInfo, id++) + } + } + } + } else { + baseData.certs.forEach { certInfo -> + addItem(CommonItemViewHolder.CERT_INFO_CREATOR, certInfo, id++) + } } // Add revocation list information with source status @@ -102,16 +142,16 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { val dateStr = publishTime?.let { io.github.vvb2060.keyattestation.attestation.AuthorizationList.formatDate(it) } ?: "" - + val locales = androidx.appcompat.app.AppCompatDelegate.getApplicationLocales() val context = if (!locales.isEmpty) { - val config = android.content.res.Configuration(app.resources.configuration) + val config = android.content.res.Configuration(app.resources.configuration) config.setLocale(locales[0]) app.createConfigurationContext(config) } else { app - } - + } + val statusLine = when (source) { RevocationList.DataSource.NETWORK_INITIAL -> context.getString(R.string.revocation_status_initial) RevocationList.DataSource.NETWORK_UPDATE -> context.getString(R.string.revocation_status_new_fetch) @@ -128,13 +168,14 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { }.trim().ifEmpty { null } addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( - R.string.revocation_list_publish_time, - R.string.revocation_list_description, - dateDisplay), ID_REVOCATION_INFO) + R.string.revocation_list_publish_time, + R.string.revocation_list_description, + dateDisplay), ID_REVOCATION_INFO) when (baseData) { is AttestationData -> updateData(baseData) is RemoteProvisioningData -> updateData(baseData) + is KeyboxData -> {} } notifyDataSetChanged() @@ -145,38 +186,79 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { var id = ID_DESCRIPTION_START val attestation = attestationData.showAttestation ?: return + + if (attestationData.isSoftwareLevel) { + addItemAt(2, HeaderViewHolder.CREATOR, HeaderData( + R.string.software_attestation, + R.string.software_attestation_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorWarning), ID_SOFTWARE_STATUS) + } + + if (attestationData.rootOfTrust?.isVerifiedBootKeySuspicious == true) { + addItemAt(2, HeaderViewHolder.CREATOR, HeaderData( + R.string.verified_boot_key_suspicious, + R.string.verified_boot_key_suspicious_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorAlert), ID_BOOT_KEY_STATUS) + } + + if (attestation.teeEnforced.isPatchLevelOutdated) { + addItemAt(2, HeaderViewHolder.CREATOR, HeaderData( + R.string.patch_level_outdated, + R.string.patch_level_outdated_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorWarning, + attestation.teeEnforced.oldestPatchLevel, + Build.VERSION.SECURITY_PATCH.take(7)), ID_PATCH_STATUS) + } + + if (attestationData.isVbmetaDigestMissing) { + addItemAt(2, HeaderViewHolder.CREATOR, HeaderData( + R.string.vbmeta_missing, + R.string.vbmeta_missing_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorWarning), ID_VBMETA_STATUS) + } else if (attestationData.isBootHashMismatch) { + addItemAt(2, HeaderViewHolder.CREATOR, HeaderData( + R.string.vbmeta_mismatch, + R.string.vbmeta_mismatch_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorAlert), ID_VBMETA_STATUS) + } + addItem(CommonItemViewHolder.SECURITY_LEVEL_CREATOR, SecurityLevelData( - R.string.attestation, - R.string.attestation_version_description, - R.string.security_level_description, - Attestation.attestationVersionToString(attestation.attestationVersion), - attestation.attestationSecurityLevel), id++) + R.string.attestation, + R.string.attestation_version_description, + R.string.security_level_description, + Attestation.attestationVersionToString(attestation.attestationVersion), + attestation.attestationSecurityLevel), id++) addItem(CommonItemViewHolder.SECURITY_LEVEL_CREATOR, SecurityLevelData( - R.string.keymaster, - R.string.keymaster_version_description, - R.string.security_level_description, - Attestation.keymasterVersionToString(attestation.keymasterVersion), - attestation.keymasterSecurityLevel), id++) + R.string.keymaster, + R.string.keymaster_version_description, + R.string.security_level_description, + Attestation.keymasterVersionToString(attestation.keymasterVersion), + attestation.keymasterSecurityLevel), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( - R.string.attestation_challenge, - R.string.attestation_challenge_description, - attestation.attestationChallenge?.let { - val stringChallenge = String(it) - if (stringChallenge.toByteArray().contentEquals(it)) stringChallenge - else Base64.encodeToString(it, 0) + " (base64)" - }), id++) + R.string.attestation_challenge, + R.string.attestation_challenge_description, + attestation.attestationChallenge?.let { + val stringChallenge = String(it) + if (stringChallenge.toByteArray().contentEquals(it)) stringChallenge + else Base64.encodeToString(it, 0) + " (base64)" + }), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( - R.string.unique_id, - R.string.unique_id_description, - attestation.uniqueId?.let { BaseEncoding.base16().lowerCase().encode(it) }), id) + R.string.unique_id, + R.string.unique_id_description, + attestation.uniqueId?.let { BaseEncoding.base16().lowerCase().encode(it) }), id) id = ID_AUTHORIZATION_LIST_START addItem(SubtitleViewHolder.CREATOR, CommonData( - R.string.authorization_list, - R.string.authorization_list_description), id++) + R.string.authorization_list, + R.string.authorization_list_description), id++) val tee = createAuthorizationItems(attestation.teeEnforced) val sw = createAuthorizationItems(attestation.softwareEnforced) @@ -188,28 +270,28 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { } addItem(CommonItemViewHolder.AUTHORIZATION_ITEM_CREATOR, AuthorizationItemData( - authorizationItemTitles[i], - authorizationItemDescriptions[i], - h, s), id++) + authorizationItemTitles[i], + authorizationItemDescriptions[i], + h, s), id++) if (h != null && s != null) { addItem(CommonItemViewHolder.AUTHORIZATION_ITEM_CREATOR, AuthorizationItemData( - authorizationItemTitles[i], - authorizationItemDescriptions[i], - s, false), id++) + authorizationItemTitles[i], + authorizationItemDescriptions[i], + s, false), id++) } } if (attestation is KnoxAttestation) { id = ID_KNOX_START addItem(SubtitleViewHolder.CREATOR, CommonData( - R.string.knox, - R.string.knox_description), id++) + R.string.knox, + R.string.knox_description), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( - R.string.knox_challenge, - R.string.knox_challenge_description, - attestation.knoxChallenge), id++) + R.string.knox_challenge, + R.string.knox_challenge_description, + attestation.knoxChallenge), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( R.string.knox_id_attest, @@ -217,14 +299,14 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { attestation.idAttest), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( - R.string.knox_integrity, - R.string.knox_integrity_description, - attestation.knoxIntegrity.toString()), id++) + R.string.knox_integrity, + R.string.knox_integrity_description, + attestation.knoxIntegrity?.toString()), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( - R.string.knox_record_hash, - R.string.knox_record_hash_description, - BaseEncoding.base16().lowerCase().encode(attestation.recordHash)), id++) + R.string.knox_record_hash, + R.string.knox_record_hash_description, + attestation.recordHash?.let { BaseEncoding.base16().lowerCase().encode(it) }), id++) } } @@ -250,18 +332,23 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( R.string.rpc_version_number, R.string.rpc_version_number_description, - hardware.versionNumber.toString()), id++) + hardware.versionNumber?.toString()), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( R.string.rpc_author_name, R.string.rpc_author_name_description, - hardware.rpcAuthorName.toString()), id++) + hardware.rpcAuthorName?.toString()), id++) addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( R.string.rpc_unique_id, R.string.rpc_unique_id_description, hardware.uniqueId), id++) + addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( + R.string.rpc_eek_curve, + R.string.rpc_eek_curve_description, + eekCurveToString(hardware.supportedEekCurve)), id) + id = ID_AUTHORIZATION_LIST_START addItem(SubtitleViewHolder.CREATOR, CommonData( R.string.rkp_device_info, @@ -270,15 +357,45 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { rkpData.deviceInfo.forEach { key, value -> addItem(CommonItemViewHolder.SIMPLE_CREATOR, Pair(key, value), id++) } + + if (rkpData.diceChain.isNotEmpty()) { + val header = when { + !rkpData.isDiceChainValid -> HeaderData( + R.string.dice_chain_invalid, + R.string.dice_chain_invalid_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorAlert) + rkpData.isDiceDegenerate -> HeaderData( + R.string.dice_chain_degenerate, + R.string.dice_chain_degenerate_summary, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorWarning) + else -> HeaderData( + R.string.dice_chain_valid, + R.string.dice_chain_valid_summary, + R.drawable.ic_trustworthy_24, + rikka.material.R.attr.colorSafe) + } + addItemAt(1, HeaderViewHolder.CREATOR, header, ID_DICE_STATUS) + + id = ID_RKP_DICE_START + addItem(SubtitleViewHolder.CREATOR, CommonData( + R.string.rkp_dice_chain, + R.string.rkp_dice_chain_description), id++) + + rkpData.diceChain.forEach { + addItem(CommonItemViewHolder.SIMPLE_CREATOR, it, id++) + } + } } fun updateData(e: AttestationException) { clear() addItem(HeaderViewHolder.CREATOR, HeaderData( - e.titleResId, - 0, - R.drawable.ic_error_outline_24, - rikka.material.R.attr.colorInactive), ID_ERROR) + e.titleResId, + 0, + R.drawable.ic_error_outline_24, + rikka.material.R.attr.colorInactive), ID_ERROR) addItem(ErrorViewHolder.CREATOR, e, ID_ERROR_MESSAGE) notifyDataSetChanged() @@ -308,155 +425,169 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { private const val ID_ERROR = 0L private const val ID_CERT_STATUS = 1L private const val ID_BOOT_STATUS = 2L + private const val ID_PATCH_STATUS = 3L + private const val ID_VBMETA_STATUS = 4L + private const val ID_SOFTWARE_STATUS = 5L + private const val ID_DICE_STATUS = 6L + private const val ID_BOOT_KEY_STATUS = 7L private const val ID_CERT_INFO_START = 1000L private const val ID_REVOCATION_INFO = 1900L private const val ID_RKP_HOSTNAME = 2000L private const val ID_DESCRIPTION_START = 3000L private const val ID_AUTHORIZATION_LIST_START = 4000L private const val ID_KNOX_START = 5000L + private const val ID_RKP_DICE_START = 6000L private const val ID_ERROR_MESSAGE = 100000L + private fun eekCurveToString(curve: Int): String { + return when (curve) { + RpcHardwareInfo.CURVE_P256 -> "P-256" + RpcHardwareInfo.CURVE_25519 -> "Ed25519" + else -> "None" + } + } + private fun createAuthorizationItems(list: AuthorizationList): Array { return arrayOf( - list.purposes?.let { AuthorizationList.purposesToString(it) }, - list.algorithm?.let { AuthorizationList.algorithmToString(it) }, - list.keySize?.toString(), - list.digests?.let { AuthorizationList.digestsToString(it) }, - list.paddingModes?.let { AuthorizationList.paddingModesToString(it) }, - list.ecCurve?.let { AuthorizationList.ecCurveAsString(it) }, - list.rsaPublicExponent?.toString(), - list.mgfDigests?.let { AuthorizationList.digestsToString(it) }, - list.rollbackResistance?.toString(), - list.earlyBootOnly?.toString(), - list.activeDateTime?.let { AuthorizationList.formatDate(it) }, - list.originationExpireDateTime?.let { AuthorizationList.formatDate(it) }, - list.usageExpireDateTime?.let { AuthorizationList.formatDate(it) }, - list.usageCountLimit?.toString(), - list.noAuthRequired?.toString(), - list.userAuthType?.let { AuthorizationList.userAuthTypeToString(it) }, - list.authTimeout?.toString(), - list.allowWhileOnBody?.toString(), - list.trustedUserPresenceReq?.toString(), - list.trustedConfirmationReq?.toString(), - list.unlockedDeviceReq?.toString(), - list.allApplications?.toString(), - list.applicationId, - list.creationDateTime?.let { AuthorizationList.formatDate(it) }, - list.origin?.let { AuthorizationList.originToString(it) }, - list.rollbackResistant?.toString(), - list.rootOfTrust?.toString(), - list.osVersion?.toString(), - list.osPatchLevel?.toString(), - list.attestationApplicationId?.toString()?.trim(), - list.brand, - list.device, - list.product, - list.serialNumber, - list.imei, - list.secondImei, - list.meid, - list.manufacturer, - list.model, - list.vendorPatchLevel?.toString(), - list.bootPatchLevel?.toString(), - list.deviceUniqueAttestation?.toString(), - list.identityCredentialKey?.toString(), - list.moduleHash?.let { BaseEncoding.base16().lowerCase().encode(it) }, + list.purposes?.let { AuthorizationList.purposesToString(it) }, + list.algorithm?.let { AuthorizationList.algorithmToString(it) }, + list.keySize?.toString(), + list.digests?.let { AuthorizationList.digestsToString(it) }, + list.paddingModes?.let { AuthorizationList.paddingModesToString(it) }, + list.ecCurve?.let { AuthorizationList.ecCurveAsString(it) }, + list.rsaPublicExponent?.toString(), + list.mgfDigests?.let { AuthorizationList.digestsToString(it) }, + list.rollbackResistance?.toString(), + list.earlyBootOnly?.toString(), + list.activeDateTime?.let { AuthorizationList.formatDate(it) }, + list.originationExpireDateTime?.let { AuthorizationList.formatDate(it) }, + list.usageExpireDateTime?.let { AuthorizationList.formatDate(it) }, + list.usageCountLimit?.toString(), + list.noAuthRequired?.toString(), + list.userAuthType?.let { AuthorizationList.userAuthTypeToString(it) }, + list.authTimeout?.toString(), + list.allowWhileOnBody?.toString(), + list.trustedUserPresenceReq?.toString(), + list.trustedConfirmationReq?.toString(), + list.unlockedDeviceReq?.toString(), + list.allApplications?.toString(), + list.applicationId, + list.creationDateTime?.let { AuthorizationList.formatDate(it) }, + list.origin?.let { AuthorizationList.originToString(it) }, + list.rollbackResistant?.toString(), + list.rootOfTrust?.toString(), + list.osVersion?.let { AuthorizationList.osVersionToString(it) }, + list.osPatchLevel?.let { AuthorizationList.patchLevelToString(it) }, + list.attestationApplicationId?.toString()?.trim(), + list.brand, + list.device, + list.product, + list.serialNumber, + list.imei, + list.secondImei, + list.meid, + list.manufacturer, + list.model, + list.vendorPatchLevel?.let { AuthorizationList.patchLevelToString(it) }, + list.bootPatchLevel?.let { AuthorizationList.patchLevelToString(it) }, + list.deviceUniqueAttestation?.toString(), + list.identityCredentialKey?.toString(), + list.moduleHash?.let { BaseEncoding.base16().lowerCase().encode(it) }, ) } private val authorizationItemTitles = arrayOf( - R.string.authorization_list_purpose, - R.string.authorization_list_algorithm, - R.string.authorization_list_keySize, - R.string.authorization_list_digest, - R.string.authorization_list_padding, - R.string.authorization_list_ecCurve, - R.string.authorization_list_rsaPublicExponent, - R.string.authorization_list_mgfDigest, - R.string.authorization_list_rollbackResistance, - R.string.authorization_list_earlyBootOnly, - R.string.authorization_list_activeDateTime, - R.string.authorization_list_originationExpireDateTime, - R.string.authorization_list_usageExpireDateTime, - R.string.authorization_list_usageCountLimit, - R.string.authorization_list_noAuthRequired, - R.string.authorization_list_userAuthType, - R.string.authorization_list_authTimeout, - R.string.authorization_list_allowWhileOnBody, - R.string.authorization_list_trustedUserPresenceRequired, - R.string.authorization_list_trustedConfirmationRequired, - R.string.authorization_list_unlockedDeviceRequired, - R.string.authorization_list_allApplications, - R.string.authorization_list_applicationId, - R.string.authorization_list_creationDateTime, - R.string.authorization_list_origin, - R.string.authorization_list_rollbackResistant, - R.string.authorization_list_rootOfTrust, - R.string.authorization_list_osVersion, - R.string.authorization_list_osPatchLevel, - R.string.authorization_list_attestationApplicationId, - R.string.authorization_list_attestationIdBrand, - R.string.authorization_list_attestationIdDevice, - R.string.authorization_list_attestationIdProduct, - R.string.authorization_list_attestationIdSerial, - R.string.authorization_list_attestationIdImei, - R.string.authorization_list_attestationIdSecondImei, - R.string.authorization_list_attestationIdMeid, - R.string.authorization_list_attestationIdManufacturer, - R.string.authorization_list_attestationIdModel, - R.string.authorization_list_vendorPatchLevel, - R.string.authorization_list_bootPatchLevel, - R.string.authorization_list_deviceUniqueAttestation, - R.string.authorization_list_identityCredentialKey, - R.string.authorization_list_moduleHash, + R.string.authorization_list_purpose, + R.string.authorization_list_algorithm, + R.string.authorization_list_keySize, + R.string.authorization_list_digest, + R.string.authorization_list_padding, + R.string.authorization_list_ecCurve, + R.string.authorization_list_rsaPublicExponent, + R.string.authorization_list_mgfDigest, + R.string.authorization_list_rollbackResistance, + R.string.authorization_list_earlyBootOnly, + R.string.authorization_list_activeDateTime, + R.string.authorization_list_originationExpireDateTime, + R.string.authorization_list_usageExpireDateTime, + R.string.authorization_list_usageCountLimit, + R.string.authorization_list_noAuthRequired, + R.string.authorization_list_userAuthType, + R.string.authorization_list_authTimeout, + R.string.authorization_list_allowWhileOnBody, + R.string.authorization_list_trustedUserPresenceRequired, + R.string.authorization_list_trustedConfirmationRequired, + R.string.authorization_list_unlockedDeviceRequired, + R.string.authorization_list_allApplications, + R.string.authorization_list_applicationId, + R.string.authorization_list_creationDateTime, + R.string.authorization_list_origin, + R.string.authorization_list_rollbackResistant, + R.string.authorization_list_rootOfTrust, + R.string.authorization_list_osVersion, + R.string.authorization_list_osPatchLevel, + R.string.authorization_list_attestationApplicationId, + R.string.authorization_list_attestationIdBrand, + R.string.authorization_list_attestationIdDevice, + R.string.authorization_list_attestationIdProduct, + R.string.authorization_list_attestationIdSerial, + R.string.authorization_list_attestationIdImei, + R.string.authorization_list_attestationIdSecondImei, + R.string.authorization_list_attestationIdMeid, + R.string.authorization_list_attestationIdManufacturer, + R.string.authorization_list_attestationIdModel, + R.string.authorization_list_vendorPatchLevel, + R.string.authorization_list_bootPatchLevel, + R.string.authorization_list_deviceUniqueAttestation, + R.string.authorization_list_identityCredentialKey, + R.string.authorization_list_moduleHash, ) private val authorizationItemDescriptions = arrayOf( - R.string.authorization_list_purpose_description, - R.string.authorization_list_algorithm_description, - R.string.authorization_list_keySize_description, - R.string.authorization_list_digest_description, - R.string.authorization_list_padding_description, - R.string.authorization_list_ecCurve_description, - R.string.authorization_list_rsaPublicExponent_description, - R.string.authorization_list_mgfDigest_description, - R.string.authorization_list_rollbackResistance_description, - R.string.authorization_list_earlyBootOnly_description, - R.string.authorization_list_activeDateTime_description, - R.string.authorization_list_originationExpireDateTime_description, - R.string.authorization_list_usageExpireDateTime_description, - R.string.authorization_list_usageCountLimit_description, - R.string.authorization_list_noAuthRequired_description, - R.string.authorization_list_userAuthType_description, - R.string.authorization_list_authTimeout_description, - R.string.authorization_list_allowWhileOnBody_description, - R.string.authorization_list_trustedUserPresenceRequired_description, - R.string.authorization_list_trustedConfirmationRequired_description, - R.string.authorization_list_unlockedDeviceRequired_description, - R.string.authorization_list_allApplications_description, - R.string.authorization_list_applicationId_description, - R.string.authorization_list_creationDateTime_description, - R.string.authorization_list_origin_description, - R.string.authorization_list_rollbackResistant_description, - R.string.authorization_list_rootOfTrust_description, - R.string.authorization_list_osVersion_description, - R.string.authorization_list_osPatchLevel_description, - R.string.authorization_list_attestationApplicationId_description, - R.string.authorization_list_attestationIdBrand_description, - R.string.authorization_list_attestationIdDevice_description, - R.string.authorization_list_attestationIdProduct_description, - R.string.authorization_list_attestationIdSerial_description, - R.string.authorization_list_attestationIdImei_description, - R.string.authorization_list_attestationIdSecondImei_description, - R.string.authorization_list_attestationIdMeid_description, - R.string.authorization_list_attestationIdManufacturer_description, - R.string.authorization_list_attestationIdModel_description, - R.string.authorization_list_vendorPatchLevel_description, - R.string.authorization_list_bootPatchLevel_description, - R.string.authorization_list_deviceUniqueAttestation_description, - R.string.authorization_list_identityCredentialKey_description, - R.string.authorization_list_moduleHash_description, + R.string.authorization_list_purpose_description, + R.string.authorization_list_algorithm_description, + R.string.authorization_list_keySize_description, + R.string.authorization_list_digest_description, + R.string.authorization_list_padding_description, + R.string.authorization_list_ecCurve_description, + R.string.authorization_list_rsaPublicExponent_description, + R.string.authorization_list_mgfDigest_description, + R.string.authorization_list_rollbackResistance_description, + R.string.authorization_list_earlyBootOnly_description, + R.string.authorization_list_activeDateTime_description, + R.string.authorization_list_originationExpireDateTime_description, + R.string.authorization_list_usageExpireDateTime_description, + R.string.authorization_list_usageCountLimit_description, + R.string.authorization_list_noAuthRequired_description, + R.string.authorization_list_userAuthType_description, + R.string.authorization_list_authTimeout_description, + R.string.authorization_list_allowWhileOnBody_description, + R.string.authorization_list_trustedUserPresenceRequired_description, + R.string.authorization_list_trustedConfirmationRequired_description, + R.string.authorization_list_unlockedDeviceRequired_description, + R.string.authorization_list_allApplications_description, + R.string.authorization_list_applicationId_description, + R.string.authorization_list_creationDateTime_description, + R.string.authorization_list_origin_description, + R.string.authorization_list_rollbackResistant_description, + R.string.authorization_list_rootOfTrust_description, + R.string.authorization_list_osVersion_description, + R.string.authorization_list_osPatchLevel_description, + R.string.authorization_list_attestationApplicationId_description, + R.string.authorization_list_attestationIdBrand_description, + R.string.authorization_list_attestationIdDevice_description, + R.string.authorization_list_attestationIdProduct_description, + R.string.authorization_list_attestationIdSerial_description, + R.string.authorization_list_attestationIdImei_description, + R.string.authorization_list_attestationIdSecondImei_description, + R.string.authorization_list_attestationIdMeid_description, + R.string.authorization_list_attestationIdManufacturer_description, + R.string.authorization_list_attestationIdModel_description, + R.string.authorization_list_vendorPatchLevel_description, + R.string.authorization_list_bootPatchLevel_description, + R.string.authorization_list_deviceUniqueAttestation_description, + R.string.authorization_list_identityCredentialKey_description, + R.string.authorization_list_moduleHash_description, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt index e6af3e0f..bf7acbea 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt @@ -30,10 +30,10 @@ import io.github.vvb2060.keyattestation.databinding.HomeBinding import io.github.vvb2060.keyattestation.keystore.KeyStoreManager import io.github.vvb2060.keyattestation.lang.AttestationException import io.github.vvb2060.keyattestation.repository.AttestationData -import io.github.vvb2060.keyattestation.util.Status import io.github.vvb2060.keyattestation.util.ColorManager import io.github.vvb2060.keyattestation.util.CrlManager import io.github.vvb2060.keyattestation.util.LocaleManager +import io.github.vvb2060.keyattestation.util.Status import rikka.html.text.HtmlCompat import rikka.html.text.toHtml import rikka.shizuku.Shizuku diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt index d146c9b4..c5b3b782 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt @@ -175,7 +175,7 @@ class HomeViewModel( attestationData.postValue(Resource.loading(null)) val result = cr.openFileDescriptor(uri, "r").use { - attestationRepository.loadCerts(it) + attestationRepository.loadCerts(it, preferAttestRsaKey) } attestationData.postValue(result) diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/keystore/AndroidKeyStore.java b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/AndroidKeyStore.java index 2a5bfd37..c1a57144 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/keystore/AndroidKeyStore.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/AndroidKeyStore.java @@ -21,7 +21,6 @@ import android.security.keystore.KeyGenParameterSpec_rename; import android.security.keystore.KeyProperties; import android.security.keystore.KeyProtection; -import android.security.keystore2.AndroidKeyStoreProvider; import android.system.Os; import android.util.Log; @@ -373,6 +372,19 @@ public RpcHardwareInfo getHardwareInfo(boolean useStrongBox, DeviceInfo deviceIn } } + @Override + public String getVbmetaDigest() { + return SystemProperties.get("ro.boot.vbmeta.digest"); + } + + @Override + public byte[] getDiceChain(boolean useStrongBox) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + throw new IllegalStateException(); + } + return RemoteProvisioning.getInstance(useStrongBox).getDiceChain(); + } + @Override public byte[] checkRemoteProvisioning(boolean useStrongBox) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/keystore/KeyBoxXmlParser.java b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/KeyBoxXmlParser.java index b6194af9..f995851e 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/keystore/KeyBoxXmlParser.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/KeyBoxXmlParser.java @@ -50,38 +50,52 @@ private KeyBoxXmlParser() throws IOException { throw new IOException(e); } } + + public KeyStore.PrivateKeyEntry parse(InputStream in) throws IOException { + return parse(in, false); + } - public KeyStore.PrivateKeyEntry parse(InputStream in) throws IOException { + public KeyStore.PrivateKeyEntry parse(InputStream in, boolean preferRsa) throws IOException { try { parser.setInput(in, StandardCharsets.UTF_8.name()); chain.clear(); privateKey = null; - readAndroidAttestation(); + readAndroidAttestation(preferRsa); } catch (XmlPullParserException e) { throw new IOException(e); } if (privateKey == null || chain.isEmpty()) { - throw new IOException("No key found"); + throw new IOException("No key found matching requested format"); } return new KeyStore.PrivateKeyEntry(privateKey, chain.toArray(new Certificate[0])); } - private void readAndroidAttestation() throws XmlPullParserException, IOException { + private void readAndroidAttestation(boolean preferRsa) throws XmlPullParserException, IOException { while (parser.next() != XmlPullParser.END_DOCUMENT) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } var name = parser.getName(); var algorithm = parser.getAttributeValue(null, "algorithm"); - if ("Key".equals(name) && "ecdsa".equals(algorithm)) { - parser.nextTag(); - readECKey(); - break; + + if ("Key".equals(name)) { + if (preferRsa && "ecdsa".equalsIgnoreCase(algorithm)) { + continue; + } + if (!preferRsa && "rsa".equalsIgnoreCase(algorithm)) { + continue; + } + + if ("ecdsa".equalsIgnoreCase(algorithm) || "rsa".equalsIgnoreCase(algorithm)) { + parser.nextTag(); + readKeyBlock(algorithm); + break; + } } } } - private void readECKey() throws XmlPullParserException, IOException { + private void readKeyBlock(String algorithm) throws XmlPullParserException, IOException { while (!(parser.getEventType() == XmlPullParser.END_TAG && "Key".equals(parser.getName()))) { if (parser.getEventType() != XmlPullParser.START_TAG) { parser.next(); @@ -92,7 +106,7 @@ private void readECKey() throws XmlPullParserException, IOException { case "PrivateKey" -> { if ("pem".equals(format)) { parser.next(); - readPrivateKey(parser.getText()); + readPrivateKey(parser.getText(), algorithm); parser.next(); } else { return; @@ -124,15 +138,26 @@ private static byte[] stringToBytes(String text) { return Base64.decode(sb.toString(), 0); } - private void readPrivateKey(String text) throws IOException { + private void readPrivateKey(String text, String algorithm) throws IOException { try { var sequence = ASN1Sequence.getInstance(stringToBytes(text)); - var ecKey = ECPrivateKey.getInstance(sequence); - var id = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, - ecKey.getParametersObject()); - var data = new PrivateKeyInfo(id, ecKey).getEncoded(); + byte[] data; + String factoryAlgo; + + if ("rsa".equalsIgnoreCase(algorithm)) { + var rsaKey = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(sequence); + var id = new AlgorithmIdentifier(org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers.rsaEncryption, org.bouncycastle.asn1.DERNull.INSTANCE); + data = new PrivateKeyInfo(id, rsaKey).getEncoded(); + factoryAlgo = "RSA"; + } else { + var ecKey = ECPrivateKey.getInstance(sequence); + var id = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, ecKey.getParametersObject()); + data = new PrivateKeyInfo(id, ecKey).getEncoded(); + factoryAlgo = "EC"; + } + var keySpec = new PKCS8EncodedKeySpec(data); - var keyFactory = KeyFactory.getInstance("EC"); + var keyFactory = KeyFactory.getInstance(factoryAlgo); privateKey = keyFactory.generatePrivate(keySpec); } catch (GeneralSecurityException e) { throw new IOException(e); diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/keystore/RemoteProvisioning.java b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/RemoteProvisioning.java index d0506943..48b96b6d 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/keystore/RemoteProvisioning.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/RemoteProvisioning.java @@ -67,6 +67,7 @@ class RemoteProvisioning { private final String requestId = UUID.randomUUID().toString(); private final IRemotelyProvisionedComponent binder; private byte[] deviceInfoData; + private byte[] diceChainData; private static class EekResponse { private final byte[] challenge; @@ -136,6 +137,10 @@ public byte[] getDeviceInfo() { return deviceInfoData; } + public byte[] getDiceChain() { + return diceChainData; + } + public byte[] check() throws RuntimeException { try { var eekResponse = fetchEek(); @@ -207,7 +212,7 @@ private DataItem httpPost(Uri uri, byte[] input) throws IOException, CborExcepti uri = uri.buildUpon().appendQueryParameter("requestId", requestId).build(); var con = (HttpsURLConnection) new URL(uri.toString()).openConnection(); con.setRequestMethod("POST"); - con.setConnectTimeout(2_000); + con.setConnectTimeout(10_000); con.setReadTimeout(20_000); con.setDoOutput(true); con.setFixedLengthStreamingMode(input.length); @@ -248,6 +253,9 @@ private byte[] generateCsr(EekResponse eekResponse) var deviceInfo = new DeviceInfo(); var protectedData = new ProtectedData(); var geekChain = eekResponse.getEekChain(hwInfo.supportedEekCurve); + if (geekChain == null) { + throw new IllegalStateException("No GEEK for supportedEekCurve=" + hwInfo.supportedEekCurve); + } var csrTag = binder.generateCertificateRequest(false, keysToSign, geekChain, eekResponse.getChallenge(), deviceInfo, protectedData); var mac0Message = buildMac0MessageForV1Csr(keysToSign[0], csrTag); @@ -269,6 +277,7 @@ private byte[] generateCsr(EekResponse eekResponse) var array = (Array) decodeCbor(csrBytes); var deviceInfo = extractDeviceInfoFromV3Csr(array); deviceInfoData = encodeCbor(deviceInfo); + diceChainData = encodeCbor(array.getDataItems().get(2)); return encodeCbor(array.add(unverifiedDeviceInfo)); } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/lang/AttestationException.kt b/app/src/main/java/io/github/vvb2060/keyattestation/lang/AttestationException.kt index fcdff3b4..4820dbea 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/lang/AttestationException.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/lang/AttestationException.kt @@ -2,7 +2,8 @@ package io.github.vvb2060.keyattestation.lang import io.github.vvb2060.keyattestation.R -class AttestationException(code: Int, cause: Throwable) : RuntimeException(cause) { +class AttestationException(code: Int, cause: Throwable?) : RuntimeException(cause) { + val hasDetailedMessage: Boolean = cause != null companion object { const val CODE_UNKNOWN = -1 @@ -15,6 +16,9 @@ class AttestationException(code: Int, cause: Throwable) : RuntimeException(cause const val CODE_UNAVAILABLE_TRANSIENT = 7 const val CODE_KEYS_NOT_PROVISIONED = 8 const val CODE_RKP = 9 + const val CODE_ATTEST_RSA_KEY_EC_ONLY = 10 + const val CODE_ATTEST_EC_KEY_RSA_ONLY = 11 + const val CODE_INVALID_FILE = 12 } val titleResId: Int = when (code) { @@ -27,6 +31,9 @@ class AttestationException(code: Int, cause: Throwable) : RuntimeException(cause CODE_UNAVAILABLE_TRANSIENT -> R.string.error_unavailable_transient CODE_KEYS_NOT_PROVISIONED -> R.string.error_keys_not_provisioned CODE_RKP -> R.string.error_remote_key_provisioning + CODE_ATTEST_RSA_KEY_EC_ONLY -> R.string.error_attest_rsa_key_ec_only + CODE_ATTEST_EC_KEY_RSA_ONLY -> R.string.error_attest_ec_key_rsa_only + CODE_INVALID_FILE -> R.string.error_invalid_file else -> R.string.error_unknown } @@ -40,6 +47,9 @@ class AttestationException(code: Int, cause: Throwable) : RuntimeException(cause CODE_UNAVAILABLE_TRANSIENT -> R.string.error_unavailable_transient_summary CODE_KEYS_NOT_PROVISIONED -> R.string.error_keys_not_provisioned_summary CODE_RKP -> R.string.error_remote_key_provisioning_summary + CODE_ATTEST_RSA_KEY_EC_ONLY -> R.string.error_attest_rsa_key_ec_only_summary + CODE_ATTEST_EC_KEY_RSA_ONLY -> R.string.error_attest_ec_key_rsa_only_summary + CODE_INVALID_FILE -> R.string.error_invalid_file_summary else -> R.string.error_unknown } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationData.java b/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationData.java index 17b4cc1e..8eae3b13 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationData.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationData.java @@ -3,6 +3,8 @@ import static io.github.vvb2060.keyattestation.attestation.Attestation.KM_SECURITY_LEVEL_SOFTWARE; import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_CANT_PARSE_CERT; +import com.google.common.io.BaseEncoding; + import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.LinkedList; @@ -17,6 +19,7 @@ public class AttestationData extends BaseData { private final RootOfTrust rootOfTrust; private final boolean sw; public Attestation showAttestation; + public String vbmetaDigest; public RootOfTrust getRootOfTrust() { return rootOfTrust; @@ -26,6 +29,24 @@ public boolean isSoftwareLevel() { return sw; } + public boolean isBootHashMismatch() { + if (vbmetaDigest == null || vbmetaDigest.isEmpty() || rootOfTrust == null) { + return false; + } + var hash = rootOfTrust.getVerifiedBootHash(); + if (hash == null) { + return false; + } + return !BaseEncoding.base16().lowerCase().encode(hash).equalsIgnoreCase(vbmetaDigest); + } + + public boolean isVbmetaDigestMissing() { + if (vbmetaDigest == null || rootOfTrust == null) { + return false; + } + return vbmetaDigest.isEmpty() || vbmetaDigest.chars().allMatch(chr -> chr == '0'); + } + private AttestationData(List certs) { init(certs); diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationRepository.java b/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationRepository.java index 06a1f497..bfcf2640 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationRepository.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationRepository.java @@ -31,6 +31,7 @@ import io.github.vvb2060.keyattestation.AppApplication; import io.github.vvb2060.keyattestation.keystore.AndroidKeyStore; import io.github.vvb2060.keyattestation.keystore.IAndroidKeyStore; +import io.github.vvb2060.keyattestation.keystore.KeyBoxXmlParser; import io.github.vvb2060.keyattestation.keystore.KeyStoreManager; import io.github.vvb2060.keyattestation.lang.AttestationException; import io.github.vvb2060.keyattestation.util.Resource; @@ -191,7 +192,14 @@ public Resource attest(boolean reset, boolean useAttestKey, if (reset) keyStore.deleteAllEntry(); doAttestation(useAttestKey, useStrongBox, includeProps, uniqueIdIncluded, idFlags, keyStoreKeyType, useSak); - return Resource.Companion.success(AttestationData.parseCertificateChain(currentCerts)); + var data = AttestationData.parseCertificateChain(currentCerts); + try { + data.vbmetaDigest = keyStore.getVbmetaDigest(); + } catch (Exception e) { + var cause = e instanceof AttestationException ? e.getCause() : e; + Log.w(AppApplication.TAG, "Get vbmeta digest error.", cause); + } + return Resource.Companion.success(data); } catch (Exception e) { var cause = e instanceof AttestationException ? e.getCause() : e; Log.w(AppApplication.TAG, "Do attestation error.", cause); @@ -204,28 +212,219 @@ public Resource attest(boolean reset, boolean useAttestKey, } } - public Resource loadCerts(ParcelFileDescriptor pfd) { + /** + * Load certificates from file, supporting multiple formats (binary, XML, PEM). + * Automatically filters to requested algorithm (RSA or EC). + */ + public Resource loadCerts(ParcelFileDescriptor pfd, boolean preferRsa) { currentCerts.clear(); try { - AttestationData data; - try (var in = new ParcelFileDescriptor.AutoCloseInputStream(pfd); - var channel = in.getChannel()) { - try { - generateCertificates(in); - data = AttestationData.parseCertificateChain(currentCerts); - } catch (CertificateException e) { - channel.position(0); - generateCertPath(in); - data = AttestationData.parseCertificateChain(currentCerts); + // 1. Read raw bytes from file + byte[] bytes; + try (var in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = in.read(buffer)) != -1) { + baos.write(buffer, 0, read); } + bytes = baos.toByteArray(); } - return Resource.Companion.success(data); + + // Strip a UTF-8 BOM if present -- it trips the plain-ASCII sanity check further down. + if (bytes.length >= 3 && (bytes[0] & 0xFF) == 0xEF && (bytes[1] & 0xFF) == 0xBB && (bytes[2] & 0xFF) == 0xBF) { + bytes = java.util.Arrays.copyOfRange(bytes, 3, bytes.length); + } + + boolean parsedBinary = false; + + // 1a. Skip binary cert parsing for XML input to avoid chain corruption. + int sniffLen = Math.min(bytes.length, 512); + String sniff = new String(bytes, 0, sniffLen, java.nio.charset.StandardCharsets.UTF_8).trim(); + boolean looksLikeKeyboxXml = sniff.startsWith(" certsCollection = factory.generateCertificates(bis); + for (java.security.cert.Certificate cert : certsCollection) { + if (cert instanceof X509Certificate) { + currentCerts.add((X509Certificate) cert); + } + } + if (!currentCerts.isEmpty()) parsedBinary = true; + } catch (Exception ignored) {} + } + } + + // 2a. Filter to RSA certs only + if (parsedBinary && preferRsa && !currentCerts.isEmpty()) { + List rsaCerts = new ArrayList<>(); + for (X509Certificate cert : currentCerts) { + if (cert.getPublicKey().getAlgorithm().equalsIgnoreCase("RSA")) { + rsaCerts.add(cert); + } + } + if (!rsaCerts.isEmpty()) { + currentCerts.clear(); + currentCerts.addAll(rsaCerts); + } + } + + // 3. Parse XML + if (!parsedBinary) { + String xmlContent = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + for (char c : xmlContent.toCharArray()) { + if (c > 127 && !Character.isWhitespace(c)) { // Non-ASCII except whitespace + throw new AttestationException(CODE_INVALID_FILE, null); + } + } + + boolean parsedViaXmlParser = false; + try (var bis = new java.io.ByteArrayInputStream(bytes)) { + var xmlEntry = KeyBoxXmlParser.getInstance().parse(bis, preferRsa); + java.security.cert.Certificate[] chain = xmlEntry.getCertificateChain(); + if (chain != null) { + for (java.security.cert.Certificate cert : chain) { + if (cert instanceof X509Certificate) { + currentCerts.add((X509Certificate) cert); + } + } + if (!currentCerts.isEmpty()) { + parsedViaXmlParser = true; + } + } + } catch (Exception ignored) {} + + // 3a: Lenient regex parser for unstructured XML/PEM + if (!parsedViaXmlParser) { + String content = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + List rawBlocks = new ArrayList<>(); + + java.util.regex.Pattern xmlPattern = java.util.regex.Pattern.compile( + "]*>(.*?)", + java.util.regex.Pattern.DOTALL | java.util.regex.Pattern.CASE_INSENSITIVE + ); + java.util.regex.Matcher xmlMatcher = xmlPattern.matcher(content); + while (xmlMatcher.find()) { + String tagContent = xmlMatcher.group(1); + if (tagContent != null) rawBlocks.add(tagContent); + } + + if (rawBlocks.isEmpty()) { + java.util.regex.Pattern pemPattern = java.util.regex.Pattern.compile( + "-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", + java.util.regex.Pattern.DOTALL + ); + java.util.regex.Matcher pemMatcher = pemPattern.matcher(content); + while (pemMatcher.find()) { + String pemContent = pemMatcher.group(1); + if (pemContent != null) rawBlocks.add(pemContent); + } + } + + for (String block : rawBlocks) { + String cleanBlock = block.replaceAll("", ""); + cleanBlock = cleanBlock.replaceAll("-----BEGIN CERTIFICATE-----", "") + .replaceAll("-----END CERTIFICATE-----", ""); + cleanBlock = cleanBlock.replaceAll("\\s+", ""); + + if (!cleanBlock.isEmpty()) { + try { + byte[] decoded = android.util.Base64.decode(cleanBlock, android.util.Base64.DEFAULT); + try (var bis = new java.io.ByteArrayInputStream(decoded)) { + java.security.cert.Certificate cert = factory.generateCertificate(bis); + if (cert instanceof X509Certificate) { + currentCerts.add((X509Certificate) cert); + } + } + } catch (Exception ignored) {} + } + } + + // Same algorithm filter as the binary path (2a) + // the parser doesn't distinguish key blocks, so a mixed-algorithm keybox would + // otherwise concatenate two unrelated chains into one list + if (!currentCerts.isEmpty()) { + List matching = new ArrayList<>(); + for (X509Certificate cert : currentCerts) { + var algo = cert.getPublicKey().getAlgorithm(); + if (algo.equalsIgnoreCase(preferRsa ? "RSA" : "EC") || + (!preferRsa && algo.equalsIgnoreCase("ECDSA")) + ) { + matching.add(cert); + } + } + if (!matching.isEmpty()) { + currentCerts.clear(); + currentCerts.addAll(matching); + } + } + } + } + + if (currentCerts.isEmpty()) { + throw new AttestationException(CODE_INVALID_FILE, null); + } + + // 4. Sort certs by attestation extension + String attestationOid = "1.3.6.1.4.1.11129.2.1.17"; + X509Certificate leafCert = null; + for (X509Certificate cert : currentCerts) { + var criticalOids = cert.getCriticalExtensionOIDs(); + var nonCriticalOids = cert.getNonCriticalExtensionOIDs(); + if ((criticalOids != null && criticalOids.contains(attestationOid)) || + (nonCriticalOids != null && nonCriticalOids.contains(attestationOid))) { + leafCert = cert; + break; + } + } + if (leafCert != null) { + currentCerts.remove(leafCert); + currentCerts.add(0, leafCert); + } + + // 5. Validate that requested key type matches the loaded leaf certificate + if (!currentCerts.isEmpty()) { + String firstCertAlgo = currentCerts.get(0).getPublicKey().getAlgorithm(); + + if (preferRsa) { + // If user wants RSA, but leaf is not RSA + if (!firstCertAlgo.equalsIgnoreCase("RSA")) { + throw new AttestationException(CODE_ATTEST_RSA_KEY_EC_ONLY, null); + } + } else { + // If user wants EC, but leaf is not EC/ECDSA + if (!firstCertAlgo.equalsIgnoreCase("EC") && !firstCertAlgo.equalsIgnoreCase("ECDSA")) { + throw new AttestationException(CODE_ATTEST_EC_KEY_RSA_ONLY, null); + } + } + } + + try { + AttestationData data = AttestationData.parseCertificateChain(currentCerts); + return Resource.Companion.success(data); + } catch (Exception originalError) { + Log.w(AppApplication.TAG, "No attestation extension, falling back to KeyboxData.", originalError); + return Resource.Companion.success(KeyboxData.fromCerts(currentCerts)); + } + } catch (Exception e) { var cause = e instanceof AttestationException ? e.getCause() : e; Log.w(AppApplication.TAG, "Load attestation error.", cause); if (e instanceof AttestationException) { - return Resource.Companion.error(e, null); + return Resource.Companion.error((AttestationException) e, null); } else if (e instanceof CertificateException) { return Resource.Companion.error(new AttestationException(CODE_CANT_PARSE_CERT, e), null); } else { @@ -263,7 +462,8 @@ public Resource checkRkp(boolean useStrongBox) { ? keyStore.getRkpHostname() : null; var deviceInfo = new DeviceInfo(); var hw = keyStore.getHardwareInfo(useStrongBox, deviceInfo); - var info = new RemoteProvisioningData(name, hw, deviceInfo); + var diceChain = keyStore.getDiceChain(useStrongBox); + var info = new RemoteProvisioningData(name, hw, deviceInfo, diceChain); try { var data = keyStore.checkRemoteProvisioning(useStrongBox); info.setCerts(factory.generateCertificates(new ByteArrayInputStream(data))); diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/repository/KeyboxData.java b/app/src/main/java/io/github/vvb2060/keyattestation/repository/KeyboxData.java new file mode 100644 index 00000000..f3889ecc --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/repository/KeyboxData.java @@ -0,0 +1,18 @@ +package io.github.vvb2060.keyattestation.repository; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import io.github.vvb2060.keyattestation.attestation.CertificateInfo; + +public class KeyboxData extends BaseData { + + public static KeyboxData fromCerts(List certs) { + var infoList = new ArrayList(certs.size()); + CertificateInfo.parse(certs, infoList); + var data = new KeyboxData(); + data.init(infoList); + return data; + } +} diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/repository/RemoteProvisioningData.java b/app/src/main/java/io/github/vvb2060/keyattestation/repository/RemoteProvisioningData.java index d0d6aeac..c370e179 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/repository/RemoteProvisioningData.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/repository/RemoteProvisioningData.java @@ -3,29 +3,61 @@ import android.hardware.security.keymint.DeviceInfo; import android.hardware.security.keymint.RpcHardwareInfo; import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; import com.google.common.io.BaseEncoding; +import org.bouncycastle.asn1.x9.ECNamedCurveTable; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.Ed25519Signer; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.MessageDigest; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborEncoder; import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.Array; import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.DataItem; import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.NegativeInteger; +import co.nstant.in.cbor.model.Number; +import co.nstant.in.cbor.model.UnicodeString; +import co.nstant.in.cbor.model.UnsignedInteger; +import io.github.vvb2060.keyattestation.AppApplication; import io.github.vvb2060.keyattestation.attestation.CertificateInfo; public class RemoteProvisioningData extends BaseData { + private static final int SUB = 2; + private static final int SUBJECT_PUBLIC_KEY = -4670552; + private static final int CONFIG_DESCRIPTOR = -4670548; + private static final int MODE = -4670551; + private static final int COMPONENT_NAME = -70002; + private static final int COMPONENT_VERSION = -70003; + private static final int SECURITY_VERSION = -70005; + private final String rkpHostname; private final RpcHardwareInfo hardwareInfo; private final java.util.Map deviceInfo = new ArrayMap<>(); + private final List> diceChain = new ArrayList<>(); + private boolean diceChainValid; + private boolean diceDegenerate; private Throwable error; public RemoteProvisioningData(String rkpHostname, RpcHardwareInfo hardwareInfo, - DeviceInfo deviceInfoData) throws CborException { + DeviceInfo deviceInfoData, byte[] diceChainData) throws CborException { this.rkpHostname = rkpHostname; this.hardwareInfo = hardwareInfo; var deviceInfo = (Map) CborDecoder.decode(deviceInfoData.deviceInfo).get(0); @@ -39,6 +71,131 @@ public RemoteProvisioningData(String rkpHostname, RpcHardwareInfo hardwareInfo, } this.deviceInfo.put(key.toString(), valueString); } + if (diceChainData != null) { + try { + parseDiceChain(diceChainData); + } catch (Exception e) { + Log.w(AppApplication.TAG, "Parse dice chain error.", e); + diceChain.clear(); + } + } + } + + private void parseDiceChain(byte[] data) throws CborException { + var entries = ((Array) CborDecoder.decode(data).get(0)).getDataItems(); + diceDegenerate = entries.size() <= 2; + diceChainValid = verifyDiceChain(entries); + for (var i = 1; i < entries.size(); i++) { + var payloadData = (ByteString) ((Array) entries.get(i)).getDataItems().get(2); + var payload = (Map) CborDecoder.decode(payloadData.getBytes()).get(0); + String name = null; + var details = ""; + var mode = payload.get(new NegativeInteger(MODE)); + if (mode instanceof ByteString bytes && bytes.getBytes().length == 1) { + details = "mode: " + diceModeToString(bytes.getBytes()[0]); + } else if (mode instanceof Number number) { + details = "mode: " + diceModeToString(number.getValue().intValue()); + } + if (payload.get(new NegativeInteger(CONFIG_DESCRIPTOR)) instanceof ByteString config) { + var descriptor = (Map) CborDecoder.decode(config.getBytes()).get(0); + if (descriptor.get(new NegativeInteger(COMPONENT_NAME)) instanceof UnicodeString s) { + name = s.getString(); + } + details = appendItem(details, "securityVersion: ", descriptor.get(new NegativeInteger(SECURITY_VERSION))); + details = appendItem(details, "componentVersion: ", descriptor.get(new NegativeInteger(COMPONENT_VERSION))); + } + if (name == null && payload.get(new UnsignedInteger(SUB)) instanceof UnicodeString s) { + name = s.getString(); + } + diceChain.add(new Pair<>(name != null ? name : "layer " + i, details)); + } + } + + private static String appendItem(String details, String label, DataItem value) { + if (value == null) return details; + if (details.isEmpty()) return label + value; + return details + '\n' + label + value; + } + + private static String diceModeToString(int mode) { + return switch (mode) { + case 1 -> "normal"; + case 2 -> "debug"; + case 3 -> "recovery"; + default -> "not configured"; + }; + } + + // Each entry is a COSE_Sign1 signed by the key of the previous layer (UDS_Pub for the first) + private static boolean verifyDiceChain(List entries) { + try { + var signingKey = (Map) entries.get(0); + for (var i = 1; i < entries.size(); i++) { + var entry = ((Array) entries.get(i)).getDataItems(); + var protectedHeader = ((ByteString) entry.get(0)).getBytes(); + var payloadData = ((ByteString) entry.get(2)).getBytes(); + var signature = ((ByteString) entry.get(3)).getBytes(); + var sigStructure = encodeCbor(new Array() + .add(new UnicodeString("Signature1")) + .add(new ByteString(protectedHeader)) + .add(new ByteString(new byte[0])) + .add(new ByteString(payloadData))); + if (!verifyCoseSign1(signingKey, sigStructure, signature)) { + return false; + } + var payload = (Map) CborDecoder.decode(payloadData).get(0); + if (payload.get(new NegativeInteger(SUBJECT_PUBLIC_KEY)) instanceof ByteString key) { + signingKey = (Map) CborDecoder.decode(key.getBytes()).get(0); + } + } + return true; + } catch (Exception e) { + Log.w(AppApplication.TAG, "Verify dice chain error.", e); + return false; + } + } + + private static boolean verifyCoseSign1(Map coseKey, byte[] message, byte[] signature) + throws Exception { + var keyType = ((Number) coseKey.get(new UnsignedInteger(1))).getValue().intValue(); + var curve = ((Number) coseKey.get(new NegativeInteger(-1))).getValue().intValue(); + var x = ((ByteString) coseKey.get(new NegativeInteger(-2))).getBytes(); + if (keyType == 1) { + var signer = new Ed25519Signer(); + signer.init(false, new Ed25519PublicKeyParameters(x, 0)); + signer.update(message, 0, message.length); + return signer.verifySignature(signature); + } else if (keyType == 2) { + var y = ((ByteString) coseKey.get(new NegativeInteger(-3))).getBytes(); + var name = switch (curve) { + case 2 -> "P-384"; + case 3 -> "P-521"; + default -> "P-256"; + }; + var hash = switch (curve) { + case 2 -> "SHA-384"; + case 3 -> "SHA-512"; + default -> "SHA-256"; + }; + var params = ECNamedCurveTable.getByName(name); + var domain = new ECDomainParameters( + params.getCurve(), params.getG(), params.getN(), params.getH()); + var point = params.getCurve().createPoint(new BigInteger(1, x), new BigInteger(1, y)); + var digest = MessageDigest.getInstance(hash).digest(message); + var signer = new ECDSASigner(); + signer.init(false, new ECPublicKeyParameters(point, domain)); + var half = signature.length / 2; + var r = new BigInteger(1, Arrays.copyOfRange(signature, 0, half)); + var s = new BigInteger(1, Arrays.copyOfRange(signature, half, signature.length)); + return signer.verifySignature(digest, r, s); + } + return false; + } + + private static byte[] encodeCbor(DataItem item) throws CborException { + var buf = new ByteArrayOutputStream(); + new CborEncoder(buf).encode(item); + return buf.toByteArray(); } @SuppressWarnings("unchecked") @@ -65,6 +222,18 @@ public java.util.Map getDeviceInfo() { return deviceInfo; } + public List> getDiceChain() { + return diceChain; + } + + public boolean isDiceChainValid() { + return diceChainValid; + } + + public boolean isDiceDegenerate() { + return diceDegenerate; + } + public Throwable getError() { return error; } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/util/ColorManager.kt b/app/src/main/java/io/github/vvb2060/keyattestation/util/ColorManager.kt index 923eede5..36e0e530 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/util/ColorManager.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/util/ColorManager.kt @@ -3,7 +3,7 @@ package io.github.vvb2060.keyattestation.util import android.app.Activity import android.content.Context import android.content.SharedPreferences -import androidx.annotation.ColorRes +import android.os.Build import androidx.annotation.StyleRes import androidx.appcompat.app.AlertDialog import io.github.vvb2060.keyattestation.R @@ -14,37 +14,30 @@ object ColorManager { private const val KEY_THEME = "theme_id" fun showColorPickerDialog(activity: Activity) { - val colors = arrayOf( - activity.getString(R.string.color_default_gray), - activity.getString(R.string.color_original), - activity.getString(R.string.color_pure_black), - activity.getString(R.string.color_blue), - activity.getString(R.string.color_green), - activity.getString(R.string.color_orange), - activity.getString(R.string.color_pink), - activity.getString(R.string.color_purple), - activity.getString(R.string.color_red) - ) - val themeResIds = intArrayOf( - R.style.Theme_Default_Gray, - R.style.Theme_Original, - R.style.Theme_Pure_Black, - R.style.Theme_Blue, - R.style.Theme_Green, - R.style.Theme_Orange, - R.style.Theme_Pink, - R.style.Theme_Purple, - R.style.Theme_Red - ) + val entries = buildList { + add(R.string.color_default_gray to R.style.Theme_Default_Gray) + add(R.string.color_original to R.style.Theme_Original) + add(R.string.color_pure_black to R.style.Theme_Pure_Black) + add(R.string.color_blue to R.style.Theme_Blue) + add(R.string.color_green to R.style.Theme_Green) + add(R.string.color_orange to R.style.Theme_Orange) + add(R.string.color_pink to R.style.Theme_Pink) + add(R.string.color_purple to R.style.Theme_Purple) + add(R.string.color_red to R.style.Theme_Red) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + add(R.string.color_dynamic to R.style.Theme_Dynamic) + } + } + val colors = Array(entries.size) { activity.getString(entries[it].first) } val prefs = getSharedPreferences(activity) val savedThemeId = prefs.getInt(KEY_THEME, R.style.Theme_Default_Gray) - val savedColorIndex = themeResIds.indexOf(savedThemeId) + val savedColorIndex = entries.indexOfFirst { it.second == savedThemeId } AlertDialog.Builder(activity) .setTitle(R.string.menu_color) .setSingleChoiceItems(colors, savedColorIndex) { dialog, which -> - saveTheme(activity, themeResIds[which]) + saveTheme(activity, entries[which].second) dialog.dismiss() activity.recreate() } diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 77c09311..f5b9a5e4 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -63,6 +63,13 @@ ⚠️ Συσκευή εκτός σύνδεσης - χρήση αποθηκευμένης CRL ❌ Εκτός σύνδεσης - Μη ενημερωμένη CRL + Δεν υπάρχει αλυσίδα πιστοποιητικών RSA +
Για εμφάνιση, απενεργοποιήστε την Βεβαίωση κλειδιού RSA.]]>
+ Δεν υπάρχει αλυσίδα πιστοποιητικών EC +
Για εμφάνιση, ενεργοποιήστε την Βεβαίωση κλειδιού RSA.]]>
+ Μη δυνατή ανάλυση αρχείου +
• Το αρχείο έχει καταστραφεί ή παραποιηθεί

• Μπορεί να περιέχει μη έγκυρους χαρακτήρες

• Μη υποστηριζόμενη μορφή]]>
+ Αναλυτικά μηνύματα: Άγνωστο σφάλμα Μη δυνατή βεβαίωση @@ -180,7 +187,7 @@ Προέλευση Αντίσταση ανατροπής Ρίζα εμπιστοσύνης - Το verifiedBootHash αντιγράφηκε στο πρόχειρο! + Αντιγράφηκε στο πρόχειρο! Έκδοση OS Επίπεδο ενημέρωσης κώδικα OS Αναγνωριστικό Εφαρμογής diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 16016db4..ceaf5ad6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -63,6 +63,13 @@ ⚠️ Dispositivo offline - CRL in cache ❌ Offline - CRL non aggiornata + Nessuna catena di certificati RSA +
Per visualizzarla, disabilita Attesta chiave RSA.]]>
+ Nessuna catena di certificati EC +
Per visualizzarla, abilita Attesta chiave RSA.]]>
+ Impossibile analizzare il file +
• File danneggiato o manomesso

• Potrebbe contenere caratteri non validi

• Formato di file non supportato]]>
+ Messaggi dettagliati: Errore sconosciuto Impossibile attestare @@ -180,7 +187,7 @@ Origine Resistente al rollback Root of trust - verifiedBootHash copiato negli appunti! + Copiato negli appunti! Versione OS Livello patch OS ID app diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 208e2130..670df5f7 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -63,6 +63,13 @@ ⚠️ Dispositivo offline - usando CRL em cache ❌ Offline - CRL desatualizada + Nenhuma cadeia de certificados RSA +
Para exibir, desative Atestar chave RSA.]]>
+ Nenhuma cadeia de certificados EC +
Para exibir, ative Atestar chave RSA.]]>
+ Não foi possível analisar o arquivo +
• Arquivo corrompido ou adulterado

• Pode conter caracteres inválidos

• Formato de arquivo não suportado]]>
+ Mensagens detalhadas: Erro desconhecido Não é possível atestar diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index b57dc91a..cd7b4252 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -63,6 +63,13 @@ ⚠️ Пристрій офлайн — використовується кешований CRL ❌ Офлайн — CRL застарілий + Немає ланцюжка сертифікатів RSA +
Щоб переглянути, вимкніть Атестувати ключ RSA.]]>
+ Немає ланцюжка сертифікатів EC +
Щоб переглянути, увімкніть Атестувати ключ RSA.]]>
+ Не вдалося проаналізувати файл +
• Файл пошкоджено або змінено

• Може містити неприпустимі символи

• Формат файлу не підтримується]]>
+ Детальні повідомлення: Невідома помилка Неможливо атестувати @@ -180,7 +187,7 @@ Походження Стійкий до відкату Корінь довіри - Скопійовано verifiedBootHash до буфера обміну! + Скопійовано до буфера обміну! Версія ОС Рівень патча ОС ID додатка атестації diff --git a/app/src/main/res/values-v31/colors.xml b/app/src/main/res/values-v31/colors.xml new file mode 100644 index 00000000..97f40385 --- /dev/null +++ b/app/src/main/res/values-v31/colors.xml @@ -0,0 +1,5 @@ + + + @android:color/system_accent1_800 + @android:color/system_accent1_700 + diff --git a/app/src/main/res/values-v31/themes_custom.xml b/app/src/main/res/values-v31/themes_custom.xml new file mode 100644 index 00000000..12cd952a --- /dev/null +++ b/app/src/main/res/values-v31/themes_custom.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b79820b0..4bf0abf7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -63,6 +63,13 @@ ⚠️ 设备离线 - 使用缓存的 CRL ❌ 离线 - CRL 已过期 + 无 RSA 证书链 +
要显示,请禁用认证 RSA 密钥。]]>
+ 无 EC 证书链 +
要显示,请启用认证 RSA 密钥。]]>
+ 无法解析文件 +
• 文件已损坏或被篡改

• 可能包含无效字符

• 不支持的文件格式]]>
+ 详细信息: 未知错误 无法认证 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 492d1288..323aac22 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -63,6 +63,13 @@ ⚠️ 裝置離線 - 使用快取的 CRL ❌ 離線 - CRL 已過期 + 無 RSA 憑證鍊 +
若要顯示,請停用認證 RSA 金鑰。]]>
+ 無 EC 憑證鍊 +
若要顯示,請啟用認證 RSA 金鑰。]]>
+ 無法解析檔案 +
• 檔案已毀損或遭篡改

• 可能包含無效字元

• 不支援的檔案格式]]>
+ 詳細資訊: 未知錯誤 無法認證 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64d08fce..09e3dbda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,16 @@ Knox attestation is signed using Samsung attestation key (SAK). OEM root certificate This device trusts this root certificate, but it may not be trusted by others. + Attestation patch level outdated + The attested patch level is older than the patch level of the running system. This is a common sign of tampering. (attestation: %1$s, system: %2$s) + Verified boot hash mismatch + The attested verified boot hash does not match the running system\'s vbmeta digest. This is a strong sign of a spoofed or replayed attestation. + Verified boot hash missing + The device reports an empty or all-zero vbmeta digest, so the boot state was never attested. + Software attestation + This key is not hardware-backed. The attestation was produced in software with no hardware root of trust. + Verified boot key is invalid + The verified boot key is only zeros, which is only expected on an unlocked or unverified bootloader. This is a strong sign of tampering. Certificate chain Certificate chain is a list of certificates used to authenticate a key. The chain starts with the certificate associated with that key, and each certificate is signed by the next in the chain. The chain ends with a root certificate, and the trustworthiness of the attestation depends on the root certificate of the chain. @@ -63,6 +73,13 @@ ⚠️ Device offline - using cached CRL ❌ Offline - CRL Out-of-date + No RSA certificate chain +
To display, disable Attest RSA key.]]>
+ No EC certificate chain +
To display, enable Attest RSA key.]]>
+ Unable to parse file +
• File corrupted or tampered

• May contain invalid characters

• Unsupported file format]]>
+ Detailed messages: Unknown error Unable to attest @@ -124,6 +141,11 @@ An opaque identifier for this IRemotelyProvisionedComponent implementation. Each implementation must have a distinct identifier from all other implementations, and it must be consistent across all devices.
+ EEK curve + + The curve used to validate the Endpoint Encryption Key (EEK) certificate chain and negotiate the GEEK for encrypting ProtectedData. + Only used by the V1/V2 CSR flow; V3 removed the EEK entirely. + Device info Device info contains information about the device that is signed by the IRemotelyProvisionedComponent HAL. @@ -131,6 +153,18 @@ crafted by an IRemotelyProvisionedComponent HAL instance is coming from the expected device based on values initially uploaded during device\'s manufacture at the factory. + DICE certificate chain + + The DICE chain mirrors the boot stages of the device. + Each entry measures the code and configuration of a boot stage and is signed by a key derived by the previous stage. + A degenerate chain contains only a single self-signed entry and carries no boot measurements. + + DICE chain verified + Every entry in the DICE chain is correctly signed by the key of the previous boot stage. + Degenerate DICE chain + The DICE chain is a single self-signed entry. Its signature is valid but it carries no boot stage measurements. + DICE chain signature invalid + A signature in the DICE chain failed to verify against the previous stage key. The chain may be forged or corrupt. Remote provisioning hostname Remote key provisioning disabled @@ -180,7 +214,7 @@ Origin Rollback resistant Root of trust - Copied verifiedBootHash to clipboard! + Copied to clipboard! OS version OS patch level App ID diff --git a/app/src/main/res/values/strings_colors.xml b/app/src/main/res/values/strings_colors.xml index 3595f12c..a1316fe1 100644 --- a/app/src/main/res/values/strings_colors.xml +++ b/app/src/main/res/values/strings_colors.xml @@ -9,4 +9,5 @@ Pink Purple Red + Dynamic (Material You) \ No newline at end of file