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 com.android.statementservice.domain
18  
19  import android.content.Context
20  import android.content.pm.verify.domain.DomainVerificationManager
21  import android.net.Network
22  import android.util.Log
23  import androidx.collection.LruCache
24  import com.android.statementservice.network.retriever.StatementRetriever
25  import com.android.statementservice.retriever.AbstractAsset
26  import com.android.statementservice.retriever.AbstractAssetMatcher
27  import com.android.statementservice.utils.Result
28  import com.android.statementservice.utils.StatementUtils
29  import com.android.statementservice.utils.component1
30  import com.android.statementservice.utils.component2
31  import com.android.statementservice.utils.component3
32  import java.net.HttpURLConnection
33  import java.util.Optional
34  import java.util.UUID
35  
36  private typealias WorkResult = androidx.work.ListenableWorker.Result
37  
38  class DomainVerifier private constructor(
39      private val appContext: Context,
40      private val manager: DomainVerificationManager
41  ) {
42      companion object {
43          private val TAG = DomainVerifier::class.java.simpleName
44          private const val DEBUG = false
45  
46          private var singleton: DomainVerifier? = null
47  
48          fun getInstance(context: Context) = when {
49              singleton != null -> singleton!!
50              else -> synchronized(this) {
51                  if (singleton == null) {
52                      val appContext = context.applicationContext
53                      val manager =
54                          appContext.getSystemService(DomainVerificationManager::class.java)!!
55                      singleton = DomainVerifier(appContext, manager)
56                  }
57                  singleton!!
58              }
59          }
60      }
61  
62      private val retriever = StatementRetriever()
63  
64      private val targetAssetCache = AssetLruCache()
65  
66      fun collectHosts(packageNames: Iterable<String>): Iterable<Triple<UUID, String, String>> {
67          return packageNames.mapNotNull { packageName ->
68              val (domainSetId, _, hostToStateMap) = try {
69                  manager.getDomainVerificationInfo(packageName)
70              } catch (ignored: Exception) {
71                  // Package disappeared, assume it will be rescheduled if the package reappears
72                  null
73              } ?: return@mapNotNull null
74  
75              val hostsToRetry = hostToStateMap
76                  .filterValues(VerifyStatus::shouldRetry)
77                  .takeIf { it.isNotEmpty() }
78                  ?.map { it.key }
79                  ?: return@mapNotNull null
80  
81              hostsToRetry.map { Triple(domainSetId, packageName, it) }
82          }
83              .flatten()
84      }
85  
86      suspend fun verifyHost(
87          host: String,
88          packageName: String,
89          network: Network? = null
90      ): Pair<WorkResult, VerifyStatus> {
91          val assetMatcher = synchronized(targetAssetCache) { targetAssetCache[packageName] }
92              .takeIf { it!!.isPresent }
93              ?: return WorkResult.failure() to VerifyStatus.FAILURE_PACKAGE_MANAGER
94          return verifyHost(host, assetMatcher.get(), network)
95      }
96  
97      private suspend fun verifyHost(
98          host: String,
99          assetMatcher: AbstractAssetMatcher,
100          network: Network? = null
101      ): Pair<WorkResult, VerifyStatus> {
102          var exception: Exception? = null
103          val resultAndStatus = try {
104              val sourceAsset = StatementUtils.createWebAssetString(host)
105                  .let(AbstractAsset::create)
106              val result = retriever.retrieve(sourceAsset, network)
107                  ?: return WorkResult.success() to VerifyStatus.FAILURE_UNKNOWN
108              when (result.responseCode) {
109                  HttpURLConnection.HTTP_MOVED_PERM,
110                  HttpURLConnection.HTTP_MOVED_TEMP -> {
111                      WorkResult.failure() to VerifyStatus.FAILURE_REDIRECT
112                  }
113                  else -> {
114                      val isVerified = result.statements.any { statement ->
115                          (StatementUtils.RELATION.matches(statement.relation) &&
116                                  assetMatcher.matches(statement.target))
117                      }
118  
119                      if (isVerified) {
120                          WorkResult.success() to VerifyStatus.SUCCESS
121                      } else {
122                          WorkResult.failure() to VerifyStatus.FAILURE_REJECTED_BY_SERVER
123                      }
124                  }
125              }
126          } catch (e: Exception) {
127              exception = e
128              WorkResult.retry() to VerifyStatus.FAILURE_UNKNOWN
129          }
130  
131          if (DEBUG) {
132              Log.d(TAG, "Verifying $host: ${resultAndStatus.second}", exception)
133          }
134  
135          return resultAndStatus
136      }
137  
138      private inner class AssetLruCache : LruCache<String, Optional<AbstractAssetMatcher>>(50) {
139          override fun create(packageName: String) =
140              StatementUtils.getCertFingerprintsFromPackageManager(appContext, packageName)
141                  .let { (it as? Result.Success)?.value }
142                  ?.let { StatementUtils.createAndroidAsset(packageName, it) }
143                  ?.let(AbstractAssetMatcher::createMatcher)
144                  .let { Optional.ofNullable(it) }
145      }
146  }
147