/* * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception */ package com.github.androidpasswordstore.autofillparser import android.annotation.SuppressLint import android.app.assist.AssistStructure import android.content.Context import android.content.IntentSender import android.content.pm.PackageManager import android.os.Build import android.service.autofill.SaveCallback import android.util.Base64 import android.view.autofill.AutofillId import android.widget.Toast import androidx.annotation.RequiresApi import java.security.MessageDigest import logcat.LogPriority.ERROR import logcat.logcat private fun ByteArray.sha256(): ByteArray { return MessageDigest.getInstance("SHA-256").run { update(this@sha256) digest() } } private fun ByteArray.base64(): String { return Base64.encodeToString(this, Base64.NO_WRAP) } private fun stableHash(array: Collection): String { val hashes = array.map { it.sha256().base64() } return hashes.sorted().joinToString(separator = ";") } /** * Computes a stable hash of all certificates associated to the installed app with package name * [appPackage]. * * In most cases apps will only have a single certificate. If there are multiple, this functions * returns all of them in sorted order and separated with `;`. */ public fun computeCertificatesHash(context: Context, appPackage: String): String { // The warning does not apply since 1) we are specifically hashing **all** signatures and 2) it // no longer applies to Android 4.4+. // Even though there is a new way to get the certificates as of Android Pie, we need to keep // hashes comparable between versions and hence default to using the deprecated API. @SuppressLint("PackageManagerGetSignatures") @Suppress("DEPRECATION") val signaturesOld = context.packageManager .getPackageInfo(appPackage, PackageManager.GET_SIGNATURES) .signatures .orEmpty() val stableHashOld = stableHash(signaturesOld.map { it.toByteArray() }) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.packageManager.getPackageInfo( appPackage, PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong()), ) } else { context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES) } val signaturesNew = info.signingInfo?.signingCertificateHistory ?: info.signingInfo?.apkContentsSigners.orEmpty() val stableHashNew = stableHash(signaturesNew.map { it.toByteArray() }) if (stableHashNew != stableHashOld) logcat("CertificatesHash", ERROR) { "Mismatch between old and new hash: $stableHashNew != $stableHashOld" } } return stableHashOld } /** * Returns the "origin" (without port information) of the [AssistStructure.ViewNode] derived from * its `webDomain` and `webScheme`, if available. */ internal val AssistStructure.ViewNode.webOrigin: String? @RequiresApi(Build.VERSION_CODES.O) get() = webDomain?.let { domain -> val scheme = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) webScheme else null) ?: "https" "$scheme://$domain" } @RequiresApi(Build.VERSION_CODES.O) public class FixedSaveCallback(private val context: Context, private val callback: SaveCallback) { private val applicationContext = context.applicationContext public fun onFailure(message: CharSequence) { callback.onFailure(message) // When targeting SDK 29, the message is no longer shown as a toast. // See // https://developer.android.com/reference/android/service/autofill/SaveCallback#onFailure(java.lang.CharSequence) if (applicationContext.applicationInfo.targetSdkVersion >= 29) { Toast.makeText(context, message, Toast.LENGTH_LONG).show() } } public fun onSuccess(intentSender: IntentSender) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { callback.onSuccess(intentSender) } else { callback.onSuccess() // On SDKs < 28, we cannot advise the Autofill framework to launch the save intent in // the context of the app that triggered the save request. Hence, we launch it here. applicationContext.startIntentSender(intentSender, null, 0, 0, 0) } } } private fun visitViewNodes(structure: AssistStructure, block: (AssistStructure.ViewNode) -> Unit) { for (i in 0 until structure.windowNodeCount) { visitViewNode(structure.getWindowNodeAt(i).rootViewNode, block) } } private fun visitViewNode( node: AssistStructure.ViewNode, block: (AssistStructure.ViewNode) -> Unit, ) { block(node) for (i in 0 until node.childCount) { visitViewNode(node.getChildAt(i), block) } } @RequiresApi(Build.VERSION_CODES.O) internal fun AssistStructure.findNodeByAutofillId( autofillId: AutofillId ): AssistStructure.ViewNode? { var node: AssistStructure.ViewNode? = null visitViewNodes(this) { if (it.autofillId == autofillId) node = it } return node }