diff --git a/app/build.gradle b/app/build.gradle index aa45df1..0c17793 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,4 +101,6 @@ dependencies { def lifecycleVersion = "2.6.2" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + + compileOnly(project(":stub")) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8ecd7a9..5719c12 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" tools:node="remove" /> + + + + + diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java index db8e00a..08517f1 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java @@ -28,6 +28,7 @@ import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1Set; import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERPrintableString; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -63,11 +64,16 @@ public static Long getLongFromAsn1(ASN1Encodable asn1Value) throws CertificatePa public static byte[] getByteArrayFromAsn1(ASN1Encodable asn1Encodable) throws CertificateParsingException { - if (asn1Encodable == null || !(asn1Encodable instanceof DEROctetString)) { + if (asn1Encodable == null) { throw new CertificateParsingException("Expected DEROctetString"); } - ASN1OctetString derOctectString = (ASN1OctetString) asn1Encodable; - return derOctectString.getOctets(); + if (asn1Encodable instanceof DEROctetString) { + return ((ASN1OctetString) asn1Encodable).getOctets(); + } + if (asn1Encodable instanceof DERPrintableString) { + return ((DERPrintableString) asn1Encodable).getOctets(); + } + throw new CertificateParsingException("Expected DEROctetString"); } public static ASN1Encodable getAsn1EncodableFromBytes(byte[] bytes) diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java index d258260..fc0a8d6 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java @@ -34,6 +34,7 @@ * contents. */ public abstract class Attestation { + static final String KNOX_EXTENSION_OID = "1.3.6.1.4.1.236.11.3.23.7"; static final String EAT_OID = "1.3.6.1.4.1.11129.2.1.25"; static final String ASN1_OID = "1.3.6.1.4.1.11129.2.1.17"; static final String KEY_USAGE_OID = "2.5.29.15"; // Standard key usage extension. @@ -64,10 +65,17 @@ public abstract class Attestation { */ public static Attestation loadFromCertificate(X509Certificate x509Cert) throws CertificateParsingException { - if (x509Cert.getExtensionValue(EAT_OID) == null + if (x509Cert.getExtensionValue(KNOX_EXTENSION_OID) == null + && x509Cert.getExtensionValue(EAT_OID) == null && x509Cert.getExtensionValue(ASN1_OID) == null) { throw new CertificateParsingException("No attestation extensions found"); } + if (x509Cert.getExtensionValue(KNOX_EXTENSION_OID) != null) { + if (x509Cert.getExtensionValue(EAT_OID) != null) { + throw new CertificateParsingException("Multiple attestation extensions found"); + } + return new KnoxAttestation(x509Cert); + } if (x509Cert.getExtensionValue(EAT_OID) != null) { if (x509Cert.getExtensionValue(ASN1_OID) != null) { throw new CertificateParsingException("Multiple attestation extensions found"); diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java new file mode 100644 index 0000000..9c5c7da --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java @@ -0,0 +1,110 @@ +package io.github.vvb2060.keyattestation.attestation; + +import com.google.common.io.BaseEncoding; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1SequenceParser; +import org.bouncycastle.asn1.ASN1TaggedObject; + +import java.io.IOException; +import java.security.cert.CertificateParsingException; + +public class AuthResult { + private static final int CALLER_AUTH_RESULT = 0; + private static final int CALLING_PACKAGE = 1; + private static final int CALLING_PACKAGE_SIGS = 2; + private static final int CALLING_PACKAGE_AUTH_RESULT = 3; + + public static final int STATUS_NORMAL = 0; + public static final int STATUS_ABNORMAL = 1; + public static final int STATUS_NOT_SUPPORT = 2; + + private int callerAuthResult; + private byte[] callingPackage; + private byte[] callingPackageSigs; + private int callingPackageAuthResult; + + public AuthResult(ASN1Encodable asn1Encodable) throws CertificateParsingException { + if (!(asn1Encodable instanceof ASN1Sequence sequence)) { + throw new CertificateParsingException("Expected sequence for caller auth, found " + + asn1Encodable.getClass().getName()); + } + + ASN1SequenceParser parser = sequence.parser(); + ASN1TaggedObject entry = parseAsn1TaggedObject(parser); + + for (; entry != null; entry = parseAsn1TaggedObject(parser)) { + int tag = entry.getTagNo(); + ASN1Primitive value = entry.getBaseObject().toASN1Primitive(); + + switch (tag) { + case CALLER_AUTH_RESULT: + callerAuthResult = Asn1Utils.getIntegerFromAsn1(value); + break; + case CALLING_PACKAGE: + callingPackage = Asn1Utils.getByteArrayFromAsn1(value); + break; + case CALLING_PACKAGE_SIGS: + callingPackageSigs = Asn1Utils.getByteArrayFromAsn1(value); + break; + case CALLING_PACKAGE_AUTH_RESULT: + callingPackageAuthResult = Asn1Utils.getIntegerFromAsn1(value); + break; + } + } + } + + private static ASN1TaggedObject parseAsn1TaggedObject(ASN1SequenceParser parser) + throws CertificateParsingException { + ASN1Encodable asn1Encodable = parseAsn1Encodable(parser); + if (asn1Encodable == null || asn1Encodable instanceof ASN1TaggedObject) { + return (ASN1TaggedObject) asn1Encodable; + } + throw new CertificateParsingException( + "Expected tagged object, found " + asn1Encodable.getClass().getName()); + } + + private static ASN1Encodable parseAsn1Encodable(ASN1SequenceParser parser) + throws CertificateParsingException { + try { + return parser.readObject(); + } catch (IOException e) { + throw new CertificateParsingException("Failed to parse ASN1 sequence", e); + } + } + + public String statusToString(int status, boolean isCallingPackageAuthResult) { + switch (status) { + case STATUS_NORMAL: + return "Normal"; + case STATUS_ABNORMAL: + return "Abnormal"; + case STATUS_NOT_SUPPORT: + return "Not support"; + default: + if (isCallingPackageAuthResult) { + return "Not support"; + } + return Integer.toHexString(status); + } + } + + @Override + public String toString() { + try { + StringBuilder sb = new StringBuilder("Caller auth result: ") + .append(statusToString(callerAuthResult, false)).append('\n') + .append("Calling package: ") + .append(new String(callingPackage)).append('\n') + .append("Calling package signatures: ") + .append(BaseEncoding.base64().encode(callingPackageSigs)).append(" (base64)").append('\n') + .append("Calling package auth result: ") + .append(statusToString(callingPackageAuthResult, true)); + return sb.toString(); + } catch (NullPointerException e) { + return "Not performed"; + } + } +} 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 6e27a72..663d035 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 @@ -226,6 +226,7 @@ public class AuthorizationList { private Boolean rollbackResistant; private Boolean rollbackResistance; private RootOfTrust rootOfTrust; + private IntegrityStatus integrityStatus; private Integer osVersion; private Integer osPatchLevel; private Integer vendorPatchLevel; @@ -767,6 +768,14 @@ public RootOfTrust getRootOfTrust() { return rootOfTrust; } + public IntegrityStatus getIntegrityStatus() { + return integrityStatus; + } + + void setIntegrityStatus(IntegrityStatus is) { + integrityStatus = is; + } + public Integer getOsVersion() { return osVersion; } @@ -960,6 +969,15 @@ public String toString() { s.append(rootOfTrust); } + if (integrityStatus != null) { + s.append("\nIntegrity Status:\n"); + s.append(integrityStatus); + if (integrityStatus.getAuthResult() != null) { + s.append("\nCaller Auth Status:\n"); + s.append(integrityStatus.getAuthResult()); + } + } + if (osVersion != null) { s.append("\nOS Version: ").append(osVersion); } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java index 212eb9d..a7f6c0f 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java @@ -10,11 +10,13 @@ import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.security.cert.CertPath; import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import co.nstant.in.cbor.CborDecoder; @@ -28,6 +30,7 @@ public class CertificateInfo { public static final int KEY_UNKNOWN = 0; public static final int KEY_AOSP = 1; public static final int KEY_GOOGLE = 2; + public static final int KEY_SAMSUNG = 3; public static final int CERT_UNKNOWN = 0; public static final int CERT_SIGN = 1; @@ -35,6 +38,24 @@ public class CertificateInfo { public static final int CERT_EXPIRED = 3; public static final int CERT_NORMAL = 4; + private static final String SAMSUNG_SAKV1_ROOT_PUBLIC_KEY = "" + + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBs9Qjr//REhkXW7jUqjY9KNwWac4r" + + "5+kdUGk+TZjRo1YEa47Axwj6AJsbOjo4QsHiYRiWTELvFeiuBsKqyuF0xyAAKvDo" + + "fBqrEq1/Ckxo2mz7Q4NQes3g4ahSjtgUSh0k85fYwwHjCeLyZ5kEqgHG9OpOH526" + + "FFAK3slSUgC8RObbxys="; + + private static final String SAMSUNG_SAKV2_ROOT_PUBLIC_KEY = "" + + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhbGuLrpql5I2WJmrE5kEVZOo+dgA" + + "46mKrVJf/sgzfzs2u7M9c1Y9ZkCEiiYkhTFE9vPbasmUfXybwgZ2EM30A1ABPd12" + + "4n3JbEDfsB/wnMH1AcgsJyJFPbETZiy42Fhwi+2BCA5bcHe7SrdkRIYSsdBRaKBo" + + "ZsapxB0gAOs0jSPRX5M="; + + private static final String SAMSUNG_SAKMV1_ROOT_PUBLIC_KEY = "" + + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB9XeEN8lg6p5xvMVWG42P2Qi/aRKX" + + "2rPRNgK92UlO9O/TIFCKHC1AWCLFitPVEow5W+yEgC2wOiYxgepY85TOoH0AuEkL" + + "oiC6ldbF2uNVU3rYYSytWAJg3GFKd1l9VLDmxox58Hyw2Jmdd5VSObGiTFQ/SgKs" + + "n2fbQPtpGlNxgEfd6Y8="; + private static final String GOOGLE_ROOT_PUBLIC_KEY = "" + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU" + "FmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5j" + @@ -59,6 +80,9 @@ public class CertificateInfo { "MdsGUmX4RFlXYfC78hdLt0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm" + "+Vfkl5YLCazOkjWFmwIDAQAB"; + private static final byte[] sakV1Key = Base64.decode(SAMSUNG_SAKV1_ROOT_PUBLIC_KEY, 0); + private static final byte[] sakV2Key = Base64.decode(SAMSUNG_SAKV2_ROOT_PUBLIC_KEY, 0); + private static final byte[] sakmV1Key = Base64.decode(SAMSUNG_SAKMV1_ROOT_PUBLIC_KEY, 0); private static final byte[] googleKey = Base64.decode(GOOGLE_ROOT_PUBLIC_KEY, 0); private static final byte[] aospEcKey = Base64.decode(AOSP_ROOT_EC_PUBLIC_KEY, 0); private static final byte[] aospRsaKey = Base64.decode(AOSP_ROOT_RSA_PUBLIC_KEY, 0); @@ -107,7 +131,11 @@ public Integer getCertsIssued() { private void checkIssuer() { var publicKey = cert.getPublicKey().getEncoded(); - if (Arrays.equals(publicKey, googleKey)) { + if (Arrays.equals(publicKey, sakV1Key) + || Arrays.equals(publicKey, sakV2Key) + || Arrays.equals(publicKey, sakmV1Key)) { + issuer = KEY_SAMSUNG; + } else if (Arrays.equals(publicKey, googleKey)) { issuer = KEY_GOOGLE; } else if (Arrays.equals(publicKey, aospEcKey)) { issuer = KEY_AOSP; @@ -207,4 +235,68 @@ public static AttestationResult parseCertificateChain(List cert return AttestationResult.form(infoList); } + + private static List sortCerts(List certs) { + if (certs.size() < 2) { + return certs; + } + + var issuer = certs.get(0).getIssuerX500Principal(); + boolean okay = true; + for (var cert : certs) { + var subject = cert.getSubjectX500Principal(); + if (issuer.equals(subject)) { + issuer = subject; + } else { + okay = false; + break; + } + } + if (okay) { + return certs; + } + + var newList = new ArrayList(certs.size()); + for (var cert : certs) { + boolean found = false; + var subject = cert.getSubjectX500Principal(); + for (var c : certs) { + if (c == cert) continue; + if (c.getIssuerX500Principal().equals(subject)) { + found = true; + break; + } + } + if (!found) { + newList.add(cert); + } + } + if (newList.size() != 1) { + return certs; + } + + var oldList = new LinkedList<>(certs); + oldList.remove(newList.get(0)); + for (int i = 0; i < newList.size(); i++) { + issuer = newList.get(i).getIssuerX500Principal(); + for (var it = oldList.iterator(); it.hasNext(); ) { + var cert = it.next(); + if (cert.getSubjectX500Principal().equals(issuer)) { + newList.add(cert); + it.remove(); + break; + } + } + } + if (!oldList.isEmpty()) { + return certs; + } + return newList; + } + + public static AttestationResult parseCertificateChain(CertPath certPath) { + // noinspection unchecked + var certs = (List) certPath.getCertificates(); + return parseCertificateChain(sortCerts(certs)); + } } diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java new file mode 100644 index 0000000..3a1f802 --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java @@ -0,0 +1,117 @@ +package io.github.vvb2060.keyattestation.attestation; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1SequenceParser; +import org.bouncycastle.asn1.ASN1TaggedObject; + +import java.io.IOException; +import java.security.cert.CertificateParsingException; + +public class IntegrityStatus { + private static final int TRUST_BOOT = 0; + private static final int WARRANTY = 1; + private static final int ICD = 2; + private static final int KERNEL_STATUS = 3; + private static final int SYSTEM_STATUS = 4; + private static final int AUTH_RESULT = 5; + + public static final int STATUS_NORMAL = 0; + public static final int STATUS_ABNORMAL = 1; + public static final int STATUS_NOT_SUPPORT = 2; + + private int trustBoot; + private int warranty; + private int icd; + private int kernelStatus; + private int systemStatus; + private AuthResult authResult; + + public IntegrityStatus(ASN1Encodable asn1Encodable) throws CertificateParsingException { + if (!(asn1Encodable instanceof ASN1Sequence sequence)) { + throw new CertificateParsingException("Expected sequence for integrity status, found " + + asn1Encodable.getClass().getName()); + } + + ASN1SequenceParser parser = sequence.parser(); + ASN1TaggedObject entry = parseAsn1TaggedObject(parser); + + for (; entry != null; entry = parseAsn1TaggedObject(parser)) { + int tag = entry.getTagNo(); + ASN1Primitive value = entry.getBaseObject().toASN1Primitive(); + + switch (tag) { + case TRUST_BOOT: + trustBoot = Asn1Utils.getIntegerFromAsn1(value); + break; + case WARRANTY: + warranty = Asn1Utils.getIntegerFromAsn1(value); + break; + case ICD: + icd = Asn1Utils.getIntegerFromAsn1(value); + break; + case KERNEL_STATUS: + kernelStatus = Asn1Utils.getIntegerFromAsn1(value); + break; + case SYSTEM_STATUS: + systemStatus = Asn1Utils.getIntegerFromAsn1(value); + break; + case AUTH_RESULT: + authResult = new AuthResult(value); + break; + } + } + } + + private static ASN1TaggedObject parseAsn1TaggedObject(ASN1SequenceParser parser) + throws CertificateParsingException { + ASN1Encodable asn1Encodable = parseAsn1Encodable(parser); + if (asn1Encodable == null || asn1Encodable instanceof ASN1TaggedObject) { + return (ASN1TaggedObject) asn1Encodable; + } + throw new CertificateParsingException( + "Expected tagged object, found " + asn1Encodable.getClass().getName()); + } + + private static ASN1Encodable parseAsn1Encodable(ASN1SequenceParser parser) + throws CertificateParsingException { + try { + return parser.readObject(); + } catch (IOException e) { + throw new CertificateParsingException("Failed to parse ASN1 sequence", e); + } + } + + public AuthResult getAuthResult() { + return authResult; + } + + public String statusToString(int status) { + switch (status) { + case STATUS_NORMAL: + return "Normal"; + case STATUS_ABNORMAL: + return "Abnormal"; + case STATUS_NOT_SUPPORT: + return "Not support"; + default: + return Integer.toHexString(status); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Trustboot: ") + .append(statusToString(trustBoot)).append('\n') + .append("Warranty bit: ") + .append(statusToString(warranty)).append('\n') + .append("ICD: ") + .append(statusToString(icd)).append('\n') + .append("Kernel status: ") + .append(statusToString(kernelStatus)).append('\n') + .append("System status: ") + .append(statusToString(systemStatus)); + return sb.toString(); + } +} 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 new file mode 100644 index 0000000..24e4fff --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java @@ -0,0 +1,93 @@ +package io.github.vvb2060.keyattestation.attestation; + +import android.os.Build; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1SequenceParser; +import org.bouncycastle.asn1.ASN1TaggedObject; + +import java.io.IOException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +import io.github.vvb2060.keyattestation.AppApplication; + +public class KnoxAttestation extends Asn1Attestation { + static final String RO_PRODUCT_FIRST_API = "ro.product.first_api_level"; + static final int KNOX_TEE_PROPERTIES_INTEGRITY_STATUS = 5; + + IntegrityStatus mKnoxIntegrity; + + /** + * Constructs a {@code KnoxAttestation} object from the provided {@link X509Certificate}, + * extracting the attestation data from the attestation extension. + * + * @param x509Cert + * @throws CertificateParsingException if the certificate does not contain a properly-formatted + * attestation extension. + */ + public KnoxAttestation(X509Certificate x509Cert) throws CertificateParsingException { + super(x509Cert); + ASN1Sequence knoxExtSeq = getKnoxExtensionSequence(x509Cert); + + if (knoxExtSeq != null) { + for (int i = 0; i < knoxExtSeq.size(); i++) { + if (knoxExtSeq.getObjectAt(i) instanceof ASN1TaggedObject entry) { + if (entry.getTagNo() == KNOX_TEE_PROPERTIES_INTEGRITY_STATUS) { + mKnoxIntegrity = new IntegrityStatus(entry.getBaseObject().toASN1Primitive()); + break; + } + } + } + } + + teeEnforced.setIntegrityStatus(mKnoxIntegrity); + } + + ASN1Sequence getKnoxExtensionSequence(X509Certificate x509Cert) + throws CertificateParsingException { + byte[] knoxExtensionSequence = x509Cert.getExtensionValue(Attestation.KNOX_EXTENSION_OID); + if (knoxExtensionSequence == null) { + Log.e(AppApplication.TAG, "getKnoxExtensionSequence : not include knox extension"); + return null; + } + + String value = bytesToHex(knoxExtensionSequence); + + int lengthOfExtension = Integer.parseInt(value.substring(2, 4), 16); + int lengthOfValue = Integer.parseInt(value.substring(10, 12), 16); + String firstApiLevel = SystemProperties.get(RO_PRODUCT_FIRST_API); + + if (!TextUtils.isEmpty(firstApiLevel) + && Integer.parseInt(firstApiLevel) < Build.VERSION_CODES.O) { + if (lengthOfExtension - 4 != lengthOfValue) { + byte[] copy = new byte[lengthOfValue + 6]; + System.arraycopy(knoxExtensionSequence, 0, + copy, 0, lengthOfValue + 6); + System.arraycopy(Integer.toHexString(lengthOfValue + 4).getBytes(), 1, + copy, 1, 1); + System.arraycopy(Integer.toHexString(lengthOfValue + 2).getBytes(), 1, + copy, 3, 1); + knoxExtensionSequence = copy; + } + } + + if (knoxExtensionSequence == null || knoxExtensionSequence.length == 0) { + throw new CertificateParsingException("Did not find extension with OID " + + KNOX_EXTENSION_OID); + } + return Asn1Utils.getAsn1SequenceFromBytes(knoxExtensionSequence); + } + + private String bytesToHex(byte[] a) { + StringBuilder sb = new StringBuilder(); + for (byte b : a) { + sb.append(String.format("%02x", Integer.valueOf(b & 255))); + } + return sb.toString(); + } +} 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 fa85456..c6dcbfc 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 @@ -140,7 +140,8 @@ open class CommonItemViewHolder(itemView: View, binding: HomeCommonItemBindin isClickable = false iconRes = R.drawable.ic_untrustworthy_24 colorAttr = rikka.material.R.attr.colorWarning - } else if (data.issuer == CertificateInfo.KEY_GOOGLE) { + } else if (data.issuer == CertificateInfo.KEY_GOOGLE + || data.issuer == CertificateInfo.KEY_SAMSUNG) { isVisible = true isClickable = false iconRes = R.drawable.ic_trustworthy_24 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 260780a..4ef3897 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 @@ -202,6 +202,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { list.origin?.let { AuthorizationList.originToString(it) }, list.rollbackResistant?.toString(), list.rootOfTrust?.toString(), + list.integrityStatus?.toString(), + list.integrityStatus?.authResult?.toString(), list.osVersion?.toString(), list.osPatchLevel?.toString(), list.attestationApplicationId?.toString()?.trim(), @@ -249,6 +251,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { R.string.authorization_list_origin, R.string.authorization_list_rollbackResistant, R.string.authorization_list_rootOfTrust, + R.string.authorization_list_integrityStatus, + R.string.authorization_list_authResult, R.string.authorization_list_osVersion, R.string.authorization_list_osPatchLevel, R.string.authorization_list_attestationApplicationId, @@ -295,6 +299,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { R.string.authorization_list_origin_description, R.string.authorization_list_rollbackResistant_description, R.string.authorization_list_rootOfTrust_description, + R.string.authorization_list_integrityStatus_description, + R.string.authorization_list_authResult_description, R.string.authorization_list_osVersion_description, R.string.authorization_list_osPatchLevel_description, R.string.authorization_list_attestationApplicationId_description, 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 f614dac..392f5d6 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 @@ -43,7 +43,11 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider { } private val save = registerForActivityResult(CreateDocument("application/x-pkcs7-certificates")) { - viewModel.save(requireContext().contentResolver, it) + viewModel.save(requireContext().contentResolver, it, "PKCS7") + } + + private val save2 = registerForActivityResult(CreateDocument("application/pkix-pkipath")) { + viewModel.save(requireContext().contentResolver, it, "PkiPath") } private val load = registerForActivityResult(OpenDocument()) { @@ -150,13 +154,18 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider { } override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.menu_use_sak).apply { + isVisible = viewModel.hasSAK + isChecked = viewModel.preferSAK + } menu.findItem(R.id.menu_use_strongbox).apply { isVisible = viewModel.hasStrongBox isChecked = viewModel.preferStrongBox } menu.findItem(R.id.menu_use_attest_key).apply { isVisible = viewModel.hasAttestKey - isChecked = viewModel.preferAttestKey + isEnabled = !viewModel.preferSAK + isChecked = !viewModel.preferSAK && viewModel.preferAttestKey } menu.findItem(R.id.menu_incluid_props).apply { isVisible = viewModel.hasDeviceIds @@ -167,6 +176,7 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider { isChecked = viewModel.preferShowAll } menu.findItem(R.id.menu_save).isVisible = viewModel.currentCerts != null + menu.findItem(R.id.menu_save2).isVisible = viewModel.currentCerts != null } override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { @@ -175,6 +185,12 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider { override fun onMenuItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.menu_use_sak -> { + val status = !item.isChecked + item.isChecked = status + viewModel.preferSAK = status + viewModel.load() + } R.id.menu_use_strongbox -> { val status = !item.isChecked item.isChecked = status @@ -202,6 +218,9 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider { R.id.menu_save -> { save.launch("${Build.PRODUCT}-${AppApplication.TAG}.p7b") } + R.id.menu_save2 -> { + save2.launch("${Build.PRODUCT}-${AppApplication.TAG}.pkipath") + } R.id.menu_load -> { load.launch(arrayOf("application/*")) } 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 f1a60ab..64be53f 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 @@ -17,6 +17,8 @@ import android.widget.Toast import androidx.core.content.edit import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.samsung.android.security.keystore.AttestParameterSpec +import com.samsung.android.security.keystore.AttestationUtils import io.github.vvb2060.keyattestation.AppApplication import io.github.vvb2060.keyattestation.attestation.AttestationResult import io.github.vvb2060.keyattestation.attestation.CertificateInfo.parseCertificateChain @@ -30,6 +32,7 @@ import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE_UNAVAILABLE_TRANSIENT import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE_UNKNOWN import io.github.vvb2060.keyattestation.util.Resource +import io.github.vvb2060.keyattestation.util.SamsungUtils import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.IOException @@ -44,12 +47,21 @@ import java.security.cert.CertificateParsingException import java.security.cert.X509Certificate import java.security.spec.ECGenParameterSpec import java.util.Date +import javax.security.auth.x500.X500Principal class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : ViewModel() { val attestationResult = MutableLiveData>() var currentCerts: List? = null + val hasSAK = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + SamsungUtils.isSecAttestationSupported() + var preferSAK = sp.getBoolean("prefer_sak", hasSAK) + set(value) { + field = value + sp.edit { putBoolean("prefer_sak", value) } + } + val hasStrongBox = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pm.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) var preferStrongBox = sp.getBoolean("prefer_strongbox", true) @@ -86,6 +98,7 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie @Throws(GeneralSecurityException::class) private fun generateKey(alias: String, + useSAK: Boolean, useStrongBox: Boolean, includeProps: Boolean, attestKeyAlias: String?) { @@ -111,15 +124,34 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie if (attestKeyAlias != null && !attestKey) { builder.setAttestKeyAlias(attestKeyAlias) } + if (attestKey) { + builder.setCertificateSubject(X500Principal("CN=App Attest Key")) + } } - val keyPairGenerator = KeyPairGenerator.getInstance( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && useSAK) { + val spec = AttestParameterSpec.Builder(alias, now.toString().toByteArray()) + .setAlgorithm(KeyProperties.KEY_ALGORITHM_EC) + .setKeyGenParameterSpec(builder.build()) + .setVerifiableIntegrity(true) + .setPackageName(AppApplication.app.packageName) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && includeProps) { + spec.setDevicePropertiesAttestationIncluded(true) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && attestKey) { + spec.setCertificateSubject(X500Principal("CN=App Attest Key")) + } + AttestationUtils().generateKeyPair(spec.build()) + } else { + val keyPairGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") - keyPairGenerator.initialize(builder.build()) - keyPairGenerator.generateKeyPair() + keyPairGenerator.initialize(builder.build()) + keyPairGenerator.generateKeyPair() + } } @Throws(AttestationException::class) - private fun doAttestation(useStrongBox: Boolean, + private fun doAttestation(useSAK: Boolean, + useStrongBox: Boolean, includeProps: Boolean, useAttestKey: Boolean): AttestationResult { val certs: List @@ -129,9 +161,9 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) if (useAttestKey && !keyStore.containsAlias(attestKeyAlias)) { - generateKey(attestKeyAlias!!, useStrongBox, includeProps, attestKeyAlias) + generateKey(attestKeyAlias!!, useSAK, useStrongBox, includeProps, attestKeyAlias) } - generateKey(alias, useStrongBox, includeProps, attestKeyAlias) + generateKey(alias, useSAK, useStrongBox, includeProps, attestKeyAlias) val chainAlias = if (useAttestKey) attestKeyAlias else alias val certificates = keyStore.getCertificateChain(chainAlias) ?: throw CertificateException("Unable to get certificate chain") @@ -181,7 +213,7 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie return parseCertificateChain(certs) } - fun save(cr: ContentResolver, uri: Uri?) = AppApplication.executor.execute { + fun save(cr: ContentResolver, uri: Uri?, encoding: String) = AppApplication.executor.execute { val certs = currentCerts if (uri == null || certs == null) return@execute var name = uri.toString() @@ -195,7 +227,7 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie try { val cf = CertificateFactory.getInstance("X.509") cr.openOutputStream(uri)?.use { - it.write(cf.generateCertPath(certs).getEncoded("PKCS7")) + it.write(cf.generateCertPath(certs).getEncoded(encoding)) } ?: throw IOException("openOutputStream $uri failed") AppApplication.mainHandler.post { Toast.makeText(AppApplication.app, name, Toast.LENGTH_SHORT).show() @@ -215,17 +247,16 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie val certPath = BufferedInputStream(cr.openInputStream(uri)).use { try { it.mark(8192) - cf.generateCertPath(it, "PKCS7") + cf.generateCertPath(it, "PkiPath") } catch (_: CertificateException) { it.reset() - cf.generateCertPath(it, "PkiPath") + cf.generateCertPath(it, "PKCS7") } } - val certs = certPath.certificates - if (certs.isEmpty()) throw CertificateParsingException("No certificate found") - @Suppress("UNCHECKED_CAST") - val attestationResult = parseCertificateChain(certs as List) - Resource.success(attestationResult) + if (certPath.certificates.isEmpty()) { + throw CertificateParsingException("No certificate found") + } + Resource.success(parseCertificateChain(certPath)) } catch (e: Throwable) { val cause = if (e is AttestationException) e.cause else e Log.w(AppApplication.TAG, "Load attestation error.", cause) @@ -244,11 +275,12 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie currentCerts = null attestationResult.postValue(Resource.loading(null)) + val useSAK = hasSAK && preferSAK val useStrongBox = hasStrongBox && preferStrongBox val includeProps = hasDeviceIds && preferIncludeProps - val useAttestKey = hasAttestKey && preferAttestKey + val useAttestKey = hasAttestKey && preferAttestKey && !useSAK val result = try { - val attestationResult = doAttestation(useStrongBox, includeProps, useAttestKey) + val attestationResult = doAttestation(useSAK, useStrongBox, includeProps, useAttestKey) Resource.success(attestationResult) } catch (e: Throwable) { val cause = if (e is AttestationException) e.cause else e diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/util/SamsungUtils.kt b/app/src/main/java/io/github/vvb2060/keyattestation/util/SamsungUtils.kt new file mode 100644 index 0000000..72434d0 --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/util/SamsungUtils.kt @@ -0,0 +1,60 @@ +package io.github.vvb2060.keyattestation.util + +import android.content.pm.PackageManager +import android.os.SystemProperties +import android.util.Log +import androidx.core.content.ContextCompat +import io.github.vvb2060.keyattestation.AppApplication + +object SamsungUtils { + private const val SAMSUNG_KEYSTORE_PERMISSION = + "com.samsung.android.security.permission.SAMSUNG_KEYSTORE_PERMISSION" + + fun isSecAttestationSupported(): Boolean { + if (!isSamsungKeystoreLibrarySupported()) { + Log.w(AppApplication.TAG, "This device has no samsungkeystoreutils library, " + + "skipping SAK.") + return false + } + + if (!isSAKSupported()) { + Log.w(AppApplication.TAG, "This device has no SAK support, " + + "skipping SAK.") + return false + } + + if (!isKeystorePermissionGranted()) { + Log.e(AppApplication.TAG, "SAMSUNG_KEYSTORE_PERMISSION has not been granted to the app, " + + "skipping SAK.") + return false + } + + return true + } + + private fun isSamsungKeystoreLibrarySupported(): Boolean { + val pm: PackageManager = AppApplication.app.packageManager + val systemSharedLibraries = pm.systemSharedLibraryNames + + if (systemSharedLibraries != null) { + for (lib in systemSharedLibraries) { + if (lib != null && lib.lowercase() == "samsungkeystoreutils") { + return true + } + } + } + + return false + } + + private fun isSAKSupported(): Boolean { + return SystemProperties.get("ro.security.keystore.keytype", "").lowercase() + .contains("sak") + } + + private fun isKeystorePermissionGranted(): Boolean{ + return ContextCompat.checkSelfPermission( + AppApplication.app, SAMSUNG_KEYSTORE_PERMISSION) == + PackageManager.PERMISSION_GRANTED + } +} diff --git a/app/src/main/res/menu/home.xml b/app/src/main/res/menu/home.xml index 63f468d..f5533c1 100644 --- a/app/src/main/res/menu/home.xml +++ b/app/src/main/res/menu/home.xml @@ -1,6 +1,12 @@ + + + + 认证设备属性 显示全部 从文件加载 - 保存到文件 + 保存到文件(p7b) + 保存到文件(pkipath) 关于 Key Attestation + Use Samsung attestation Use StrongBox Use app generated attest key Attest device props Show all Load from file - Save to file + Save to file (p7b) + Save to file (pkipath) About Origin Rollback resistant Root of trust + Integrity status + Caller auth status OS version OS patch level Attestation application ID @@ -210,6 +214,12 @@ This software is open source under %2$s (%1$s). + + diff --git a/settings.gradle b/settings.gradle index d586cc2..8c72f78 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,3 +19,4 @@ dependencyResolutionManagement { } rootProject.name = "KeyAttestation" include(":app") +include(":stub") diff --git a/stub/.gitignore b/stub/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/stub/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/stub/build.gradle b/stub/build.gradle new file mode 100644 index 0000000..cef16a0 --- /dev/null +++ b/stub/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdk 34 + namespace 'io.github.vvb2060.keyattestation.stub' + defaultConfig { + minSdk 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation 'androidx.annotation:annotation:1.6.0' +} diff --git a/stub/src/main/AndroidManifest.xml b/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000..568741e --- /dev/null +++ b/stub/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/stub/src/main/java/android/os/SystemProperties.java b/stub/src/main/java/android/os/SystemProperties.java new file mode 100644 index 0000000..f05f872 --- /dev/null +++ b/stub/src/main/java/android/os/SystemProperties.java @@ -0,0 +1,78 @@ +package android.os; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class SystemProperties { + @NonNull + public static String get(@NonNull String key) { + throw new RuntimeException("Stub!"); + } + + @NonNull + public static String get(@NonNull String key, @Nullable String def) { + throw new RuntimeException("Stub!"); + } + + public static int getInt(@NonNull String key, int def) { + throw new RuntimeException("Stub!"); + } + + public static long getLong(@NonNull String key, long def) { + throw new RuntimeException("Stub!"); + } + + public static boolean getBoolean(@NonNull String key, boolean def) { + throw new RuntimeException("Stub!"); + } + + public static void set(@NonNull String key, @Nullable String val) { + throw new RuntimeException("Stub!"); + } + + public static void addChangeCallback(@NonNull Runnable callback) { + throw new RuntimeException("Stub!"); + } + + public static void removeChangeCallback(@NonNull Runnable callback) { + throw new RuntimeException("Stub!"); + } + + public static void reportSyspropChanged() { + throw new RuntimeException("Stub!"); + } + + public static @NonNull String digestOf(@NonNull String... keys) { + throw new RuntimeException("Stub!"); + } + + private SystemProperties() { + throw new RuntimeException("Stub!"); + } + + @Nullable public static Handle find(@NonNull String name) { + throw new RuntimeException("Stub!"); + } + + public static final class Handle { + @NonNull public String get() { + throw new RuntimeException("Stub!"); + } + + public int getInt(int def) { + throw new RuntimeException("Stub!"); + } + + public long getLong(long def) { + throw new RuntimeException("Stub!"); + } + + public boolean getBoolean(boolean def) { + throw new RuntimeException("Stub!"); + } + + private Handle(long nativeHandle) { + throw new RuntimeException("Stub!"); + } + } +} diff --git a/stub/src/main/java/com/samsung/android/security/keystore/AttestParameterSpec.java b/stub/src/main/java/com/samsung/android/security/keystore/AttestParameterSpec.java new file mode 100644 index 0000000..09f8a9d --- /dev/null +++ b/stub/src/main/java/com/samsung/android/security/keystore/AttestParameterSpec.java @@ -0,0 +1,98 @@ +package com.samsung.android.security.keystore; + +import android.security.keystore.KeyGenParameterSpec; +import androidx.annotation.RequiresApi; +import javax.security.auth.x500.X500Principal; + +@RequiresApi(29) +public class AttestParameterSpec { + public AttestParameterSpec(String algorithm, byte[] challenge, boolean reqAttestDevice, + boolean checkIntegrity, boolean devicePropertiesAttestationIncluded, + String packageName, KeyGenParameterSpec spec, + X500Principal certificateSubject) { + throw new RuntimeException("Stub!"); + } + + public AttestParameterSpec(String algorithm, byte[] challenge, boolean reqAttestDevice, + boolean checkIntegrity, String packageName, KeyGenParameterSpec spec, + X500Principal certificateSubject) { + throw new RuntimeException("Stub!"); + } + + public String getAlgorithm() { + throw new RuntimeException("Stub!"); + } + + public byte[] getChallenge() { + throw new RuntimeException("Stub!"); + } + + public X500Principal getCertificateSubject() { + throw new RuntimeException("Stub!"); + } + + public boolean isDeviceAttestation() { + throw new RuntimeException("Stub!"); + } + + public boolean isVerifiableIntegrity() { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(33) + public boolean isDevicePropertiesAttestationIncluded() { + throw new RuntimeException("Stub!"); + } + + public String getPackageName() { + throw new RuntimeException("Stub!"); + } + + public KeyGenParameterSpec getKeyGenParameterSpec() { + throw new RuntimeException("Stub!"); + } + + public static final class Builder { + public Builder(String alias, byte[] challenge) { + throw new RuntimeException("Stub!"); + } + + public Builder(AttestParameterSpec sourceSpec) { + throw new RuntimeException("Stub!"); + } + + public Builder setAlgorithm(String algorithm) { + throw new RuntimeException("Stub!"); + } + + public Builder setDeviceAttestation(boolean requested) { + throw new RuntimeException("Stub!"); + } + + public Builder setVerifiableIntegrity(boolean checked) { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(33) + public Builder setDevicePropertiesAttestationIncluded( + boolean devicePropertiesAttestationIncluded) { + throw new RuntimeException("Stub!"); + } + + public Builder setPackageName(String packageName) { + throw new RuntimeException("Stub!"); + } + + public Builder setKeyGenParameterSpec(KeyGenParameterSpec spec) { + throw new RuntimeException("Stub!"); + } + + public Builder setCertificateSubject(X500Principal subject) { + throw new RuntimeException("Stub!"); + } + + public AttestParameterSpec build() { + throw new RuntimeException("Stub!"); + } + } +} diff --git a/stub/src/main/java/com/samsung/android/security/keystore/AttestationUtils.java b/stub/src/main/java/com/samsung/android/security/keystore/AttestationUtils.java new file mode 100644 index 0000000..a831230 --- /dev/null +++ b/stub/src/main/java/com/samsung/android/security/keystore/AttestationUtils.java @@ -0,0 +1,76 @@ +package com.samsung.android.security.keystore; + +import android.content.Context; +import androidx.annotation.RequiresApi; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStoreException; +import java.security.ProviderException; +import java.security.cert.Certificate; + +@RequiresApi(28) +public class AttestationUtils { + public static final String DEFAULT_KEYSTORE = "AndroidKeyStore"; + public static final String PUBKEY_DIGEST_ALGORITHM = "SHA-256"; + + public Iterable attestKey(String alias, byte[] challenge) + throws IllegalArgumentException, ProviderException, NullPointerException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public Iterable attestKey(AttestParameterSpec spec) + throws IllegalArgumentException, ProviderException, NullPointerException { + throw new RuntimeException("Stub!"); + } + + public Iterable attestDevice(String alias, byte[] challenge) + throws IllegalArgumentException, ProviderException, NullPointerException, + DeviceIdAttestationException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public Iterable attestDevice(AttestParameterSpec spec) + throws IllegalArgumentException, ProviderException, NullPointerException, + DeviceIdAttestationException { + throw new RuntimeException("Stub!"); + } + + public void storeCertificateChain(String alias, Iterable iterable) + throws KeyStoreException, NullPointerException, ProviderException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public KeyPair generateKeyPair(String alias, byte[] challenge) + throws IllegalArgumentException, ProviderException, NullPointerException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public KeyPair generateKeyPair(AttestParameterSpec spec) + throws IllegalArgumentException, ProviderException, NullPointerException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public Certificate[] getCertificateChain(String alias) { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public Key getKey(String alias) throws KeyStoreException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(29) + public void deleteKey(String alias) throws KeyStoreException { + throw new RuntimeException("Stub!"); + } + + @RequiresApi(33) + public boolean isSupportDeviceAttestation(Context context) { + throw new RuntimeException("Stub!"); + } +} diff --git a/stub/src/main/java/com/samsung/android/security/keystore/DeviceIdAttestationException.java b/stub/src/main/java/com/samsung/android/security/keystore/DeviceIdAttestationException.java new file mode 100644 index 0000000..9c46fb8 --- /dev/null +++ b/stub/src/main/java/com/samsung/android/security/keystore/DeviceIdAttestationException.java @@ -0,0 +1,11 @@ +package com.samsung.android.security.keystore; + +public class DeviceIdAttestationException extends Exception { + public DeviceIdAttestationException(String detailMessage) { + super(detailMessage); + } + + public DeviceIdAttestationException(String message, Throwable cause) { + super(message, cause); + } +}