Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
17708dc
feat(RKP): Add DICE chain details
secp192k1 Jul 2, 2026
996f4c5
feat(Theme): Add dynamic color scheme
secp192k1 Jul 3, 2026
296db60
feat: Add outdated patch level check
secp192k1 Jul 3, 2026
1e32c1f
chore: Ignore kotlin compiler session files
secp192k1 Jul 3, 2026
c51d3d9
feat(RKP): Verify DICE chain signatures
secp192k1 Jul 3, 2026
251fcf0
docs: P-384 KeyMint root cert rotation
secp192k1 Jul 3, 2026
7ff496b
feat: Increase network timeout
secp192k1 Jul 3, 2026
fbc7389
fix: Attestation display
secp192k1 Jul 3, 2026
54ea4a5
feat: Attest vbmeta digest against boot hash
secp192k1 Jul 3, 2026
ac895cc
feat: Check for missing vbmeta digest
secp192k1 Jul 3, 2026
275a5f6
feat: Check for software key
secp192k1 Jul 3, 2026
539dde4
fix(SoftwareAttestation): Forgot to flip check... whoops!
secp192k1 Jul 3, 2026
791df91
feat: Check for all zero boot key
secp192k1 Jul 6, 2026
a51fef8
style: Better osVersion display
secp192k1 Jul 6, 2026
dca4d58
feat(Attestation): Format patch level fields as dates
secp192k1 Jul 6, 2026
1424b45
chore: Optimized imports
secp192k1 Jul 6, 2026
a25a876
feat(RKP): Show EEK curve in hardware info
secp192k1 Jul 6, 2026
378b1ad
feat: Long-press to copy root-of-trust
secp192k1 Jul 6, 2026
9bd086a
Feature: Load XML file
VisionR1 Jul 13, 2026
0926e40
Merge branch 'master' of https://github.com/secp192k1/KeyAttestation
secp192k1 Jul 13, 2026
73a0d98
fix: Bugs because I was trolling
secp192k1 Jul 13, 2026
116135d
fix(certs): Use preferred algorithm in fallback loader
secp192k1 Jul 13, 2026
7b5c2ff
fix(KnoxAttestation): Null record hash
secp192k1 Jul 13, 2026
027a359
feat(RKP): Failsafe for empty EEK curve
secp192k1 Jul 13, 2026
723eccc
fix Implement hashCode for AttestationApplicationId/Package with vers…
secp192k1 Jul 13, 2026
a8a857f
feat(AttestationException): Wildcard import
secp192k1 Jul 13, 2026
5aed1a3
feat(EatClaim): Wildcard imports of `AuthorizationList`
secp192k1 Jul 13, 2026
f3951ed
fix(network): Abort stuck connection on refresh timeout
secp192k1 Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
.externalNativeBuild
.cxx
/*.jks
/.kotlin/sessions/kotlin-compiler-*.salive
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttestationApplicationId> {
Expand Down Expand Up @@ -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<AttestationPackageInfo> parseAttestationPackageInfos(ASN1Encodable asn1Encodable)
throws CertificateParsingException {
if (!(asn1Encodable instanceof ASN1Set set)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttestationPackageInfo> {
private static final int PACKAGE_NAME_INDEX = 0;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@

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;
import java.security.cert.X509Certificate;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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<HttpURLConnection> 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;
Expand Down Expand Up @@ -132,12 +134,15 @@ private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime)
}

private static NetworkResult fetchNetworkWithTimeout(String url, long cachedTime) {
Future<NetworkResult> future = networkExecutor.submit(() -> fetchFromNetwork(url, cachedTime));
var connRef = new AtomicReference<HttpURLConnection>();
Future<NetworkResult> 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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@ 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

open class CommonItemViewHolder<T>(itemView: View, binding: HomeCommonItemBinding) :
HomeViewHolder<T, HomeCommonItemBinding>(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<Pair<String, String>> { inflater, parent ->
val binding = HomeCommonItemBinding.inflate(inflater, parent, false)
object : CommonItemViewHolder<Pair<String, String>>(binding.root, binding) {
Expand All @@ -37,7 +48,11 @@ open class CommonItemViewHolder<T>(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
}
}
}
}
Expand Down Expand Up @@ -97,29 +112,11 @@ open class CommonItemViewHolder<T>(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
}
}
Expand Down
Loading