diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..48a3e3c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("com.android.application") + kotlin("android") +} + +android { + namespace = "com.android.attestation.app" // UPDATED + compileSdk = 34 + + defaultConfig { + applicationId = "com.android.attestation.app" // UPDATED + minSdk = 33 // MATCHING MANIFEST + targetSdk = 33 // MATCHING MANIFEST + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false // Consider enabling for production releases + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } +} + +dependencies { + implementation(project(":")) // Depends on the root library project + + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.activity:activity:1.9.0") + + // Use the Android version of Guava + implementation("com.google.guava:guava:33.2.1-android") + implementation("com.google.protobuf:protobuf-javalite:4.28.3") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7cbb3f5 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/android/attestation/app/AttestationUiManager.java b/app/src/main/java/com/android/attestation/app/AttestationUiManager.java new file mode 100644 index 0000000..29d2be9 --- /dev/null +++ b/app/src/main/java/com/android/attestation/app/AttestationUiManager.java @@ -0,0 +1,451 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.attestation.app; + +import android.app.Activity; +import android.content.res.ColorStateList; +import android.os.Handler; +import android.os.Looper; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; + +/** + * Utility class for updating the UI of the Attestation Verifier app. + * + *

This class provides methods for updating the UI based on the verification result, such as + * updating the summary card, bootloader banner, and parameter details. + */ +public final class AttestationUiManager { + private static final String TAG = "AttestationUiManager"; + private final Activity activity; + private final Handler handler = new Handler(Looper.getMainLooper()); + + public static final int EIGHT_DIGIT_PATCH_LEVEL = 8; + public static final int SIX_DIGIT_PATCH_LEVEL = 6; + + public AttestationUiManager(Activity activity) { + this.activity = activity; + } + + /** Enum representing the different parameter names displayed in the UI. */ + public enum AttestationParameter { + VERIFICATION_RESULT(R.string.label_verification_result), + ATTESTATION_CHALLENGE(R.string.label_attestation_challenge), + ATTESTATION_SECURITY_LEVEL(R.string.label_attestation_security_level), + KEYMASTER_VERSION(R.string.label_keymaster_version), + KEYMASTER_SECURITY_LEVEL(R.string.label_keymaster_security_level), + CREATION_DATE(R.string.label_creation_date), + APPLICATION_ID(R.string.label_id_device), + ATTESTATION_PURPOSES(R.string.label_attestation_purposes), + ATTESTATION_ALGORITHMS(R.string.label_attestation_algorithms), + KEY_SIZE(R.string.label_key_size), + DIGESTS(R.string.label_digests), + EC_CURVE(R.string.label_ec_curve), + ATTESTATION_ORIGIN(R.string.label_origin), + ROOT_OF_TRUST(R.string.label_root_of_trust), + ROT_STATE(R.string.label_rot_state), + ROT_LOCKED(R.string.label_rot_locked), + NO_AUTH_REQUIRED(R.string.label_no_auth_required), + ACTIVE_DATE(R.string.label_active_date), + ORIGINATION_EXPIRE_DATE(R.string.label_origination_expire_date), + USAGE_EXPIRE_DATE(R.string.label_usage_expire_date), + USER_AUTH_TYPE(R.string.label_user_auth_type), + AUTH_TIMEOUT(R.string.label_auth_timeout), + TRUSTED_USER_PRESENCE_REQUIRED(R.string.label_presence_required), + UNLOCKED_DEVICE_REQUIRED(R.string.label_unlocked_required), + ATTESTATION_OS_VERSION(R.string.label_os_version), + ATTESTATION_OS_PATCH(R.string.label_os_patch), + VENDOR_PATCH_LEVEL(R.string.label_vendor_patch), + BOOT_PATCH_LEVEL(R.string.label_boot_patch), + ATTESTATION_ID_BRAND(R.string.label_id_brand), + ATTESTATION_ID_MODEL(R.string.label_id_model), + ATTESTATION_ID_SERIAL(R.string.label_id_serial), + ATTESTATION_ID_IMEI(R.string.label_id_imei), + RSA_PUBLIC_EXPONENT(R.string.label_rsa_exponent), + ATTESTATION_MODULE_HASH(R.string.label_module_hash), + ERROR(R.string.label_error); + + private final int labelResId; + + AttestationParameter(@StringRes int labelResId) { + this.labelResId = labelResId; + } + + @StringRes + public int getLabelResId() { + return labelResId; + } + } + + /** + * Initializes the Activity's Action Bar with the application icon and title. + * + *

This method should be called during {@code Activity.onCreate}, immediately after {@code + * setContentView}. It is safe to call this method multiple times; subsequent calls will simply + * re-apply the icon and display configurations to the existing Action Bar. + */ + public void setupActionBar() { + if (activity instanceof AppCompatActivity appCompatActivity) { + ActionBar actionBar = appCompatActivity.getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.app_name); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setIcon(activity.getApplicationInfo().icon); + } + } + } + + public void updateSummaryStatus(boolean isVerified) { + handler.post( + () -> { + View summaryCard = activity.findViewById(R.id.summary_card); + if (summaryCard == null) { + return; + } + + summaryCard.setVisibility(View.VISIBLE); + TextView summaryTitle = activity.findViewById(R.id.summary_title); + TextView summarySubtitle = activity.findViewById(R.id.summary_subtitle); + + if (isVerified) { + summaryTitle.setText(R.string.summary_title_verified); + summaryTitle.setTextColor(ContextCompat.getColor(activity, R.color.brand_success)); + summaryTitle.setCompoundDrawablesWithIntrinsicBounds( + 0, R.drawable.ic_check_circle, 0, 0); + summaryTitle.setCompoundDrawableTintList( + ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_success))); + + summarySubtitle.setText(R.string.summary_subtitle_verified); + } else { + summaryTitle.setText(R.string.summary_title_unverified); + summaryTitle.setTextColor(ContextCompat.getColor(activity, R.color.brand_grey)); + summaryTitle.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_warning, 0, 0); + summaryTitle.setCompoundDrawableTintList( + ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_grey))); + + summarySubtitle.setText(R.string.summary_subtitle_unverified); + } + }); + } + + public String getParameterDescription( + AttestationParameter parameter, EnforcementType enforcement) { + String description = ""; + String example = ""; + switch (parameter) { + case VERIFICATION_RESULT -> { + description = activity.getString(R.string.desc_verification_result); + example = activity.getString(R.string.ex_verification_result); + } + case ATTESTATION_CHALLENGE -> { + description = activity.getString(R.string.desc_attestation_challenge); + example = activity.getString(R.string.ex_attestation_challenge); + } + case ATTESTATION_SECURITY_LEVEL -> { + description = activity.getString(R.string.desc_attestation_security_level); + example = activity.getString(R.string.ex_attestation_security_level); + } + case KEYMASTER_VERSION -> { + description = activity.getString(R.string.desc_keymaster_version); + example = activity.getString(R.string.ex_keymaster_version); + } + case KEYMASTER_SECURITY_LEVEL -> { + description = activity.getString(R.string.desc_keymaster_security_level); + example = activity.getString(R.string.ex_keymaster_security_level); + } + case CREATION_DATE -> { + description = activity.getString(R.string.desc_creation_date); + example = activity.getString(R.string.ex_creation_date); + } + case APPLICATION_ID -> { + description = activity.getString(R.string.desc_application_id); + example = activity.getString(R.string.ex_application_id); + } + case ATTESTATION_PURPOSES -> { + description = activity.getString(R.string.desc_attestation_purposes); + example = activity.getString(R.string.ex_attestation_purposes); + } + case ATTESTATION_ALGORITHMS -> { + description = activity.getString(R.string.desc_attestation_algorithms); + example = activity.getString(R.string.ex_attestation_algorithms); + } + case KEY_SIZE -> { + description = activity.getString(R.string.desc_key_size); + example = activity.getString(R.string.ex_key_size); + } + case DIGESTS -> { + description = activity.getString(R.string.desc_digests); + example = activity.getString(R.string.ex_digests); + } + case EC_CURVE -> { + description = activity.getString(R.string.desc_ec_curve); + example = activity.getString(R.string.ex_ec_curve); + } + case ATTESTATION_ORIGIN -> { + description = activity.getString(R.string.desc_attestation_origin); + example = activity.getString(R.string.ex_attestation_origin); + } + case ROOT_OF_TRUST -> { + description = activity.getString(R.string.desc_root_of_trust); + example = activity.getString(R.string.ex_root_of_trust); + } + case ROT_STATE -> { + description = activity.getString(R.string.desc_rot_state); + example = activity.getString(R.string.ex_rot_state); + } + case ROT_LOCKED -> { + description = activity.getString(R.string.desc_rot_locked); + example = activity.getString(R.string.ex_rot_locked); + } + case NO_AUTH_REQUIRED -> { + description = activity.getString(R.string.desc_no_auth_required); + example = activity.getString(R.string.ex_no_auth_required); + } + case ACTIVE_DATE -> description = activity.getString(R.string.desc_active_date); + case ORIGINATION_EXPIRE_DATE -> + description = activity.getString(R.string.desc_origination_expire_date); + case USAGE_EXPIRE_DATE -> description = activity.getString(R.string.desc_usage_expire_date); + case USER_AUTH_TYPE -> { + description = activity.getString(R.string.desc_user_auth_type); + example = activity.getString(R.string.ex_user_auth_type); + } + case AUTH_TIMEOUT -> description = activity.getString(R.string.desc_auth_timeout); + case TRUSTED_USER_PRESENCE_REQUIRED -> + description = activity.getString(R.string.desc_trusted_user_presence_required); + case UNLOCKED_DEVICE_REQUIRED -> + description = activity.getString(R.string.desc_unlocked_device_required); + case ATTESTATION_OS_VERSION -> { + description = activity.getString(R.string.desc_attestation_os_version); + example = activity.getString(R.string.ex_attestation_os_version); + } + case ATTESTATION_OS_PATCH -> { + description = activity.getString(R.string.desc_attestation_os_patch); + example = activity.getString(R.string.ex_attestation_os_patch); + } + case VENDOR_PATCH_LEVEL -> { + description = activity.getString(R.string.desc_vendor_patch_level); + example = activity.getString(R.string.ex_vendor_patch_level); + } + case BOOT_PATCH_LEVEL -> { + description = activity.getString(R.string.desc_boot_patch_level); + example = activity.getString(R.string.ex_boot_patch_level); + } + case ATTESTATION_ID_BRAND -> { + description = activity.getString(R.string.desc_attestation_id_brand); + example = activity.getString(R.string.ex_attestation_id_brand); + } + case ATTESTATION_ID_MODEL -> { + description = activity.getString(R.string.desc_attestation_id_model); + example = activity.getString(R.string.ex_attestation_id_model); + } + case ATTESTATION_ID_SERIAL -> + description = activity.getString(R.string.desc_attestation_id_serial); + case ATTESTATION_ID_IMEI -> + description = activity.getString(R.string.desc_attestation_id_imei); + case RSA_PUBLIC_EXPONENT -> { + description = activity.getString(R.string.desc_rsa_public_exponent); + example = activity.getString(R.string.ex_rsa_public_exponent); + } + case ATTESTATION_MODULE_HASH -> + description = activity.getString(R.string.desc_attestation_module_hash); + default -> description = activity.getString(R.string.desc_default); + } + + StringBuilder popupText = new StringBuilder(description); + + if (example != null && !example.isEmpty()) { + popupText + .append("\n\n") + .append(activity.getString(R.string.label_example_prefix)) + .append(" ") + .append(example); + } + + if (enforcement != null && enforcement != EnforcementType.NONE) { + String footer = + switch (enforcement) { + case TEE -> activity.getString(R.string.enforced_by_tee); + case SOFTWARE -> activity.getString(R.string.enforced_by_sw); + case HARDWARE -> activity.getString(R.string.enforced_by_hw); + default -> ""; + }; + popupText.append("\n\n").append(footer); + } + + return popupText.toString(); + } + + public void showAboutDialog() { + new MaterialAlertDialogBuilder(activity) + .setTitle(R.string.about_key_attestation_title) + .setMessage(activity.getString(R.string.about_key_attestation_dialog_message)) + .setPositiveButton(R.string.ok, null) + .show(); + } + + public void showParameterDetailDialog(String title, String message) { + new MaterialAlertDialogBuilder(activity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show(); + } + + public void updateBootloaderBanner(boolean isLocked) { + handler.post( + () -> { + MaterialCardView banner = activity.findViewById(R.id.bootloader_banner); + TextView statusText = activity.findViewById(R.id.bootloader_text); + ImageView icon = activity.findViewById(R.id.bootloader_icon); + + if (banner == null) { + return; + } + banner.setVisibility(View.VISIBLE); + + if (isLocked) { + statusText.setText(R.string.bootloader_locked); + banner.setCardBackgroundColor( + ContextCompat.getColor(activity, R.color.brand_blue_light)); + statusText.setTextColor(ContextCompat.getColor(activity, R.color.brand_blue_dark)); + icon.setImageResource(android.R.drawable.ic_lock_lock); + icon.setImageTintList( + ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_blue_dark))); + } else { + statusText.setText(R.string.bootloader_unlocked); + banner.setCardBackgroundColor( + ContextCompat.getColor(activity, R.color.brand_warning_light)); + statusText.setTextColor(ContextCompat.getColor(activity, R.color.brand_brown_dark)); + icon.setImageResource(android.R.drawable.ic_lock_idle_lock); + icon.setImageTintList( + ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_brown_dark))); + } + }); + } + + public String formatDate(BigInteger timestamp) { + if (timestamp == null || timestamp.equals(BigInteger.ZERO)) { + return activity.getString(R.string.not_applicable); + } + try { + Instant instant = Instant.ofEpochMilli(timestamp.longValue()); + + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("MMMM dd, yyyy hh:mm a").withZone(ZoneId.systemDefault()); + + return formatter.format(instant); + } catch (RuntimeException e) { + return String.valueOf(timestamp); + } + } + + public String formatPatchLevel(Object value) { + if (value == null) { + return activity.getString(R.string.not_applicable); + } + String patchStr = String.valueOf(value); + + try { + if (patchStr.length() == EIGHT_DIGIT_PATCH_LEVEL) { + LocalDate date = LocalDate.parse(patchStr, DateTimeFormatter.ofPattern("yyyyMMdd")); + return date.format( + DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.getDefault())); + + } else if (patchStr.length() == SIX_DIGIT_PATCH_LEVEL) { + int year = Integer.parseInt(patchStr.substring(0, 4)); + int month = Integer.parseInt(patchStr.substring(4, 6)); + YearMonth yearMonth = YearMonth.of(year, month); + return yearMonth.format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.getDefault())); + } + } catch (RuntimeException e) { + Log.w(TAG, "Failed to parse patch level: " + patchStr); + } + return patchStr; + } + + public String formatChallenge(ByteString challenge) { + if (challenge == null || challenge.isEmpty()) { + return activity.getString(R.string.not_applicable); + } + + byte[] bytes = challenge.toByteArray(); + + if (isPrintableUtf8(bytes)) { + return String.format("\"%s\" (string representation)", challenge.toStringUtf8()); + } else { + String hex = BaseEncoding.base16().lowerCase().encode(bytes); + return String.format("\"%s\" (hex representation)", hex); + } + } + + private boolean isPrintableUtf8(byte[] bytes) { + try { + String str = new String(bytes, StandardCharsets.UTF_8); + return str.chars().allMatch(c -> c >= 32 && c < 127); + } catch (RuntimeException e) { + return false; + } + } + + public String formatAppId(Object appId) { + if (appId == null) { + return activity.getString(R.string.not_applicable); + } + return String.valueOf(appId).trim(); + } + + /** + * The enforcement type of the parameter, which is either TEE, SOFTWARE, HARDWARE, or NONE. + * + *

The enforcement type is used to determine the color of the parameter and the enforcement + * footer. + */ + public enum EnforcementType { + TEE("TEE"), + SOFTWARE("SW"), + HARDWARE("HW"), + NONE(""); + + private final String label; + + EnforcementType(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + } +} diff --git a/app/src/main/java/com/android/attestation/app/AttestationUtils.java b/app/src/main/java/com/android/attestation/app/AttestationUtils.java new file mode 100644 index 0000000..e0ebeac --- /dev/null +++ b/app/src/main/java/com/android/attestation/app/AttestationUtils.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.attestation.app; + +import android.app.Activity; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.security.KeyStoreException; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +/** + * Utility class providing helper methods to verify Android device states, build configurations, and + * user profile environments. + */ +public final class AttestationUtils { + + public static void showAlertDialog( + Activity activity, String title, String message, String buttonText) { + new MaterialAlertDialogBuilder(activity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(buttonText, null) + .setCancelable(false) + .show(); + } + + /** + * Returns {@code true} if the network is likely available, or if its status cannot be determined. + * + *

This method fails open: it returns {@code false} only when it's definitively known that the + * network is unavailable. If there's insufficient information or an exception occurs during the + * check, it defaults to returning {@code true}. + */ + public static boolean isNetworkAvailable(Context context) { + try { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + if (cm == null) { + return true; + } + + Network activeNetwork = cm.getActiveNetwork(); + if (activeNetwork == null) { + return false; + } + + NetworkCapabilities capabilities = cm.getNetworkCapabilities(activeNetwork); + return capabilities == null + || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + } catch (RuntimeException e) { + return true; + } + } + + public static boolean isCannotAttestIdsError(Exception e) { + int errorCode = getKeystoreErrorCode(e); + + return errorCode == KeyStoreException.ERROR_ID_ATTESTATION_FAILURE; + } + + public static boolean isNetworkError(Exception e) { + int errorCode = getKeystoreErrorCode(e); + + return errorCode == KeyStoreException.RETRY_WHEN_CONNECTIVITY_AVAILABLE + || e instanceof UnknownHostException + || e instanceof SocketTimeoutException + || e instanceof ConnectException; + } + + private static int getKeystoreErrorCode(Exception e) { + Throwable throwable = e instanceof KeyStoreException ? e : e.getCause(); + if (throwable instanceof KeyStoreException kse) { + return kse.getNumericErrorCode(); + } + return Integer.MAX_VALUE; + } + + private AttestationUtils() {} +} diff --git a/app/src/main/java/com/android/attestation/app/MainActivity.java b/app/src/main/java/com/android/attestation/app/MainActivity.java new file mode 100644 index 0000000..1e83ff2 --- /dev/null +++ b/app/src/main/java/com/android/attestation/app/MainActivity.java @@ -0,0 +1,619 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.attestation.app; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.content.res.AssetFileDescriptor; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.core.content.ContextCompat; +import com.android.keyattestation.verifier.AuthorizationList; +import com.android.keyattestation.verifier.GoogleTrustAnchors; +import com.android.keyattestation.verifier.KeyDescription; +import com.android.keyattestation.verifier.ProvisioningInfoMap; +import com.android.keyattestation.verifier.RootOfTrust; +import com.android.keyattestation.verifier.VerificationResult; +import com.android.keyattestation.verifier.Verifier; +import com.android.keyattestation.verifier.X509CertificateExtKt; +import com.android.keyattestation.verifier.challengecheckers.ChallengeMatcher; +import com.android.keyattestation.verifier.provider.KeyAttestationCertPath; +import com.google.android.libraries.security.content.SafeContentResolver; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.google.common.io.BaseEncoding; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** Main activity for the Attestation Verifier app. */ +public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + private static final String KEY_ALIAS = "attestation_key"; + private Button attestButton; + private Button exportButton; + private AttestationUiManager uiManager; + + private LinearLayout attestationContainer; + private LinearLayout keymasterContainer; + private LinearLayout authListContainer; + + private View attestationCard; + private View keymasterCard; + private View authListCard; + + private boolean showAllDetails = false; + private boolean attestDeviceProps = true; + private boolean useStrongbox = false; + + private ExecutorService executorService; + private Handler handler; + private List certChain; + private ActivityResultLauncher exportCertChainLauncher; + + private int currentTaskId = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + uiManager = new AttestationUiManager(this); + uiManager.setupActionBar(); + + attestButton = findViewById(R.id.attest_button); + exportButton = findViewById(R.id.export_button); + + attestationContainer = findViewById(R.id.attestation_container); + keymasterContainer = findViewById(R.id.keymaster_container); + authListContainer = findViewById(R.id.auth_list_container); + + attestationCard = findViewById(R.id.attestation_card); + keymasterCard = findViewById(R.id.keymaster_card); + authListCard = findViewById(R.id.auth_list_card); + SwitchMaterial idToggle = findViewById(R.id.toggle_id_attestation); + idToggle.setChecked(attestDeviceProps); + + idToggle.setOnCheckedChangeListener( + (buttonView, isChecked) -> { + attestDeviceProps = isChecked; + resetAndAttest(); + }); + + executorService = Executors.newSingleThreadExecutor(); + handler = new Handler(Looper.getMainLooper()); + + attestButton.setOnClickListener(unused -> resetAndAttest()); + + exportCertChainLauncher = + registerForActivityResult( + new ActivityResultContracts.CreateDocument("application/x-pem-file"), + this::exportCertChain); + + exportButton.setOnClickListener(unused -> showExportInstructions()); + } + + private void resetAndAttest() { + currentTaskId++; + final int taskId = currentTaskId; + + attestationContainer.removeAllViews(); + keymasterContainer.removeAllViews(); + authListContainer.removeAllViews(); + + attestationCard.setVisibility(View.GONE); + keymasterCard.setVisibility(View.GONE); + authListCard.setVisibility(View.GONE); + + exportButton.setEnabled(false); + certChain = null; + + View summaryCard = findViewById(R.id.summary_card); + if (summaryCard != null) { + summaryCard.setVisibility(View.GONE); + } + + executorService.execute( + () -> { + if (!runPreChecks()) { + return; + } + try { + generateAndVerifyAttestation(taskId); + } catch (Exception e) { + Log.e(TAG, "Attestation failed", e); + if (taskId == currentTaskId) { + handler.post( + () -> + addAttestationRow( + attestationContainer, + AttestationUiManager.AttestationParameter.ERROR, + e.getMessage(), + null)); + } + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.menu_show_all).setChecked(showAllDetails); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.menu_use_strongbox) { + item.setChecked(!item.isChecked()); + this.useStrongbox = item.isChecked(); + return true; + } else if (id == R.id.menu_show_all) { + item.setChecked(!item.isChecked()); + showAllDetails = item.isChecked(); + resetAndAttest(); + return true; + } else if (id == R.id.menu_save_to_file) { + showExportInstructions(); + return true; + } else if (id == R.id.menu_about) { + uiManager.showAboutDialog(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void exportCertChain(Uri uri) { + try (AssetFileDescriptor fd = SafeContentResolver.openAssetFileDescriptor(this, uri, "wt"); + OutputStream out = fd.createOutputStream()) { + for (X509Certificate cert : certChain) { + out.write("-----BEGIN CERTIFICATE-----\n".getBytes(UTF_8)); + out.write( + Base64.getEncoder() + .encodeToString(cert.getEncoded()) + .replaceAll("(.{64})", "$1\n") + .getBytes(UTF_8)); + out.write("\n-----END CERTIFICATE-----\n".getBytes(UTF_8)); + } + } catch (Exception e) { + Log.e(TAG, "Export failed", e); + } + } + + private void addAttestationRow( + ViewGroup container, + AttestationUiManager.AttestationParameter parameter, + Object value, + AttestationUiManager.EnforcementType enforcement) { + String name = getString(parameter.getLabelResId()); + if (value == null || String.valueOf(value).trim().isEmpty()) { + return; + } + + handler.post( + () -> { + View itemView = getLayoutInflater().inflate(R.layout.attestation_item, container, false); + TextView titleView = itemView.findViewById(R.id.item_title); + TextView summaryView = itemView.findViewById(R.id.item_summary); + TextView enforcementView = itemView.findViewById(R.id.item_enforcement); + + titleView.setText(name); + summaryView.setText(String.valueOf(value)); + AttestationUiManager.EnforcementType type = + (enforcement != null) ? enforcement : AttestationUiManager.EnforcementType.NONE; + enforcementView.setText(type.getLabel()); + + int colorRes = + switch (type) { + case TEE, HARDWARE -> R.color.brand_success; + case SOFTWARE -> R.color.brand_warning; + case NONE -> 0; + }; + + if (colorRes != 0) { + enforcementView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_shield, 0); + enforcementView.setCompoundDrawableTintList( + ColorStateList.valueOf(ContextCompat.getColor(this, colorRes))); + } else { + enforcementView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + + itemView.setOnClickListener( + v -> + uiManager.showParameterDetailDialog( + name, uiManager.getParameterDescription(parameter, enforcement))); + + container.addView(itemView); + if (container.getParent() instanceof View) { + ((View) container.getParent()).setVisibility(View.VISIBLE); + } + }); + } + + private String mapPurposeToString(BigInteger p) { + return switch (p.intValue()) { + case 1 -> getString(R.string.purpose_sign); + case 2 -> getString(R.string.purpose_verify); + case 3 -> getString(R.string.purpose_encrypt); + case 4 -> getString(R.string.purpose_decrypt); + case 10 -> getString(R.string.purpose_attest); + default -> getString(R.string.purpose_unknown, p.toString()); + }; + } + + private String mapAuthTypeToString(int val) { + return switch (val) { + case 1 -> getString(R.string.auth_type_password); + case 2 -> getString(R.string.auth_type_biometric); + default -> getString(R.string.auth_type_none); + }; + } + + private void showExportInstructions() { + handler.post( + () -> { + new MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.export_certificate_chain_title)) + .setMessage(getString(R.string.export_certificate_chain_dialog_message)) + .setPositiveButton( + "Continue", + (dialog, which) -> exportCertChainLauncher.launch("attestation_certs.pem")) + .setNegativeButton("Cancel", null) + .show(); + }); + } + + private boolean runPreChecks() { + + if (!AttestationUtils.isNetworkAvailable(this)) { + handler.post( + () -> + AttestationUtils.showAlertDialog( + this, + getString(R.string.network_error_alert_dialog_title), + getString(R.string.network_error_alert_dialog_msg), + getString(R.string.ok))); + return false; + } + return true; + } + + private void handleAttestationError(Exception e) { + if (AttestationUtils.isCannotAttestIdsError(e)) { + handler.post( + () -> { + String message = getString(R.string.cannot_attest_ids_alert_dialog_msg); + if (attestDeviceProps) { + message += getString(R.string.cannot_attest_ids_tip); + } + AttestationUtils.showAlertDialog( + this, + getString(R.string.cannot_attest_ids_alert_dialog_title), + message, + getString(R.string.ok)); + }); + } else if (AttestationUtils.isNetworkError(e)) { + handler.post( + () -> + AttestationUtils.showAlertDialog( + this, + getString(R.string.network_error_alert_dialog_title), + getString(R.string.network_error_alert_dialog_msg), + getString(R.string.ok))); + } else { + handler.post( + () -> + AttestationUtils.showAlertDialog( + this, + getString(R.string.unexpected_error_alert_dialog_title), + getString(R.string.unexpected_error_alert_dialog_msg), + getString(R.string.ok))); + } + } + + private void generateAndVerifyAttestation(int taskId) + throws GeneralSecurityException, IOException { + + // Generate a new key pair in the Android Keystore. + KeyPairGenerator keyPairGenerator = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + // The attestation challenge is a random nonce that is used to prevent replay attacks. + byte[] challenge = UUID.randomUUID().toString().getBytes(UTF_8); + KeyGenParameterSpec spec = + new KeyGenParameterSpec.Builder( + KEY_ALIAS, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAttestationChallenge(challenge) + .setDevicePropertiesAttestationIncluded(attestDeviceProps) + .setIsStrongBoxBacked(useStrongbox) + .build(); + + try { + keyPairGenerator.initialize(spec); + keyPairGenerator.generateKeyPair(); + } catch (Exception e) { + handleAttestationError(e); + return; + } + + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + Certificate[] certs = keyStore.getCertificateChain(KEY_ALIAS); + this.certChain = + Arrays.stream(certs).map(c -> (X509Certificate) c).collect(Collectors.toList()); + + KeyAttestationCertPath certPath = new KeyAttestationCertPath(this.certChain); + KeyDescription ext = X509CertificateExtKt.keyDescription(certPath.leafCert()); + var unused = ProvisioningInfoMap.parseFrom(certPath.attestationCert()); + + Verifier verifier = + new Verifier(GoogleTrustAnchors.INSTANCE, Collections::emptySet, Instant::now); + VerificationResult result = verifier.verify(certChain, new ChallengeMatcher(challenge)); + + handler.post( + () -> { + if (taskId != currentTaskId) { + return; + } + exportButton.setEnabled(true); + + boolean isVerified = (result instanceof VerificationResult.Success); + uiManager.updateSummaryStatus(isVerified); + String resultName = isVerified ? "Success" : result.getClass().getSimpleName(); + + addAttestationRow( + attestationContainer, + AttestationUiManager.AttestationParameter.VERIFICATION_RESULT, + resultName, + isVerified + ? AttestationUiManager.EnforcementType.TEE + : AttestationUiManager.EnforcementType.SOFTWARE); + addAttestationRow( + attestationContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_CHALLENGE, + uiManager.formatChallenge(ext.getAttestationChallenge()), + AttestationUiManager.EnforcementType.NONE); + addAttestationRow( + attestationContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_SECURITY_LEVEL, + ext.getAttestationSecurityLevel().name(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + keymasterContainer, + AttestationUiManager.AttestationParameter.KEYMASTER_VERSION, + ext.getKeyMintVersion(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + keymasterContainer, + AttestationUiManager.AttestationParameter.KEYMASTER_SECURITY_LEVEL, + ext.getKeyMintSecurityLevel().name(), + AttestationUiManager.EnforcementType.TEE); + AuthorizationList sw = ext.getSoftwareEnforced(); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.CREATION_DATE, + uiManager.formatDate(sw.getCreationDateTime()), + AttestationUiManager.EnforcementType.SOFTWARE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.APPLICATION_ID, + uiManager.formatAppId(sw.getAttestationApplicationId()), + AttestationUiManager.EnforcementType.SOFTWARE); + + AuthorizationList hw = ext.getHardwareEnforced(); + if (hw.getPurposes() != null) { + String purposes = + hw.getPurposes().stream() + .map(this::mapPurposeToString) + .collect(Collectors.joining(", ")); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_PURPOSES, + purposes, + AttestationUiManager.EnforcementType.TEE); + } + + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_ALGORITHMS, + hw.getAlgorithms(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_OS_VERSION, + hw.getOsVersion(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_OS_PATCH, + uiManager.formatPatchLevel(hw.getOsPatchLevel()), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_ORIGIN, + hw.getOrigin(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_ID_BRAND, + hw.getAttestationIdBrand(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.APPLICATION_ID, + hw.getAttestationIdDevice(), + AttestationUiManager.EnforcementType.TEE); + + if (hw.getRootOfTrust() != null) { + RootOfTrust rot = hw.getRootOfTrust(); + uiManager.updateBootloaderBanner(rot.getDeviceLocked()); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ROT_STATE, + rot.getVerifiedBootState().name(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ROT_LOCKED, + rot.getDeviceLocked(), + AttestationUiManager.EnforcementType.HARDWARE); + } + + if (showAllDetails) { + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ACTIVE_DATE, + uiManager.formatDate(hw.getActiveDateTime()), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ORIGINATION_EXPIRE_DATE, + uiManager.formatDate(hw.getOriginationExpireDateTime()), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.USAGE_EXPIRE_DATE, + uiManager.formatDate(hw.getUsageExpireDateTime()), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.NO_AUTH_REQUIRED, + hw.getNoAuthRequired(), + AttestationUiManager.EnforcementType.TEE); + int authTypeVal = (hw.getUserAuthType() != null) ? hw.getUserAuthType().intValue() : 0; + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.USER_AUTH_TYPE, + mapAuthTypeToString(authTypeVal), + AttestationUiManager.EnforcementType.TEE); + + if (hw.getAuthTimeout() != null) { + int timeoutSeconds = hw.getAuthTimeout().intValue(); + String formattedTimeout = + getResources() + .getQuantityString(R.plurals.unit_seconds, timeoutSeconds, timeoutSeconds); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.AUTH_TIMEOUT, + formattedTimeout, + AttestationUiManager.EnforcementType.TEE); + } + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.TRUSTED_USER_PRESENCE_REQUIRED, + hw.getTrustedUserPresenceRequired(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.UNLOCKED_DEVICE_REQUIRED, + hw.getUnlockedDeviceRequired(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.VENDOR_PATCH_LEVEL, + uiManager.formatPatchLevel(hw.getVendorPatchLevel()), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.BOOT_PATCH_LEVEL, + uiManager.formatPatchLevel(hw.getBootPatchLevel()), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_ID_MODEL, + hw.getAttestationIdModel(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_ID_SERIAL, + hw.getAttestationIdSerial(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_ID_IMEI, + hw.getAttestationIdImei(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.KEY_SIZE, + hw.getKeySize(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.DIGESTS, + hw.getDigests(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.EC_CURVE, + hw.getEcCurve(), + AttestationUiManager.EnforcementType.TEE); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.RSA_PUBLIC_EXPONENT, + hw.getRsaPublicExponent() != null ? hw.getRsaPublicExponent().toString() : "", + AttestationUiManager.EnforcementType.TEE); + + if (hw.getModuleHash() != null && !hw.getModuleHash().isEmpty()) { + String hash = + BaseEncoding.base16().lowerCase().encode(hw.getModuleHash().toByteArray()); + addAttestationRow( + authListContainer, + AttestationUiManager.AttestationParameter.ATTESTATION_MODULE_HASH, + hash, + AttestationUiManager.EnforcementType.TEE); + } + } + }); + } +} diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..0ef7e68 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_verifier.xml b/app/src/main/res/drawable/ic_launcher_verifier.xml new file mode 100644 index 0000000..e045a56 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_verifier.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shield.xml b/app/src/main/res/drawable/ic_shield.xml new file mode 100644 index 0000000..47bbacd --- /dev/null +++ b/app/src/main/res/drawable/ic_shield.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml new file mode 100644 index 0000000..a9c1357 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1091eaa --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/attestation_item.xml b/app/src/main/res/layout/attestation_item.xml new file mode 100644 index 0000000..e1a9ebf --- /dev/null +++ b/app/src/main/res/layout/attestation_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/menu/main_menu.xml b/app/src/main/res/layout/menu/main_menu.xml new file mode 100644 index 0000000..62c3e64 --- /dev/null +++ b/app/src/main/res/layout/menu/main_menu.xml @@ -0,0 +1,26 @@ + +

+ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/summary_card.xml b/app/src/main/res/layout/summary_card.xml new file mode 100644 index 0000000..a72491d --- /dev/null +++ b/app/src/main/res/layout/summary_card.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..9666ab1 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,26 @@ + + #D0BCFF + #381E72 + #4F378B + #EADDFF + #CCC2DC + #332D41 + #4A4458 + #E8DEF8 + #EFB8C8 + #492532 + #633B48 + #FFD8E4 + #F2B8B5 + #601410 + #8C1D18 + #F9DEDC + #1C1B1F + #E6E1E5 + #1C1B1F + #E6E1E5 + #49454F + #CAC4D0 + #938F99 + #D0BCFF + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..b798494 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,37 @@ + + #4285F4 + #D2E3FC + #6750A4 + #80868B + #174EA6 + #3E2723 + #AECBFA + #FDE293 + #EA4335 + #FBBC05 + #34A853 + + #6750A4 + #FFFFFF + #EADDFF + #21005D + #625B71 + #FFFFFF + #E8DEF8 + #1D192B + #7D5260 + #FFFFFF + #FFD8E4 + #31111D + #B3261E + #FFFFFF + #F9DEDC + #410E0B + #FFFBFE + #1C1B1F + #FFFBFE + #1C1B1F + #E7E0EC + #49454F + #79747E + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..2eadbd1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,183 @@ + + Attestation Verifier + Attestation output will be shown here. + Attestation failed: %s + Attestation generated successfully. + Generate and verify attestation + Use StrongBox + Export certificate chain + OK + Attest Device IDs (IMEI/Serial) + Security enforcement level indicator + Verification status icon + Attestation + Keymaster + Authorization List + Attest device props + Show all + Save to file + About + Bootloader is locked + Bootloader is unlocked + Export Certificate Chain + An unexpected error occurred during attestation. + Save the PEM chain to your device. Retrieve it via:\n\nadb pull /sdcard/Download/attestation_certs.pem + About Key Attestation + Verifies Android Keystore integrity using official Trust Anchors. + Unexpected Error + Network Error + Cannot Attest IDs + "The attestation process requires an internet connection.\n\nSteps to fix:\n1. Check your Wi-Fi or Data connection.\n2. Ensure you are connected to the network.\n3. Try attesting again." + "The device was unable to attest to hardware identifiers (Device IDs).\n\nReason may include:\n Device not having provisioned identifiers or Device identifiers are not supported .\n" + \n\nTip: Key generation failed. If this device does not support ID attestation, try disabling the \'Attest Device IDs\' toggle on the main screen. + Device Secured + Hardware-backed attestation verified successfully. + Security Warning + Attestation failed or is software-enforced. + The outcome of validating the certificate chain against the official Root of Trust. + A \'Success\' indicates the chain is authentic and untampered. + + + A unique value provided to ensure the attestation is fresh and not a replay of a previous one. + It is typically represented as a Base64 string or a timestamp. + + The security level of the environment that generated the attestation. + Levels include \'TRUSTED_ENVIRONMENT\' (TEE) or \'STRONG_BOX\'. + + The version of the hardware-backed Keymaster or KeyMint implementation on the device. + For example, version 400 represents Keymaster 4.0. + + Specifies where the keys themselves are stored and managed. + This is typically \'TRUSTED_ENVIRONMENT\' for TEE-backed keys. + + The date and time when the key was created, specified in milliseconds since the epoch. + For example, January 1, 2021 is represented as 1609459200000. + + Identifies the specific Android application and its signature that requested this key. + It contains the package name and the SHA-256 hash of the signing certificate. + + The cryptographic operations that the hardware allows this key to perform. + Purposes include \'Sign\', \'Verify\', \'Encrypt\', or \'Decrypt\'. + + The cryptographic algorithm used for the key pair. + Standard values include \'RSA\' (1) or \'EC\' (3). + + The bit length of the generated key. + For example, 2048 for RSA or 256 for Elliptic Curve. + + The hashing algorithms permitted for use with this key. + Commonly used digests include \'SHA-256\' or \'SHA-512\'. + + The elliptic curve used for EC key generation. + For example, \'P-256\' (secp256r1) is represented as 1. + + The source of the key material. + Levels include \'Generated\' (0) for keys created inside the hardware. + + The foundational security state of the device, including bootloader and verified boot info. + It confirms if the device bootloader is \'Locked\' or \'Unlocked\'. + + If true, the key can be used without requiring a user to authenticate (PIN/Biometric). + A \'true\' value means no biometric prompt is needed to use this key. + + The date before which the key cannot be used. + + The date after which the key can no longer be used for signing. + + The date after which the key can no longer be used for verification. + + The type of authentication required to access the key. + Common values include 1 (Password/PIN) or 2 (Fingerprint). + + The duration in seconds that the user\'s authentication remains valid. + + If true, the user must provide physical proof of presence (e.g., button press). + + If true, the key can only be used when the device screen is currently unlocked. + + The version of the Android OS currently running on the device. + For example, 14 represents Android 14. + + The month and year associated with the security patch being used within the Keymaster. + For example, the August 2018 patch is represented as 201808. + + The security patch level of the vendor-specific partition. + It follows the YYYYMMDD format to denote the specific driver update. + + The security patch level of the bootloader image. + Represented as YYYYMMDD. + + The device brand name verified by the hardware. + For example: \'samsung\'. + + The device model name verified by the hardware. + For example: \'Pixel 8 Pro\'. + The hardware serial number of the device, if attestation is permitted. + The International Mobile Equipment Identity of the primary SIM slot. + The public exponent value used in the RSA algorithm math. + Usually 65537. + A hash representing the state of the security modules loaded in the OS. + A technical security parameter defined by the Android Keystore attestation schema. + The current state of the device\'s root of trust. + This indicates whether the device is \'Locked\' or \'Unlocked\'. + If true, the device bootloader is locked, which is required for strong attestation. + A \'true\' value means the bootloader is locked and the device is secured. + Enforced by TEE + Enforced by Software + Enforced by Hardware + Example: + N/A + Verification Result + Attestation Challenge + Attestation Security Level + Keymaster Version + Keymaster Security Level + Creation Date + Application ID + Attestation Purposes + Attestation Algorithms + Attestation OS Version + Attestation OS Patch + Attestation Origin + Attestation ID Brand + Attestation ID Device + Root of Trust (State) + Root of Trust + Root of Trust (Locked) + Active Date + Origination Expire Date + Usage Expire Date + No Auth Required + User Auth Type + Auth Timeout + Trusted User Presence Required + Unlocked Device Required + Vendor Patch Level + Boot Patch Level + Attestation ID Model + Attestation ID Serial + Attestation ID IMEI + Key Size + Digests + EC Curve + RSA Public Exponent + Attestation Module Hash + Sign + Verify + Encrypt + Decrypt + Attest + Unknown (%1$s) + Password/PIN + Biometric + None + Attestation Verified + The key and its properties were successfully verified. + Verification Failed + The certificate chain or challenge could not be verified. + + 1 second + %1$d seconds + + Error + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..c795395 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,29 @@ + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8166462..85d3810 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,3 +17,7 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } rootProject.name = "keyattestation" + +include(":app") + +project(":app").projectDir = file("app")