Add content scanner service (#4392)

* Add content scanner APIs

* Move to content scanner matrix SDK to FOSS

* Update file service

* Refactoring

* Replace matrix callbacks by coroutines

* Fix lint errors

* Add changelog

Co-authored-by: yostyle <yoanp@element.io>
This commit is contained in:
Benoit Marty 2021-11-17 11:18:20 +01:00 committed by GitHub
commit 855b672f48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1321 additions and 12 deletions

2
changelog.d/4392.removal Normal file
View file

@ -0,0 +1,2 @@
Add content scanner API from MSC1453
API documentation : https://github.com/matrix-org/matrix-content-scanner#api

View file

@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerError
import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException
import javax.net.ssl.HttpsURLConnection
@ -100,3 +102,19 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
error.code == MatrixError.M_INVALID_USERNAME ||
error.code == MatrixError.M_EXCLUSIVE)
}
/**
* Try to convert to a ScanFailure. Return null in the cases it's not possible
*/
fun Throwable.toScanFailure(): ScanFailure? {
return if (this is Failure.OtherServerError) {
tryOrNull {
MoshiProvider.providesMoshi()
.adapter(ContentScannerError::class.java)
.fromJson(errorBody)
}
?.let { ScanFailure(it, httpCode, this) }
} else {
null
}
}

View file

