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 45d1aaa commit 3f61183
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 21 deletions.
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 3f61183

Please sign in to comment.