diff --git a/app/build.gradle b/app/build.gradle index 58a628c..d2584b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,4 +100,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/CertificateInfo.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java index b3e10d3..5159681 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 @@ -64,16 +64,30 @@ public class CertificateInfo { "MdsGUmX4RFlXYfC78hdLt0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm" + "+Vfkl5YLCazOkjWFmwIDAQAB"; + private static final String KNOX_SAKV1_ROOT_PUBLIC_KEY = "" + + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBs9Qjr//REhkXW7jUqjY9KNwWac4r" + + "5+kdUGk+TZjRo1YEa47Axwj6AJsbOjo4QsHiYRiWTELvFeiuBsKqyuF0xyAAKvDo" + + "fBqrEq1/Ckxo2mz7Q4NQes3g4ahSjtgUSh0k85fYwwHjCeLyZ5kEqgHG9OpOH526" + + "FFAK3slSUgC8RObbxys="; + private static final String KNOX_SAKV2_ROOT_PUBLIC_KEY = "" + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhbGuLrpql5I2WJmrE5kEVZOo+dgA" + "46mKrVJf/sgzfzs2u7M9c1Y9ZkCEiiYkhTFE9vPbasmUfXybwgZ2EM30A1ABPd12" + "4n3JbEDfsB/wnMH1AcgsJyJFPbETZiy42Fhwi+2BCA5bcHe7SrdkRIYSsdBRaKBo" + "ZsapxB0gAOs0jSPRX5M="; + private static final String KNOX_SAKMV1_ROOT_PUBLIC_KEY = "" + + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB9XeEN8lg6p5xvMVWG42P2Qi/aRKX" + + "2rPRNgK92UlO9O/TIFCKHC1AWCLFitPVEow5W+yEgC2wOiYxgepY85TOoH0AuEkL" + + "oiC6ldbF2uNVU3rYYSytWAJg3GFKd1l9VLDmxox58Hyw2Jmdd5VSObGiTFQ/SgKs" + + "n2fbQPtpGlNxgEfd6Y8="; + 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); + private static final byte[] knoxSakv1Key = Base64.decode(KNOX_SAKV1_ROOT_PUBLIC_KEY, 0); private static final byte[] knoxSakv2Key = Base64.decode(KNOX_SAKV2_ROOT_PUBLIC_KEY, 0); + private static final byte[] knoxSakmv1Key = Base64.decode(KNOX_SAKMV1_ROOT_PUBLIC_KEY, 0); private static final Set oemKeys = getOemPublicKey(); private final X509Certificate cert; @@ -125,7 +139,9 @@ private void checkIssuer() { issuer = KEY_AOSP; } else if (Arrays.equals(publicKey, aospRsaKey)) { issuer = KEY_AOSP; - } else if (Arrays.equals(publicKey, knoxSakv2Key)) { + } else if (Arrays.equals(publicKey, knoxSakv1Key) + || Arrays.equals(publicKey, knoxSakv2Key) + || Arrays.equals(publicKey, knoxSakmv1Key)) { issuer = KEY_KNOX; } else if (oemKeys != null) { for (var key : oemKeys) { 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 5642693..d8b3078 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 @@ -150,13 +150,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 @@ -171,6 +176,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 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 b03b048..2470434 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 @@ -53,6 +56,14 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie 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) @@ -84,6 +95,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?) { @@ -112,14 +124,30 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie builder.setAttestKeyAlias(attestKeyAlias) } } - 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 = ArrayList() @@ -127,9 +155,9 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie val attestKeyAlias = if (useAttestKey) "${alias}_persistent" else null try { 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 certChain = keyStore.getCertificateChain(alias) ?: throw CertificateException("Unable to get certificate chain") @@ -239,11 +267,12 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie } } + 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 c4178a9..ee6a513 100644 --- a/app/src/main/res/menu/home.xml +++ b/app/src/main/res/menu/home.xml @@ -1,6 +1,12 @@ + + Key Attestation + Use Samsung attestation Use StrongBox Use app generated attest key Attest device props 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); + } +}