@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.cache.CacheService
import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@ -192,6 +193,11 @@ interface Session :
*/
fun cryptoService(): CryptoService
/**
* Returns the ContentScannerService associated with the session
*/
fun contentScannerService(): ContentScannerService
/**
* Returns the identity service associated with the session
*/

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.content
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
/**
* This interface defines methods for accessing content from the current session.
*/
@ -39,6 +41,15 @@ interface ContentUrlResolver {
*/
fun resolveFullSize(contentUrl: String?): String?
/**
* Get the ResolvedMethod to download a URL
*
* @param contentUrl the Matrix media content URI (in the form of "mxc://...").
* @param elementToDecrypt Encryption data may be required if you use a content scanner
* @return the Method to access resource, or null if invalid
*/
fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt? = null): ResolvedMethod?
/**
* Get the actual URL for accessing the thumbnail image of a given Matrix media content URI.
*
@ -49,4 +60,9 @@ interface ContentUrlResolver {
* @return the URL to access the described resource, or null if the url is invalid.
*/
fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String?
sealed class ResolvedMethod {
data class GET(val url: String) : ResolvedMethod()
data class POST(val url: String, val jsonBody: String) : ResolvedMethod()
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.contentscanner
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ContentScannerError(
@Json(name = "info") val info: String? = null,
@Json(name = "reason") val reason: String? = null
) {
companion object {
// 502 The server failed to request media from the media repo.
const val REASON_MCS_MEDIA_REQUEST_FAILED = "MCS_MEDIA_REQUEST_FAILED"
/* 400 The server failed to decrypt the encrypted media downloaded from the media repo.*/
const val REASON_MCS_MEDIA_FAILED_TO_DECRYPT = "MCS_MEDIA_FAILED_TO_DECRYPT"
/* 403 The server scanned the downloaded media but the antivirus script returned a non-zero exit code.*/
const val REASON_MCS_MEDIA_NOT_CLEAN = "MCS_MEDIA_NOT_CLEAN"
/* 403 The provided encrypted_body could not be decrypted. The client should request the public key of the server and then retry (once).*/
const val REASON_MCS_BAD_DECRYPTION = "MCS_BAD_DECRYPTION"
/* 400 The request body contains malformed JSON.*/
const val REASON_MCS_MALFORMED_JSON = "MCS_MALFORMED_JSON"
}
}
class ScanFailure(val error: ContentScannerError, val httpCode: Int, cause: Throwable? = null) : Throwable(cause = cause)
// For Glide, which deals with Exception and not with Throwable
fun ScanFailure.toException() = Exception(this)
fun Throwable.toScanFailure() = this.cause as? ScanFailure

View file

@ -0,0 +1,40 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.contentscanner
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
interface ContentScannerService {
val serverPublicKey: String?
fun getContentScannerServer(): String?
fun setScannerUrl(url: String?)
fun enableScanner(enabled: Boolean)
fun isScannerEnabled(): Boolean
fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean = true, fileInfo: ElementToDecrypt? = null): LiveData<Optional<ScanStatusInfo>>
fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo?
/**
* Get the current public curve25519 key that the AV server is advertising.
* @param callback on success callback containing the server public key
*/
suspend fun getServerPublicKey(forceDownload: Boolean = false): String?
suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.contentscanner
enum class ScanState {
TRUSTED,
INFECTED,
UNKNOWN,
IN_PROGRESS
}
data class ScanStatusInfo(
val state: ScanState,
val scanDateTimestamp: Long?,
val humanReadableMessage: String?
)

View file

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistration
import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService
internal class DefaultLoginWizard(
private val authAPI: AuthAPI,
@ -44,7 +45,7 @@ internal class DefaultLoginWizard(
private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
authAPI,
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
)
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {

View file

@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class IdentityDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ContentScannerDatabase

View file

@ -38,6 +38,9 @@ internal object NetworkConstants {
// Integration
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
// Content scanner
const val URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE = "_matrix/media_proxy/unstable/"
// Federation
const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
}

View file

@ -23,8 +23,10 @@ import androidx.core.content.FileProvider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.completeWith
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
@ -118,12 +120,24 @@ internal class DefaultFileService @Inject constructor(
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
if (!cachedFiles.file.exists()) {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
val resolvedUrl = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null")
val request = Request.Builder()
.url(resolvedUrl)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.build()
val request = when (resolvedUrl) {
is ContentUrlResolver.ResolvedMethod.GET -> {
Request.Builder()
.url(resolvedUrl.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.build()
}
is ContentUrlResolver.ResolvedMethod.POST -> {
Request.Builder()
.url(resolvedUrl.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType()))
.build()
}
}
val response = try {
okHttpClient.newCall(request).execute()

View file

@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.cache.CacheService
import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@ -124,6 +125,7 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val eventService: Lazy<EventService>,
private val contentScannerService: Lazy<ContentScannerService>,
private val identityService: IdentityService,
private val integrationManagerService: IntegrationManagerService,
private val thirdPartyService: Lazy<ThirdPartyService>,
@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(
override fun cryptoService(): CryptoService = cryptoService.get()
override fun contentScannerService(): ContentScannerService = contentScannerService.get()
override fun identityService() = identityService
override fun fileService(): FileService = defaultFileService.get()

View file

@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.cache.CacheModule
import org.matrix.android.sdk.internal.session.call.CallModule
import org.matrix.android.sdk.internal.session.content.ContentModule
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerModule
import org.matrix.android.sdk.internal.session.filter.FilterModule
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.group.GroupModule
@ -94,6 +95,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
AccountModule::class,
FederationModule::class,
CallModule::class,
ContentScannerModule::class,
SearchModule::class,
ThirdPartyModule::class,
SpaceModule::class,

View file

@ -20,16 +20,41 @@ import org.matrix.android.sdk.api.MatrixUrls
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
import org.matrix.android.sdk.internal.session.contentscanner.model.toJson
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import javax.inject.Inject
internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
internal class DefaultContentUrlResolver @Inject constructor(
homeServerConnectionConfig: HomeServerConnectionConfig,
private val scannerService: ContentScannerService
) : ContentUrlResolver {
private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? {
return if (scannerService.isScannerEnabled() && elementToDecrypt != null) {
val baseUrl = scannerService.getContentScannerServer()
val sep = if (baseUrl?.endsWith("/") == true) "" else "/"
val url = baseUrl + sep + NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted"
ContentUrlResolver.ResolvedMethod.POST(
url = url,
jsonBody = ScanEncryptorUtils
.getDownloadBodyAndEncryptIfNeeded(scannerService.serverPublicKey, contentUrl ?: "", elementToDecrypt)
.toJson()
)
} else {
resolveFullSize(contentUrl)?.let { ContentUrlResolver.ResolvedMethod.GET(it) }
}
}
override fun resolveFullSize(contentUrl: String?): String? {
return contentUrl
// do not allow non-mxc content URLs
@ -37,7 +62,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
?.let {
resolve(
contentUrl = it,
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/"
toThumbnail = false
)
}
}
@ -49,16 +74,27 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
?.let {
resolve(
contentUrl = it,
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/",
toThumbnail = true,
params = "?width=$width&height=$height&method=${method.value}"
)
}
}
private fun resolve(contentUrl: String,
prefix: String,
toThumbnail: Boolean,
params: String = ""): String? {
var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME)
val apiPath = if (scannerService.isScannerEnabled()) {
NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE
} else {
NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0
}
val prefix = if (toThumbnail) {
apiPath + "thumbnail/"
} else {
apiPath + "download/"
}
val fragmentOffset = serverAndMediaId.indexOf("#")
var fragment = ""
if (fragmentOffset >= 0) {
@ -66,6 +102,11 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
}
return baseUrl + prefix + serverAndMediaId + params + fragment
val resolvedUrl = if (scannerService.isScannerEnabled()) {
scannerService.getContentScannerServer()!!.ensureTrailingSlash()
} else {
baseUrl
}
return resolvedUrl + prefix + serverAndMediaId + params + fragment
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner
import okhttp3.ResponseBody
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
/**
* https://github.com/matrix-org/matrix-content-scanner
*/
internal interface ContentScannerApi {
@POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted")
suspend fun downloadEncrypted(@Body info: DownloadBody): ResponseBody
@POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan_encrypted")
suspend fun scanFile(@Body info: DownloadBody): ScanResponse
@GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "public_key")
suspend fun getServerPublicKey(): ServerPublicKeyResponse
@GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan/{domain}/{mediaId}")
suspend fun scanMedia(@Path(value = "domain") domain: String, @Path(value = "mediaId") mediaId: String): ScanResponse
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class ContentScannerApiProvider @Inject constructor() {
var contentScannerApi: ContentScannerApi? = null
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner
import dagger.Binds
import dagger.Module
import dagger.Provides
import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
import org.matrix.android.sdk.internal.database.RealmKeysUtils
import org.matrix.android.sdk.internal.di.ContentScannerDatabase
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.di.UserMd5
import org.matrix.android.sdk.internal.session.SessionModule
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
import org.matrix.android.sdk.internal.session.contentscanner.db.ContentScannerRealmModule
import org.matrix.android.sdk.internal.session.contentscanner.db.RealmContentScannerStore
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultDownloadEncryptedTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultGetServerPublicKeyTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanEncryptedTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanMediaTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DownloadEncryptedTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
import java.io.File
@Module
internal abstract class ContentScannerModule {
@Module
companion object {
@JvmStatic
@Provides
@ContentScannerDatabase
@SessionScope
fun providesContentScannerRealmConfiguration(realmKeysUtils: RealmKeysUtils,
@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.name("matrix-sdk-content-scanning.realm")
.apply {
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.allowWritesOnUiThread(true)
.modules(ContentScannerRealmModule())
.build()
}
}
@Binds
abstract fun bindContentScannerService(service: DisabledContentScannerService): ContentScannerService
@Binds
abstract fun bindContentScannerStore(store: RealmContentScannerStore): ContentScannerStore
@Binds
abstract fun bindDownloadEncryptedTask(task: DefaultDownloadEncryptedTask): DownloadEncryptedTask
@Binds
abstract fun bindGetServerPublicKeyTask(task: DefaultGetServerPublicKeyTask): GetServerPublicKeyTask
@Binds
abstract fun bindScanMediaTask(task: DefaultScanMediaTask): ScanMediaTask
@Binds
abstract fun bindScanEncryptedTask(task: DefaultScanEncryptedTask): ScanEncryptedTask
}

View file

@ -0,0 +1,131 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner
import androidx.lifecycle.LiveData
import dagger.Lazy
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.network.RetrofitFactory
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class DefaultContentScannerService @Inject constructor(
private val retrofitFactory: RetrofitFactory,
@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val contentScannerApiProvider: ContentScannerApiProvider,
private val contentScannerStore: ContentScannerStore,
private val getServerPublicKeyTask: GetServerPublicKeyTask,
private val scanEncryptedTask: ScanEncryptedTask,
private val scanMediaTask: ScanMediaTask,
private val taskExecutor: TaskExecutor
) : ContentScannerService {
// Cache public key in memory
override var serverPublicKey: String? = null
private set
override fun getContentScannerServer(): String? {
return contentScannerStore.getScannerUrl()
}
override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException("No content scanner define")
if (!forceDownload && serverPublicKey != null) {
return serverPublicKey
}
return getServerPublicKeyTask.execute(GetServerPublicKeyTask.Params(api)).also {
serverPublicKey = it
}
}
override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
val result = if (fileInfo != null) {
scanEncryptedTask.execute(ScanEncryptedTask.Params(
mxcUrl = mxcUrl,
publicServerKey = getServerPublicKey(false),
encryptedInfo = fileInfo
))
} else {
scanMediaTask.execute(ScanMediaTask.Params(mxcUrl))
}
return ScanStatusInfo(
state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED,
humanReadableMessage = result.info,
scanDateTimestamp = System.currentTimeMillis()
)
}
override fun setScannerUrl(url: String?) = contentScannerStore.setScannerUrl(url).also {
if (url == null) {
contentScannerApiProvider.contentScannerApi = null
serverPublicKey = null
} else {
val api = retrofitFactory
.create(okHttpClient, url)
.create(ContentScannerApi::class.java)
contentScannerApiProvider.contentScannerApi = api
taskExecutor.executorScope.launch {
try {
getServerPublicKey(true)
} catch (failure: Throwable) {
Timber.e("Failed to get public server api")
}
}
}
}
override fun enableScanner(enabled: Boolean) = contentScannerStore.enableScanner(enabled)
override fun isScannerEnabled(): Boolean = contentScannerStore.isScanEnabled()
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
return contentScannerStore.getScanResult(mxcUrl)
}
override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData<Optional<ScanStatusInfo>> {
val data = contentScannerStore.getLiveScanResult(mxcUrl)
if (fetchIfNeeded && !contentScannerStore.isScanResultKnownOrInProgress(mxcUrl, getContentScannerServer())) {
taskExecutor.executorScope.launch {
try {
getScanResultForAttachment(mxcUrl, fileInfo)
} catch (failure: Throwable) {
Timber.e("Failed to get file status : ${failure.localizedMessage}")
}
}
}
return data
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
/**
* Created to by-pass ProfileTask execution in LoginWizard.
*/
@SessionScope
internal class DisabledContentScannerService @Inject constructor() : ContentScannerService {
override val serverPublicKey: String?
get() = null
override fun getContentScannerServer(): String? {
return null
}
override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
return null
}
override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
TODO("Not yet implemented")
}
override fun setScannerUrl(url: String?) {
}
override fun enableScanner(enabled: Boolean) {
}
override fun isScannerEnabled(): Boolean {
return false
}
override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData<Optional<ScanStatusInfo>> {
return MutableLiveData()
}
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
return null
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.matrix.android.sdk.internal.crypto.tools.withOlmEncryption
import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
import org.matrix.android.sdk.internal.session.contentscanner.model.EncryptedBody
import org.matrix.android.sdk.internal.session.contentscanner.model.toCanonicalJson
internal object ScanEncryptorUtils {
fun getDownloadBodyAndEncryptIfNeeded(publicServerKey: String?, mxcUrl: String, elementToDecrypt: ElementToDecrypt): DownloadBody {
// TODO, upstream refactoring changed the object model here...
// it's bad we have to recreate and use hardcoded values
val encryptedInfo = EncryptedFileInfo(
url = mxcUrl,
iv = elementToDecrypt.iv,
hashes = mapOf("sha256" to elementToDecrypt.sha256),
key = EncryptedFileKey(
k = elementToDecrypt.k,
alg = "A256CTR",
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
ext = true
),
v = "v2"
)
return if (publicServerKey != null) {
// We should encrypt
withOlmEncryption { olm ->
olm.setRecipientKey(publicServerKey)
val olmResult = olm.encrypt(DownloadBody(encryptedInfo).toCanonicalJson())
DownloadBody(
encryptedBody = EncryptedBody(
cipherText = olmResult.mCipherText,
ephemeral = olmResult.mEphemeralKey,
mac = olmResult.mMac
)
)
}
} else {
DownloadBody(encryptedInfo)
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.data
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
import org.matrix.android.sdk.api.util.Optional
internal interface ContentScannerStore {
fun getScannerUrl(): String?
fun setScannerUrl(url: String?)
fun enableScanner(enabled: Boolean)
fun isScanEnabled(): Boolean
fun getScanResult(mxcUrl: String): ScanStatusInfo?
fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>>
fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean
fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?)
fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String)
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.db
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
internal open class ContentScanResultEntity(
@Index
var mediaUrl: String? = null,
var scanStatusString: String? = null,
var humanReadableMessage: String? = null,
var scanDateTimestamp: Long? = null,
var scannerUrl: String? = null
) : RealmObject() {
var scanResult: ScanState
get() {
return scanStatusString
?.let {
tryOrNull { ScanState.valueOf(it) }
}
?: ScanState.UNKNOWN
}
set(result) {
scanStatusString = result.name
}
fun toModel(): ScanStatusInfo {
return ScanStatusInfo(
state = this.scanResult,
humanReadableMessage = humanReadableMessage,
scanDateTimestamp = scanDateTimestamp
)
}
companion object
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.db
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun ContentScanResultEntity.Companion.get(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity? {
return realm.where<ContentScanResultEntity>()
.equalTo(ContentScanResultEntityFields.MEDIA_URL, attachmentUrl)
.apply {
contentScannerUrl?.let {
equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
}
}
.findFirst()
}
internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity {
return ContentScanResultEntity.get(realm, attachmentUrl, contentScannerUrl)
?: realm.createObject<ContentScanResultEntity>().also {
it.mediaUrl = attachmentUrl
it.scanDateTimestamp = System.currentTimeMillis()
it.scannerUrl = contentScannerUrl
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.db
import io.realm.RealmObject
internal open class ContentScannerInfoEntity(
var serverUrl: String? = null,
var enabled: Boolean? = null
) : RealmObject() {
companion object
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.db
import io.realm.annotations.RealmModule
/**
* Realm module for content scanner classes
*/
@RealmModule(library = true,
classes = [
ContentScannerInfoEntity::class,
ContentScanResultEntity::class
])
internal class ContentScannerRealmModule

View file

@ -0,0 +1,143 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.db
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.di.ContentScannerDatabase
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
import org.matrix.android.sdk.internal.util.isValidUrl
import javax.inject.Inject
@SessionScope
internal class RealmContentScannerStore @Inject constructor(
@ContentScannerDatabase
private val realmConfiguration: RealmConfiguration
) : ContentScannerStore {
private val monarchy = Monarchy.Builder()
.setRealmConfiguration(realmConfiguration)
.build()
override fun getScannerUrl(): String? {
return monarchy.fetchAllMappedSync(
{ realm ->
realm.where<ContentScannerInfoEntity>()
}, {
it.serverUrl
}
).firstOrNull()
}
override fun setScannerUrl(url: String?) {
monarchy.runTransactionSync { realm ->
val info = realm.where<ContentScannerInfoEntity>().findFirst()
?: realm.createObject()
info.serverUrl = url
}
}
override fun enableScanner(enabled: Boolean) {
monarchy.runTransactionSync { realm ->
val info = realm.where<ContentScannerInfoEntity>().findFirst()
?: realm.createObject()
info.enabled = enabled
}
}
override fun isScanEnabled(): Boolean {
return monarchy.fetchAllMappedSync(
{ realm ->
realm.where<ContentScannerInfoEntity>()
}, {
it.enabled.orFalse() && it.serverUrl?.isValidUrl().orFalse()
}
).firstOrNull().orFalse()
}
override fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?) {
monarchy.runTransactionSync {
ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).scanResult = state
}
}
override fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String) {
monarchy.runTransactionSync {
ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).apply {
scanResult = state
scanDateTimestamp = System.currentTimeMillis()
humanReadableMessage = humanReadable
}
}
}
override fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean {
var isKnown = false
monarchy.runTransactionSync {
val info = ContentScanResultEntity.get(it, mxcUrl, scannerUrl)?.scanResult
isKnown = when (info) {
ScanState.IN_PROGRESS,
ScanState.TRUSTED,
ScanState.INFECTED -> true
else -> false
}
}
return isKnown
}
override fun getScanResult(mxcUrl: String): ScanStatusInfo? {
return monarchy.fetchAllMappedSync({ realm ->
realm.where<ContentScanResultEntity>()
.equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
.apply {
getScannerUrl()?.let {
equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
}
}
}, {
it.toModel()
})
.firstOrNull()
}
override fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<ContentScanResultEntity>()
.equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
.equalTo(ContentScanResultEntityFields.SCANNER_URL, getScannerUrl())
},
{ entity ->
entity.toModel()
}
)
return Transformations.map(liveData) {
it.firstOrNull().toOptional()
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
@JsonClass(generateAdapter = true)
internal data class DownloadBody(
@Json(name = "file") val file: EncryptedFileInfo? = null,
@Json(name = "encrypted_body") val encryptedBody: EncryptedBody? = null
)
@JsonClass(generateAdapter = true)
internal data class EncryptedBody(
@Json(name = "ciphertext") val cipherText: String,
@Json(name = "mac") val mac: String,
@Json(name = "ephemeral") val ephemeral: String
)
internal fun DownloadBody.toJson(): String = MoshiProvider.providesMoshi().adapter(DownloadBody::class.java).toJson(this)
internal fun DownloadBody.toCanonicalJson() = JsonCanonicalizer.getCanonicalJson(DownloadBody::class.java, this)

View file

@ -0,0 +1,33 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* {
* "clean": true,
* "info": "File clean at 6/7/2018, 6:02:40 PM"
* }
*/
@JsonClass(generateAdapter = true)
internal data class ScanResponse(
@Json(name = "clean") val clean: Boolean,
/** Human-readable information about the result. */
@Json(name = "info") val info: String?
)

View file

@ -0,0 +1,26 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class ServerPublicKeyResponse(
@Json(name = "public_key")
val publicKey: String?
)

View file

@ -0,0 +1,51 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.tasks
import okhttp3.ResponseBody
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface DownloadEncryptedTask : Task<DownloadEncryptedTask.Params, ResponseBody> {
data class Params(
val publicServerKey: String?,
val encryptedInfo: ElementToDecrypt,
val mxcUrl: String
)
}
internal class DefaultDownloadEncryptedTask @Inject constructor(
private val contentScannerApiProvider: ContentScannerApiProvider
) : DownloadEncryptedTask {
override suspend fun execute(params: DownloadEncryptedTask.Params): ResponseBody {
val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(
params.publicServerKey,
params.mxcUrl,
params.encryptedInfo
)
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
return executeRequest(null) {
api.downloadEncrypted(dlBody)
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.tasks
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApi
import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface GetServerPublicKeyTask : Task<GetServerPublicKeyTask.Params, String?> {
data class Params(
val contentScannerApi: ContentScannerApi
)
}
internal class DefaultGetServerPublicKeyTask @Inject constructor() : GetServerPublicKeyTask {
override suspend fun execute(params: GetServerPublicKeyTask.Params): String? {
return executeRequest<ServerPublicKeyResponse>(null) {
params.contentScannerApi.getServerPublicKey()
}.publicKey
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.tasks
import org.matrix.android.sdk.api.failure.toScanFailure
import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface ScanEncryptedTask : Task<ScanEncryptedTask.Params, ScanResponse> {
data class Params(
val mxcUrl: String,
val publicServerKey: String?,
val encryptedInfo: ElementToDecrypt
)
}
internal class DefaultScanEncryptedTask @Inject constructor(
private val contentScannerApiProvider: ContentScannerApiProvider,
private val contentScannerStore: ContentScannerStore
) : ScanEncryptedTask {
override suspend fun execute(params: ScanEncryptedTask.Params): ScanResponse {
val mxcUrl = params.mxcUrl
val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(params.publicServerKey, params.mxcUrl, params.encryptedInfo)
val scannerUrl = contentScannerStore.getScannerUrl()
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
try {
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
val executeRequest = executeRequest<ScanResponse>(null) {
api.scanFile(dlBody)
}
contentScannerStore.updateScanResultForContent(
mxcUrl,
scannerUrl,
ScanState.TRUSTED.takeIf { executeRequest.clean } ?: ScanState.INFECTED,
executeRequest.info ?: ""
)
return executeRequest
} catch (failure: Throwable) {
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
throw failure.toScanFailure() ?: failure
}
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.contentscanner.tasks
import org.matrix.android.sdk.api.failure.toScanFailure
import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface ScanMediaTask : Task<ScanMediaTask.Params, ScanResponse> {
data class Params(
val mxcUrl: String
)
}
internal class DefaultScanMediaTask @Inject constructor(
private val contentScannerApiProvider: ContentScannerApiProvider,
private val contentScannerStore: ContentScannerStore
) : ScanMediaTask {
override suspend fun execute(params: ScanMediaTask.Params): ScanResponse {
// "mxc://server.org/QNDpzLopkoQYNikJfoZCQuCXJ"
if (!params.mxcUrl.startsWith("mxc://")) {
throw IllegalAccessException("Invalid mxc url")
}
val scannerUrl = contentScannerStore.getScannerUrl()
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
var serverAndMediaId = params.mxcUrl.removePrefix("mxc://")
val fragmentOffset = serverAndMediaId.indexOf("#")
if (fragmentOffset >= 0) {
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
}
val split = serverAndMediaId.split("/")
if (split.size != 2) {
throw IllegalAccessException("Invalid mxc url")
}
try {
val scanResponse = executeRequest<ScanResponse>(null) {
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
api.scanMedia(split[0], split[1])
}
contentScannerStore.updateScanResultForContent(
params.mxcUrl,
scannerUrl,
ScanState.TRUSTED.takeIf { scanResponse.clean } ?: ScanState.INFECTED,
scanResponse.info ?: ""
)
return scanResponse
} catch (failure: Throwable) {
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
throw failure.toScanFailure() ?: failure
}
}
}

View file

@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===107
enum class===108
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3