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/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/EatClaim.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatClaim.java index a6977e7a..e9cdaa5b 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatClaim.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatClaim.java @@ -1,6 +1,47 @@ package io.github.vvb2060.keyattestation.attestation; -import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.*; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KEYMASTER_TAG_TYPE_MASK; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ACTIVE_DATETIME; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ALGORITHM; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ALLOW_WHILE_ON_BODY; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_APPLICATION_ID; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_APPLICATION_ID; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_BRAND; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_DEVICE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_MANUFACTURER; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_MEID; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_MODEL; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_PRODUCT; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ATTESTATION_ID_SERIAL; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_AUTH_TIMEOUT; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_BLOCK_MODE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_BOOT_PATCHLEVEL; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_CALLER_NONCE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_DEVICE_UNIQUE_ATTESTATION; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_DIGEST; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_EARLY_BOOT_ONLY; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_EC_CURVE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_IDENTITY_CREDENTIAL_KEY; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_KDF; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_KEY_SIZE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_MIN_MAC_LENGTH; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_NO_AUTH_REQUIRED; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ORIGIN; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ORIGINATION_EXPIRE_DATETIME; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_OS_PATCHLEVEL; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_OS_VERSION; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_PADDING; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_PURPOSE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ROLLBACK_RESISTANCE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_ROLLBACK_RESISTANT; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_RSA_OAEP_MGF_DIGEST; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_RSA_PUBLIC_EXPONENT; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_TRUSTED_CONFIRMATION_REQUIRED; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_UNLOCKED_DEVICE_REQUIRED; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_USAGE_EXPIRE_DATETIME; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_USER_AUTH_TYPE; +import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.KM_TAG_VENDOR_PATCHLEVEL; class EatClaim { public static final int IAT = 6; 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..5b9ddc74 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 @@ -95,8 +95,8 @@ private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime) URL url = new URL(statusUrl); connection = (HttpURLConnection) url.openConnection(); 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; 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/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..0aa13bd4 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 @@ -87,11 +89,13 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { } 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++) + if (baseData.certs.isNotEmpty()) { + 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++) + } } // Add revocation list information with source status @@ -145,6 +149,42 @@ 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, @@ -262,6 +302,11 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { 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,6 +315,36 @@ 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) { @@ -308,14 +383,28 @@ 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) }, @@ -345,8 +434,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { list.origin?.let { AuthorizationList.originToString(it) }, list.rollbackResistant?.toString(), list.rootOfTrust?.toString(), - list.osVersion?.toString(), - list.osPatchLevel?.toString(), + list.osVersion?.let { AuthorizationList.osVersionToString(it) }, + list.osPatchLevel?.let { AuthorizationList.patchLevelToString(it) }, list.attestationApplicationId?.toString()?.trim(), list.brand, list.device, @@ -357,8 +446,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { list.meid, list.manufacturer, list.model, - list.vendorPatchLevel?.toString(), - list.bootPatchLevel?.toString(), + 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) }, 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/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/RemoteProvisioning.java b/app/src/main/java/io/github/vvb2060/keyattestation/keystore/RemoteProvisioning.java index d0506943..9b3eefa3 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); @@ -269,6 +274,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/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..0cdffbc2 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 @@ -3,7 +3,16 @@ import static android.security.KeyStoreException.ERROR_ATTESTATION_KEYS_UNAVAILABLE; import static android.security.KeyStoreException.ERROR_ID_ATTESTATION_FAILURE; import static android.security.KeyStoreException.ERROR_KEYMINT_FAILURE; -import static io.github.vvb2060.keyattestation.lang.AttestationException.*; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_CANT_PARSE_CERT; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_DEVICEIDS_UNAVAILABLE; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_KEYS_NOT_PROVISIONED; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_OUT_OF_KEYS; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_OUT_OF_KEYS_TRANSIENT; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_RKP; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_STRONGBOX_UNAVAILABLE; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_UNAVAILABLE; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_UNAVAILABLE_TRANSIENT; +import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_UNKNOWN; import android.annotation.SuppressLint; import android.hardware.security.keymint.DeviceInfo; @@ -191,7 +200,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); @@ -263,7 +279,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/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..38506efe 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -180,7 +180,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..c661c55c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -180,7 +180,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-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index b57dc91a..6c897263 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -180,7 +180,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/strings.xml b/app/src/main/res/values/strings.xml index 64d08fce..8cce76f6 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. @@ -124,6 +134,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 +146,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 +207,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