├── .gitattributes ├── .github └── workflows │ └── android.yml ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ ├── android │ │ └── hardware │ │ │ └── security │ │ │ └── keymint │ │ │ ├── DeviceInfo.aidl │ │ │ ├── IRemotelyProvisionedComponent.aidl │ │ │ ├── MacedPublicKey.aidl │ │ │ ├── ProtectedData.aidl │ │ │ └── RpcHardwareInfo.aidl │ └── io │ │ └── github │ │ └── vvb2060 │ │ └── keyattestation │ │ └── keystore │ │ └── IAndroidKeyStore.aidl │ ├── java │ └── io │ │ └── github │ │ └── vvb2060 │ │ └── keyattestation │ │ ├── AppApplication.kt │ │ ├── app │ │ ├── AlertDialogFragment.kt │ │ ├── AppActivity.kt │ │ ├── AppBarActivity.kt │ │ └── AppFragment.kt │ │ ├── attestation │ │ ├── Asn1Attestation.java │ │ ├── Asn1Utils.java │ │ ├── Attestation.java │ │ ├── AttestationApplicationId.java │ │ ├── AttestationPackageInfo.java │ │ ├── AuthResult.java │ │ ├── AuthorizationList.java │ │ ├── CborUtils.java │ │ ├── CertificateInfo.java │ │ ├── EatAttestation.java │ │ ├── EatClaim.java │ │ ├── IntegrityStatus.java │ │ ├── KnoxAttestation.java │ │ ├── ProvisioningInfo.java │ │ ├── RevocationList.java │ │ ├── RootOfTrust.java │ │ └── RootPublicKey.java │ │ ├── home │ │ ├── BootStateViewHolder.kt │ │ ├── CommonItemViewHolder.kt │ │ ├── Data.kt │ │ ├── ErrorViewHolder.kt │ │ ├── HeaderViewHolder.kt │ │ ├── HomeActivity.kt │ │ ├── HomeAdapter.kt │ │ ├── HomeFragment.kt │ │ ├── HomeItemDecoration.kt │ │ ├── HomeViewHolder.kt │ │ ├── HomeViewModel.kt │ │ └── SubtitleViewHolder.kt │ │ ├── keystore │ │ ├── AndroidKeyStore.java │ │ ├── ContextHook.java │ │ ├── KeyBoxXmlParser.java │ │ ├── KeyStoreManager.java │ │ └── RemoteProvisioning.java │ │ ├── lang │ │ └── AttestationException.kt │ │ ├── repository │ │ ├── AttestationData.java │ │ ├── AttestationRepository.java │ │ ├── BaseData.java │ │ └── RemoteProvisioningData.java │ │ └── util │ │ ├── Resource.kt │ │ └── ViewBindingViewHolder.kt │ └── res │ ├── color │ ├── material_on_surface_emphasis_high_type.xml │ ├── material_on_surface_emphasis_medium.xml │ └── mtrl_popupmenu_overlay_color.xml │ ├── drawable-v26 │ └── ic_launcher.xml │ ├── drawable │ ├── home_item_background_solid.xml │ ├── ic_boot_locked_24.xml │ ├── ic_boot_unknown_24.xml │ ├── ic_boot_unlocked_24.xml │ ├── ic_error_outline_24.xml │ ├── ic_help_outline_24.xml │ ├── ic_info_outline_24.xml │ ├── ic_launcher.xml │ ├── ic_trustworthy_24.xml │ ├── ic_untrustworthy_24.xml │ └── ic_warning_24.xml │ ├── layout │ ├── appbar.xml │ ├── appbar_activity.xml │ ├── appbar_fragment_activity.xml │ ├── home.xml │ ├── home_boot_state.xml │ ├── home_common_item.xml │ ├── home_error.xml │ ├── home_header.xml │ └── home_subtitle.xml │ ├── menu │ └── home.xml │ ├── mipmap-hdpi │ ├── ic_key_attestation.png │ ├── ic_key_attestation_background.png │ └── ic_key_attestation_foreground.png │ ├── mipmap-xhdpi │ ├── ic_key_attestation.png │ ├── ic_key_attestation_background.png │ └── ic_key_attestation_foreground.png │ ├── mipmap-xxhdpi │ ├── ic_key_attestation.png │ ├── ic_key_attestation_background.png │ └── ic_key_attestation_foreground.png │ ├── mipmap-xxxhdpi │ ├── ic_key_attestation.png │ ├── ic_key_attestation_background.png │ └── ic_key_attestation_foreground.png │ ├── raw │ └── status.json │ ├── resources.properties │ ├── values-el │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-sw600dp │ └── dimens.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ ├── strings_no_translate.xml │ ├── styles.xml │ ├── themes.xml │ └── themes_override.xml ├── art ├── icon.ai └── icon_playstore.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── stub ├── .gitignore ├── build.gradle └── src └── main ├── AndroidManifest.xml └── java ├── android ├── app │ ├── ActivityThread.java │ └── ContextImpl.java ├── os │ ├── ServiceManager.java │ ├── ServiceSpecificException.java │ └── SystemProperties.java ├── security │ ├── keystore │ │ ├── AndroidKeyStoreProvider.java │ │ ├── AttestationUtils.java │ │ ├── DeviceIdAttestationException.java │ │ └── KeyGenParameterSpec_rename.java │ └── keystore2 │ │ └── AndroidKeyStoreProvider.java └── telephony │ └── TelephonyManager_rename.java └── com └── samsung └── android └── security └── keystore ├── AttestParameterSpec.java └── AttestationUtils.java /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.jar binary 5 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [ ubuntu-latest, windows-latest, macOS-latest ] 13 | 14 | steps: 15 | - name: Check out 16 | uses: actions/checkout@v4 17 | - name: Set up JDK 21 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: '21' 22 | - name: Setup Gradle 23 | uses: gradle/actions/setup-gradle@v4 24 | with: 25 | build-scan-publish: true 26 | build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" 27 | build-scan-terms-of-use-agree: "yes" 28 | - name: Build with Gradle 29 | shell: bash 30 | run: | 31 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 32 | echo 'org.gradle.caching=true' >> gradle.properties 33 | echo 'org.gradle.parallel=true' >> gradle.properties 34 | ./gradlew assemble 35 | - name: Upload build artifact 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: ${{ matrix.os }}-artifact 39 | path: app/build/outputs 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | /*.jks 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android Key Attestation Test App 2 | ============================== 3 | 4 | This app supports generating, saving, loading, parsing and verifying Android [key and ID attestation](https://source.android.com/docs/security/features/keystore/attestation) data. 5 | 6 | The app is used for self-testing, so it has no network permission. The certificate revocation data is embedded in the apk and will not be updated online. If the system is compromised, parsing and verifying is not safe, you should save the data to a file and then load the file on another device to verify it. 7 | 8 | This app also supports loading certificate chain generated by other software. 9 | 10 | Useful links 11 | --- 12 | 13 | [Official documentation](https://developer.android.com/privacy-and-security/security-key-attestation) 14 | 15 | [Official implementation](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java) 16 | 17 | [Authorization tags](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Tag.aidl) 18 | 19 | [Key attestation extension data schema](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl) 20 | 21 | [RKP documentation](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/rkp/README.md) 22 | 23 | License 24 | ------- 25 | 26 | Licensed under the Apache License, Version 2.0 (the "License"); 27 | you may not use this file except in compliance with the License. 28 | You may obtain a copy of the License at 29 | 30 | http://www.apache.org/licenses/LICENSE-2.0 31 | 32 | Unless required by applicable law or agreed to in writing, software 33 | distributed under the License is distributed on an "AS IS" BASIS, 34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 35 | See the License for the specific language governing permissions and 36 | limitations under the License. 37 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import com.android.build.api.instrumentation.* 2 | import com.android.build.gradle.internal.tasks.CompileArtProfileTask 3 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 4 | import org.objectweb.asm.ClassVisitor 5 | import org.objectweb.asm.commons.ClassRemapper 6 | import org.objectweb.asm.commons.Remapper 7 | 8 | plugins { 9 | id 'com.android.application' 10 | id 'org.jetbrains.kotlin.android' 11 | } 12 | 13 | def gitCommitCount 14 | try { 15 | gitCommitCount = providers.exec { 16 | commandLine("git", "rev-list", "--count", "HEAD") 17 | }.standardOutput.asText.get().toInteger() 18 | } catch (ignored) { 19 | gitCommitCount = 1 20 | } 21 | 22 | def properties = new Properties() 23 | def local = project.rootProject.file("local.properties") 24 | if (local.exists()) { 25 | properties.load(local.newDataInputStream()) 26 | } 27 | 28 | android { 29 | compileSdk = 35 30 | buildToolsVersion = '35.0.1' 31 | namespace = 'io.github.vvb2060.keyattestation' 32 | defaultConfig { 33 | minSdk = 24 34 | targetSdk = 35 35 | versionCode = gitCommitCount 36 | versionName = '1.8.4' 37 | resourceConfigurations += ['en', 'zh-rCN', 'zh-rTW', 'pt-rBR', 'el'] 38 | optimization { 39 | keepRules { 40 | ignoreFromAllExternalDependencies true 41 | } 42 | } 43 | } 44 | 45 | signingConfigs { 46 | debug { 47 | if (properties.getProperty("storeFile") != null) { 48 | storeFile file(properties.getProperty("storeFile")) 49 | storePassword properties.getProperty("storePassword") 50 | keyAlias properties.getProperty("keyAlias") 51 | keyPassword properties.getProperty("keyPassword") 52 | } 53 | } 54 | } 55 | buildTypes { 56 | debug { 57 | versionNameSuffix '-debug' 58 | } 59 | release { 60 | minifyEnabled true 61 | shrinkResources true 62 | vcsInfo.include false 63 | signingConfig signingConfigs.debug 64 | proguardFiles 'proguard-rules.pro' 65 | } 66 | } 67 | 68 | compileOptions { 69 | sourceCompatibility = JavaVersion.VERSION_21 70 | targetCompatibility = JavaVersion.VERSION_21 71 | } 72 | kotlinOptions { 73 | jvmTarget = "21" 74 | options.languageVersion = KotlinVersion.KOTLIN_2_0 75 | options.progressiveMode = true 76 | } 77 | 78 | buildFeatures { 79 | viewBinding = true 80 | buildConfig = true 81 | aidl = true 82 | } 83 | 84 | packagingOptions { 85 | resources { 86 | excludes += '**' 87 | } 88 | } 89 | 90 | androidResources { 91 | generateLocaleConfig true 92 | } 93 | 94 | installation { 95 | installOptions += ["--user 0"] 96 | } 97 | 98 | lint.checkReleaseBuilds false 99 | dependenciesInfo.includeInApk false 100 | } 101 | 102 | tasks.withType(CompileArtProfileTask.class).configureEach { 103 | enabled = false 104 | } 105 | 106 | configurations.configureEach { 107 | exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat' 108 | exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7' 109 | exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' 110 | } 111 | 112 | dependencies { 113 | compileOnly project(':stub') 114 | 115 | implementation 'org.bouncycastle:bcprov-jdk18on:1.80' 116 | implementation 'com.google.guava:guava:33.4.0-android' 117 | implementation 'co.nstant.in:cbor:0.9' 118 | 119 | //noinspection GradleDependency 120 | implementation 'dev.rikka.rikkax.material:material:1.6.6' 121 | implementation 'dev.rikka.rikkax.html:html-ktx:1.1.2' 122 | implementation 'dev.rikka.rikkax.recyclerview:recyclerview-adapter:1.3.0' 123 | implementation 'dev.rikka.rikkax.widget:borderview:1.1.0' 124 | implementation 'dev.rikka.shizuku:api:13.1.5' 125 | 126 | implementation 'androidx.core:core-ktx:1.15.0' 127 | implementation 'androidx.appcompat:appcompat:1.7.0' 128 | implementation 'androidx.activity:activity-ktx:1.10.0' 129 | implementation 'androidx.fragment:fragment-ktx:1.8.5' 130 | implementation 'androidx.recyclerview:recyclerview:1.4.0' 131 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7' 132 | implementation 'com.google.android.material:material:1.12.0' 133 | } 134 | 135 | androidComponents { 136 | onVariants(selector().all(), { 137 | instrumentation.transformClassesWith(ClassVisitorFactory.class, 138 | InstrumentationScope.PROJECT) {} 139 | }) 140 | } 141 | 142 | abstract class ClassVisitorFactory implements AsmClassVisitorFactory { 143 | ClassVisitor createClassVisitor(ClassContext classContext, ClassVisitor classVisitor) { 144 | return new ClassRemapper(classVisitor, new Remapper() { 145 | String map(String name) { 146 | var index = name.indexOf('$') 147 | if (index != -1) { 148 | return map(name.substring(0, index)) + name.substring(index) 149 | } 150 | if (name.endsWith("_rename")) { 151 | return name.substring(0, name.length() - 7) 152 | } 153 | return name 154 | } 155 | }) 156 | } 157 | 158 | boolean isInstrumentable(ClassData classData) { 159 | return classData.className.startsWith("io.github.vvb2060.keyattestation.keystore.") 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -allowaccessmodification 2 | -repackageclasses 3 | 4 | -keepclassmembers class * implements android.os.Parcelable { 5 | public static final ** CREATOR; 6 | } 7 | 8 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 9 | public static void check*(...); 10 | public static void throw*(...); 11 | } 12 | 13 | -assumenosideeffects class java.util.Objects{ 14 | ** requireNonNull(...); 15 | } 16 | 17 | -assumenosideeffects class android.util.Log { 18 | public static int v(...); 19 | public static int d(...); 20 | } 21 | 22 | -keep class com.google.android.material.theme.MaterialComponentsViewInflater { 23 | (); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 16 | 17 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 49 | 50 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/hardware/security/keymint/DeviceInfo.aidl: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.hardware.security.keymint; 18 | 19 | /** 20 | * DeviceInfo contains information about the device that's signed by the 21 | * IRemotelyProvisionedComponent HAL. These values are intended to be checked by the server to 22 | * verify that the certificate signing request crafted by an IRemotelyProvisionedComponent HAL 23 | * instance is coming from the expected device based on values initially uploaded during device 24 | * manufacture in the factory. 25 | * @hide 26 | */ 27 | parcelable DeviceInfo { 28 | /** 29 | * DeviceInfo is a CBOR Map structure described by the following CDDL. DeviceInfo must be 30 | * ordered according to the Length-First Map Key Ordering specified in RFC 8949, 31 | * Section 4.2.3. Please note that the ordering presented here groups similar entries 32 | * semantically, and not in the correct order per RFC 8949, Section 4.2.3. 33 | * 34 | * The DeviceInfo has changed across versions 1, 2, and 3 of the HAL. All versions of the 35 | * DeviceInfo CDDL are described in the DeviceInfoV*.cddl files. Please refer to the CDDL 36 | * structure version that corresponds to the HAL version you are working with. 37 | * 38 | */ 39 | byte[] deviceInfo; 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/hardware/security/keymint/MacedPublicKey.aidl: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.hardware.security.keymint; 18 | 19 | /** 20 | * MacedPublicKey contains a CBOR-encoded public key, MACed by an IRemotelyProvisionedComponent, to 21 | * prove that the key pair was generated by that component. 22 | * @hide 23 | */ 24 | parcelable MacedPublicKey { 25 | /** 26 | * key is a COSE_Mac0 structure containing the new public key. It's MACed by a key available 27 | * only to the secure environment, as proof that the public key was generated by that 28 | * environment. In CDDL, assuming the contained key is a P-256 public key: 29 | * 30 | * See MacedPublicKey.cddl for CDDL definition. 31 | * 32 | */ 33 | byte[] macedKey; 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/hardware/security/keymint/RpcHardwareInfo.aidl: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.hardware.security.keymint; 18 | 19 | /** 20 | * RpcHardwareInfo is the hardware information returned by calling RemotelyProvisionedComponent 21 | * getHardwareInfo() 22 | * @hide 23 | */ 24 | parcelable RpcHardwareInfo { 25 | const int CURVE_NONE = 0; 26 | const int CURVE_P256 = 1; 27 | const int CURVE_25519 = 2; 28 | 29 | /** 30 | * Implementation version of the remotely provisioned component hardware. The version provided 31 | * here must match the version reported in the CsrPayload produced by the HAL interface. This 32 | * field primarily acts as a convenience for the system components interacting with the HALs. 33 | */ 34 | int versionNumber; 35 | 36 | /** 37 | * rpcAuthorName is the name of the author of the IRemotelyProvisionedComponent implementation 38 | * (organization name, not individual). This name is implementation defined, so it can be used 39 | * to distinguish between different implementations from the same author. 40 | */ 41 | String rpcAuthorName; 42 | 43 | /** 44 | * NOTE: This field is no longer used as of version 3 of the HAL interface. This is because the 45 | * Endpoint Encryption Key is no longer used in the provisioning scheme. 46 | * 47 | * supportedEekCurve returns an int representing which curve is supported for validating 48 | * signatures over the Endpoint Encryption Key certificate chain and for using the corresponding 49 | * signed encryption key in ECDH. Only one curve should be supported, with preference for 25519 50 | * if it's available. These values are defined as constants above. 51 | * 52 | * CURVE_NONE is made the default to help ensure that an implementor doesn't accidentally forget 53 | * to provide the correct information here, as the VTS tests will check to make certain that 54 | * a passing implementation does not provide CURVE_NONE. 55 | */ 56 | int supportedEekCurve = CURVE_NONE; 57 | 58 | /** 59 | * uniqueId is an opaque identifier for this IRemotelyProvisionedComponent implementation. The 60 | * client should NOT interpret the content of the identifier in any way. The client can only 61 | * compare identifiers to determine if two IRemotelyProvisionedComponents share the same 62 | * implementation. Each IRemotelyProvisionedComponent implementation must have a distinct 63 | * identifier from all other implementations, and it must be consistent across all devices. 64 | * It's critical that this identifier not be usable to uniquely identify a specific device. 65 | * 66 | * This identifier must be consistent across reboots, as it is used to store and track 67 | * provisioned keys in a persistent, on-device database. 68 | * 69 | * uniqueId may not be empty, and must not be any longer than 32 characters. 70 | * 71 | * A recommended construction for this value is "[Vendor] [Component Name] [Major Version]", 72 | * e.g. "Google Trusty KeyMint 1". 73 | * 74 | * This field was added in API version 2. 75 | * 76 | */ 77 | String uniqueId; 78 | 79 | /** 80 | * supportedNumKeysInCsr is the maximum number of keys in a CSR that this implementation can 81 | * support. This value is implementation defined. 82 | * 83 | * From version 3 onwards, supportedNumKeysInCsr must be larger or equal to 84 | * MIN_SUPPORTED_NUM_KEYS_IN_CSR. 85 | * 86 | * The default value was chosen as the value enforced by the VTS test in versions 1 and 2 of 87 | * this interface. 88 | */ 89 | const int MIN_SUPPORTED_NUM_KEYS_IN_CSR = 20; 90 | int supportedNumKeysInCsr = 4; 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/aidl/io/github/vvb2060/keyattestation/keystore/IAndroidKeyStore.aidl: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.keystore; 2 | 3 | import android.hardware.security.keymint.DeviceInfo; 4 | import android.hardware.security.keymint.RpcHardwareInfo; 5 | 6 | interface IAndroidKeyStore { 7 | byte[] getCertificateChain(String alias); 8 | boolean containsAlias(String alias); 9 | void deleteAllEntry(); 10 | void importKeyBox(String alias, boolean useStrongBox, in ParcelFileDescriptor pfd); 11 | byte[] generateKeyPair(String alias, String attestKeyAlias, boolean useStrongBox, 12 | boolean includeProps, boolean uniqueIdIncluded, int idFlags, 13 | boolean useSak); 14 | byte[] attestDeviceIds(int idFlags); 15 | void setRkpHostname(String hostname); 16 | String getRkpHostname(); 17 | boolean canRemoteProvisioning(boolean useStrongBox); 18 | RpcHardwareInfo getHardwareInfo(boolean useStrongBox, out DeviceInfo deviceInfo); 19 | byte[] checkRemoteProvisioning(boolean useStrongBox); 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/AppApplication.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import android.widget.Toast 8 | import androidx.arch.core.executor.ArchTaskExecutor 9 | import io.github.vvb2060.keyattestation.keystore.KeyStoreManager 10 | import org.bouncycastle.jce.provider.BouncyCastleProvider 11 | import rikka.html.text.HtmlCompat 12 | import rikka.material.app.DayNightDelegate 13 | import rikka.sui.Sui 14 | import java.security.Security 15 | import java.util.concurrent.ExecutorService 16 | import java.util.concurrent.Executors 17 | 18 | class AppApplication : Application() { 19 | companion object { 20 | const val TAG = "KeyAttestation" 21 | lateinit var app: AppApplication 22 | val executor: ExecutorService = Executors.newSingleThreadExecutor() 23 | 24 | @SuppressLint("RestrictedApi") 25 | fun toast(text: String?) { 26 | ArchTaskExecutor.getInstance().postToMainThread { 27 | Toast.makeText(app, text, Toast.LENGTH_LONG).show() 28 | } 29 | } 30 | } 31 | 32 | override fun onCreate() { 33 | super.onCreate() 34 | app = this 35 | DayNightDelegate.setApplicationContext(this) 36 | DayNightDelegate.setDefaultNightMode(DayNightDelegate.MODE_NIGHT_FOLLOW_SYSTEM) 37 | HtmlCompat.setContext(this) 38 | installProvider(this) 39 | 40 | if (Sui.init(BuildConfig.APPLICATION_ID)) { 41 | KeyStoreManager.requestPermission(); 42 | } else { 43 | KeyStoreManager.requestBinder(this) 44 | } 45 | } 46 | 47 | private fun installProvider(context: Context) { 48 | if (BuildConfig.DEBUG) { 49 | Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) 50 | Security.insertProviderAt(BouncyCastleProvider(), 1) 51 | } else runCatching { 52 | context.packageManager.getApplicationInfo("com.google.android.gms", 53 | PackageManager.MATCH_SYSTEM_ONLY) 54 | val gms = context.createPackageContext("com.google.android.gms", 55 | CONTEXT_INCLUDE_CODE or CONTEXT_IGNORE_SECURITY) 56 | gms.classLoader 57 | .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") 58 | .getMethod("insertProvider", Context::class.java) 59 | .invoke(null, gms) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/app/AlertDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.app 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.content.Intent 7 | import android.os.Bundle 8 | import android.os.Parcel 9 | import android.os.Parcelable 10 | import android.text.method.LinkMovementMethod 11 | import android.widget.Button 12 | import android.widget.TextView 13 | import androidx.appcompat.app.AlertDialog 14 | import androidx.fragment.app.DialogFragment 15 | import androidx.fragment.app.FragmentManager 16 | 17 | open class AlertDialogFragment : DialogFragment() { 18 | 19 | fun show(fragmentManager: FragmentManager) { 20 | if (fragmentManager.isStateSaved) return 21 | show(fragmentManager, javaClass.simpleName) 22 | } 23 | 24 | open fun onCreateAlertDialogBuilder(context: Context): AlertDialog.Builder { 25 | return AlertDialog.Builder(context) 26 | } 27 | 28 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 29 | val context = requireContext() 30 | val builder = onCreateAlertDialogBuilder(context) 31 | onBuildAlertDialog(builder, savedInstanceState) 32 | val dialog = builder.create() 33 | dialog.setOnShowListener { onShow(dialog) } 34 | onAlertDialogCreated(dialog, savedInstanceState) 35 | return dialog 36 | } 37 | 38 | open fun onBuildAlertDialog(builder: AlertDialog.Builder, savedInstanceState: Bundle?) { 39 | val args = requireArguments() 40 | if (args.containsKey(INTERNAL_BUILDER_ARGS)) { 41 | val dialogBuilder: Builder = args.getParcelable(INTERNAL_BUILDER_ARGS)!! 42 | builder.setTitle(dialogBuilder.title) 43 | builder.setMessage(dialogBuilder.message) 44 | if (dialogBuilder.positiveButtonText != null) { 45 | builder.setPositiveButton(dialogBuilder.positiveButtonText) { _: DialogInterface?, _: Int -> launchIntent(dialogBuilder.positiveButtonIntent) } 46 | } 47 | if (dialogBuilder.negativeButtonText != null) { 48 | builder.setNegativeButton(dialogBuilder.negativeButtonText) { _: DialogInterface?, _: Int -> launchIntent(dialogBuilder.negativeButtonIntent) } 49 | } 50 | if (dialogBuilder.neutralButtonText != null) { 51 | builder.setNeutralButton(dialogBuilder.neutralButtonText) { _: DialogInterface?, _: Int -> launchIntent(dialogBuilder.neutralButtonIntent) } 52 | } 53 | } 54 | } 55 | 56 | open fun onAlertDialogCreated(dialog: AlertDialog, savedInstanceState: Bundle?) {} 57 | 58 | open fun onShow(dialog: AlertDialog) { 59 | dialog.findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() 60 | } 61 | 62 | override fun getDialog(): AlertDialog? { 63 | return super.getDialog() as AlertDialog? 64 | } 65 | 66 | open fun getButton(whichButton: Int): Button? { 67 | return dialog?.getButton(whichButton) 68 | } 69 | 70 | private fun launchIntent(intent: Intent?) { 71 | if (intent != null) { 72 | //Do nothing. 73 | } 74 | } 75 | 76 | class Builder(private val context: Context?) : Parcelable { 77 | 78 | var title: CharSequence? = null 79 | private set 80 | 81 | var message: CharSequence? = null 82 | private set 83 | 84 | var positiveButtonText: CharSequence? = null 85 | private set 86 | 87 | var negativeButtonText: CharSequence? = null 88 | private set 89 | 90 | var neutralButtonText: CharSequence? = null 91 | private set 92 | 93 | var positiveButtonIntent: Intent? = null 94 | private set 95 | 96 | var negativeButtonIntent: Intent? = null 97 | private set 98 | 99 | var neutralButtonIntent: Intent? = null 100 | private set 101 | 102 | private constructor(`in`: Parcel) : this(null) { 103 | title = `in`.readString() 104 | message = `in`.readString() 105 | positiveButtonText = `in`.readString() 106 | negativeButtonText = `in`.readString() 107 | neutralButtonText = `in`.readString() 108 | positiveButtonIntent = `in`.readParcelable(Intent::class.java.classLoader) 109 | negativeButtonIntent = `in`.readParcelable(Intent::class.java.classLoader) 110 | neutralButtonIntent = `in`.readParcelable(Intent::class.java.classLoader) 111 | } 112 | 113 | fun title(title: CharSequence?) = apply { this.title = title } 114 | 115 | fun title(title: Int) = title(context!!.getString(title)) 116 | 117 | fun message(message: CharSequence?) = apply { this.message = message } 118 | 119 | fun message(message: Int) = message(context!!.getString(message)) 120 | 121 | fun positiveButton(text: CharSequence?, intent: Intent? = null) = apply { 122 | positiveButtonText = text 123 | positiveButtonIntent = intent 124 | } 125 | 126 | fun positiveButton(text: Int, intent: Intent? = null) = positiveButton(context!!.getString(text), intent) 127 | 128 | fun negativeButton(text: CharSequence?, intent: Intent? = null) = apply { 129 | negativeButtonText = text 130 | negativeButtonIntent = intent 131 | } 132 | 133 | fun negativeButton(text: Int, intent: Intent? = null) = negativeButton(context!!.getString(text), intent) 134 | 135 | fun neutralButton(text: CharSequence?, intent: Intent? = null) = apply { 136 | neutralButtonText = text 137 | neutralButtonIntent = intent 138 | } 139 | 140 | fun neutralButton(text: Int, intent: Intent? = null) = neutralButton(context!!.getString(text), intent) 141 | 142 | fun build(): AlertDialogFragment { 143 | val fragment = AlertDialogFragment() 144 | val args = Bundle() 145 | args.putParcelable(INTERNAL_BUILDER_ARGS, this) 146 | fragment.arguments = args 147 | return fragment 148 | } 149 | 150 | override fun describeContents(): Int { 151 | return 0 152 | } 153 | 154 | override fun writeToParcel(dest: Parcel, flags: Int) { 155 | dest.writeString(title?.toString()) 156 | dest.writeString(message?.toString()) 157 | dest.writeString(positiveButtonText?.toString()) 158 | dest.writeString(negativeButtonText?.toString()) 159 | dest.writeString(neutralButtonText?.toString()) 160 | dest.writeParcelable(positiveButtonIntent, flags) 161 | dest.writeParcelable(negativeButtonIntent, flags) 162 | dest.writeParcelable(neutralButtonIntent, flags) 163 | } 164 | 165 | companion object { 166 | 167 | @JvmField 168 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 169 | override fun createFromParcel(`in`: Parcel): Builder { 170 | return Builder(`in`) 171 | } 172 | 173 | override fun newArray(size: Int): Array { 174 | return arrayOfNulls(size) 175 | } 176 | } 177 | } 178 | } 179 | 180 | companion object { 181 | private val INTERNAL_BUILDER_ARGS = AlertDialogFragment::class.java.name + ".BUILDER_ARGS" 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/app/AppActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.app 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Color 5 | import android.os.Build 6 | import rikka.core.res.resolveColor 7 | import rikka.material.app.MaterialActivity 8 | 9 | open class AppActivity : MaterialActivity() { 10 | 11 | override fun shouldApplyTranslucentSystemBars(): Boolean { 12 | return true 13 | } 14 | 15 | override fun onApplyTranslucentSystemBars() { 16 | super.onApplyTranslucentSystemBars() 17 | 18 | val window = window 19 | val theme = theme 20 | 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 22 | window?.decorView?.post { 23 | if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 >= Resources.getSystem().displayMetrics.density * 40) { 24 | window.navigationBarColor = theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 26 | window.isNavigationBarContrastEnforced = false 27 | } 28 | } else { 29 | window.navigationBarColor = Color.TRANSPARENT 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 31 | window.isNavigationBarContrastEnforced = true 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/app/AppBarActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.app 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.FrameLayout 8 | import androidx.annotation.LayoutRes 9 | import androidx.appcompat.widget.Toolbar 10 | import io.github.vvb2060.keyattestation.R 11 | import rikka.material.widget.AppBarLayout 12 | 13 | abstract class AppBarActivity : AppActivity() { 14 | 15 | private val rootView: ViewGroup by lazy { 16 | findViewById(R.id.root) 17 | } 18 | 19 | private val toolbarContainer: AppBarLayout by lazy { 20 | findViewById(R.id.toolbar_container) 21 | } 22 | 23 | private val toolbar: Toolbar by lazy { 24 | findViewById(R.id.toolbar) 25 | } 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | super.setContentView(getLayoutId()) 30 | 31 | setAppBar(toolbarContainer, toolbar) 32 | } 33 | 34 | @LayoutRes 35 | open fun getLayoutId(): Int { 36 | return R.layout.appbar_activity 37 | } 38 | 39 | override fun setContentView(layoutResID: Int) { 40 | layoutInflater.inflate(layoutResID, rootView, true) 41 | rootView.bringChildToFront(toolbarContainer) 42 | } 43 | 44 | override fun setContentView(view: View?) { 45 | setContentView(view, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) 46 | } 47 | 48 | override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) { 49 | rootView.addView(view, 0, params) 50 | } 51 | 52 | override fun shouldApplyTranslucentSystemBars(): Boolean { 53 | return true 54 | } 55 | 56 | override fun onApplyTranslucentSystemBars() { 57 | super.onApplyTranslucentSystemBars() 58 | window?.statusBarColor = Color.TRANSPARENT 59 | } 60 | } 61 | 62 | abstract class AppBarFragmentActivity : AppBarActivity() { 63 | 64 | override fun getLayoutId(): Int { 65 | return R.layout.appbar_fragment_activity 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/app/AppFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.app 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | open class AppFragment : Fragment() { 6 | 7 | val appActivity: AppActivity? get() = activity as AppActivity? 8 | 9 | fun requireAppActivity(): AppActivity { 10 | return appActivity ?: throw IllegalStateException("Fragment $this not attached to an activity.") 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Attestation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.vvb2060.keyattestation.attestation; 18 | 19 | import org.bouncycastle.asn1.ASN1Sequence; 20 | 21 | import java.security.cert.CertificateParsingException; 22 | import java.security.cert.X509Certificate; 23 | 24 | public class Asn1Attestation extends Attestation { 25 | static final int ATTESTATION_VERSION_INDEX = 0; 26 | static final int ATTESTATION_SECURITY_LEVEL_INDEX = 1; 27 | static final int KEYMASTER_VERSION_INDEX = 2; 28 | static final int KEYMASTER_SECURITY_LEVEL_INDEX = 3; 29 | static final int ATTESTATION_CHALLENGE_INDEX = 4; 30 | static final int UNIQUE_ID_INDEX = 5; 31 | static final int SW_ENFORCED_INDEX = 6; 32 | static final int TEE_ENFORCED_INDEX = 7; 33 | 34 | int attestationSecurityLevel; 35 | 36 | /** 37 | * Constructs an {@code Asn1Attestation} object from the provided {@link X509Certificate}, 38 | * extracting the attestation data from the attestation extension. 39 | * 40 | * @throws CertificateParsingException if the certificate does not contain a properly-formatted 41 | * attestation extension. 42 | */ 43 | 44 | public Asn1Attestation(X509Certificate x509Cert) throws CertificateParsingException { 45 | super(x509Cert); 46 | ASN1Sequence seq = getAttestationSequence(x509Cert); 47 | 48 | attestationVersion = 49 | Asn1Utils.getIntegerFromAsn1(seq.getObjectAt(ATTESTATION_VERSION_INDEX)); 50 | attestationSecurityLevel = 51 | Asn1Utils.getIntegerFromAsn1(seq.getObjectAt(ATTESTATION_SECURITY_LEVEL_INDEX)); 52 | keymasterVersion = Asn1Utils.getIntegerFromAsn1(seq.getObjectAt(KEYMASTER_VERSION_INDEX)); 53 | keymasterSecurityLevel = 54 | Asn1Utils.getIntegerFromAsn1(seq.getObjectAt(KEYMASTER_SECURITY_LEVEL_INDEX)); 55 | 56 | attestationChallenge = 57 | Asn1Utils.getByteArrayFromAsn1(seq.getObjectAt(ATTESTATION_CHALLENGE_INDEX)); 58 | 59 | uniqueId = Asn1Utils.getByteArrayFromAsn1(seq.getObjectAt(UNIQUE_ID_INDEX)); 60 | 61 | softwareEnforced = new AuthorizationList(seq.getObjectAt(SW_ENFORCED_INDEX)); 62 | teeEnforced = new AuthorizationList(seq.getObjectAt(TEE_ENFORCED_INDEX)); 63 | } 64 | 65 | ASN1Sequence getAttestationSequence(X509Certificate x509Cert) 66 | throws CertificateParsingException { 67 | byte[] attestationExtensionBytes = x509Cert.getExtensionValue(Attestation.ASN1_OID); 68 | if (attestationExtensionBytes == null || attestationExtensionBytes.length == 0) { 69 | throw new CertificateParsingException("Did not find extension with OID " + ASN1_OID); 70 | } 71 | return Asn1Utils.getAsn1SequenceFromBytes(attestationExtensionBytes); 72 | } 73 | 74 | public int getAttestationSecurityLevel() { 75 | return attestationSecurityLevel; 76 | } 77 | 78 | public RootOfTrust getRootOfTrust() { 79 | RootOfTrust tee = teeEnforced.getRootOfTrust(); 80 | if (tee != null) return tee; 81 | return softwareEnforced.getRootOfTrust(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.vvb2060.keyattestation.attestation; 18 | 19 | import com.google.common.collect.ImmutableSet; 20 | 21 | import org.bouncycastle.asn1.ASN1Boolean; 22 | import org.bouncycastle.asn1.ASN1Encodable; 23 | import org.bouncycastle.asn1.ASN1Enumerated; 24 | import org.bouncycastle.asn1.ASN1InputStream; 25 | import org.bouncycastle.asn1.ASN1Integer; 26 | import org.bouncycastle.asn1.ASN1OctetString; 27 | import org.bouncycastle.asn1.ASN1Primitive; 28 | import org.bouncycastle.asn1.ASN1PrintableString; 29 | import org.bouncycastle.asn1.ASN1Sequence; 30 | import org.bouncycastle.asn1.ASN1Set; 31 | import org.bouncycastle.asn1.DEROctetString; 32 | 33 | import java.io.IOException; 34 | import java.math.BigInteger; 35 | import java.nio.charset.StandardCharsets; 36 | import java.security.cert.CertificateParsingException; 37 | import java.util.Date; 38 | import java.util.Enumeration; 39 | import java.util.Set; 40 | 41 | public class Asn1Utils { 42 | 43 | public static int getIntegerFromAsn1(ASN1Encodable asn1Value) 44 | throws CertificateParsingException { 45 | if (asn1Value instanceof ASN1Integer) { 46 | return bigIntegerToInt(((ASN1Integer) asn1Value).getValue()); 47 | } else if (asn1Value instanceof ASN1Enumerated) { 48 | return bigIntegerToInt(((ASN1Enumerated) asn1Value).getValue()); 49 | } else { 50 | throw new CertificateParsingException( 51 | "Integer value expected, " + asn1Value.getClass().getName() + " found."); 52 | } 53 | } 54 | 55 | public static Long getLongFromAsn1(ASN1Encodable asn1Value) throws CertificateParsingException { 56 | if (asn1Value instanceof ASN1Integer) { 57 | return bigIntegerToLong(((ASN1Integer) asn1Value).getValue()); 58 | } else { 59 | throw new CertificateParsingException( 60 | "Integer value expected, " + asn1Value.getClass().getName() + " found."); 61 | } 62 | } 63 | 64 | public static byte[] getByteArrayFromAsn1(ASN1Encodable asn1Encodable) 65 | throws CertificateParsingException { 66 | if (!(asn1Encodable instanceof DEROctetString derOctectString)) { 67 | throw new CertificateParsingException("Expected DEROctetString"); 68 | } 69 | return derOctectString.getOctets(); 70 | } 71 | 72 | public static ASN1Encodable getAsn1EncodableFromBytes(byte[] bytes) 73 | throws CertificateParsingException { 74 | try (ASN1InputStream asn1InputStream = new ASN1InputStream(bytes)) { 75 | return asn1InputStream.readObject(); 76 | } catch (IOException e) { 77 | throw new CertificateParsingException("Failed to parse Encodable", e); 78 | } 79 | } 80 | 81 | public static ASN1Sequence getAsn1SequenceFromBytes(byte[] bytes) 82 | throws CertificateParsingException { 83 | try (ASN1InputStream asn1InputStream = new ASN1InputStream(bytes)) { 84 | return getAsn1SequenceFromStream(asn1InputStream); 85 | } catch (IOException e) { 86 | throw new CertificateParsingException("Failed to parse SEQUENCE", e); 87 | } 88 | } 89 | 90 | public static ASN1Sequence getAsn1SequenceFromStream(final ASN1InputStream asn1InputStream) 91 | throws IOException, CertificateParsingException { 92 | ASN1Primitive asn1Primitive = asn1InputStream.readObject(); 93 | if (!(asn1Primitive instanceof ASN1OctetString)) { 94 | throw new CertificateParsingException( 95 | "Expected octet stream, found " + asn1Primitive.getClass().getName()); 96 | } 97 | try (ASN1InputStream seqInputStream = new ASN1InputStream( 98 | ((ASN1OctetString) asn1Primitive).getOctets())) { 99 | asn1Primitive = seqInputStream.readObject(); 100 | if (!(asn1Primitive instanceof ASN1Sequence)) { 101 | throw new CertificateParsingException( 102 | "Expected sequence, found " + asn1Primitive.getClass().getName()); 103 | } 104 | return (ASN1Sequence) asn1Primitive; 105 | } 106 | } 107 | 108 | public static Set getIntegersFromAsn1Set(ASN1Encodable set) 109 | throws CertificateParsingException { 110 | if (!(set instanceof ASN1Set)) { 111 | throw new CertificateParsingException( 112 | "Expected set, found " + set.getClass().getName()); 113 | } 114 | 115 | ImmutableSet.Builder builder = ImmutableSet.builder(); 116 | for (Enumeration e = ((ASN1Set) set).getObjects(); e.hasMoreElements();) { 117 | builder.add(getIntegerFromAsn1((ASN1Integer) e.nextElement())); 118 | } 119 | return builder.build(); 120 | } 121 | 122 | public static String getStringFromAsn1OctetStreamAssumingUTF8(ASN1Encodable encodable) 123 | throws CertificateParsingException { 124 | if (!(encodable instanceof ASN1OctetString octetString)) { 125 | throw new CertificateParsingException( 126 | "Expected octet string, found " + encodable.getClass().getName()); 127 | } 128 | 129 | return new String(octetString.getOctets(), StandardCharsets.UTF_8); 130 | } 131 | 132 | public static String getStringFromASN1PrintableString(ASN1Encodable encodable) 133 | throws CertificateParsingException { 134 | if (!(encodable instanceof ASN1PrintableString printableString)) { 135 | throw new CertificateParsingException( 136 | "Expected printable string, found " + encodable.getClass().getName()); 137 | } 138 | return printableString.getString(); 139 | } 140 | 141 | public static Date getDateFromAsn1(ASN1Primitive value) throws CertificateParsingException { 142 | return new Date(getLongFromAsn1(value)); 143 | } 144 | 145 | public static boolean getBooleanFromAsn1(ASN1Encodable value) 146 | throws CertificateParsingException { 147 | if (!(value instanceof ASN1Boolean booleanValue)) { 148 | throw new CertificateParsingException( 149 | "Expected boolean, found " + value.getClass().getName()); 150 | } 151 | if (booleanValue.equals(ASN1Boolean.TRUE)) { 152 | return true; 153 | } else if (booleanValue.equals((ASN1Boolean.FALSE))) { 154 | return false; 155 | } 156 | 157 | throw new CertificateParsingException( 158 | "DER-encoded boolean values must contain either 0x00 or 0xFF"); 159 | } 160 | 161 | private static int bigIntegerToInt(BigInteger bigInt) throws CertificateParsingException { 162 | if (bigInt.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0 163 | || bigInt.compareTo(BigInteger.ZERO) < 0) { 164 | throw new CertificateParsingException("INTEGER out of bounds"); 165 | } 166 | return bigInt.intValue(); 167 | } 168 | 169 | private static long bigIntegerToLong(BigInteger bigInt) throws CertificateParsingException { 170 | if (bigInt.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 171 | || bigInt.compareTo(BigInteger.ZERO) < 0) { 172 | throw new CertificateParsingException("INTEGER out of bounds"); 173 | } 174 | return bigInt.longValue(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.vvb2060.keyattestation.attestation; 18 | 19 | import android.util.Base64; 20 | 21 | import com.google.common.collect.ImmutableSet; 22 | import com.google.common.io.BaseEncoding; 23 | 24 | import java.security.cert.CertificateParsingException; 25 | import java.security.cert.X509Certificate; 26 | import java.util.Arrays; 27 | import java.util.Set; 28 | 29 | import co.nstant.in.cbor.CborException; 30 | 31 | /** 32 | * Parses an attestation certificate and provides an easy-to-use interface for examining the 33 | * contents. 34 | */ 35 | public abstract class Attestation { 36 | static final String EAT_OID = "1.3.6.1.4.1.11129.2.1.25"; 37 | static final String ASN1_OID = "1.3.6.1.4.1.11129.2.1.17"; 38 | static final String KNOX_OID = "1.3.6.1.4.1.236.11.3.23.7"; 39 | static final String KEY_USAGE_OID = "2.5.29.15"; // Standard key usage extension. 40 | 41 | public static final int KM_SECURITY_LEVEL_SOFTWARE = 0; 42 | public static final int KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 1; 43 | public static final int KM_SECURITY_LEVEL_STRONG_BOX = 2; 44 | 45 | int attestationVersion; 46 | int keymasterVersion; 47 | int keymasterSecurityLevel; 48 | byte[] attestationChallenge; 49 | byte[] uniqueId; 50 | AuthorizationList softwareEnforced; 51 | AuthorizationList teeEnforced; 52 | Set unexpectedExtensionOids; 53 | 54 | /** 55 | * Constructs an {@code Attestation} object from the provided {@link X509Certificate}, 56 | * extracting the attestation data from the attestation extension. 57 | * 58 | *

This method ensures that at most one attestation extension is included in the certificate. 59 | * 60 | * @throws CertificateParsingException if the certificate does not contain a properly-formatted 61 | * attestation extension, if it contains multiple attestation extensions, or if the 62 | * attestation extension can not be parsed. 63 | */ 64 | 65 | public static Attestation loadFromCertificate(X509Certificate x509Cert) throws CertificateParsingException { 66 | if (x509Cert.getExtensionValue(EAT_OID) == null 67 | && x509Cert.getExtensionValue(ASN1_OID) == null) { 68 | throw new CertificateParsingException("No attestation extensions found"); 69 | } 70 | if (x509Cert.getExtensionValue(EAT_OID) != null) { 71 | if (x509Cert.getExtensionValue(ASN1_OID) != null) { 72 | throw new CertificateParsingException("Multiple attestation extensions found"); 73 | } 74 | try { 75 | return new EatAttestation(x509Cert); 76 | } catch (CborException cbe) { 77 | throw new CertificateParsingException("Unable to parse EAT extension", cbe); 78 | } 79 | } 80 | if (x509Cert.getExtensionValue(KNOX_OID) != null) { 81 | return new KnoxAttestation(x509Cert); 82 | } 83 | return new Asn1Attestation(x509Cert); 84 | } 85 | 86 | Attestation(X509Certificate x509Cert) { 87 | unexpectedExtensionOids = retrieveUnexpectedExtensionOids(x509Cert); 88 | } 89 | 90 | public static String securityLevelToString(int attestationSecurityLevel) { 91 | return switch (attestationSecurityLevel) { 92 | case KM_SECURITY_LEVEL_SOFTWARE -> "Software"; 93 | case KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> "TEE"; 94 | case KM_SECURITY_LEVEL_STRONG_BOX -> "StrongBox"; 95 | default -> "Unknown (" + attestationSecurityLevel + ")"; 96 | }; 97 | } 98 | 99 | public static String attestationVersionToString(int version) { 100 | return switch (version) { 101 | case 1 -> "Keymaster 2.0"; 102 | case 2 -> "Keymaster 3.0"; 103 | case 3 -> "Keymaster 4.0"; 104 | case 4 -> "Keymaster 4.1"; 105 | case 100 -> "KeyMint 1.0"; 106 | case 200 -> "KeyMint 2.0"; 107 | case 300 -> "KeyMint 3.0"; 108 | case 400 -> "KeyMint 4.0"; 109 | default -> "Unknown (" + version + ")"; 110 | }; 111 | } 112 | 113 | public static String keymasterVersionToString(int version) { 114 | return switch (version) { 115 | case 0 -> "Keymaster 0.2 or 0.3"; 116 | case 1 -> "Keymaster 1.0"; 117 | case 2 -> "Keymaster 2.0"; 118 | case 3 -> "Keymaster 3.0"; 119 | case 4 -> "Keymaster 4.0"; 120 | case 41 -> "Keymaster 4.1"; 121 | case 100 -> "KeyMint 1.0"; 122 | case 200 -> "KeyMint 2.0"; 123 | case 300 -> "KeyMint 3.0"; 124 | case 400 -> "KeyMint 4.0"; 125 | default -> "Unknown (" + version + ")"; 126 | }; 127 | } 128 | 129 | public int getAttestationVersion() { 130 | return attestationVersion; 131 | } 132 | 133 | public abstract int getAttestationSecurityLevel(); 134 | 135 | public abstract RootOfTrust getRootOfTrust(); 136 | 137 | // Returns one of the KM_VERSION_* values define above. 138 | public int getKeymasterVersion() { 139 | return keymasterVersion; 140 | } 141 | 142 | public int getKeymasterSecurityLevel() { 143 | return keymasterSecurityLevel; 144 | } 145 | 146 | public byte[] getAttestationChallenge() { 147 | return attestationChallenge; 148 | } 149 | 150 | public byte[] getUniqueId() { 151 | return uniqueId; 152 | } 153 | 154 | public AuthorizationList getSoftwareEnforced() { 155 | return softwareEnforced; 156 | } 157 | 158 | public AuthorizationList getTeeEnforced() { 159 | return teeEnforced; 160 | } 161 | 162 | public Set getUnexpectedExtensionOids() { 163 | return unexpectedExtensionOids; 164 | } 165 | 166 | @Override 167 | public String toString() { 168 | StringBuilder s = new StringBuilder(); 169 | s.append("Extension type: " + getClass()); 170 | s.append("\nAttest version: " + attestationVersionToString(attestationVersion)); 171 | s.append("\nAttest security: " + securityLevelToString(getAttestationSecurityLevel())); 172 | s.append("\nKM version: " + keymasterVersionToString(keymasterVersion)); 173 | s.append("\nKM security: " + securityLevelToString(keymasterSecurityLevel)); 174 | 175 | s.append("\nChallenge"); 176 | String stringChallenge = 177 | attestationChallenge != null ? new String(attestationChallenge) : ""; 178 | if (Arrays.equals(attestationChallenge, stringChallenge.getBytes())) { 179 | s.append(": [" + stringChallenge + "]"); 180 | } else if (attestationChallenge != null) { 181 | s.append(" (base64): [" + Base64.encodeToString(attestationChallenge, 0) + "]"); 182 | } 183 | if (uniqueId != null) { 184 | s.append("\nUnique ID: [" + BaseEncoding.base16().lowerCase().encode(uniqueId) + "]"); 185 | } 186 | 187 | s.append("\n-- SW enforced --"); 188 | s.append(softwareEnforced); 189 | s.append("\n-- TEE enforced --"); 190 | s.append(teeEnforced); 191 | 192 | return s.toString(); 193 | } 194 | 195 | Set retrieveUnexpectedExtensionOids(X509Certificate x509Cert) { 196 | return new ImmutableSet.Builder() 197 | .addAll(x509Cert.getCriticalExtensionOIDs() 198 | .stream() 199 | .filter(s -> !KEY_USAGE_OID.equals(s)) 200 | .iterator()) 201 | .addAll(x509Cert.getNonCriticalExtensionOIDs() 202 | .stream() 203 | .filter(s -> !ASN1_OID.equals(s) && !EAT_OID.equals(s)) 204 | .iterator()) 205 | .build(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationApplicationId.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.github.vvb2060.keyattestation.attestation; 16 | 17 | import android.content.Context; 18 | import android.content.pm.PackageInfo; 19 | import android.content.pm.PackageManager; 20 | import android.content.pm.PackageManager.NameNotFoundException; 21 | import android.content.pm.Signature; 22 | 23 | import com.google.common.io.BaseEncoding; 24 | 25 | import org.bouncycastle.asn1.ASN1Encodable; 26 | import org.bouncycastle.asn1.ASN1Sequence; 27 | import org.bouncycastle.asn1.ASN1Set; 28 | 29 | import java.security.MessageDigest; 30 | import java.security.NoSuchAlgorithmException; 31 | import java.security.cert.CertificateParsingException; 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | public class AttestationApplicationId implements java.lang.Comparable { 36 | private static final int PACKAGE_INFOS_INDEX = 0; 37 | private static final int SIGNATURE_DIGESTS_INDEX = 1; 38 | 39 | private final List packageInfos; 40 | private final List signatureDigests; 41 | 42 | public AttestationApplicationId(Context context) 43 | throws NoSuchAlgorithmException, NameNotFoundException { 44 | PackageManager pm = context.getPackageManager(); 45 | int uid = context.getApplicationInfo().uid; 46 | String[] packageNames = pm.getPackagesForUid(uid); 47 | if (packageNames == null || packageNames.length == 0) { 48 | throw new NameNotFoundException("No names found for uid"); 49 | } 50 | packageInfos = new ArrayList(); 51 | for (String packageName : packageNames) { 52 | // get the package info for the given package name including 53 | // the signatures 54 | PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); 55 | packageInfos.add(new AttestationPackageInfo(packageName, packageInfo.versionCode)); 56 | } 57 | // The infos must be sorted, the implementation of Comparable relies on it. 58 | packageInfos.sort(null); 59 | 60 | // compute the sha256 digests of the signature blobs 61 | signatureDigests = new ArrayList(); 62 | PackageInfo packageInfo = pm.getPackageInfo(packageNames[0], PackageManager.GET_SIGNATURES); 63 | for (Signature signature : packageInfo.signatures) { 64 | MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); 65 | signatureDigests.add(sha256.digest(signature.toByteArray())); 66 | } 67 | // The digests must be sorted. the implementation of Comparable relies on it 68 | signatureDigests.sort(new ByteArrayComparator()); 69 | } 70 | 71 | public AttestationApplicationId(ASN1Encodable asn1Encodable) 72 | throws CertificateParsingException { 73 | if (!(asn1Encodable instanceof ASN1Sequence sequence)) { 74 | throw new CertificateParsingException( 75 | "Expected sequence for AttestationApplicationId, found " 76 | + asn1Encodable.getClass().getName()); 77 | } 78 | 79 | packageInfos = parseAttestationPackageInfos(sequence.getObjectAt(PACKAGE_INFOS_INDEX)); 80 | // The infos must be sorted, the implementation of Comparable relies on it. 81 | packageInfos.sort(null); 82 | signatureDigests = parseSignatures(sequence.getObjectAt(SIGNATURE_DIGESTS_INDEX)); 83 | // The digests must be sorted. the implementation of Comparable relies on it 84 | signatureDigests.sort(new ByteArrayComparator()); 85 | } 86 | 87 | public List getAttestationPackageInfos() { 88 | return packageInfos; 89 | } 90 | 91 | public List getSignatureDigests() { 92 | return signatureDigests; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | StringBuilder sb = new StringBuilder(); 98 | int noOfInfos = packageInfos.size(); 99 | int i = 1; 100 | for (AttestationPackageInfo info : packageInfos) { 101 | sb.append("Package info " + i++ + "/" + noOfInfos + ":\n"); 102 | sb.append(info); 103 | sb.append('\n'); 104 | } 105 | sb.append('\n'); 106 | i = 1; 107 | int noOfSigs = signatureDigests.size(); 108 | for (byte[] sig : signatureDigests) { 109 | sb.append("Certificate sha256 digest " + i++ + "/" + noOfSigs + ":\n"); 110 | sb.append(BaseEncoding.base16().lowerCase().encode(sig)); 111 | sb.append('\n'); 112 | } 113 | return sb.toString(); 114 | } 115 | 116 | @Override 117 | public int compareTo(AttestationApplicationId other) { 118 | int res = Integer.compare(packageInfos.size(), other.packageInfos.size()); 119 | if (res != 0) return res; 120 | for (int i = 0; i < packageInfos.size(); ++i) { 121 | res = packageInfos.get(i).compareTo(other.packageInfos.get(i)); 122 | if (res != 0) return res; 123 | } 124 | res = Integer.compare(signatureDigests.size(), other.signatureDigests.size()); 125 | if (res != 0) return res; 126 | ByteArrayComparator cmp = new ByteArrayComparator(); 127 | for (int i = 0; i < signatureDigests.size(); ++i) { 128 | res = cmp.compare(signatureDigests.get(i), other.signatureDigests.get(i)); 129 | if (res != 0) return res; 130 | } 131 | return res; 132 | } 133 | 134 | @Override 135 | public boolean equals(Object o) { 136 | return (o instanceof AttestationApplicationId) 137 | && (0 == compareTo((AttestationApplicationId) o)); 138 | } 139 | 140 | private List parseAttestationPackageInfos(ASN1Encodable asn1Encodable) 141 | throws CertificateParsingException { 142 | if (!(asn1Encodable instanceof ASN1Set set)) { 143 | throw new CertificateParsingException( 144 | "Expected set for AttestationApplicationsInfos, found " 145 | + asn1Encodable.getClass().getName()); 146 | } 147 | 148 | List result = new ArrayList(); 149 | for (ASN1Encodable e : set) { 150 | result.add(new AttestationPackageInfo(e)); 151 | } 152 | return result; 153 | } 154 | 155 | private List parseSignatures(ASN1Encodable asn1Encodable) 156 | throws CertificateParsingException { 157 | if (!(asn1Encodable instanceof ASN1Set set)) { 158 | throw new CertificateParsingException("Expected set for Signature digests, found " 159 | + asn1Encodable.getClass().getName()); 160 | } 161 | 162 | List result = new ArrayList(); 163 | 164 | for (ASN1Encodable e : set) { 165 | result.add(Asn1Utils.getByteArrayFromAsn1(e)); 166 | } 167 | return result; 168 | } 169 | 170 | private static class ByteArrayComparator implements java.util.Comparator { 171 | @Override 172 | public int compare(byte[] a, byte[] b) { 173 | int res = Integer.compare(a.length, b.length); 174 | if (res != 0) return res; 175 | for (int i = 0; i < a.length; ++i) { 176 | res = Byte.compare(a[i], b[i]); 177 | if (res != 0) return res; 178 | } 179 | return res; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/AttestationPackageInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.github.vvb2060.keyattestation.attestation; 16 | 17 | import org.bouncycastle.asn1.ASN1Encodable; 18 | import org.bouncycastle.asn1.ASN1Sequence; 19 | 20 | import java.security.cert.CertificateParsingException; 21 | 22 | public class AttestationPackageInfo implements java.lang.Comparable { 23 | private static final int PACKAGE_NAME_INDEX = 0; 24 | private static final int VERSION_INDEX = 1; 25 | 26 | private final String packageName; 27 | private final long version; 28 | 29 | public AttestationPackageInfo(String packageName, long version) { 30 | this.packageName = packageName; 31 | this.version = version; 32 | } 33 | 34 | public AttestationPackageInfo(ASN1Encodable asn1Encodable) throws CertificateParsingException { 35 | if (!(asn1Encodable instanceof ASN1Sequence sequence)) { 36 | throw new CertificateParsingException( 37 | "Expected sequence for AttestationPackageInfo, found " 38 | + asn1Encodable.getClass().getName()); 39 | } 40 | 41 | packageName = Asn1Utils.getStringFromAsn1OctetStreamAssumingUTF8( 42 | sequence.getObjectAt(PACKAGE_NAME_INDEX)); 43 | version = Asn1Utils.getLongFromAsn1(sequence.getObjectAt(VERSION_INDEX)); 44 | } 45 | 46 | public String getPackageName() { 47 | return packageName; 48 | } 49 | 50 | public long getVersion() { 51 | return version; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return getPackageName() + " (version code " + getVersion() + ")"; 57 | } 58 | 59 | @Override 60 | public int compareTo(AttestationPackageInfo other) { 61 | int res = packageName.compareTo(other.packageName); 62 | if (res != 0) return res; 63 | res = Long.compare(version, other.version); 64 | if (res != 0) return res; 65 | return res; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object o) { 70 | return (o instanceof AttestationPackageInfo) 71 | && (0 == compareTo((AttestationPackageInfo) o)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import org.bouncycastle.asn1.ASN1Encodable; 4 | import org.bouncycastle.asn1.ASN1Sequence; 5 | import org.bouncycastle.asn1.ASN1TaggedObject; 6 | 7 | import java.security.cert.CertificateParsingException; 8 | 9 | public class AuthResult { 10 | private static final int CALLER_AUTH_RESULT = 0; 11 | private static final int CALLING_PACKAGE = 1; 12 | private static final int CALLING_PACKAGE_SIGS = 2; 13 | private static final int CALLING_PACKAGE_AUTH_RESULT = 3; 14 | 15 | private int callerAuthResult = IntegrityStatus.STATUS_NOT_SUPPORT; 16 | private String callingPackage; 17 | private String callingPackageSigs; 18 | private int callingPackageAuthResult = IntegrityStatus.STATUS_NOT_SUPPORT; 19 | 20 | public AuthResult(ASN1Encodable asn1Encodable) throws CertificateParsingException { 21 | if (!(asn1Encodable instanceof ASN1Sequence sequence)) { 22 | throw new CertificateParsingException("Expected sequence for caller auth, found " 23 | + asn1Encodable.getClass().getName()); 24 | } 25 | for (var entry : sequence) { 26 | if (!(entry instanceof ASN1TaggedObject taggedObject)) { 27 | throw new CertificateParsingException( 28 | "Expected tagged object, found " + entry.getClass().getName()); 29 | } 30 | int tag = taggedObject.getTagNo(); 31 | var value = taggedObject.getBaseObject().toASN1Primitive(); 32 | switch (tag) { 33 | case CALLER_AUTH_RESULT -> callerAuthResult = Asn1Utils.getIntegerFromAsn1(value); 34 | case CALLING_PACKAGE -> 35 | callingPackage = Asn1Utils.getStringFromASN1PrintableString(value); 36 | case CALLING_PACKAGE_SIGS -> 37 | callingPackageSigs = Asn1Utils.getStringFromASN1PrintableString(value); 38 | case CALLING_PACKAGE_AUTH_RESULT -> 39 | callingPackageAuthResult = Asn1Utils.getIntegerFromAsn1(value); 40 | default -> throw new CertificateParsingException("invalid tag no: " + tag); 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "\nCaller Auth Result: " + IntegrityStatus.statusToString(callerAuthResult) + 48 | "\nCalling Package: " + callingPackage + 49 | "\nCalling Package Signatures: " + callingPackageSigs + 50 | "\nCalling Package Auth Result: " + IntegrityStatus.statusToString(callingPackageAuthResult); 51 | } 52 | 53 | public static AuthResult parse(ASN1Encodable asn1Encodable) throws CertificateParsingException { 54 | var auth = new AuthResult(asn1Encodable); 55 | if (auth.callerAuthResult == IntegrityStatus.STATUS_NOT_SUPPORT && 56 | auth.callingPackage == null && 57 | auth.callingPackageSigs == null && 58 | auth.callingPackageAuthResult == IntegrityStatus.STATUS_NOT_SUPPORT) { 59 | return null; 60 | } 61 | return auth; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/CborUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.vvb2060.keyattestation.attestation; 18 | 19 | import co.nstant.in.cbor.CborDecoder; 20 | import co.nstant.in.cbor.CborEncoder; 21 | import co.nstant.in.cbor.CborException; 22 | import co.nstant.in.cbor.model.Array; 23 | import co.nstant.in.cbor.model.ByteString; 24 | import co.nstant.in.cbor.model.DataItem; 25 | import co.nstant.in.cbor.model.Map; 26 | import co.nstant.in.cbor.model.NegativeInteger; 27 | import co.nstant.in.cbor.model.Number; 28 | import co.nstant.in.cbor.model.SimpleValue; 29 | import co.nstant.in.cbor.model.SimpleValueType; 30 | import co.nstant.in.cbor.model.UnicodeString; 31 | import co.nstant.in.cbor.model.UnsignedInteger; 32 | 33 | import java.io.ByteArrayOutputStream; 34 | import java.nio.charset.StandardCharsets; 35 | 36 | import java.util.ArrayList; 37 | import java.util.Date; 38 | import java.util.HashSet; 39 | import java.util.List; 40 | import java.util.Set; 41 | 42 | 43 | class CborUtils { 44 | public static Number toNumber(long l) { 45 | return l >= 0 ? new UnsignedInteger(l) : new NegativeInteger(l); 46 | } 47 | 48 | public static int getInt(Map map, long index) { 49 | DataItem item = map.get(CborUtils.toNumber(index)); 50 | return ((Number) item).getValue().intValue(); 51 | } 52 | 53 | public static int getInt(Map map, DataItem index) { 54 | DataItem item = map.get(index); 55 | return ((Number) item).getValue().intValue(); 56 | } 57 | 58 | public static long getLong(Map map, DataItem index) { 59 | DataItem item = map.get(index); 60 | return ((Number) item).getValue().longValue(); 61 | } 62 | 63 | public static Set getIntSet(Map map, DataItem index) { 64 | Array array = (Array) map.get(index); 65 | Set result = new HashSet<>(); 66 | for (DataItem item : array.getDataItems()) { 67 | result.add(((Number) item).getValue().intValue()); 68 | } 69 | return result; 70 | } 71 | 72 | public static Boolean getBoolean(Map map, DataItem index) { 73 | SimpleValueType value = ((SimpleValue) map.get(index)).getSimpleValueType(); 74 | if (value != SimpleValueType.TRUE && value != SimpleValueType.FALSE) { 75 | throw new RuntimeException("Only expecting boolean values for " + index); 76 | } 77 | return (value == SimpleValueType.TRUE); 78 | } 79 | 80 | public static List getBooleanList(Map map, DataItem index) { 81 | Array array = (Array) map.get(index); 82 | List result = new ArrayList<>(); 83 | for (DataItem item : array.getDataItems()) { 84 | SimpleValueType value = ((SimpleValue) item).getSimpleValueType(); 85 | if (value == SimpleValueType.FALSE) { 86 | result.add(false); 87 | } else if (value == SimpleValueType.TRUE) { 88 | result.add(true); 89 | } else { 90 | throw new RuntimeException("Map contains more than booleans: " + map); 91 | } 92 | } 93 | return result; 94 | } 95 | 96 | public static Date getDate(Map map, DataItem index) { 97 | DataItem item = map.get(index); 98 | long epochMillis = ((Number) item).getValue().longValue(); 99 | return new Date(epochMillis); 100 | } 101 | 102 | public static byte[] getBytes(Map map, DataItem index) { 103 | DataItem item = map.get(index); 104 | return ((ByteString) item).getBytes(); 105 | } 106 | 107 | public static String getString(Map map, DataItem index) { 108 | byte[] bytes = getBytes(map, index); 109 | return new String(bytes, StandardCharsets.UTF_8); 110 | } 111 | 112 | public static String getUnicodeString(Map map, DataItem index) { 113 | DataItem item = map.get(index); 114 | return ((UnicodeString) item).getString(); 115 | } 116 | 117 | public static DataItem decodeCbor(byte[] encodedBytes) throws CborException { 118 | return CborDecoder.decode(encodedBytes).get(0); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import android.util.Log; 4 | 5 | import java.security.GeneralSecurityException; 6 | import java.security.PublicKey; 7 | import java.security.cert.CertificateException; 8 | import java.security.cert.CertificateParsingException; 9 | import java.security.cert.X509Certificate; 10 | import java.util.List; 11 | 12 | import io.github.vvb2060.keyattestation.AppApplication; 13 | 14 | public class CertificateInfo { 15 | public static final int CERT_UNKNOWN = 0; 16 | public static final int CERT_SIGN = 1; 17 | public static final int CERT_REVOKED = 2; 18 | public static final int CERT_EXPIRED = 3; 19 | public static final int CERT_NORMAL = 4; 20 | 21 | private final X509Certificate cert; 22 | private RootPublicKey.Status issuer = RootPublicKey.Status.UNKNOWN; 23 | private int status = CERT_UNKNOWN; 24 | private GeneralSecurityException securityException; 25 | private Attestation attestation; 26 | private CertificateParsingException certException; 27 | 28 | private ProvisioningInfo provisioningInfo; 29 | 30 | private CertificateInfo(X509Certificate cert) { 31 | this.cert = cert; 32 | } 33 | 34 | public X509Certificate getCert() { 35 | return cert; 36 | } 37 | 38 | public RootPublicKey.Status getIssuer() { 39 | return issuer; 40 | } 41 | 42 | public int getStatus() { 43 | return status; 44 | } 45 | 46 | public GeneralSecurityException getSecurityException() { 47 | return securityException; 48 | } 49 | 50 | public Attestation getAttestation() { 51 | return attestation; 52 | } 53 | 54 | public CertificateParsingException getCertException() { 55 | return certException; 56 | } 57 | 58 | public ProvisioningInfo getProvisioningInfo() { 59 | return provisioningInfo; 60 | } 61 | 62 | private void checkIssuer() { 63 | var publicKey = cert.getPublicKey().getEncoded(); 64 | issuer = RootPublicKey.check(publicKey); 65 | } 66 | 67 | private void checkStatus(PublicKey parentKey) { 68 | try { 69 | status = CERT_SIGN; 70 | cert.verify(parentKey); 71 | status = CERT_REVOKED; 72 | var certStatus = RevocationList.get(cert.getSerialNumber()); 73 | if (certStatus != null) { 74 | throw new CertificateException("Certificate revocation " + certStatus); 75 | } 76 | status = CERT_EXPIRED; 77 | cert.checkValidity(); 78 | status = CERT_NORMAL; 79 | } catch (GeneralSecurityException e) { 80 | Log.e(AppApplication.TAG, "checkStatus", e); 81 | securityException = e; 82 | } 83 | } 84 | 85 | private boolean checkAttestation() { 86 | boolean terminate; 87 | try { 88 | attestation = Attestation.loadFromCertificate(cert); 89 | // If key purpose included KeyPurpose::SIGN, 90 | // then it could be used to sign arbitrary data, including any tbsCertificate, 91 | // and so an attestation produced by the key would have no security properties. 92 | // If the parent certificate can attest that the key purpose is only KeyPurpose::ATTEST_KEY, 93 | // then the child certificate can be trusted. 94 | var purposes = attestation.getTeeEnforced().getPurposes(); 95 | terminate = purposes == null || !purposes.contains(AuthorizationList.KM_PURPOSE_ATTEST_KEY); 96 | } catch (CertificateParsingException e) { 97 | certException = e; 98 | terminate = false; 99 | provisioningInfo = ProvisioningInfo.get(cert); 100 | } 101 | return terminate; 102 | } 103 | 104 | public static void parse(List certs, List infoList) { 105 | var parent = certs.get(certs.size() - 1); 106 | for (int i = certs.size() - 1; i >= 0; i--) { 107 | var parentKey = parent.getPublicKey(); 108 | var info = new CertificateInfo(certs.get(i)); 109 | infoList.add(info); 110 | info.checkStatus(parentKey); 111 | if (parent == info.cert) { 112 | info.checkIssuer(); 113 | } else { 114 | parent = info.cert; 115 | } 116 | if (info.checkAttestation()) { 117 | break; 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatAttestation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.vvb2060.keyattestation.attestation; 18 | 19 | import android.util.Log; 20 | 21 | import co.nstant.in.cbor.CborDecoder; 22 | import co.nstant.in.cbor.CborException; 23 | import co.nstant.in.cbor.model.DataItem; 24 | import co.nstant.in.cbor.model.Map; 25 | import co.nstant.in.cbor.model.Number; 26 | import co.nstant.in.cbor.model.UnicodeString; 27 | 28 | import org.bouncycastle.asn1.ASN1Encodable; 29 | 30 | import java.security.cert.CertificateParsingException; 31 | import java.security.cert.X509Certificate; 32 | import java.util.Arrays; 33 | import java.util.List; 34 | 35 | public class EatAttestation extends Attestation { 36 | static final String TAG = "EatAttestation"; 37 | final Map extension; 38 | final RootOfTrust rootOfTrust; 39 | 40 | /** 41 | * Constructs an {@code EatAttestation} object from the provided {@link X509Certificate}, 42 | * extracting the attestation data from the attestation extension. 43 | * 44 | * @throws CertificateParsingException if the certificate does not contain a properly-formatted 45 | * attestation extension. 46 | */ 47 | public EatAttestation(X509Certificate x509Cert) 48 | throws CertificateParsingException, CborException { 49 | super(x509Cert); 50 | extension = getEatExtension(x509Cert); 51 | 52 | RootOfTrust.Builder rootOfTrustBuilder = new RootOfTrust.Builder(); 53 | List bootState = null; 54 | boolean officialBuild = false; 55 | 56 | for (DataItem key : extension.getKeys()) { 57 | int keyInt = ((Number) key).getValue().intValue(); 58 | switch (keyInt) { 59 | default: 60 | throw new CertificateParsingException( 61 | "Unknown EAT tag: " + key + "\n in EAT extension:\n" + this); 62 | 63 | case EatClaim.ATTESTATION_VERSION: 64 | attestationVersion = CborUtils.getInt(extension, key); 65 | break; 66 | case EatClaim.KEYMASTER_VERSION: 67 | keymasterVersion = CborUtils.getInt(extension, key); 68 | break; 69 | case EatClaim.SECURITY_LEVEL: 70 | keymasterSecurityLevel = 71 | eatSecurityLevelToKeymintSecurityLevel( 72 | CborUtils.getInt(extension, key)); 73 | break; 74 | case EatClaim.SUBMODS: 75 | Map submods = (Map) extension.get(key); 76 | softwareEnforced = 77 | new AuthorizationList( 78 | (Map) submods.get(new UnicodeString(EatClaim.SUBMOD_SOFTWARE))); 79 | teeEnforced = 80 | new AuthorizationList( 81 | (Map) submods.get(new UnicodeString(EatClaim.SUBMOD_TEE))); 82 | break; 83 | case EatClaim.VERIFIED_BOOT_KEY: 84 | rootOfTrustBuilder.setVerifiedBootKey(CborUtils.getBytes(extension, key)); 85 | break; 86 | case EatClaim.DEVICE_LOCKED: 87 | rootOfTrustBuilder.setDeviceLocked(CborUtils.getBoolean(extension, key)); 88 | break; 89 | case EatClaim.BOOT_STATE: 90 | bootState = CborUtils.getBooleanList(extension, key); 91 | break; 92 | case EatClaim.OFFICIAL_BUILD: 93 | officialBuild = CborUtils.getBoolean(extension, key); 94 | break; 95 | case EatClaim.NONCE: 96 | attestationChallenge = CborUtils.getBytes(extension, key); 97 | break; 98 | case EatClaim.CTI: 99 | Log.i(TAG, "Got CTI claim: " + Arrays.toString(CborUtils.getBytes(extension, key))); 100 | uniqueId = CborUtils.getBytes(extension, key); 101 | break; 102 | case EatClaim.VERIFIED_BOOT_HASH: 103 | rootOfTrustBuilder.setVerifiedBootHash(CborUtils.getBytes(extension, key)); 104 | break; 105 | } 106 | } 107 | 108 | if (bootState != null) { 109 | rootOfTrustBuilder.setVerifiedBootState( 110 | eatBootStateTypeToVerifiedBootState(bootState, officialBuild)); 111 | } 112 | rootOfTrust = rootOfTrustBuilder.build(); 113 | } 114 | 115 | /** Find the submod containing the key information, and return its security level. */ 116 | public int getAttestationSecurityLevel() { 117 | if (teeEnforced != null && teeEnforced.getAlgorithm() != null) { 118 | return teeEnforced.getSecurityLevel(); 119 | } else if (softwareEnforced != null && softwareEnforced.getAlgorithm() != null) { 120 | return softwareEnforced.getSecurityLevel(); 121 | } else { 122 | return -1; 123 | } 124 | } 125 | 126 | public RootOfTrust getRootOfTrust() { 127 | return rootOfTrust; 128 | } 129 | 130 | public String toString() { 131 | return super.toString() + "\nEncoded CBOR: " + extension; 132 | } 133 | 134 | Map getEatExtension(X509Certificate x509Cert) 135 | throws CertificateParsingException, CborException { 136 | byte[] attestationExtensionBytes = x509Cert.getExtensionValue(Attestation.EAT_OID); 137 | if (attestationExtensionBytes == null || attestationExtensionBytes.length == 0) { 138 | throw new CertificateParsingException("Did not find extension with OID " + EAT_OID); 139 | } 140 | ASN1Encodable asn1 = Asn1Utils.getAsn1EncodableFromBytes(attestationExtensionBytes); 141 | byte[] cborBytes = Asn1Utils.getByteArrayFromAsn1(asn1); 142 | return (Map) CborUtils.decodeCbor(cborBytes); 143 | } 144 | 145 | static int eatSecurityLevelToKeymintSecurityLevel(int eatSecurityLevel) { 146 | switch (eatSecurityLevel) { 147 | case EatClaim.SECURITY_LEVEL_UNRESTRICTED: 148 | return Attestation.KM_SECURITY_LEVEL_SOFTWARE; 149 | case EatClaim.SECURITY_LEVEL_SECURE_RESTRICTED: 150 | return Attestation.KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT; 151 | case EatClaim.SECURITY_LEVEL_HARDWARE: 152 | return Attestation.KM_SECURITY_LEVEL_STRONG_BOX; 153 | default: 154 | throw new RuntimeException("Invalid EAT security level: " + eatSecurityLevel); 155 | } 156 | } 157 | 158 | static int eatBootStateTypeToVerifiedBootState(List bootState, Boolean officialBuild) { 159 | if (bootState.size() != 5) { 160 | throw new RuntimeException("Boot state map has unexpected size: " + bootState.size()); 161 | } 162 | if (bootState.get(4)) { 163 | throw new RuntimeException("debug-permanent-disable must never be true: " + bootState); 164 | } 165 | boolean verifiedOrSelfSigned = bootState.get(0); 166 | if (verifiedOrSelfSigned != bootState.get(1) 167 | && verifiedOrSelfSigned != bootState.get(2) 168 | && verifiedOrSelfSigned != bootState.get(3)) { 169 | throw new RuntimeException("Unexpected boot state: " + bootState); 170 | } 171 | 172 | if (officialBuild) { 173 | if (!verifiedOrSelfSigned) { 174 | throw new AssertionError("Non-verified official build"); 175 | } 176 | return RootOfTrust.KM_VERIFIED_BOOT_VERIFIED; 177 | } else { 178 | return verifiedOrSelfSigned 179 | ? RootOfTrust.KM_VERIFIED_BOOT_SELF_SIGNED 180 | : RootOfTrust.KM_VERIFIED_BOOT_UNVERIFIED; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/EatClaim.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import static io.github.vvb2060.keyattestation.attestation.AuthorizationList.*; 4 | 5 | class EatClaim { 6 | public static final int IAT = 6; 7 | public static final int CTI = 7; 8 | 9 | public static final int NONCE = -75008; 10 | public static final int UEID = -75009; 11 | 12 | public static final int SECURITY_LEVEL = -76002; 13 | public static final int SECURITY_LEVEL_UNRESTRICTED = 1; 14 | public static final int SECURITY_LEVEL_SECURE_RESTRICTED = 3; 15 | public static final int SECURITY_LEVEL_HARDWARE = 4; 16 | 17 | public static final int BOOT_STATE = -76003; 18 | public static final int SUBMODS = -76000; 19 | 20 | private static final int PRIVATE_BASE = -80000; 21 | 22 | public static final int PURPOSE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_PURPOSE); 23 | public static final int ALGORITHM = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ALGORITHM); 24 | public static final int KEY_SIZE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_KEY_SIZE); 25 | public static final int BLOCK_MODE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_BLOCK_MODE); 26 | public static final int DIGEST = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_DIGEST); 27 | public static final int PADDING = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_PADDING); 28 | public static final int CALLER_NONCE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_CALLER_NONCE); 29 | public static final int MIN_MAC_LENGTH = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_MIN_MAC_LENGTH); 30 | public static final int KDF = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_KDF); 31 | 32 | public static final int EC_CURVE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_EC_CURVE); 33 | public static final int EAT_EC_CURVE_P_224 = 0; 34 | public static final int EAT_EC_CURVE_P_256 = 1; 35 | public static final int EAT_EC_CURVE_P_384 = 2; 36 | public static final int EAT_EC_CURVE_P_521 = 3; 37 | 38 | public static final int RSA_PUBLIC_EXPONENT = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_RSA_PUBLIC_EXPONENT); 39 | public static final int RSA_OAEP_MGF_DIGEST = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_RSA_OAEP_MGF_DIGEST); 40 | public static final int ROLLBACK_RESISTANCE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ROLLBACK_RESISTANCE); 41 | public static final int EARLY_BOOT_ONLY = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_EARLY_BOOT_ONLY); 42 | 43 | public static final int ACTIVE_DATETIME = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ACTIVE_DATETIME); 44 | public static final int ORIGINATION_EXPIRE_DATETIME = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ORIGINATION_EXPIRE_DATETIME); 45 | public static final int USAGE_EXPIRE_DATETIME = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_USAGE_EXPIRE_DATETIME); 46 | 47 | public static final int NO_AUTH_REQUIRED = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_NO_AUTH_REQUIRED); 48 | public static final int USER_AUTH_TYPE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_USER_AUTH_TYPE); 49 | public static final int AUTH_TIMEOUT = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_AUTH_TIMEOUT); 50 | public static final int ALLOW_WHILE_ON_BODY = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ALLOW_WHILE_ON_BODY); 51 | public static final int USER_PRESENCE_REQUIRED = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED); 52 | public static final int TRUSTED_CONFIRMATION_REQUIRED = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_TRUSTED_CONFIRMATION_REQUIRED); 53 | public static final int UNLOCKED_DEVICE_REQUIRED = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_UNLOCKED_DEVICE_REQUIRED); 54 | 55 | public static final int APPLICATION_ID = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_APPLICATION_ID); 56 | 57 | public static final int ORIGIN = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ORIGIN); 58 | public static final int ROLLBACK_RESISTANT = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ROLLBACK_RESISTANT); 59 | public static final int OS_VERSION = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_OS_VERSION); 60 | public static final int OS_PATCHLEVEL = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_OS_PATCHLEVEL); 61 | public static final int ATTESTATION_APPLICATION_ID = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_APPLICATION_ID); 62 | public static final int ATTESTATION_ID_BRAND = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_BRAND); 63 | public static final int ATTESTATION_ID_DEVICE = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_DEVICE); 64 | public static final int ATTESTATION_ID_PRODUCT = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_PRODUCT); 65 | public static final int ATTESTATION_ID_SERIAL = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_SERIAL); 66 | public static final int ATTESTATION_ID_MEID = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_MEID); 67 | public static final int ATTESTATION_ID_MANUFACTURER = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_MANUFACTURER); 68 | public static final int ATTESTATION_ID_MODEL = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_ATTESTATION_ID_MODEL); 69 | public static final int VENDOR_PATCHLEVEL = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_VENDOR_PATCHLEVEL); 70 | public static final int BOOT_PATCHLEVEL = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_BOOT_PATCHLEVEL); 71 | public static final int DEVICE_UNIQUE_ATTESTATION = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_DEVICE_UNIQUE_ATTESTATION); 72 | public static final int IDENTITY_CREDENTIAL_KEY = PRIVATE_BASE - (KEYMASTER_TAG_TYPE_MASK & KM_TAG_IDENTITY_CREDENTIAL_KEY); 73 | 74 | private static final int NON_KM_BASE = PRIVATE_BASE - 2000; 75 | 76 | public static final int VERIFIED_BOOT_KEY = NON_KM_BASE - 1; 77 | public static final int DEVICE_LOCKED = NON_KM_BASE - 2; 78 | public static final int VERIFIED_BOOT_HASH = NON_KM_BASE - 3; 79 | public static final int ATTESTATION_VERSION = NON_KM_BASE - 4; 80 | public static final int KEYMASTER_VERSION = NON_KM_BASE - 5; 81 | public static final int OFFICIAL_BUILD = NON_KM_BASE - 6; 82 | 83 | public static final String SUBMOD_SOFTWARE = "software"; 84 | public static final String SUBMOD_TEE = "tee"; 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import org.bouncycastle.asn1.ASN1Encodable; 4 | import org.bouncycastle.asn1.ASN1Sequence; 5 | import org.bouncycastle.asn1.ASN1TaggedObject; 6 | 7 | import java.security.cert.CertificateParsingException; 8 | 9 | public class IntegrityStatus { 10 | private static final int TRUST_BOOT = 0; 11 | private static final int WARRANTY = 1; 12 | private static final int ICD = 2; 13 | private static final int KERNEL_STATUS = 3; 14 | private static final int SYSTEM_STATUS = 4; 15 | private static final int AUTH_RESULT = 5; 16 | 17 | public static final int STATUS_NORMAL = 0; 18 | public static final int STATUS_ABNORMAL = 1; 19 | public static final int STATUS_NOT_SUPPORT = 2; 20 | 21 | private int trustBoot = STATUS_NOT_SUPPORT; 22 | private int warranty = STATUS_NOT_SUPPORT; 23 | private int icd = STATUS_NOT_SUPPORT; 24 | private int kernelStatus = STATUS_NOT_SUPPORT; 25 | private int systemStatus = STATUS_NOT_SUPPORT; 26 | private AuthResult authResult; 27 | 28 | public IntegrityStatus(ASN1Encodable asn1Encodable) throws CertificateParsingException { 29 | if (!(asn1Encodable instanceof ASN1Sequence sequence)) { 30 | throw new CertificateParsingException("Expected sequence for integrity status, found " 31 | + asn1Encodable.getClass().getName()); 32 | } 33 | for (var entry : sequence) { 34 | if (!(entry instanceof ASN1TaggedObject taggedObject)) { 35 | throw new CertificateParsingException( 36 | "Expected tagged object, found " + entry.getClass().getName()); 37 | } 38 | int tag = taggedObject.getTagNo(); 39 | var value = taggedObject.getBaseObject().toASN1Primitive(); 40 | switch (tag) { 41 | case TRUST_BOOT -> trustBoot = Asn1Utils.getIntegerFromAsn1(value); 42 | case WARRANTY -> warranty = Asn1Utils.getIntegerFromAsn1(value); 43 | case ICD -> icd = Asn1Utils.getIntegerFromAsn1(value); 44 | case KERNEL_STATUS -> kernelStatus = Asn1Utils.getIntegerFromAsn1(value); 45 | case SYSTEM_STATUS -> systemStatus = Asn1Utils.getIntegerFromAsn1(value); 46 | case AUTH_RESULT -> authResult = AuthResult.parse(value); 47 | default -> throw new CertificateParsingException("invalid tag no: " + tag); 48 | } 49 | } 50 | } 51 | 52 | public static String statusToString(int status) { 53 | return switch (status) { 54 | case STATUS_NORMAL -> "Normal"; 55 | case STATUS_ABNORMAL -> "Abnormal"; 56 | case STATUS_NOT_SUPPORT -> "Not support"; 57 | default -> Integer.toHexString(status); 58 | }; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "TrustBoot: " + statusToString(trustBoot) + 64 | "\nWarranty: " + statusToString(warranty) + 65 | "\nICD: " + statusToString(icd) + 66 | "\nKernel Status: " + statusToString(kernelStatus) + 67 | "\nSystem Status: " + statusToString(systemStatus) + 68 | "\nCaller auth(with PROCA) Status: " + 69 | (authResult == null ? "Not performed" : authResult.toString()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import com.google.common.io.BaseEncoding; 4 | 5 | import org.bouncycastle.asn1.ASN1Sequence; 6 | import org.bouncycastle.asn1.ASN1TaggedObject; 7 | 8 | import java.security.cert.CertificateParsingException; 9 | import java.security.cert.X509Certificate; 10 | 11 | // https://docs.samsungknox.com/dev/knox-attestation/ 12 | public class KnoxAttestation extends Asn1Attestation { 13 | private static final int CHALLENGE = 0; 14 | private static final int ID_ATTEST = 4; 15 | private static final int INTEGRITY = 5; 16 | private static final int ATTESTATION_RECORD_HASH = 6; 17 | 18 | private String challenge; 19 | private String idAttest; 20 | private IntegrityStatus knoxIntegrity; 21 | private byte[] recordHash; 22 | 23 | public KnoxAttestation(X509Certificate x509Cert) throws CertificateParsingException { 24 | super(x509Cert); 25 | ASN1Sequence knoxExtSeq = getKnoxExtensionSequence(x509Cert); 26 | for (var entry : knoxExtSeq) { 27 | if (!(entry instanceof ASN1TaggedObject taggedObject)) { 28 | throw new CertificateParsingException( 29 | "Expected tagged object, found " + entry.getClass().getName()); 30 | } 31 | int tag = taggedObject.getTagNo(); 32 | var value = taggedObject.getBaseObject().toASN1Primitive(); 33 | switch (tag) { 34 | case CHALLENGE -> challenge = Asn1Utils.getStringFromASN1PrintableString(value); 35 | case ID_ATTEST -> idAttest = Asn1Utils.getStringFromASN1PrintableString(value); 36 | case INTEGRITY -> knoxIntegrity = new IntegrityStatus(value); 37 | case ATTESTATION_RECORD_HASH -> recordHash = Asn1Utils.getByteArrayFromAsn1(value); 38 | default -> throw new CertificateParsingException("invalid tag no: " + tag); 39 | } 40 | } 41 | } 42 | 43 | ASN1Sequence getKnoxExtensionSequence(X509Certificate x509Cert) 44 | throws CertificateParsingException { 45 | byte[] knoxExtensionSequence = x509Cert.getExtensionValue(KNOX_OID); 46 | if (knoxExtensionSequence == null || knoxExtensionSequence.length == 0) { 47 | throw new CertificateParsingException("Did not find extension with OID " + KNOX_OID); 48 | } 49 | return Asn1Utils.getAsn1SequenceFromBytes(knoxExtensionSequence); 50 | } 51 | 52 | public String getKnoxChallenge() { 53 | return challenge; 54 | } 55 | 56 | public String getIdAttest() { 57 | return idAttest; 58 | } 59 | 60 | public IntegrityStatus getKnoxIntegrity() { 61 | return knoxIntegrity; 62 | } 63 | 64 | public byte[] getRecordHash() { 65 | return recordHash; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return super.toString() + 71 | "\n\nExtension type: " + getClass().getSimpleName() + 72 | "\nID attestation: " + idAttest + 73 | "\nChallenge: " + challenge + 74 | "\nIntegrity status: " + knoxIntegrity + 75 | "\nAttestation record hash: " + BaseEncoding.base16().lowerCase().encode(recordHash); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/ProvisioningInfo.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import android.util.Log; 4 | 5 | import java.security.cert.CertificateParsingException; 6 | import java.security.cert.X509Certificate; 7 | 8 | import co.nstant.in.cbor.CborException; 9 | import co.nstant.in.cbor.model.Map; 10 | import co.nstant.in.cbor.model.Number; 11 | import io.github.vvb2060.keyattestation.AppApplication; 12 | 13 | public class ProvisioningInfo { 14 | private static final String OID = "1.3.6.1.4.1.11129.2.1.30"; 15 | 16 | private Integer certsIssued; 17 | private String manufacturer; 18 | 19 | private ProvisioningInfo(Map map) { 20 | for (var key : map.getKeys()) { 21 | switch (((Number) key).getValue().intValue()) { 22 | case 1 -> certsIssued = CborUtils.getInt(map, key); 23 | case 3 -> manufacturer = CborUtils.getUnicodeString(map, key); 24 | default -> Log.w(AppApplication.TAG, "new provisioning info: " 25 | + key + " = " + map.get(key)); 26 | } 27 | } 28 | } 29 | 30 | public static ProvisioningInfo get(X509Certificate cert) { 31 | var bytes = cert.getExtensionValue(OID); 32 | if (bytes == null) return null; 33 | try { 34 | var asn1 = Asn1Utils.getAsn1EncodableFromBytes(bytes); 35 | var cborBytes = Asn1Utils.getByteArrayFromAsn1(asn1); 36 | var map = (Map) CborUtils.decodeCbor(cborBytes); 37 | return new ProvisioningInfo(map); 38 | } catch (CborException | CertificateParsingException e) { 39 | Log.e(AppApplication.TAG, "decode", e); 40 | return null; 41 | } 42 | } 43 | 44 | public Integer getCertsIssued() { 45 | return certsIssued; 46 | } 47 | 48 | public String getManufacturer() { 49 | return manufacturer; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.Network; 6 | import android.net.NetworkCapabilities; 7 | import android.os.Build; 8 | import android.util.Log; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import org.json.JSONException; 13 | import org.json.JSONObject; 14 | 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.math.BigInteger; 19 | import java.net.HttpURLConnection; 20 | import java.net.URL; 21 | import java.nio.charset.StandardCharsets; 22 | 23 | import io.github.vvb2060.keyattestation.AppApplication; 24 | import io.github.vvb2060.keyattestation.R; 25 | 26 | public record RevocationList(String status, String reason) { 27 | private static final JSONObject data = getStatus(); 28 | 29 | private static String toString(InputStream input) throws IOException { 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 31 | return new String(input.readAllBytes(), StandardCharsets.UTF_8); 32 | } else { 33 | var output = new ByteArrayOutputStream(8192); 34 | var buffer = new byte[8192]; 35 | for (int length; (length = input.read(buffer)) != -1; ) { 36 | output.write(buffer, 0, length); 37 | } 38 | return output.toString(); 39 | } 40 | } 41 | 42 | private static JSONObject getStatus() { 43 | if (isConnectedToInternet()) { 44 | HttpURLConnection connection = null; 45 | try { 46 | connection = getHttpURLConnection(); 47 | 48 | if (connection == null) 49 | throw new Exception(); 50 | 51 | String str = toString(connection.getInputStream()); 52 | 53 | return new JSONObject(str); 54 | 55 | } catch (Throwable t) { 56 | Log.e(AppApplication.TAG, "getStatus [remote]", t); 57 | } finally { 58 | if (connection != null) { 59 | connection.disconnect(); 60 | } 61 | } 62 | } 63 | 64 | try (InputStream inputStream = AppApplication.app.getResources().openRawResource(R.raw.status)) { 65 | return new JSONObject(toString(inputStream)); 66 | } catch (Throwable t) { 67 | Log.e(AppApplication.TAG, "getStatus [local]", t); 68 | } 69 | 70 | return new JSONObject(); 71 | } 72 | 73 | public static RevocationList get(BigInteger serialNumber) { 74 | String serialNumberString = serialNumber.toString(16).toLowerCase(); 75 | try { 76 | JSONObject entries = data.getJSONObject("entries"); 77 | JSONObject revocationEntry = entries.optJSONObject(serialNumberString); 78 | 79 | if (revocationEntry == null) 80 | return null; 81 | 82 | return new RevocationList( 83 | revocationEntry.getString("status"), 84 | revocationEntry.getString("reason") 85 | ); 86 | } catch (JSONException e) { 87 | Log.e(AppApplication.TAG, "Error parsing JSON entries", e); 88 | } 89 | return null; 90 | } 91 | 92 | private static HttpURLConnection getHttpURLConnection() { 93 | try { 94 | URL url = new URL("https://android.googleapis.com/attestation/status"); 95 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 96 | 97 | connection.setUseCaches(false); 98 | connection.setDefaultUseCaches(false); 99 | 100 | connection.setRequestMethod("GET"); 101 | connection.setRequestProperty("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate"); 102 | connection.setRequestProperty("Pragma", "no-cache"); 103 | connection.setRequestProperty("Expires", "0"); 104 | 105 | return connection; 106 | } catch (Throwable t) { 107 | Log.e(AppApplication.TAG, "getHttpURLConnection", t); 108 | } 109 | return null; 110 | } 111 | 112 | private static boolean isConnectedToInternet() { 113 | ConnectivityManager connectivityManager = (ConnectivityManager) AppApplication.app.getSystemService(Context.CONNECTIVITY_SERVICE); 114 | if (connectivityManager == null) return false; 115 | 116 | Network network = connectivityManager.getActiveNetwork(); 117 | if (network == null) return false; 118 | 119 | NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); 120 | return capabilities != null && 121 | (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || 122 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || 123 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)); 124 | } 125 | 126 | @NonNull 127 | @Override 128 | public String toString() { 129 | return "status is " + status + ", reason is " + reason; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootOfTrust.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.vvb2060.keyattestation.attestation; 18 | 19 | import com.google.common.io.BaseEncoding; 20 | 21 | import org.bouncycastle.asn1.ASN1Encodable; 22 | import org.bouncycastle.asn1.ASN1Sequence; 23 | 24 | import java.security.cert.CertificateParsingException; 25 | 26 | public class RootOfTrust { 27 | private static final int VERIFIED_BOOT_KEY_INDEX = 0; 28 | private static final int DEVICE_LOCKED_INDEX = 1; 29 | private static final int VERIFIED_BOOT_STATE_INDEX = 2; 30 | private static final int VERIFIED_BOOT_HASH_INDEX = 3; 31 | 32 | public static final int KM_VERIFIED_BOOT_VERIFIED = 0; 33 | public static final int KM_VERIFIED_BOOT_SELF_SIGNED = 1; 34 | public static final int KM_VERIFIED_BOOT_UNVERIFIED = 2; 35 | public static final int KM_VERIFIED_BOOT_FAILED = 3; 36 | 37 | private final byte[] verifiedBootKey; 38 | private final boolean deviceLocked; 39 | private final int verifiedBootState; 40 | private final byte[] verifiedBootHash; 41 | 42 | public RootOfTrust(ASN1Encodable asn1Encodable) throws CertificateParsingException { 43 | if (!(asn1Encodable instanceof ASN1Sequence sequence)) { 44 | throw new CertificateParsingException("Expected sequence for root of trust, found " 45 | + asn1Encodable.getClass().getName()); 46 | } 47 | 48 | verifiedBootKey = 49 | Asn1Utils.getByteArrayFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_KEY_INDEX)); 50 | deviceLocked = Asn1Utils.getBooleanFromAsn1(sequence.getObjectAt(DEVICE_LOCKED_INDEX)); 51 | verifiedBootState = 52 | Asn1Utils.getIntegerFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_STATE_INDEX)); 53 | if (sequence.size() == 3) verifiedBootHash = null; 54 | else verifiedBootHash = 55 | Asn1Utils.getByteArrayFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_HASH_INDEX)); 56 | } 57 | 58 | RootOfTrust(byte[] verifiedBootKey, boolean deviceLocked, 59 | int verifiedBootState, byte[] verifiedBootHash) { 60 | this.verifiedBootKey = verifiedBootKey; 61 | this.deviceLocked = deviceLocked; 62 | this.verifiedBootState = verifiedBootState; 63 | this.verifiedBootHash = verifiedBootHash; 64 | } 65 | 66 | public static String verifiedBootStateToString(int verifiedBootState) { 67 | switch (verifiedBootState) { 68 | case KM_VERIFIED_BOOT_VERIFIED: 69 | return "Verified"; 70 | case KM_VERIFIED_BOOT_SELF_SIGNED: 71 | return "Self-signed"; 72 | case KM_VERIFIED_BOOT_UNVERIFIED: 73 | return "Unverified"; 74 | case KM_VERIFIED_BOOT_FAILED: 75 | return "Failed"; 76 | default: 77 | return "Unknown (" + verifiedBootState + ")"; 78 | } 79 | } 80 | 81 | public byte[] getVerifiedBootKey() { 82 | return verifiedBootKey; 83 | } 84 | 85 | public boolean isDeviceLocked() { 86 | return deviceLocked; 87 | } 88 | 89 | public int getVerifiedBootState() { 90 | return verifiedBootState; 91 | } 92 | 93 | public byte[] getVerifiedBootHash() { 94 | return verifiedBootHash; 95 | } 96 | 97 | @Override 98 | public String toString() { 99 | StringBuilder sb = new StringBuilder() 100 | .append("verifiedBootKey: ") 101 | .append(BaseEncoding.base16().lowerCase().encode(verifiedBootKey)) 102 | .append("\ndeviceLocked: ") 103 | .append(deviceLocked) 104 | .append("\nverifiedBootState: ") 105 | .append(verifiedBootStateToString(verifiedBootState)); 106 | if (verifiedBootHash != null) { 107 | sb.append("\nverifiedBootHash: ") 108 | .append(BaseEncoding.base16().lowerCase().encode(verifiedBootHash)); 109 | } 110 | return sb.toString(); 111 | } 112 | 113 | public static class Builder { 114 | private byte[] verifiedBootKey; 115 | private boolean deviceLocked = false; 116 | private int verifiedBootState = -1; 117 | private byte[] verifiedBootHash; 118 | 119 | public Builder setVerifiedBootKey(byte[] verifiedBootKey) { 120 | this.verifiedBootKey = verifiedBootKey; 121 | return this; 122 | } 123 | 124 | public Builder setDeviceLocked(boolean deviceLocked) { 125 | this.deviceLocked = deviceLocked; 126 | return this; 127 | } 128 | 129 | public Builder setVerifiedBootState(int verifiedBootState) { 130 | this.verifiedBootState = verifiedBootState; 131 | return this; 132 | } 133 | 134 | public Builder setVerifiedBootHash(byte[] verifiedBootHash) { 135 | this.verifiedBootHash = verifiedBootHash; 136 | return this; 137 | } 138 | 139 | public RootOfTrust build() { 140 | return new RootOfTrust(verifiedBootKey, deviceLocked, 141 | verifiedBootState, verifiedBootHash); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/attestation/RootPublicKey.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.attestation; 2 | 3 | import android.util.Base64; 4 | import android.util.Log; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.security.PublicKey; 8 | import java.security.cert.CertificateException; 9 | import java.security.cert.CertificateFactory; 10 | import java.util.Arrays; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | import io.github.vvb2060.keyattestation.AppApplication; 15 | 16 | public class RootPublicKey { 17 | public enum Status { 18 | NULL, 19 | FAILED, 20 | UNKNOWN, 21 | AOSP, 22 | GOOGLE, 23 | GOOGLE_RKP, 24 | KNOX, 25 | OEM, 26 | } 27 | 28 | private static final String GOOGLE_ROOT_PUBLIC_KEY = """ 29 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU\ 30 | FmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5j\ 31 | lRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y\ 32 | //0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73X\ 33 | pXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYI\ 34 | mQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB\ 35 | +TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7q\ 36 | uvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgp\ 37 | Zrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7\ 38 | gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82\ 39 | ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+\ 40 | NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ=="""; 41 | 42 | private static final String AOSP_ROOT_EC_PUBLIC_KEY = """ 43 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamgu\ 44 | D/9/SQ59dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpA=="""; 45 | 46 | private static final String AOSP_ROOT_RSA_PUBLIC_KEY = """ 47 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMf\ 48 | d5tjkWP/96r/C3JHTsAsQ+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmg\ 49 | MdsGUmX4RFlXYfC78hdLt0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm\ 50 | +Vfkl5YLCazOkjWFmwIDAQAB"""; 51 | 52 | private static final String KNOX_SAKV1_ROOT_PUBLIC_KEY = """ 53 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBs9Qjr//REhkXW7jUqjY9KNwWac4r\ 54 | 5+kdUGk+TZjRo1YEa47Axwj6AJsbOjo4QsHiYRiWTELvFeiuBsKqyuF0xyAAKvDo\ 55 | fBqrEq1/Ckxo2mz7Q4NQes3g4ahSjtgUSh0k85fYwwHjCeLyZ5kEqgHG9OpOH526\ 56 | FFAK3slSUgC8RObbxys="""; 57 | 58 | private static final String KNOX_SAKV2_ROOT_PUBLIC_KEY = """ 59 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhbGuLrpql5I2WJmrE5kEVZOo+dgA\ 60 | 46mKrVJf/sgzfzs2u7M9c1Y9ZkCEiiYkhTFE9vPbasmUfXybwgZ2EM30A1ABPd12\ 61 | 4n3JbEDfsB/wnMH1AcgsJyJFPbETZiy42Fhwi+2BCA5bcHe7SrdkRIYSsdBRaKBo\ 62 | ZsapxB0gAOs0jSPRX5M="""; 63 | 64 | private static final String KNOX_SAKMV1_ROOT_PUBLIC_KEY = """ 65 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB9XeEN8lg6p5xvMVWG42P2Qi/aRKX\ 66 | 2rPRNgK92UlO9O/TIFCKHC1AWCLFitPVEow5W+yEgC2wOiYxgepY85TOoH0AuEkL\ 67 | oiC6ldbF2uNVU3rYYSytWAJg3GFKd1l9VLDmxox58Hyw2Jmdd5VSObGiTFQ/SgKs\ 68 | n2fbQPtpGlNxgEfd6Y8="""; 69 | 70 | private static final byte[] googleKey = Base64.decode(GOOGLE_ROOT_PUBLIC_KEY, 0); 71 | private static final byte[] aospEcKey = Base64.decode(AOSP_ROOT_EC_PUBLIC_KEY, 0); 72 | private static final byte[] aospRsaKey = Base64.decode(AOSP_ROOT_RSA_PUBLIC_KEY, 0); 73 | private static final byte[] knoxSakv1Key = Base64.decode(KNOX_SAKV1_ROOT_PUBLIC_KEY, 0); 74 | private static final byte[] knoxSakv2Key = Base64.decode(KNOX_SAKV2_ROOT_PUBLIC_KEY, 0); 75 | private static final byte[] knoxSakmv1Key = Base64.decode(KNOX_SAKMV1_ROOT_PUBLIC_KEY, 0); 76 | private static final Set oemKeys = getOemPublicKey(); 77 | 78 | private static Set getOemPublicKey() { 79 | var resName = "android:array/vendor_required_attestation_certificates"; 80 | var res = AppApplication.app.getResources(); 81 | // noinspection DiscouragedApi 82 | var id = res.getIdentifier(resName, null, null); 83 | if (id == 0) { 84 | return null; 85 | } 86 | var set = new HashSet(); 87 | try { 88 | var cf = CertificateFactory.getInstance("X.509"); 89 | for (var s : res.getStringArray(id)) { 90 | var cert = s.replaceAll("\\s+", "\n") 91 | .replaceAll("-BEGIN\\nCERTIFICATE-", "-BEGIN CERTIFICATE-") 92 | .replaceAll("-END\\nCERTIFICATE-", "-END CERTIFICATE-"); 93 | var input = new ByteArrayInputStream(cert.getBytes()); 94 | var publicKey = cf.generateCertificate(input).getPublicKey(); 95 | set.add(publicKey); 96 | } 97 | } catch (CertificateException e) { 98 | Log.e(AppApplication.TAG, "getOemKeys: ", e); 99 | return null; 100 | } 101 | set.removeIf(key -> Arrays.equals(key.getEncoded(), googleKey)); 102 | if (set.isEmpty()) { 103 | return null; 104 | } 105 | set.forEach(key -> Log.i(AppApplication.TAG, "getOemKeys: " + key)); 106 | return set; 107 | } 108 | 109 | public static Status check(byte[] publicKey) { 110 | if (Arrays.equals(publicKey, googleKey)) { 111 | return Status.GOOGLE; 112 | } else if (Arrays.equals(publicKey, aospEcKey)) { 113 | return Status.AOSP; 114 | } else if (Arrays.equals(publicKey, aospRsaKey)) { 115 | return Status.AOSP; 116 | } else if (Arrays.equals(publicKey, knoxSakv2Key)) { 117 | return Status.KNOX; 118 | } else if (Arrays.equals(publicKey, knoxSakv1Key)) { 119 | return Status.KNOX; 120 | } else if (Arrays.equals(publicKey, knoxSakmv1Key)) { 121 | return Status.KNOX; 122 | } else if (oemKeys != null) { 123 | for (var key : oemKeys) { 124 | if (Arrays.equals(publicKey, key.getEncoded())) { 125 | return Status.OEM; 126 | } 127 | } 128 | } 129 | return Status.UNKNOWN; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/BootStateViewHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.content.res.ColorStateList 4 | import android.view.View 5 | import androidx.core.view.isVisible 6 | import io.github.vvb2060.keyattestation.R 7 | import io.github.vvb2060.keyattestation.repository.AttestationData 8 | import io.github.vvb2060.keyattestation.attestation.RootOfTrust 9 | import io.github.vvb2060.keyattestation.databinding.HomeHeaderBinding 10 | import rikka.core.res.resolveColor 11 | 12 | class BootStateViewHolder(itemView: View, binding: HomeHeaderBinding) : 13 | HomeViewHolder(itemView, binding) { 14 | 15 | companion object { 16 | 17 | val CREATOR = Creator { inflater, parent -> 18 | val binding = HomeHeaderBinding.inflate(inflater, parent, false) 19 | BootStateViewHolder(binding.root, binding) 20 | } 21 | } 22 | 23 | override fun onBind() { 24 | val context = itemView.context 25 | 26 | val rootOfTrust = data.rootOfTrust 27 | val locked = rootOfTrust?.isDeviceLocked 28 | val bootUnverified = rootOfTrust?.verifiedBootState != RootOfTrust.KM_VERIFIED_BOOT_VERIFIED 29 | 30 | val titleRes: Int 31 | val summaryRes: Int 32 | val iconRes: Int 33 | val colorAttrRes: Int 34 | 35 | if (locked == null) { 36 | titleRes = R.string.bootloader_unknown 37 | iconRes = R.drawable.ic_boot_unknown_24 38 | colorAttrRes = rikka.material.R.attr.colorInactive 39 | } else if (!locked) { 40 | titleRes = R.string.bootloader_unlocked 41 | iconRes = R.drawable.ic_boot_unlocked_24 42 | colorAttrRes = rikka.material.R.attr.colorWarning 43 | } else if (bootUnverified) { 44 | titleRes = R.string.bootloader_user 45 | iconRes = R.drawable.ic_boot_locked_24 46 | colorAttrRes = rikka.material.R.attr.colorInactive 47 | } else { 48 | titleRes = R.string.bootloader_locked 49 | iconRes = R.drawable.ic_boot_locked_24 50 | colorAttrRes = rikka.material.R.attr.colorSafe 51 | } 52 | 53 | if (data.isSoftwareLevel) { 54 | summaryRes = R.string.bootloader_summary_sw_level 55 | } else { 56 | summaryRes = 0 57 | } 58 | 59 | val color = context.theme.resolveColor(colorAttrRes) 60 | 61 | binding.apply { 62 | title.setText(titleRes) 63 | icon.setImageDrawable(context.getDrawable(iconRes)) 64 | root.backgroundTintList = ColorStateList.valueOf(color) 65 | if (summaryRes == 0) { 66 | summary.isVisible = false 67 | } else { 68 | summary.isVisible = true 69 | summary.setText(summaryRes) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/Data.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.content.Context 4 | import io.github.vvb2060.keyattestation.R 5 | import rikka.html.text.HtmlCompat 6 | import rikka.html.text.toHtml 7 | 8 | abstract class Data { 9 | abstract val title: Int 10 | abstract val description: Int 11 | open fun getMessage(context: Context): CharSequence = 12 | context.getString(description).toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) 13 | } 14 | 15 | class CommonData( 16 | override val title: Int, 17 | override val description: Int, 18 | val data: String? = null 19 | ) : Data() 20 | 21 | class StringData( 22 | override val title: Int, 23 | val data: String, 24 | ) : Data() { 25 | override val description = 0 26 | override fun getMessage(context: Context) = data 27 | } 28 | 29 | class HeaderData( 30 | override val title: Int, 31 | override val description: Int, 32 | val icon: Int, 33 | val color: Int 34 | ) : Data() 35 | 36 | class AuthorizationItemData( 37 | override val title: Int, 38 | override val description: Int, 39 | val data: String, 40 | val tee: Boolean 41 | ) : Data() { 42 | constructor(title: Int, description: Int, data: String?, fallback: String?) : 43 | this(title, description, data ?: fallback!!, data != null) 44 | 45 | override fun getMessage(context: Context): CharSequence { 46 | val id = if (tee) R.string.tee_enforced_description else R.string.sw_enforced_description 47 | return "${context.getString(description)}

* ${context.getString(id)}" 48 | .toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) 49 | } 50 | } 51 | 52 | class SecurityLevelData( 53 | override val title: Int, 54 | override val description: Int, 55 | val securityLevelDescription: Int, 56 | val version: String, 57 | val securityLevel: Int 58 | ) : Data() { 59 | override fun getMessage(context: Context): CharSequence { 60 | val flags = HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or 61 | HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE 62 | return ("${context.getString(description)}

" + 63 | context.getString(securityLevelDescription)).toHtml(flags) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/ErrorViewHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.view.View 4 | import io.github.vvb2060.keyattestation.R 5 | import io.github.vvb2060.keyattestation.databinding.HomeErrorBinding 6 | import io.github.vvb2060.keyattestation.lang.AttestationException 7 | import rikka.html.text.HtmlCompat 8 | import rikka.html.text.toHtml 9 | 10 | class ErrorViewHolder(itemView: View, binding: HomeErrorBinding) : HomeViewHolder(itemView, binding) { 11 | 12 | companion object { 13 | 14 | val CREATOR = Creator { inflater, parent -> 15 | val binding = HomeErrorBinding.inflate(inflater, parent, false) 16 | ErrorViewHolder(binding.root, binding) 17 | } 18 | } 19 | 20 | override fun onBind() { 21 | val context = itemView.context 22 | binding.apply { 23 | val sb = StringBuilder() 24 | sb.append(context.getString(data.descriptionResId)).append("

") 25 | 26 | sb.append(context.getString(R.string.error_message_subtitle)).append("
") 27 | sb.append("") 28 | var tr = data.cause 29 | while (tr != null) { 30 | sb.append(tr).append("
") 31 | tr = tr.cause 32 | } 33 | sb.append("
") 34 | text1.text = sb.toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/HeaderViewHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.view.View 4 | import androidx.core.view.isVisible 5 | import io.github.vvb2060.keyattestation.databinding.HomeHeaderBinding 6 | import rikka.core.res.resolveColorStateList 7 | import rikka.recyclerview.BaseViewHolder.Creator 8 | 9 | class HeaderViewHolder(itemView: View, binding: HomeHeaderBinding) : HomeViewHolder(itemView, binding) { 10 | 11 | companion object { 12 | 13 | val CREATOR = Creator { inflater, parent -> 14 | val binding = HomeHeaderBinding.inflate(inflater, parent, false) 15 | HeaderViewHolder(binding.root, binding) 16 | } 17 | } 18 | 19 | override fun onBind() { 20 | binding.apply { 21 | val context = root.context 22 | root.backgroundTintList = context.theme.resolveColorStateList(data.color) 23 | icon.setImageDrawable(context.getDrawable(data.icon)) 24 | title.setText(data.title) 25 | if (data.description != 0) { 26 | summary.setText(data.description) 27 | summary.isVisible = true 28 | } else { 29 | summary.isVisible = false 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.commit 5 | import io.github.vvb2060.keyattestation.BuildConfig 6 | import io.github.vvb2060.keyattestation.R 7 | import io.github.vvb2060.keyattestation.app.AppBarFragmentActivity 8 | 9 | class HomeActivity : AppBarFragmentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | supportActionBar?.subtitle = BuildConfig.VERSION_NAME 13 | 14 | if (savedInstanceState == null) { 15 | supportFragmentManager.commit { 16 | setReorderingAllowed(true) 17 | add(R.id.fragment_container, HomeFragment()) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/HomeItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Rect 6 | import android.graphics.drawable.Drawable 7 | import android.view.View 8 | import androidx.recyclerview.widget.RecyclerView 9 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration 10 | import rikka.core.res.resolveDrawable 11 | import kotlin.math.roundToInt 12 | 13 | class HomeItemDecoration(context: Context) : ItemDecoration() { 14 | 15 | private val drawable: Drawable = context.theme.resolveDrawable(rikka.material.R.attr.outlineButtonBackground)!! 16 | private val cardMargin: Int = (context.resources.displayMetrics.density * 8).roundToInt() 17 | private val cardPadding: Int = (context.resources.displayMetrics.density * 8).roundToInt() 18 | 19 | private fun hasTopMargin(@Suppress("UNUSED_PARAMETER") adapter: HomeAdapter, position: Int): Boolean { 20 | return position == 0 21 | } 22 | 23 | private fun hasBottomMargin(adapter: HomeAdapter, position: Int): Boolean { 24 | return position == adapter.itemCount - 1 || !(adapter.allowFrameAt(position) && adapter.allowFrameAt(position + 1) && !adapter.shouldCommitFrameAt(position)) 25 | } 26 | 27 | private fun hasTopPadding(adapter: HomeAdapter, position: Int): Boolean { 28 | return adapter.allowFrameAt(position) && (position == 0 || adapter.shouldCommitFrameAt(position - 1) || !adapter.allowFrameAt(position - 1)) 29 | } 30 | 31 | private fun hasBottomPadding(adapter: HomeAdapter, position: Int): Boolean { 32 | return adapter.shouldCommitFrameAt(position)// && (position == adapter.itemCount - 1 || adapter.shouldCommitFrameAt(position)) 33 | } 34 | 35 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 36 | val adapter = parent.adapter as HomeAdapter 37 | val position = parent.getChildAdapterPosition(view) 38 | 39 | if (hasTopMargin(adapter, position)) { 40 | outRect.top = cardMargin 41 | } 42 | if (hasTopPadding(adapter, position)) { 43 | outRect.top += cardPadding 44 | } 45 | 46 | if (hasBottomMargin(adapter, position)) { 47 | outRect.bottom = cardMargin 48 | } 49 | if (hasBottomPadding(adapter, position)) { 50 | outRect.bottom += cardPadding 51 | } 52 | 53 | outRect.left = cardMargin 54 | outRect.right = cardMargin 55 | } 56 | 57 | override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 58 | if (parent.childCount == 0) { 59 | return 60 | } 61 | val adapter = parent.adapter as HomeAdapter 62 | var invalidatedPosition = true 63 | var left = 0 64 | var top = 0 65 | var right = 0 66 | var bottom: Int 67 | 68 | for (i in 0 until parent.childCount) { 69 | val child = parent.getChildAt(i) 70 | val position = parent.getChildAdapterPosition(child) 71 | 72 | if (!adapter.allowFrameAt(position)) { 73 | continue 74 | } 75 | 76 | if (invalidatedPosition) { 77 | left = child.left 78 | top = child.top 79 | right = child.right 80 | invalidatedPosition = false 81 | } 82 | 83 | if ((i == parent.childCount - 1) || adapter.shouldCommitFrameAt(position)) { 84 | bottom = child.bottom 85 | 86 | drawable.setBounds(left, top - cardPadding, right, bottom + cardPadding) 87 | drawable.draw(c) 88 | 89 | invalidatedPosition = true 90 | } else { 91 | left = child.left.coerceAtLeast(left) 92 | right = child.right.coerceAtLeast(right) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.view.View 4 | import androidx.viewbinding.ViewBinding 5 | import io.github.vvb2060.keyattestation.util.ViewBindingViewHolder 6 | 7 | abstract class HomeViewHolder(itemView: View, binding: VB) : ViewBindingViewHolder(itemView, binding) -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/home/SubtitleViewHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.home 2 | 3 | import android.view.View 4 | import io.github.vvb2060.keyattestation.databinding.HomeSubtitleBinding 5 | 6 | class SubtitleViewHolder(itemView: View, binding: HomeSubtitleBinding) : 7 | HomeViewHolder(itemView, binding) { 8 | 9 | companion object { 10 | 11 | val CREATOR = Creator { inflater, parent -> 12 | val binding = HomeSubtitleBinding.inflate(inflater, parent, false) 13 | SubtitleViewHolder(binding.root, binding) 14 | } 15 | } 16 | 17 | init { 18 | itemView.setOnClickListener { 19 | listener.onCommonDataClick(data) 20 | } 21 | } 22 | 23 | override fun onBind() { 24 | binding.title.setText(data.title) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/keystore/ContextHook.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.keystore; 2 | 3 | import android.content.Context; 4 | import android.content.ContextWrapper; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.SystemProperties; 8 | import android.telephony.TelephonyManager_rename; 9 | import android.util.Log; 10 | 11 | import java.util.concurrent.Executor; 12 | 13 | import io.github.vvb2060.keyattestation.AppApplication; 14 | 15 | public class ContextHook extends ContextWrapper { 16 | private final TelephonyManager_rename telephonyService = new TelephonyManager_rename(this) { 17 | @Override 18 | public String getImei(int slotIndex) { 19 | String imei = null; 20 | try { 21 | imei = super.getImei(slotIndex); 22 | } catch (SecurityException e) { 23 | Log.w(AppApplication.TAG, "getImei", e); 24 | } 25 | if (imei == null) { 26 | var slot = slotIndex == 0 ? "" : "2"; 27 | var prop = SystemProperties.get("ro.ril.oem.imei" + slot); 28 | return prop.isEmpty() ? null : prop; 29 | } 30 | return imei; 31 | } 32 | 33 | @Override 34 | public String getMeid(int slotIndex) { 35 | String meid = null; 36 | try { 37 | meid = super.getMeid(slotIndex); 38 | } catch (SecurityException e) { 39 | Log.w(AppApplication.TAG, "getMeid", e); 40 | } 41 | if (meid == null) { 42 | var prop = SystemProperties.get("ro.ril.oem.meid"); 43 | return prop.isEmpty() ? null : prop; 44 | } 45 | return meid; 46 | } 47 | }; 48 | 49 | private ContextHook(Context base) { 50 | super(base); 51 | } 52 | 53 | @Override 54 | public Object getSystemService(String name) { 55 | if (Context.TELEPHONY_SERVICE.equals(name)) { 56 | return telephonyService; 57 | } 58 | return super.getSystemService(name); 59 | } 60 | 61 | @Override 62 | public boolean bindService(Intent service, int flags, Executor executor, ServiceConnection conn) { 63 | return false; 64 | } 65 | 66 | @Override 67 | public boolean bindService(Intent service, ServiceConnection conn, int flags) { 68 | return false; 69 | } 70 | 71 | public static void hook(ContextWrapper context) throws Exception { 72 | var wrapper = new ContextHook(context.getBaseContext()); 73 | //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi 74 | var base = ContextWrapper.class.getDeclaredField("mBase"); 75 | base.setAccessible(true); 76 | base.set(context, wrapper); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/keystore/KeyBoxXmlParser.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.keystore; 2 | 3 | import android.util.Base64; 4 | import android.util.Xml; 5 | 6 | import org.bouncycastle.asn1.ASN1Sequence; 7 | import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 8 | import org.bouncycastle.asn1.sec.ECPrivateKey; 9 | import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 10 | import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; 11 | import org.xmlpull.v1.XmlPullParser; 12 | import org.xmlpull.v1.XmlPullParserException; 13 | 14 | import java.io.ByteArrayInputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.nio.charset.StandardCharsets; 18 | import java.security.GeneralSecurityException; 19 | import java.security.KeyFactory; 20 | import java.security.KeyStore; 21 | import java.security.PrivateKey; 22 | import java.security.cert.Certificate; 23 | import java.security.cert.CertificateException; 24 | import java.security.cert.CertificateFactory; 25 | import java.security.spec.PKCS8EncodedKeySpec; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | public class KeyBoxXmlParser { 30 | private final XmlPullParser parser; 31 | private final CertificateFactory certificateFactory; 32 | private final List chain; 33 | private PrivateKey privateKey; 34 | 35 | private static KeyBoxXmlParser instance; 36 | 37 | public static KeyBoxXmlParser getInstance() throws IOException { 38 | if (instance == null) { 39 | instance = new KeyBoxXmlParser(); 40 | } 41 | return instance; 42 | } 43 | 44 | private KeyBoxXmlParser() throws IOException { 45 | parser = Xml.newPullParser(); 46 | chain = new ArrayList<>(); 47 | try { 48 | certificateFactory = CertificateFactory.getInstance("X.509"); 49 | } catch (GeneralSecurityException e) { 50 | throw new IOException(e); 51 | } 52 | } 53 | 54 | public KeyStore.PrivateKeyEntry parse(InputStream in) throws IOException { 55 | try { 56 | parser.setInput(in, StandardCharsets.UTF_8.name()); 57 | chain.clear(); 58 | privateKey = null; 59 | readAndroidAttestation(); 60 | } catch (XmlPullParserException e) { 61 | throw new IOException(e); 62 | } 63 | if (privateKey == null || chain.isEmpty()) { 64 | throw new IOException("No key found"); 65 | } 66 | return new KeyStore.PrivateKeyEntry(privateKey, chain.toArray(new Certificate[0])); 67 | } 68 | 69 | private void readAndroidAttestation() throws XmlPullParserException, IOException { 70 | while (parser.next() != XmlPullParser.END_DOCUMENT) { 71 | if (parser.getEventType() != XmlPullParser.START_TAG) { 72 | continue; 73 | } 74 | var name = parser.getName(); 75 | var algorithm = parser.getAttributeValue(null, "algorithm"); 76 | if ("Key".equals(name) && "ecdsa".equals(algorithm)) { 77 | parser.nextTag(); 78 | readECKey(); 79 | break; 80 | } 81 | } 82 | } 83 | 84 | private void readECKey() throws XmlPullParserException, IOException { 85 | while (!(parser.getEventType() == XmlPullParser.END_TAG && "Key".equals(parser.getName()))) { 86 | if (parser.getEventType() != XmlPullParser.START_TAG) { 87 | parser.next(); 88 | continue; 89 | } 90 | var format = parser.getAttributeValue(null, "format"); 91 | switch (parser.getName()) { 92 | case "PrivateKey" -> { 93 | if ("pem".equals(format)) { 94 | parser.next(); 95 | readPrivateKey(parser.getText()); 96 | parser.next(); 97 | } else { 98 | return; 99 | } 100 | } 101 | case "Certificate" -> { 102 | if ("pem".equals(format)) { 103 | parser.next(); 104 | readCertificateChain(parser.getText()); 105 | parser.next(); 106 | } else { 107 | return; 108 | } 109 | } 110 | default -> parser.next(); 111 | } 112 | } 113 | } 114 | 115 | private static byte[] stringToBytes(String text) { 116 | var sb = new StringBuilder(); 117 | for (var s : text.split("\n")) { 118 | var line = s.trim(); 119 | if (line.isEmpty()) continue; 120 | if (line.charAt(0) == '-') continue; 121 | sb.append(line); 122 | sb.append("\n"); 123 | } 124 | return Base64.decode(sb.toString(), 0); 125 | } 126 | 127 | private void readPrivateKey(String text) throws IOException { 128 | try { 129 | var sequence = ASN1Sequence.getInstance(stringToBytes(text)); 130 | var ecKey = ECPrivateKey.getInstance(sequence); 131 | var id = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, 132 | ecKey.getParametersObject()); 133 | var data = new PrivateKeyInfo(id, ecKey).getEncoded(); 134 | var keySpec = new PKCS8EncodedKeySpec(data); 135 | var keyFactory = KeyFactory.getInstance("EC"); 136 | privateKey = keyFactory.generatePrivate(keySpec); 137 | } catch (GeneralSecurityException e) { 138 | throw new IOException(e); 139 | } 140 | } 141 | 142 | private void readCertificateChain(String text) throws IOException { 143 | try { 144 | var data = new ByteArrayInputStream(stringToBytes(text)); 145 | chain.add(certificateFactory.generateCertificate(data)); 146 | } catch (CertificateException e) { 147 | throw new IOException(e); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/keystore/KeyStoreManager.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.keystore; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.content.pm.PackageManager; 9 | import android.os.Binder; 10 | import android.os.Bundle; 11 | import android.os.IBinder; 12 | import android.os.Parcel; 13 | import android.os.RemoteException; 14 | import android.util.Log; 15 | 16 | import androidx.annotation.NonNull; 17 | 18 | import io.github.vvb2060.keyattestation.AppApplication; 19 | import io.github.vvb2060.keyattestation.BuildConfig; 20 | import rikka.shizuku.Shizuku; 21 | 22 | public class KeyStoreManager { 23 | private static IAndroidKeyStore remoteKeyStore; 24 | private static boolean installed; 25 | 26 | public static IAndroidKeyStore getRemoteKeyStore() { 27 | return remoteKeyStore; 28 | } 29 | 30 | public static boolean isShizukuInstalled() { 31 | return installed; 32 | } 33 | 34 | private static void bindUserService() { 35 | if (remoteKeyStore != null) { 36 | return; 37 | } 38 | var name = new ComponentName(BuildConfig.APPLICATION_ID, AndroidKeyStore.class.getName()); 39 | var args = new Shizuku.UserServiceArgs(name) 40 | .daemon(false) 41 | .debuggable(BuildConfig.DEBUG) 42 | .version(BuildConfig.VERSION_CODE) 43 | .processNameSuffix("keystore"); 44 | var connection = new ServiceConnection() { 45 | @Override 46 | public void onServiceConnected(ComponentName name, IBinder service) { 47 | remoteKeyStore = IAndroidKeyStore.Stub.asInterface(service); 48 | } 49 | 50 | @Override 51 | public void onServiceDisconnected(ComponentName name) { 52 | remoteKeyStore = null; 53 | } 54 | }; 55 | Shizuku.bindUserService(args, connection); 56 | } 57 | 58 | public static void requestPermission() { 59 | if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { 60 | bindUserService(); 61 | } else if (Shizuku.shouldShowRequestPermissionRationale()) { 62 | Log.w(AppApplication.TAG, "shizuku permission denied"); 63 | } else { 64 | Shizuku.addRequestPermissionResultListener(new Shizuku.OnRequestPermissionResultListener() { 65 | @Override 66 | public void onRequestPermissionResult(int requestCode, int grantResult) { 67 | Shizuku.removeRequestPermissionResultListener(this); 68 | if (grantResult == PackageManager.PERMISSION_GRANTED) { 69 | bindUserService(); 70 | } else { 71 | Log.w(AppApplication.TAG, "shizuku permission denied"); 72 | } 73 | } 74 | }); 75 | Shizuku.requestPermission(0); 76 | } 77 | } 78 | 79 | public static void requestBinder(Context context) { 80 | var receiver = new Binder() { 81 | @SuppressLint("RestrictedApi") 82 | @Override 83 | protected boolean onTransact(int code, @NonNull Parcel data, Parcel reply, int flags) throws RemoteException { 84 | if (code == 1) { 85 | installed = true; 86 | var binder = data.readStrongBinder(); 87 | if (binder != null) { 88 | Shizuku.onBinderReceived(binder, BuildConfig.APPLICATION_ID); 89 | requestPermission(); 90 | } else { 91 | Log.w(AppApplication.TAG, "shizuku is not running"); 92 | } 93 | 94 | return true; 95 | } 96 | return super.onTransact(code, data, reply, flags); 97 | } 98 | }; 99 | var data = new Bundle(); 100 | data.putBinder("binder", receiver); 101 | var intent = new Intent("rikka.shizuku.intent.action.REQUEST_BINDER") 102 | .setPackage("moe.shizuku.privileged.api") 103 | .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 104 | .putExtra("data", data); 105 | context.sendBroadcast(intent); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/lang/AttestationException.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.lang 2 | 3 | import io.github.vvb2060.keyattestation.R 4 | 5 | class AttestationException(code: Int, cause: Throwable) : RuntimeException(cause) { 6 | 7 | companion object { 8 | const val CODE_UNKNOWN = -1 9 | const val CODE_UNAVAILABLE = 0 10 | const val CODE_CANT_PARSE_CERT = 2 11 | const val CODE_STRONGBOX_UNAVAILABLE = 3 12 | const val CODE_DEVICEIDS_UNAVAILABLE = 4 13 | const val CODE_OUT_OF_KEYS = 5 14 | const val CODE_OUT_OF_KEYS_TRANSIENT = 6 15 | const val CODE_UNAVAILABLE_TRANSIENT = 7 16 | const val CODE_KEYS_NOT_PROVISIONED = 8 17 | const val CODE_RKP = 9 18 | } 19 | 20 | val titleResId: Int = when (code) { 21 | CODE_UNAVAILABLE -> R.string.error_unavailable 22 | CODE_CANT_PARSE_CERT -> R.string.error_cant_parse_cert 23 | CODE_STRONGBOX_UNAVAILABLE -> R.string.error_strongbox_unavailable 24 | CODE_DEVICEIDS_UNAVAILABLE -> R.string.error_deviceids_unavailable 25 | CODE_OUT_OF_KEYS -> R.string.error_out_of_keys 26 | CODE_OUT_OF_KEYS_TRANSIENT -> R.string.error_out_of_keys_transient 27 | CODE_UNAVAILABLE_TRANSIENT -> R.string.error_unavailable_transient 28 | CODE_KEYS_NOT_PROVISIONED -> R.string.error_keys_not_provisioned 29 | CODE_RKP -> R.string.error_remote_key_provisioning 30 | else -> R.string.error_unknown 31 | } 32 | 33 | val descriptionResId: Int = when (code) { 34 | CODE_UNAVAILABLE -> R.string.error_unavailable_summary 35 | CODE_CANT_PARSE_CERT -> R.string.error_cant_parse_cert_summary 36 | CODE_STRONGBOX_UNAVAILABLE -> R.string.error_strongbox_unavailable_summary 37 | CODE_DEVICEIDS_UNAVAILABLE -> R.string.error_deviceids_unavailable_summary 38 | CODE_OUT_OF_KEYS -> R.string.error_out_of_keys_summary 39 | CODE_OUT_OF_KEYS_TRANSIENT -> R.string.error_out_of_keys_transient_summary 40 | CODE_UNAVAILABLE_TRANSIENT -> R.string.error_unavailable_transient_summary 41 | CODE_KEYS_NOT_PROVISIONED -> R.string.error_keys_not_provisioned_summary 42 | CODE_RKP -> R.string.error_remote_key_provisioning_summary 43 | else -> R.string.error_unknown 44 | } 45 | 46 | override fun fillInStackTrace() = this 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/repository/AttestationData.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.repository; 2 | 3 | import static io.github.vvb2060.keyattestation.attestation.Attestation.KM_SECURITY_LEVEL_SOFTWARE; 4 | import static io.github.vvb2060.keyattestation.lang.AttestationException.CODE_CANT_PARSE_CERT; 5 | 6 | import java.security.cert.X509Certificate; 7 | import java.util.ArrayList; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | 11 | import io.github.vvb2060.keyattestation.attestation.Attestation; 12 | import io.github.vvb2060.keyattestation.attestation.CertificateInfo; 13 | import io.github.vvb2060.keyattestation.attestation.RootOfTrust; 14 | import io.github.vvb2060.keyattestation.lang.AttestationException; 15 | 16 | public class AttestationData extends BaseData { 17 | private final RootOfTrust rootOfTrust; 18 | private final boolean sw; 19 | public Attestation showAttestation; 20 | 21 | public RootOfTrust getRootOfTrust() { 22 | return rootOfTrust; 23 | } 24 | 25 | public boolean isSoftwareLevel() { 26 | return sw; 27 | } 28 | 29 | private AttestationData(List certs) { 30 | init(certs); 31 | 32 | var info = certs.get(certs.size() - 1); 33 | var attestation = info.getAttestation(); 34 | if (attestation != null) { 35 | this.showAttestation = attestation; 36 | this.rootOfTrust = attestation.getRootOfTrust(); 37 | this.sw = attestation.getAttestationSecurityLevel() == KM_SECURITY_LEVEL_SOFTWARE; 38 | } else { 39 | throw new AttestationException(CODE_CANT_PARSE_CERT, info.getCertException()); 40 | } 41 | } 42 | 43 | private static List sortCerts(List certs) { 44 | if (certs.size() < 2) { 45 | return certs; 46 | } 47 | 48 | var issuer = certs.get(0).getIssuerX500Principal(); 49 | boolean okay = true; 50 | for (var cert : certs) { 51 | var subject = cert.getSubjectX500Principal(); 52 | if (issuer.equals(subject)) { 53 | issuer = subject; 54 | } else { 55 | okay = false; 56 | break; 57 | } 58 | } 59 | if (okay) { 60 | return certs; 61 | } 62 | 63 | var newList = new ArrayList(certs.size()); 64 | for (var cert : certs) { 65 | boolean found = false; 66 | var subject = cert.getSubjectX500Principal(); 67 | for (var c : certs) { 68 | if (c == cert) continue; 69 | if (c.getIssuerX500Principal().equals(subject)) { 70 | found = true; 71 | break; 72 | } 73 | } 74 | if (!found) { 75 | newList.add(cert); 76 | } 77 | } 78 | if (newList.size() != 1) { 79 | return certs; 80 | } 81 | 82 | var oldList = new LinkedList<>(certs); 83 | oldList.remove(newList.get(0)); 84 | for (int i = 0; i < newList.size(); i++) { 85 | issuer = newList.get(i).getIssuerX500Principal(); 86 | for (var it = oldList.iterator(); it.hasNext(); ) { 87 | var cert = it.next(); 88 | if (cert.getSubjectX500Principal().equals(issuer)) { 89 | newList.add(cert); 90 | it.remove(); 91 | break; 92 | } 93 | } 94 | } 95 | if (!oldList.isEmpty()) { 96 | return certs; 97 | } 98 | return newList; 99 | } 100 | 101 | static AttestationData parseCertificateChain(List certs) { 102 | var infoList = new ArrayList(certs.size()); 103 | CertificateInfo.parse(sortCerts(certs), infoList); 104 | return new AttestationData(infoList); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/repository/BaseData.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.repository; 2 | 3 | import java.util.List; 4 | 5 | import io.github.vvb2060.keyattestation.attestation.CertificateInfo; 6 | import io.github.vvb2060.keyattestation.attestation.RootPublicKey; 7 | 8 | public abstract class BaseData { 9 | protected List certs; 10 | protected RootPublicKey.Status status; 11 | 12 | protected void init(List certs) { 13 | this.certs = certs; 14 | if (certs.isEmpty()) { 15 | this.status = RootPublicKey.Status.NULL; 16 | return; 17 | } 18 | 19 | var status = certs.get(0).getIssuer(); 20 | for (var cert : certs) { 21 | if (cert.getStatus() < CertificateInfo.CERT_EXPIRED) { 22 | status = RootPublicKey.Status.FAILED; 23 | break; 24 | } 25 | } 26 | if (status == RootPublicKey.Status.GOOGLE) { 27 | for (int i = 1; i < certs.size(); i++) { 28 | if (certs.get(i).getCert().getSubjectX500Principal().getName().contains("Google LLC")) { 29 | continue; 30 | } 31 | if (certs.get(i).getProvisioningInfo() != null) { 32 | status = RootPublicKey.Status.GOOGLE_RKP; 33 | } 34 | break; 35 | } 36 | } 37 | this.status = status; 38 | } 39 | 40 | public List getCerts() { 41 | return certs; 42 | } 43 | 44 | public RootPublicKey.Status getStatus() { 45 | return status; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/repository/RemoteProvisioningData.java: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.repository; 2 | 3 | import android.hardware.security.keymint.DeviceInfo; 4 | import android.hardware.security.keymint.RpcHardwareInfo; 5 | import android.util.ArrayMap; 6 | 7 | import com.google.common.io.BaseEncoding; 8 | 9 | import java.security.cert.Certificate; 10 | import java.security.cert.X509Certificate; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | 15 | import co.nstant.in.cbor.CborDecoder; 16 | import co.nstant.in.cbor.CborException; 17 | import co.nstant.in.cbor.model.ByteString; 18 | import co.nstant.in.cbor.model.Map; 19 | import io.github.vvb2060.keyattestation.attestation.CertificateInfo; 20 | 21 | public class RemoteProvisioningData extends BaseData { 22 | private final String rkpHostname; 23 | private final RpcHardwareInfo hardwareInfo; 24 | private final java.util.Map deviceInfo = new ArrayMap<>(); 25 | private Throwable error; 26 | 27 | public RemoteProvisioningData(String rkpHostname, RpcHardwareInfo hardwareInfo, 28 | DeviceInfo deviceInfoData) throws CborException { 29 | this.rkpHostname = rkpHostname; 30 | this.hardwareInfo = hardwareInfo; 31 | var deviceInfo = (Map) CborDecoder.decode(deviceInfoData.deviceInfo).get(0); 32 | for (var key : deviceInfo.getKeys()) { 33 | var value = deviceInfo.get(key); 34 | String valueString; 35 | if (value instanceof ByteString byteString) { 36 | valueString = BaseEncoding.base16().lowerCase().encode(byteString.getBytes()); 37 | } else { 38 | valueString = value.toString(); 39 | } 40 | this.deviceInfo.put(key.toString(), valueString); 41 | } 42 | } 43 | 44 | @SuppressWarnings("unchecked") 45 | public void setCerts(Collection data) { 46 | var infoList = new ArrayList(data.size()); 47 | CertificateInfo.parse((List) data, infoList); 48 | init(infoList); 49 | } 50 | 51 | public void setError(Throwable error) { 52 | this.error = error; 53 | init(List.of()); 54 | } 55 | 56 | public String getRkpHostname() { 57 | return rkpHostname; 58 | } 59 | 60 | public RpcHardwareInfo getHardwareInfo() { 61 | return hardwareInfo; 62 | } 63 | 64 | public java.util.Map getDeviceInfo() { 65 | return deviceInfo; 66 | } 67 | 68 | public Throwable getError() { 69 | return error; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/util/Resource.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.util 2 | 3 | /** 4 | * A generic class that holds a value with its loading status. 5 | * @param 6 | */ 7 | open class Resource(val status: Status, val data: T?, val error: Throwable) { 8 | 9 | companion object { 10 | private val noError = Throwable("No error") 11 | 12 | fun success(data: T?): Resource { 13 | return Resource(Status.SUCCESS, data, noError) 14 | } 15 | 16 | fun error(error: Throwable, data: T?): Resource { 17 | return Resource(Status.ERROR, data, error) 18 | } 19 | 20 | fun loading(data: T?): Resource { 21 | return Resource(Status.LOADING, data, noError) 22 | } 23 | } 24 | 25 | override fun equals(other: Any?): Boolean { 26 | if (this === other) return true 27 | if (javaClass != other?.javaClass) return false 28 | 29 | other as Resource<*> 30 | 31 | if (status != other.status) return false 32 | if (data != other.data) return false 33 | if (error != other.error) return false 34 | 35 | return true 36 | } 37 | 38 | override fun hashCode(): Int { 39 | var result = status.hashCode() 40 | result = 31 * result + (data?.hashCode() ?: 0) 41 | result = 31 * result + error.hashCode() 42 | return result 43 | } 44 | } 45 | 46 | enum class Status { 47 | SUCCESS, 48 | ERROR, 49 | LOADING 50 | } 51 | 52 | class SourcedResource(status: Status, data: T?, error: Throwable, val source: S?) : Resource(status, data, error) { 53 | 54 | companion object { 55 | private val noError = Throwable("No error") 56 | 57 | fun success(data: T?, source: S?): SourcedResource { 58 | return SourcedResource(Status.SUCCESS, data, noError, source) 59 | } 60 | 61 | fun error(error: Throwable, data: T?, source: S?): SourcedResource { 62 | return SourcedResource(Status.ERROR, data, error, source) 63 | } 64 | 65 | fun loading(data: T?, source: S?): SourcedResource { 66 | return SourcedResource(Status.LOADING, data, noError, source) 67 | } 68 | } 69 | 70 | override fun equals(other: Any?): Boolean { 71 | if (this === other) return true 72 | if (javaClass != other?.javaClass) return false 73 | if (!super.equals(other)) return false 74 | 75 | other as SourcedResource<*, *> 76 | 77 | if (source != other.source) return false 78 | return true 79 | } 80 | 81 | override fun hashCode(): Int { 82 | var result = super.hashCode() 83 | result = 31 * result + (source?.hashCode() ?: 0) 84 | return result 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/vvb2060/keyattestation/util/ViewBindingViewHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.vvb2060.keyattestation.util 2 | 3 | import android.view.View 4 | import androidx.viewbinding.ViewBinding 5 | import rikka.recyclerview.BaseListenerViewHolder 6 | 7 | open class ViewBindingViewHolder(itemView: View, internal val binding: VB) : BaseListenerViewHolder(itemView) 8 | -------------------------------------------------------------------------------- /app/src/main/res/color/material_on_surface_emphasis_high_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/material_on_surface_emphasis_medium.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/mtrl_popupmenu_overlay_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_item_background_solid.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_boot_locked_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_boot_unknown_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_boot_unlocked_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_help_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_outline_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_trustworthy_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_untrustworthy_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_warning_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/appbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/appbar_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/appbar_fragment_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 28 | 29 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_boot_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_common_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 30 | 31 | 32 | 33 | 44 | 45 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 25 | 26 | 35 | 36 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_subtitle.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/home.xml: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 11 | 12 | 17 | 18 | 23 | 24 | 29 | 30 | 35 | 36 | 40 | 41 | 46 | 47 | 48 | 53 | 54 | 59 | 60 | 65 | 66 | 67 | 72 | 73 | 77 | 78 | 82 | 83 | 87 | 88 | 92 | 93 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_key_attestation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-hdpi/ic_key_attestation.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_key_attestation_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-hdpi/ic_key_attestation_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_key_attestation_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-hdpi/ic_key_attestation_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_key_attestation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xhdpi/ic_key_attestation.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_key_attestation_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xhdpi/ic_key_attestation_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_key_attestation_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xhdpi/ic_key_attestation_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_key_attestation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xxhdpi/ic_key_attestation.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_key_attestation_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xxhdpi/ic_key_attestation_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_key_attestation_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xxhdpi/ic_key_attestation_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_key_attestation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xxxhdpi/ic_key_attestation.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_key_attestation_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xxxhdpi/ic_key_attestation_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_key_attestation_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/app/src/main/res/mipmap-xxxhdpi/ic_key_attestation_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #365161 4 | #465f6d 5 | #1765a4 6 | #efe0f7fA 7 | #efffd180 8 | #efff8a80 9 | #88ffffff 10 | #E1F5FE 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings_no_translate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright © vvb2060, Rikka 4 | https://github.com/vvb2060/KeyAttestation 5 | https://shizuku.rikka.app 6 | Apache License 2.0 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 22 | 23 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35 | 36 | 42 | 43 | 46 | 47 | 51 | 52 | 56 | 57 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes_override.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 16 | 17 | 21 | 22 | 26 | -------------------------------------------------------------------------------- /art/icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/art/icon.ai -------------------------------------------------------------------------------- /art/icon_playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/art/icon_playstore.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/build.gradle -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | org.gradle.configuration-cache=true 3 | org.gradle.configuration-cache.parallel=true 4 | org.gradle.configureondemand=true 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/KeyAttestation/125cc7bcc96dee5ed64c058e3f2c78599064d6ba/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | plugins { 7 | id("com.android.application") version '8.8.0' 8 | id("com.android.library") version '8.8.0' 9 | id("org.jetbrains.kotlin.android") version "2.1.0" 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | google { 16 | content { 17 | includeGroupAndSubgroups("androidx") 18 | includeGroupAndSubgroups("com.android") 19 | includeGroupAndSubgroups("com.google") 20 | } 21 | } 22 | mavenCentral() 23 | } 24 | } 25 | rootProject.name = "KeyAttestation" 26 | include(":app", ":stub") 27 | -------------------------------------------------------------------------------- /stub/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /stub/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | 5 | android { 6 | namespace = "stub" 7 | buildToolsVersion = '35.0.1' 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | minSdk = 24 12 | } 13 | 14 | compileOptions { 15 | sourceCompatibility = JavaVersion.VERSION_21 16 | targetCompatibility = JavaVersion.VERSION_21 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stub/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /stub/src/main/java/android/app/ActivityThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class ActivityThread { 4 | public static Application currentApplication() { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | 8 | public ContextImpl getSystemContext() { 9 | throw new RuntimeException("Stub!"); 10 | } 11 | 12 | public static ActivityThread systemMain() { 13 | throw new RuntimeException("Stub!"); 14 | } 15 | 16 | public static void initializeMainlineModules() { 17 | throw new RuntimeException("Stub!"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stub/src/main/java/android/app/ContextImpl.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.content.Context; 4 | 5 | public abstract class ContextImpl extends Context { 6 | } 7 | -------------------------------------------------------------------------------- /stub/src/main/java/android/os/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class ServiceManager { 4 | public static IBinder getService(String name) { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | 8 | public static boolean isDeclared(String name) { 9 | throw new RuntimeException("Stub!"); 10 | } 11 | 12 | public static String[] getDeclaredInstances(String iface) { 13 | throw new RuntimeException("Stub!"); 14 | } 15 | 16 | public static IBinder waitForDeclaredService(String name) { 17 | throw new RuntimeException("Stub!"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stub/src/main/java/android/os/ServiceSpecificException.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class ServiceSpecificException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /stub/src/main/java/android/os/SystemProperties.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class SystemProperties { 4 | public static String get(String key) { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | 8 | public static String get(String key, String def) { 9 | throw new RuntimeException("Stub!"); 10 | } 11 | 12 | public static int getInt(String key, int def) { 13 | throw new RuntimeException("Stub!"); 14 | } 15 | 16 | public static long getLong(String key, long def) { 17 | throw new RuntimeException("Stub!"); 18 | } 19 | 20 | public static boolean getBoolean(String key, boolean def) { 21 | throw new RuntimeException("Stub!"); 22 | } 23 | 24 | public static void set(String key, String val) { 25 | throw new RuntimeException("Stub!"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stub/src/main/java/android/security/keystore/AndroidKeyStoreProvider.java: -------------------------------------------------------------------------------- 1 | package android.security.keystore; 2 | 3 | public class AndroidKeyStoreProvider { 4 | public static void install() { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stub/src/main/java/android/security/keystore/AttestationUtils.java: -------------------------------------------------------------------------------- 1 | package android.security.keystore; 2 | 3 | import android.content.Context; 4 | 5 | import java.security.cert.X509Certificate; 6 | 7 | public abstract class AttestationUtils { 8 | public static X509Certificate[] attestDeviceIds(Context context, 9 | int[] idTypes, 10 | byte[] attestationChallenge 11 | ) throws DeviceIdAttestationException { 12 | throw new RuntimeException("Stub!"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /stub/src/main/java/android/security/keystore/DeviceIdAttestationException.java: -------------------------------------------------------------------------------- 1 | package android.security.keystore; 2 | 3 | public class DeviceIdAttestationException extends Exception { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /stub/src/main/java/android/security/keystore/KeyGenParameterSpec_rename.java: -------------------------------------------------------------------------------- 1 | package android.security.keystore; 2 | 3 | import java.math.BigInteger; 4 | import java.security.spec.AlgorithmParameterSpec; 5 | import java.util.Date; 6 | 7 | import javax.security.auth.x500.X500Principal; 8 | 9 | public class KeyGenParameterSpec_rename { 10 | 11 | public static class Builder { 12 | public Builder(String keystoreAlias, int purposes) { 13 | } 14 | 15 | public Builder setAlgorithmParameterSpec(AlgorithmParameterSpec spec) { 16 | return this; 17 | } 18 | 19 | public Builder setCertificateSubject(X500Principal subject) { 20 | return this; 21 | } 22 | 23 | public Builder setCertificateSerialNumber(BigInteger serialNumber) { 24 | return this; 25 | } 26 | 27 | public Builder setCertificateNotBefore(Date date) { 28 | return this; 29 | } 30 | 31 | public Builder setCertificateNotAfter(Date date) { 32 | return this; 33 | } 34 | 35 | public Builder setDigests(String... digests) { 36 | return this; 37 | } 38 | 39 | public Builder setAttestationChallenge(byte[] attestationChallenge) { 40 | return this; 41 | } 42 | 43 | public Builder setDevicePropertiesAttestationIncluded(boolean devicePropertiesAttestationIncluded) { 44 | return this; 45 | } 46 | 47 | public Builder setAttestationIds(int[] attestationIds) { 48 | return this; 49 | } 50 | 51 | public Builder setUniqueIdIncluded(boolean uniqueIdIncluded) { 52 | return this; 53 | } 54 | 55 | public Builder setIsStrongBoxBacked(boolean isStrongBoxBacked) { 56 | return this; 57 | } 58 | 59 | public Builder setAttestKeyAlias(String attestKeyAlias) { 60 | return this; 61 | } 62 | 63 | public KeyGenParameterSpec_rename build() { 64 | throw new RuntimeException("Stub!"); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /stub/src/main/java/android/security/keystore2/AndroidKeyStoreProvider.java: -------------------------------------------------------------------------------- 1 | package android.security.keystore2; 2 | 3 | public class AndroidKeyStoreProvider { 4 | public static void install() { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stub/src/main/java/android/telephony/TelephonyManager_rename.java: -------------------------------------------------------------------------------- 1 | package android.telephony; 2 | 3 | import android.content.Context; 4 | 5 | public class TelephonyManager_rename { 6 | public TelephonyManager_rename(Context context) { 7 | throw new RuntimeException("Stub!"); 8 | } 9 | 10 | public String getImei(int slotIndex) { 11 | throw new RuntimeException("Stub!"); 12 | } 13 | 14 | public String getMeid(int slotIndex) { 15 | throw new RuntimeException("Stub!"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /stub/src/main/java/com/samsung/android/security/keystore/AttestParameterSpec.java: -------------------------------------------------------------------------------- 1 | package com.samsung.android.security.keystore; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | import android.security.keystore.KeyGenParameterSpec; 6 | 7 | @TargetApi(Build.VERSION_CODES.Q) 8 | public class AttestParameterSpec { 9 | public boolean isDeviceAttestation() { 10 | throw new RuntimeException("Stub!"); 11 | } 12 | 13 | public static class Builder { 14 | public Builder(String alias, byte[] challenge) { 15 | throw new RuntimeException("Stub!"); 16 | } 17 | 18 | public Builder setAlgorithm(String algorithm) { 19 | throw new RuntimeException("Stub!"); 20 | } 21 | 22 | public Builder setDeviceAttestation(boolean requested) { 23 | throw new RuntimeException("Stub!"); 24 | } 25 | 26 | public Builder setVerifiableIntegrity(boolean checked) { 27 | throw new RuntimeException("Stub!"); 28 | } 29 | 30 | public Builder setPackageName(String packageName) { 31 | throw new RuntimeException("Stub!"); 32 | } 33 | 34 | public Builder setKeyGenParameterSpec(KeyGenParameterSpec spec) { 35 | throw new RuntimeException("Stub!"); 36 | } 37 | 38 | public AttestParameterSpec build() { 39 | throw new RuntimeException("Stub!"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /stub/src/main/java/com/samsung/android/security/keystore/AttestationUtils.java: -------------------------------------------------------------------------------- 1 | package com.samsung.android.security.keystore; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | 6 | @TargetApi(Build.VERSION_CODES.Q) 7 | public class AttestationUtils { 8 | public static String DEFAULT_KEYSTORE = "AndroidKeyStore"; 9 | 10 | public Iterable attestKey(AttestParameterSpec spec) { 11 | throw new RuntimeException("Stub!"); 12 | } 13 | 14 | public Iterable attestDevice(AttestParameterSpec spec) { 15 | throw new RuntimeException("Stub!"); 16 | } 17 | 18 | public void storeCertificateChain(String alias, Iterable iterable) { 19 | throw new RuntimeException("Stub!"); 20 | } 21 | } 22 | --------------------------------------------------------------------------------