Skip to content

Commit

Permalink
Add blocking algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Dec 10, 2024
1 parent ca1717c commit 0974e8e
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import com.duckduckgo.autoconsent.api.Autoconsent
import com.duckduckgo.autofill.api.BrowserAutofill
import com.duckduckgo.autofill.api.InternalTestUserChecker
import com.duckduckgo.browser.api.JsInjectorPlugin
import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.browser.api.WebViewVersionProvider
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.utils.CurrentTimeProvider
Expand All @@ -79,7 +80,6 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Off
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailable
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.subscriptions.api.Subscriptions
import com.duckduckgo.user.agent.api.ClientBrandHintProvider
Expand Down Expand Up @@ -153,7 +153,7 @@ class BrowserWebViewClientTest {
private val mockUriLoadedManager: UriLoadedManager = mock()
private val mockAndroidBrowserConfigFeature: AndroidBrowserConfigFeature = mock()
private val mockAndroidFeaturesHeaderPlugin = AndroidFeaturesHeaderPlugin(mockDuckDuckGoUrlDetector, mockAndroidBrowserConfigFeature, mock())
private val mockMaliciousSiteProtection: MaliciousSiteProtection = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()

@UiThreadTest
@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.subscriptions.api.Subscriptions
import com.duckduckgo.user.agent.api.ClientBrandHintProvider
import com.google.android.material.snackbar.Snackbar
import java.net.URI
import javax.inject.Inject
import kotlinx.coroutines.*
Expand Down Expand Up @@ -128,10 +129,6 @@ class BrowserWebViewClient @Inject constructor(

private var shouldOpenDuckPlayerInNewTab: Boolean = true

private val onSiteBlockedAsync: () -> Unit = {
// TODO (cbarreiro): Handle site blocked asynchronously
}

init {
appCoroutineScope.launch {
duckPlayer.observeShouldOpenInNewTab().collect {
Expand Down Expand Up @@ -165,6 +162,11 @@ class BrowserWebViewClient @Inject constructor(
Timber.v("shouldOverride webViewUrl: ${webView.url} URL: $url")
webViewClientListener?.onShouldOverride()

val onSiteBlockedAsync: () -> Unit = {
Snackbar.make(webView, "Site blocked", Snackbar.LENGTH_SHORT).show()
// TODO (cbarreiro): Handle site blocked asynchronously
}

if (runBlocking {
maliciousSiteProtectionWebViewIntegration.shouldOverrideUrlLoading(
url,
Expand All @@ -174,6 +176,7 @@ class BrowserWebViewClient @Inject constructor(
)
}
) {
Snackbar.make(webView, "Site blocked", Snackbar.LENGTH_SHORT).show()
// TODO (cbarreiro): Handle site blocked synchronously
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.duckduckgo.httpsupgrade.api.HttpsUpgrader
import com.duckduckgo.privacy.config.api.Gpc
import com.duckduckgo.request.filterer.api.RequestFilterer
import com.duckduckgo.user.agent.api.UserAgentProvider
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.withContext
import timber.log.Timber

Expand Down Expand Up @@ -100,11 +101,13 @@ class WebViewRequestInterceptor(
val url: Uri? = request.url

val onSiteBlockedAsync: () -> Unit = {
Snackbar.make(webView, "Site blocked", Snackbar.LENGTH_SHORT).show()
// TODO (cbarreiro): Handle site blocked asynchronously
}

maliciousSiteProtectionWebViewIntegration.shouldIntercept(request, documentUri, onSiteBlockedAsync)?.let {
// TODO (cbarreiro): Handle site blocked synchronously
Snackbar.make(webView, "Site blocked", Snackbar.LENGTH_SHORT).show()
return it
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dagger.SingleInstanceIn
import java.security.MessageDigest

@Module
@ContributesTo(AppScope::class)
Expand All @@ -45,4 +46,10 @@ class MaliciousSiteModule {
fun provideMaliciousSiteDao(database: MaliciousSitesDatabase): MaliciousSiteDao {
return database.maliciousSiteDao()
}

@Provides
@SingleInstanceIn(AppScope::class)
fun provideMessageDigest(): MessageDigest {
return MessageDigest.getInstance("SHA-256")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.malicioussiteprotection.impl.MaliciousSiteProtectionFeature
import com.duckduckgo.malicioussiteprotection.impl.data.db.MaliciousSiteDao
import com.duckduckgo.malicioussiteprotection.impl.data.embedded.MaliciousSiteProtectionEmbeddedDataProvider
import com.duckduckgo.malicioussiteprotection.impl.data.network.MaliciousSiteService
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber

interface MaliciousSiteRepository {
suspend fun containsHashPrefix(hashPrefix: String): Boolean
suspend fun getFilter(hash: String): Filter?
suspend fun matches(hashPrefix: String): List<Match>
}

@ContributesBinding(AppScope::class)
Expand All @@ -41,6 +44,7 @@ class RealMaliciousSiteRepository @Inject constructor(
private val maliciousSiteDao: MaliciousSiteDao,
@IsMainProcess private val isMainProcess: Boolean,
maliciousSiteProtectionFeature: MaliciousSiteProtectionFeature,
private val maliciousSiteService: MaliciousSiteService,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
dispatcherProvider: DispatcherProvider,
) : MaliciousSiteRepository {
Expand Down Expand Up @@ -87,6 +91,17 @@ class RealMaliciousSiteRepository @Inject constructor(
Filter(it.hash, it.regex)
}
}

override suspend fun matches(hashPrefix: String): List<Match> {
return try {
maliciousSiteService.getMatches(hashPrefix).matches.also {
Timber.d("\uD83D\uDFE2 Cris: Fetched $it matches for hash prefix $hashPrefix")
}
} catch (e: Exception) {
Timber.d("\uD83D\uDD34 Cris: Failed to fetch matches for hash prefix $hashPrefix")
listOf()
}
}
}

data class Match(
Expand All @@ -96,20 +111,6 @@ data class Match(
val hash: String,
)

data class HashPrefixResponse(
val insert: Set<String>,
val delete: Set<String>,
val revision: Int,
val replace: Boolean,
)

data class FilterSetResponse(
val insert: Set<Filter>,
val delete: Set<Filter>,
val revision: Int,
val replace: Boolean,
)

data class Filter(
val hash: String,
val regex: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import android.content.Context
import androidx.annotation.RawRes
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.malicioussiteprotection.impl.R
import com.duckduckgo.malicioussiteprotection.impl.data.FilterSetResponse
import com.duckduckgo.malicioussiteprotection.impl.data.HashPrefixResponse
import com.duckduckgo.malicioussiteprotection.impl.data.network.FilterSetResponse
import com.duckduckgo.malicioussiteprotection.impl.data.network.HashPrefixResponse
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.moshi.Moshi
import javax.inject.Inject
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* 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.duckduckgo.malicioussiteprotection.impl.data.network

import com.duckduckgo.anvil.annotations.ContributesServiceApi
import com.duckduckgo.common.utils.AppUrl.Url.API
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.malicioussiteprotection.impl.data.Filter
import com.duckduckgo.malicioussiteprotection.impl.data.Match
import retrofit2.http.GET
import retrofit2.http.Query

private const val BASE_URL = "$API/api/protection"
private const val HASH_PREFIX_PATH = "/hashPrefix"
private const val FILTER_SET_PATH = "/filterSet"
private const val CATEGORY = "category"
private const val PHISHING = "phishing"
private const val MALWARE = "malware"

@ContributesServiceApi(AppScope::class)
interface MaliciousSiteService {
@GET("$BASE_URL$HASH_PREFIX_PATH?$CATEGORY=$PHISHING")
suspend fun getPhishingHashPrefixes(@Query("revision") revision: Int): HashPrefixResponse

@GET("$BASE_URL$HASH_PREFIX_PATH?$CATEGORY=$MALWARE")
suspend fun getMalwareHashPrefixes(@Query("revision") revision: Int): HashPrefixResponse

@GET("$BASE_URL$FILTER_SET_PATH?$CATEGORY=$PHISHING")
suspend fun getPhishingFilterSet(@Query("revision") revision: Int): FilterSetResponse

@GET("$BASE_URL$FILTER_SET_PATH?$CATEGORY=$MALWARE")
suspend fun getMalwareFilterSet(@Query("revision") revision: Int): FilterSetResponse

@GET("$BASE_URL/matches")
suspend fun getMatches(@Query("hashPrefix") hashPrefix: String): MatchesResponse
}

data class MatchesResponse(
val matches: List<Match>,
)

data class HashPrefixResponse(
val insert: Set<String>,
val delete: Set<String>,
val revision: Int,
val replace: Boolean,
)

data class FilterSetResponse(
val insert: Set<Filter>,
val delete: Set<Filter>,
val revision: Int,
val replace: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection
import com.duckduckgo.malicioussiteprotection.impl.MaliciousSiteProtectionFeature
import com.duckduckgo.malicioussiteprotection.impl.data.MaliciousSiteRepository
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import java.security.MessageDigest
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
Expand All @@ -39,6 +42,8 @@ class RealMaliciousSiteProtection @Inject constructor(
private val maliciousSiteProtectionFeature: MaliciousSiteProtectionFeature,
@IsMainProcess private val isMainProcess: Boolean,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val maliciousSiteRepository: MaliciousSiteRepository,
private val messageDigest: MessageDigest,
) : MaliciousSiteProtection, PrivacyConfigCallbackPlugin {

private var _isFeatureEnabled = false
Expand Down Expand Up @@ -72,7 +77,46 @@ class RealMaliciousSiteProtection @Inject constructor(

override suspend fun isMalicious(url: Uri, onSiteBlockedAsync: () -> Unit): Boolean {
Timber.tag("MaliciousSiteProtection").d("isMalicious $url")
// TODO (cbarreiro): Implement the logic to check if the URL is malicious

val hostname = url.host ?: return false
val hash = messageDigest
.digest(hostname.toByteArray())
.joinToString("") { "%02x".format(it) }
val hashPrefix = hash.substring(0, 8)

if (!maliciousSiteRepository.containsHashPrefix(hashPrefix)) {
Timber.d("\uD83D\uDFE2 Cris: should not block (no hash) $hashPrefix, $url")
return false
}
maliciousSiteRepository.getFilter(hash)?.let {
if (Pattern.matches(it.regex, url.toString())) {
Timber.d("\uD83D\uDFE2 Cris: shouldBlock $url")
return true
}
}
appCoroutineScope.launch(dispatchers.io()) {
matches(hashPrefix, url, hostname, hash).let { matches ->
if (matches) {
onSiteBlockedAsync()
}
}
}
return false
}

private suspend fun matches(
hashPrefix: String,
url: Uri,
hostname: String,
hash: String,
): Boolean {
val matches = maliciousSiteRepository.matches(hashPrefix)
return matches.any { match ->
Pattern.matches(match.regex, url.toString()) &&
(hostname == match.hostname) &&
(hash == match.hash)
}.also { matched ->
Timber.d("\uD83D\uDFE2 Cris: should block $matched")
}
}
}
Empty file.
Loading

0 comments on commit 0974e8e

Please sign in to comment.