aboutsummaryrefslogtreecommitdiffstats
path: root/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillHelper.kt
blob: bac5d7f6e0adb59fc5ea680bb96d32f7826b9704 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/*
 * 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<ByteArray>): 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
}