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 @@