From 1bcf84c509c026fb6b3746a848835dfbbf609157 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 7 Nov 2024 10:03:39 +0100 Subject: [PATCH 01/23] feat(AppIntegrity): Create Module and repository --- Core2/AppIntegrity/.gitignore | 1 + Core2/AppIntegrity/build.gradle.kts | 45 ++++++++ Core2/AppIntegrity/consumer-rules.pro | 0 Core2/AppIntegrity/proguard-rules.pro | 21 ++++ .../appintegrity/ExampleInstrumentedTest.kt | 24 ++++ .../AppIntegrity/src/main/AndroidManifest.xml | 4 + .../appintegrity/ApiClientProvider.kt | 106 ++++++++++++++++++ .../com/infomaniak/appintegrity/ApiRoutes.kt | 21 ++++ .../appintegrity/AppIntegrityManager.kt | 82 ++++++++++++++ .../appintegrity/AppIntegrityRepository.kt | 60 ++++++++++ .../appintegrity/exceptions/ApiException.kt | 29 +++++ .../exceptions/ContainerErrorsException.kt | 90 +++++++++++++++ .../exceptions/EmailValidationException.kt | 55 +++++++++ .../exceptions/NetworkException.kt | 25 +++++ .../UnexpectedApiErrorFormatException.kt | 29 +++++ .../exceptions/UnknownException.kt | 35 ++++++ .../appintegrity/models/ApiError.kt | 26 +++++ .../appintegrity/models/ApiResponse.kt | 26 +++++ .../appintegrity/models/ApiResponseStatus.kt | 37 ++++++ .../appintegrity/ExampleUnitTest.kt | 17 +++ Core2/gradle/core2.versions.toml | 10 ++ settings.gradle.kts | 1 + 22 files changed, 744 insertions(+) create mode 100644 Core2/AppIntegrity/.gitignore create mode 100644 Core2/AppIntegrity/build.gradle.kts create mode 100644 Core2/AppIntegrity/consumer-rules.pro create mode 100644 Core2/AppIntegrity/proguard-rules.pro create mode 100644 Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt create mode 100644 Core2/AppIntegrity/src/main/AndroidManifest.xml create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt create mode 100644 Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt diff --git a/Core2/AppIntegrity/.gitignore b/Core2/AppIntegrity/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/Core2/AppIntegrity/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Core2/AppIntegrity/build.gradle.kts b/Core2/AppIntegrity/build.gradle.kts new file mode 100644 index 000000000..a0dab1074 --- /dev/null +++ b/Core2/AppIntegrity/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + kotlin("plugin.serialization") version libs.versions.kotlin +} + +android { + namespace = "com.infomaniak.appintegrity" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + // implementation(libs.androidx.core.ktx) + implementation(core2.integrity) + implementation(core2.ktor.client.core) + implementation(core2.ktor.client.content.negociation) + implementation(core2.ktor.client.json) + implementation(core2.ktor.client.encoding) + implementation(core2.ktor.client.okhttp) + api(core2.kotlinx.serialization.json) + // testImplementation(core2.junit) + // androidTestImplementation(core2.androidx.junit) +} diff --git a/Core2/AppIntegrity/consumer-rules.pro b/Core2/AppIntegrity/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/Core2/AppIntegrity/proguard-rules.pro b/Core2/AppIntegrity/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/Core2/AppIntegrity/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt b/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..ea1c86194 --- /dev/null +++ b/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.infomaniak.appintegrity + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.infomaniak.appintegrity.test", appContext.packageName) + } +} diff --git a/Core2/AppIntegrity/src/main/AndroidManifest.xml b/Core2/AppIntegrity/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/Core2/AppIntegrity/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt new file mode 100644 index 000000000..8cad6ba84 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt @@ -0,0 +1,106 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity + +import com.infomaniak.appintegrity.exceptions.ApiException +import com.infomaniak.appintegrity.exceptions.UnexpectedApiErrorFormatException +import com.infomaniak.appintegrity.exceptions.UnknownException +import com.infomaniak.appintegrity.models.ApiError +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.HttpClientEngineFactory +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.HttpResponseValidator +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.errors.IOException +import kotlinx.serialization.json.Json + +class ApiClientProvider internal constructor(engine: HttpClientEngineFactory<*>? = null) { + + constructor() : this(null) + + val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + isLenient = true + useAlternativeNames = false + } + + val httpClient = createHttpClient(engine) + + fun createHttpClient(engine: HttpClientEngineFactory<*>?): HttpClient { + val block: HttpClientConfig<*>.() -> Unit = { + expectSuccess = true + install(ContentNegotiation) { + json(this@ApiClientProvider.json) + } + install(ContentEncoding) { + gzip() + } + install(HttpTimeout) { + requestTimeoutMillis = REQUEST_TIMEOUT + connectTimeoutMillis = REQUEST_TIMEOUT + socketTimeoutMillis = REQUEST_TIMEOUT + } + install(HttpRequestRetry) { + retryOnExceptionIf(maxRetries = MAX_RETRY) { _, cause -> + cause.isNetworkException() + } + delayMillis { retry -> + retry * 500L + } + } + HttpResponseValidator { + validateResponse { response: HttpResponse -> + val statusCode = response.status.value + if (statusCode >= 300) { + val bodyResponse = response.bodyAsText() + runCatching { + val apiError = json.decodeFromString(bodyResponse) + throw ApiException(apiError.errorCode, apiError.message) + }.onFailure { + throw UnexpectedApiErrorFormatException(statusCode, bodyResponse) + } + } + } + handleResponseExceptionWithRequest { cause, _ -> + when (cause) { + is IOException -> throw com.infomaniak.appintegrity.exceptions.NetworkException("Network error: ${cause.message}") + is ApiException, is UnexpectedApiErrorFormatException -> throw cause + else -> throw UnknownException(cause) + } + } + } + } + + return if (engine != null) HttpClient(engine, block) else HttpClient(block) + } + + private fun Throwable.isNetworkException() = this is IOException + + companion object { + private const val REQUEST_TIMEOUT = 10_000L + private const val MAX_RETRY = 3 + const val REQUEST_LONG_TIMEOUT = 60_000L + } +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt new file mode 100644 index 000000000..0f797795e --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt @@ -0,0 +1,21 @@ +/* + * Infomaniak SwissTransfer - Android + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity + +class ApiRoutes { +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt new file mode 100644 index 000000000..5caa16598 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt @@ -0,0 +1,82 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity + +import android.content.Context +import android.util.Log +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.StandardIntegrityManager.* +import com.infomaniak.appintegrity.exceptions.ApiException + +class AppIntegrityManager(private val packageName: String) { + + private var appIntegrityTokenProvider: StandardIntegrityTokenProvider? = null + + fun warmUpTokenProvider(appContext: Context, appCloudNumber: Long, onFailure: (Exception) -> Unit) { + val integrityManager = IntegrityManagerFactory.createStandard(appContext) + integrityManager.prepareIntegrityToken( + PrepareIntegrityTokenRequest.builder().setCloudProjectNumber(appCloudNumber).build() + ).addOnSuccessListener { tokenProvider -> + appIntegrityTokenProvider = tokenProvider + Log.e("TOTO", "warmUpTokenProvider: Success") + }.addOnFailureListener(onFailure) + } + + fun requestIntegrityVerdictToken( + requestHash: String, + onSuccess: (String) -> Unit, + onFailure: (Exception?) -> Unit, + onNullTokenProvider: (String) -> Unit, + ) { + if (appIntegrityTokenProvider == null) { + onNullTokenProvider("Integrity token provider is null during a verdict request. This should not be possible") + } else { + appIntegrityTokenProvider?.request(StandardIntegrityTokenRequest.builder().setRequestHash(requestHash).build()) + ?.addOnSuccessListener { response -> onSuccess(response.token()) } + ?.addOnFailureListener(onFailure) + } + } + + suspend fun requestApiJwtToken(integrityToken: String, targetUrl: String): String? = runCatching { + AppIntegrityRepository.getJwtToken(integrityToken, packageName, targetUrl).data + }.getOrElse { exception -> + if (exception is ApiException) { + when (exception.message) { + "invalid_attestation" -> "Integrity is invalid" + else -> "unknown ApiError" + } + } else { + null + } + } + + suspend fun callDemoRoute(mobileToken: String): String? = runCatching { + AppIntegrityRepository.demo(mobileToken).data + }.getOrElse { exception -> + if (exception is ApiException) { + when (exception.message) { + "already_used_token" -> "The JWT token has been already used" + "expired_token" -> "The JWT token has expired" + "invalid_mobile_token" -> "Mobile token is missing or invalid" + else -> "unknown ApiError" + } + } else { + null + } + } +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt new file mode 100644 index 000000000..4c8a30901 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt @@ -0,0 +1,60 @@ +/* + * Infomaniak SwissTransfer - Android + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity + +import com.infomaniak.appintegrity.exceptions.UnknownException +import com.infomaniak.appintegrity.models.ApiResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.Url +import io.ktor.http.contentType + +object AppIntegrityRepository { + + private val apiClientProvider by lazy { ApiClientProvider() } + + suspend fun getJwtToken(integrityToken: String, packageName: String, targetUrl: String): ApiResponse { + val body = mapOf("token" to integrityToken, "package_name" to packageName, "target_url" to targetUrl) + return post>(url = Url("http://api-core.devd471.dev.infomaniak.ch/1/attest/integrity"), data = body) + } + + suspend fun demo(mobileToken: String): ApiResponse { + val body = mapOf("mobile_token" to mobileToken) + return post>(url = Url("http://api-core.devd471.dev.infomaniak.ch/1/attest/demo"), data = body) + } + + private suspend inline fun post( + url: Url, + data: Any?, + httpClient: HttpClient = apiClientProvider.httpClient + ): R { + return httpClient.post(url) { + contentType(ContentType.Application.Json) + setBody(data) + }.decode() + } + + + private suspend inline fun HttpResponse.decode(): R { + return runCatching { body() }.getOrElse { throw UnknownException(it) } + } +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt new file mode 100644 index 000000000..e5ba4fd1f --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt @@ -0,0 +1,29 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.exceptions + +/** + * Thrown when an API call fails due to an error identified by a specific error code. + * + * This exception is used to represent errors returned by an API, with an associated error code + * and message describing the problem. + * + * @param errorCode The specific error code returned by the API. + * @param errorMessage The detailed error message explaining the cause of the failure. + */ +open class ApiException(val errorCode: Int, errorMessage: String) : Exception(errorMessage) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt new file mode 100644 index 000000000..2dfa28131 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt @@ -0,0 +1,90 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.exceptions + +/** + * A sealed class representing various container error exceptions that extend [ApiException]. + * This class is used to handle API errors that occur when creating a container through the API. + * Each specific error exception is associated with an HTTP status code and a descriptive error message. + * + * @constructor Creates a [ContainerErrorsException] with the given status code and error message. + * + * @param statusCode The HTTP status code for the error. + * @param errorMessage A descriptive error message for the error. + */ +sealed class ContainerErrorsException(val statusCode: Int, errorMessage: String) : ApiException(statusCode, errorMessage) { + + /** + * Exception indicating that email address validation is required. + * This corresponds to an HTTP 401 Unauthorized status. + */ + class EmailValidationRequired : ContainerErrorsException(401, "Email address validation required") + + /** + * Exception indicating that the domain was automatically blocked for security reasons. + * This corresponds to an HTTP 403 Forbidden status. + */ + class DomainBlockedException : ContainerErrorsException(403, "The domain was automatically blocked for security reasons") + + /** + * Exception indicating that the daily transfer limit has been reached. + * This corresponds to an HTTP 404 Not Found status. + */ + class DailyTransferLimitReachedException : ContainerErrorsException(404, "Daily transfer limit reached") + + /** + * Exception indicating that the provided captcha is not valid. + * This corresponds to an HTTP 422 Unprocessable Entity status. + */ + class CaptchaNotValidException : ContainerErrorsException(422, "Captcha not valid") + + /** + * Exception indicating that too many codes have been generated. + * This corresponds to an HTTP 429 Too Many Requests status. + */ + class TooManyCodesGeneratedException : ContainerErrorsException(429, "Too many codes generated") + + internal companion object { + /** + * Extension function to convert an instance of [UnexpectedApiErrorFormatException] to a more specific exception + * based on its HTTP status code. + * + * This function maps the status codes to specific exceptions as follows: + * - 401: [EmailValidationRequired] + * - 403: [DomainBlockedException] + * - 404: [DailyTransferLimitReachedException] + * - 422: [CaptchaNotValidException] + * - 429: [TooManyCodesGeneratedException] + * - Other status codes: The original [UnexpectedApiErrorFormatException] instance + * + * @receiver An instance of [UnexpectedApiErrorFormatException]. + * @return An instance of [Exception] which can be one of the specific exceptions mentioned above, + * or the original [UnexpectedApiErrorFormatException] if the status code does not match any predefined values. + */ + fun UnexpectedApiErrorFormatException.toContainerErrorsException(): Exception { + return when (statusCode) { + 401 -> EmailValidationRequired() + 403 -> DomainBlockedException() + 404 -> DailyTransferLimitReachedException() + 422 -> CaptchaNotValidException() + 429 -> TooManyCodesGeneratedException() + else -> this + } + } + } +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt new file mode 100644 index 000000000..0f0e48d4d --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt @@ -0,0 +1,55 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.exceptions + +/** + * A sealed class representing exceptions related to email validation that extend [ApiException]. + * This class is used to handle specific errors that occur during the email validation process. + * + * @property statusCode The HTTP status code associated with the error. + * @constructor Creates an [EmailValidationException] with the given status code. + * + * @param statusCode The HTTP status code for the error. + */ +sealed class EmailValidationException(statusCode: Int) : ApiException(statusCode, "") { + + /** + * Exception indicating that the provided password is invalid during email validation. + * This corresponds to an HTTP 401 Unauthorized status. + */ + class InvalidPasswordException : EmailValidationException(401) + + internal companion object { + /** + * Extension function to convert an instance of [UnexpectedApiErrorFormatException] to a specific + * [EmailValidationException] based on its HTTP status code. + * + * This function maps the status codes to specific exceptions as follows: + * - 401: [InvalidPasswordException] + * - Other status codes: The original [UnexpectedApiErrorFormatException] instance + * + * @receiver An instance of [UnexpectedApiErrorFormatException]. + * @return An instance of [EmailValidationException] which can be [InvalidPasswordException] + * or the original [UnexpectedApiErrorFormatException] if the status code does not match any predefined values. + */ + fun UnexpectedApiErrorFormatException.toEmailValidationException() = when (statusCode) { + 401 -> InvalidPasswordException() + else -> this + } + } +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt new file mode 100644 index 000000000..074d5e68e --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt @@ -0,0 +1,25 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.exceptions + +/** + * Thrown when a network-related error occurs, such as connectivity issues or timeouts. + * + * @param message A detailed message describing the network error. + */ +class NetworkException(message: String) : Exception(message) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt new file mode 100644 index 000000000..bb87cdcd8 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt @@ -0,0 +1,29 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.exceptions + +/** + * Thrown when an API call returns an error in an unexpected format that cannot be parsed. + * + * This exception indicates that the API response format is different from what was expected, + * preventing proper parsing of the error details. + * + * @param statusCode The HTTP status code returned by the API. + * @param bodyResponse The raw response body from the API that could not be parsed. + */ +class UnexpectedApiErrorFormatException(val statusCode: Int, val bodyResponse: String) : Exception(bodyResponse) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt new file mode 100644 index 000000000..5e1be0979 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt @@ -0,0 +1,35 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.exceptions + +/** + * Represents an unknown exception that can occur during the execution of the application. + * + * This exception is used to encapsulate unexpected or unknown errors that are not covered + * by other specific exception types. + * + * @property message The detailed message describing the error. + * @property cause The underlying cause of this exception, if any. + * + * @constructor Creates an instance of `UnknownException` with a detailed error message and an optional cause. + * + * @param cause The underlying exception that caused this exception. + */ +class UnknownException(cause: Throwable) : Exception(cause) { + override val message: String = cause.message ?: cause.toString() +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt new file mode 100644 index 000000000..8fd91a299 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt @@ -0,0 +1,26 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiError( + val errorCode: Int, + val message: String, +) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt new file mode 100644 index 000000000..2898814d0 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt @@ -0,0 +1,26 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiResponse( + val result: ApiResponseStatus = ApiResponseStatus.UNKNOWN, + val data: T? = null, +) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt new file mode 100644 index 000000000..58633448d --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt @@ -0,0 +1,37 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.appintegrity.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class ApiResponseStatus { + + @SerialName("error") + ERROR, + + @SerialName("success") + SUCCESS, + + @SerialName("asynchronous") + ASYNCHRONOUS, + + @SerialName("unknown") + UNKNOWN; +} diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt new file mode 100644 index 000000000..96857cabe --- /dev/null +++ b/Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.infomaniak.appintegrity + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/Core2/gradle/core2.versions.toml b/Core2/gradle/core2.versions.toml index f3ce715f9..49156cb43 100644 --- a/Core2/gradle/core2.versions.toml +++ b/Core2/gradle/core2.versions.toml @@ -1,6 +1,9 @@ [versions] coreKtx = "1.15.0" composeBom = "2024.11.00" +integrity = "1.4.0" +kotlinxSerializationJson = "1.7.1" +ktor = "2.3.12" matomo = "4.1.4" sentry-android = "7.15.0" @@ -12,5 +15,12 @@ compose-material3 = { group = "androidx.compose.material3", name = "material3-an compose-runtime = { group = "androidx.compose.runtime", name = "runtime-android" } compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview-android" } +integrity = { module = "com.google.android.play:integrity", version.ref = "integrity" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +ktor-client-content-negociation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } +ktor-client-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } matomo = { module = "com.github.matomo-org:matomo-sdk-android", version.ref = "matomo" } sentry-android = { module = "io.sentry:sentry-android", version.ref = "sentry-android" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8db1d9ae5..2c58b37fb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,3 +36,4 @@ include(":Core2:Network") include(":Core2:Onboarding") include(":Core2:Compose:Core") include(":FileTypes") +include(":Core2:AppIntegrity") From d405c22881dd6a9b77197fd4cb00484f90c9f39a Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 7 Nov 2024 13:40:11 +0100 Subject: [PATCH 02/23] feat(AppIntegrity): POC to call our api --- .../appintegrity/ApiClientProvider.kt | 3 +- .../appintegrity/AppIntegrityManager.kt | 20 +++-- .../appintegrity/AppIntegrityRepository.kt | 5 +- .../appintegrity/exceptions/ApiException.kt | 2 +- .../exceptions/ContainerErrorsException.kt | 90 ------------------- .../exceptions/EmailValidationException.kt | 55 ------------ .../appintegrity/models/ApiError.kt | 4 +- app/build.gradle.kts | 1 + .../swisstransfer/ui/NewTransferActivity.kt | 15 +++- .../newtransfer/ImportFilesViewModel.kt | 35 +++++++- .../screen/newtransfer/NewTransferNavHost.kt | 4 +- .../screen/newtransfer/NewTransferScreen.kt | 8 +- .../importfiles/ImportFilesScreen.kt | 4 +- .../main/res/xml/network_security_config.xml | 23 +++++ 14 files changed, 106 insertions(+), 163 deletions(-) delete mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt delete mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt index 8cad6ba84..8a6ee8a08 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt @@ -18,6 +18,7 @@ package com.infomaniak.appintegrity import com.infomaniak.appintegrity.exceptions.ApiException +import com.infomaniak.appintegrity.exceptions.NetworkException import com.infomaniak.appintegrity.exceptions.UnexpectedApiErrorFormatException import com.infomaniak.appintegrity.exceptions.UnknownException import com.infomaniak.appintegrity.models.ApiError @@ -85,7 +86,7 @@ class ApiClientProvider internal constructor(engine: HttpClientEngineFactory<*>? } handleResponseExceptionWithRequest { cause, _ -> when (cause) { - is IOException -> throw com.infomaniak.appintegrity.exceptions.NetworkException("Network error: ${cause.message}") + is IOException -> throw NetworkException("Network error: ${cause.message}") is ApiException, is UnexpectedApiErrorFormatException -> throw cause else -> throw UnknownException(cause) } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt index 5caa16598..50b9714fe 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt @@ -22,6 +22,7 @@ import android.util.Log import com.google.android.play.core.integrity.IntegrityManagerFactory import com.google.android.play.core.integrity.StandardIntegrityManager.* import com.infomaniak.appintegrity.exceptions.ApiException +import kotlinx.coroutines.coroutineScope class AppIntegrityManager(private val packageName: String) { @@ -37,24 +38,29 @@ class AppIntegrityManager(private val packageName: String) { }.addOnFailureListener(onFailure) } - fun requestIntegrityVerdictToken( + suspend fun requestIntegrityVerdictToken( requestHash: String, onSuccess: (String) -> Unit, onFailure: (Exception?) -> Unit, onNullTokenProvider: (String) -> Unit, - ) { + ) = coroutineScope { if (appIntegrityTokenProvider == null) { onNullTokenProvider("Integrity token provider is null during a verdict request. This should not be possible") } else { + Log.e("TOTO", "requestIntegrityVerdictToken: b") appIntegrityTokenProvider?.request(StandardIntegrityTokenRequest.builder().setRequestHash(requestHash).build()) - ?.addOnSuccessListener { response -> onSuccess(response.token()) } + ?.addOnSuccessListener { response -> + onSuccess(response.token()) + } ?.addOnFailureListener(onFailure) } } suspend fun requestApiJwtToken(integrityToken: String, targetUrl: String): String? = runCatching { - AppIntegrityRepository.getJwtToken(integrityToken, packageName, targetUrl).data + Log.e("TOTO", "requestApiJwtToken: successful integrity call token = $integrityToken") + AppIntegrityRepository.getJwtToken(integrityToken, packageName, targetUrl).data?.let { callDemoRoute(it) } }.getOrElse { exception -> + exception.printStackTrace() if (exception is ApiException) { when (exception.message) { "invalid_attestation" -> "Integrity is invalid" @@ -65,8 +71,10 @@ class AppIntegrityManager(private val packageName: String) { } } - suspend fun callDemoRoute(mobileToken: String): String? = runCatching { - AppIntegrityRepository.demo(mobileToken).data + private suspend fun callDemoRoute(mobileToken: String): String? = runCatching { + val apiResponse = AppIntegrityRepository.demo(mobileToken) + Log.e("TOTO", "callDemoRoute: success ${apiResponse.data}") + apiResponse.data }.getOrElse { exception -> if (exception is ApiException) { when (exception.message) { diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt index 4c8a30901..d54f365b6 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt @@ -34,7 +34,10 @@ object AppIntegrityRepository { suspend fun getJwtToken(integrityToken: String, packageName: String, targetUrl: String): ApiResponse { val body = mapOf("token" to integrityToken, "package_name" to packageName, "target_url" to targetUrl) - return post>(url = Url("http://api-core.devd471.dev.infomaniak.ch/1/attest/integrity"), data = body) + return post>( + url = Url("https://api-core.devd471.dev.infomaniak.ch:443/1/attest/integrity"), + data = body + ) } suspend fun demo(mobileToken: String): ApiResponse { diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt index e5ba4fd1f..23407fd02 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt @@ -26,4 +26,4 @@ package com.infomaniak.appintegrity.exceptions * @param errorCode The specific error code returned by the API. * @param errorMessage The detailed error message explaining the cause of the failure. */ -open class ApiException(val errorCode: Int, errorMessage: String) : Exception(errorMessage) +open class ApiException(val errorCode: String, errorMessage: String) : Exception(errorMessage) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt deleted file mode 100644 index 2dfa28131..000000000 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ContainerErrorsException.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Infomaniak SwissTransfer - Multiplatform - * Copyright (C) 2024 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.appintegrity.exceptions - -/** - * A sealed class representing various container error exceptions that extend [ApiException]. - * This class is used to handle API errors that occur when creating a container through the API. - * Each specific error exception is associated with an HTTP status code and a descriptive error message. - * - * @constructor Creates a [ContainerErrorsException] with the given status code and error message. - * - * @param statusCode The HTTP status code for the error. - * @param errorMessage A descriptive error message for the error. - */ -sealed class ContainerErrorsException(val statusCode: Int, errorMessage: String) : ApiException(statusCode, errorMessage) { - - /** - * Exception indicating that email address validation is required. - * This corresponds to an HTTP 401 Unauthorized status. - */ - class EmailValidationRequired : ContainerErrorsException(401, "Email address validation required") - - /** - * Exception indicating that the domain was automatically blocked for security reasons. - * This corresponds to an HTTP 403 Forbidden status. - */ - class DomainBlockedException : ContainerErrorsException(403, "The domain was automatically blocked for security reasons") - - /** - * Exception indicating that the daily transfer limit has been reached. - * This corresponds to an HTTP 404 Not Found status. - */ - class DailyTransferLimitReachedException : ContainerErrorsException(404, "Daily transfer limit reached") - - /** - * Exception indicating that the provided captcha is not valid. - * This corresponds to an HTTP 422 Unprocessable Entity status. - */ - class CaptchaNotValidException : ContainerErrorsException(422, "Captcha not valid") - - /** - * Exception indicating that too many codes have been generated. - * This corresponds to an HTTP 429 Too Many Requests status. - */ - class TooManyCodesGeneratedException : ContainerErrorsException(429, "Too many codes generated") - - internal companion object { - /** - * Extension function to convert an instance of [UnexpectedApiErrorFormatException] to a more specific exception - * based on its HTTP status code. - * - * This function maps the status codes to specific exceptions as follows: - * - 401: [EmailValidationRequired] - * - 403: [DomainBlockedException] - * - 404: [DailyTransferLimitReachedException] - * - 422: [CaptchaNotValidException] - * - 429: [TooManyCodesGeneratedException] - * - Other status codes: The original [UnexpectedApiErrorFormatException] instance - * - * @receiver An instance of [UnexpectedApiErrorFormatException]. - * @return An instance of [Exception] which can be one of the specific exceptions mentioned above, - * or the original [UnexpectedApiErrorFormatException] if the status code does not match any predefined values. - */ - fun UnexpectedApiErrorFormatException.toContainerErrorsException(): Exception { - return when (statusCode) { - 401 -> EmailValidationRequired() - 403 -> DomainBlockedException() - 404 -> DailyTransferLimitReachedException() - 422 -> CaptchaNotValidException() - 429 -> TooManyCodesGeneratedException() - else -> this - } - } - } -} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt deleted file mode 100644 index 0f0e48d4d..000000000 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/EmailValidationException.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Infomaniak SwissTransfer - Multiplatform - * Copyright (C) 2024 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.appintegrity.exceptions - -/** - * A sealed class representing exceptions related to email validation that extend [ApiException]. - * This class is used to handle specific errors that occur during the email validation process. - * - * @property statusCode The HTTP status code associated with the error. - * @constructor Creates an [EmailValidationException] with the given status code. - * - * @param statusCode The HTTP status code for the error. - */ -sealed class EmailValidationException(statusCode: Int) : ApiException(statusCode, "") { - - /** - * Exception indicating that the provided password is invalid during email validation. - * This corresponds to an HTTP 401 Unauthorized status. - */ - class InvalidPasswordException : EmailValidationException(401) - - internal companion object { - /** - * Extension function to convert an instance of [UnexpectedApiErrorFormatException] to a specific - * [EmailValidationException] based on its HTTP status code. - * - * This function maps the status codes to specific exceptions as follows: - * - 401: [InvalidPasswordException] - * - Other status codes: The original [UnexpectedApiErrorFormatException] instance - * - * @receiver An instance of [UnexpectedApiErrorFormatException]. - * @return An instance of [EmailValidationException] which can be [InvalidPasswordException] - * or the original [UnexpectedApiErrorFormatException] if the status code does not match any predefined values. - */ - fun UnexpectedApiErrorFormatException.toEmailValidationException() = when (statusCode) { - 401 -> InvalidPasswordException() - else -> this - } - } -} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt index 8fd91a299..af4310084 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt @@ -21,6 +21,6 @@ import kotlinx.serialization.Serializable @Serializable data class ApiError( - val errorCode: Int, - val message: String, + val errorCode: String = "", + val message: String = "", ) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6aeaef25a..b3913ad88 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { implementation(project(":Core2:Sentry")) implementation(project(":Core2:Matomo")) implementation(project(":Core2:Network")) + implementation(project(":Core2:AppIntegrity")) implementation(project(":Core2:Onboarding")) implementation(project(":Core2:Compose:Core")) implementation(project(":FileTypes")) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index 7621d7333..156c358e8 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -21,19 +21,32 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.swisstransfer.BuildConfig import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferScreen import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import dagger.hilt.android.AndroidEntryPoint +import io.sentry.Sentry +import io.sentry.SentryLevel @AndroidEntryPoint class NewTransferActivity : ComponentActivity() { + private val appIntegrityManager by lazy { AppIntegrityManager(BuildConfig.APPLICATION_ID) } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { SwissTransferTheme { - NewTransferScreen(closeActivity = { finish() }) + NewTransferScreen(closeActivity = { finish() }, appIntegrityManager) + } + } + appIntegrityManager.warmUpTokenProvider(applicationContext, appCloudNumber = 364109398419) { exception -> + exception.printStackTrace() + Sentry.captureMessage("Exception during AppIntegrityManager's warmup", SentryLevel.ERROR) { scope -> + scope.setTag("exception", exception.message.toString()) + scope.setExtra("stacktrace", exception.printStackTrace().toString()) } } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 1875d0c79..7897a1ffb 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -18,6 +18,8 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import android.net.Uri +import android.util.Log +import androidx.compose.runtime.* import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,6 +27,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.infomaniak.appintegrity.AppIntegrityManager import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.RemoteUploadFile import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.UploadFileSession import com.infomaniak.multiplatform_swisstransfer.common.utils.mapToList @@ -48,6 +51,8 @@ import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks import com.infomaniak.swisstransfer.workers.UploadWorker import dagger.hilt.android.lifecycle.HiltViewModel +import io.sentry.Sentry +import io.sentry.SentryLevel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* @@ -128,10 +133,38 @@ class ImportFilesViewModel @Inject constructor( } } - fun sendTransfer() { + fun sendTransfer(appIntegrityManager: AppIntegrityManager) { _sendActionResult.update { SendActionResult.Pending } viewModelScope.launch(ioDispatcher) { runCatching { + appIntegrityManager.requestIntegrityVerdictToken( + requestHash = "", + onSuccess = { token -> + viewModelScope.launch(ioDispatcher) { + Log.e("TOTO", "requestIntegrityVerdictToken: m + Sentry.captureMessage( + "Error when requiring an integrity token during account creation", + SentryLevel.ERROR, + ) { scope -> + scope.setTag("exception", exception?.message.toString()) + scope.setExtra("stacktrace", exception?.printStackTrace().toString()) + } + Log.e("TOTO", "sendTransfer: failed ${exception?.message}") + }, + onNullTokenProvider = { message -> + Sentry.captureMessage(message, SentryLevel.ERROR) + // TODO: Better error ? + Log.e("TOTO", "sendTransfer: nullTokenprovider : $message") + }, + ) val uuid = uploadManager.createAndGetUpload(generateNewUploadSession()).uuid uploadManager.initUploadSession(recaptcha = "Recaptcha")!! // TODO: Handle ContainerErrorsException here uploadWorkerScheduler.scheduleWork(uuid) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt index 6c04aad2e..c38cc6a3f 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt @@ -22,6 +22,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute +import com.infomaniak.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation.* import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.ImportFilesScreen @@ -31,7 +32,7 @@ import com.infomaniak.swisstransfer.ui.screen.newtransfer.upload.UploadProgressS import com.infomaniak.swisstransfer.ui.screen.newtransfer.upload.UploadSuccessScreen @Composable -fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Unit) { +fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Unit, appIntegrityManager: AppIntegrityManager) { NavHost(navController, NewTransferNavigation.startDestination) { composable { @@ -40,6 +41,7 @@ fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Un navigateToUploadProgress = { transferType, totalSize -> navController.navigate(UploadProgressDestination(transferType, totalSize)) }, + appIntegrityManager = appIntegrityManager ) } composable { diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt index e12920249..bd08b63af 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt @@ -19,19 +19,21 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import androidx.compose.runtime.Composable import androidx.navigation.compose.rememberNavController +import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.swisstransfer.BuildConfig import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewAllWindows @Composable -fun NewTransferScreen(closeActivity: () -> Unit) { +fun NewTransferScreen(closeActivity: () -> Unit, appIntegrityManager: AppIntegrityManager) { val navController = rememberNavController() - NewTransferNavHost(navController, closeActivity) + NewTransferNavHost(navController, closeActivity, appIntegrityManager) } @PreviewAllWindows @Composable private fun NewTransferPreview() { SwissTransferTheme { - NewTransferScreen {} + NewTransferScreen({}, AppIntegrityManager(BuildConfig.APPLICATION_ID)) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 0001d1354..1ec441307 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.infomaniak.appintegrity.AppIntegrityManager import com.infomaniak.core2.isEmail import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.FileUi import com.infomaniak.swisstransfer.R @@ -61,6 +62,7 @@ fun ImportFilesScreen( importFilesViewModel: ImportFilesViewModel = hiltViewModel(), closeActivity: () -> Unit, navigateToUploadProgress: (transferType: TransferTypeUi, totalSize: Long) -> Unit, + appIntegrityManager: AppIntegrityManager, ) { val files by importFilesViewModel.importedFilesDebounced.collectAsStateWithLifecycle() val filesToImportCount by importFilesViewModel.filesToImportCount.collectAsStateWithLifecycle() @@ -134,8 +136,8 @@ fun ImportFilesScreen( transferOptionsCallbacks = transferOptionsCallbacks, addFiles = importFilesViewModel::importFiles, closeActivity = closeActivity, + sendTransfer = { importFilesViewModel.sendTransfer(appIntegrityManager) }, shouldStartByPromptingUserForFiles = true, - sendTransfer = importFilesViewModel::sendTransfer, isTransferStarted = { sendActionResult != SendActionResult.NotStarted }, snackbarHostState = snackbarHostState, ) diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..14f5da1b8 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,23 @@ + + + + + From 5fc2c5055b659de5a9fae7db1cb6ffde5f239f2b Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 14 Nov 2024 11:42:49 +0100 Subject: [PATCH 03/23] feat(AppIntegrity): Add AppIntegrityRoutes and update Manager with classic request --- .../appintegrity/AppIntegrityManager.kt | 107 ++++++++++++------ .../appintegrity/AppIntegrityRepository.kt | 31 +++-- .../{ApiRoutes.kt => AppIntegrityRoutes.kt} | 10 +- .../appintegrity/models/ApiResponse.kt | 5 +- .../swisstransfer/ui/NewTransferActivity.kt | 5 +- .../newtransfer/ImportFilesViewModel.kt | 11 +- .../screen/newtransfer/NewTransferScreen.kt | 4 +- 7 files changed, 114 insertions(+), 59 deletions(-) rename Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/{ApiRoutes.kt => AppIntegrityRoutes.kt} (65%) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt index 50b9714fe..92629fec6 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt @@ -1,5 +1,5 @@ /* - * Infomaniak Core - Android + * Infomaniak Core2 - Android * Copyright (C) 2024 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify @@ -18,73 +18,106 @@ package com.infomaniak.appintegrity import android.content.Context +import android.util.Base64 import android.util.Log import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.IntegrityTokenRequest import com.google.android.play.core.integrity.StandardIntegrityManager.* -import com.infomaniak.appintegrity.exceptions.ApiException -import kotlinx.coroutines.coroutineScope +import java.util.UUID -class AppIntegrityManager(private val packageName: String) { +class AppIntegrityManager(private val appContext: Context) { private var appIntegrityTokenProvider: StandardIntegrityTokenProvider? = null + private val classicIntegrityTokenProvider by lazy { IntegrityManagerFactory.create(appContext) } - fun warmUpTokenProvider(appContext: Context, appCloudNumber: Long, onFailure: (Exception) -> Unit) { + private var challenge = "" + private var challengeId = "" + + fun warmUpTokenProvider(appCloudNumber: Long, onFailure: (Exception) -> Unit) { val integrityManager = IntegrityManagerFactory.createStandard(appContext) integrityManager.prepareIntegrityToken( PrepareIntegrityTokenRequest.builder().setCloudProjectNumber(appCloudNumber).build() ).addOnSuccessListener { tokenProvider -> appIntegrityTokenProvider = tokenProvider - Log.e("TOTO", "warmUpTokenProvider: Success") + Log.i(APP_INTEGRITY_MANAGER_TAG, "warmUpTokenProvider: Success") }.addOnFailureListener(onFailure) } - suspend fun requestIntegrityVerdictToken( + fun requestIntegrityVerdictToken( requestHash: String, onSuccess: (String) -> Unit, onFailure: (Exception?) -> Unit, onNullTokenProvider: (String) -> Unit, - ) = coroutineScope { + ) { if (appIntegrityTokenProvider == null) { onNullTokenProvider("Integrity token provider is null during a verdict request. This should not be possible") } else { - Log.e("TOTO", "requestIntegrityVerdictToken: b") appIntegrityTokenProvider?.request(StandardIntegrityTokenRequest.builder().setRequestHash(requestHash).build()) - ?.addOnSuccessListener { response -> - onSuccess(response.token()) - } + ?.addOnSuccessListener { response -> onSuccess(response.token()) } ?.addOnFailureListener(onFailure) } } - suspend fun requestApiJwtToken(integrityToken: String, targetUrl: String): String? = runCatching { - Log.e("TOTO", "requestApiJwtToken: successful integrity call token = $integrityToken") - AppIntegrityRepository.getJwtToken(integrityToken, packageName, targetUrl).data?.let { callDemoRoute(it) } - }.getOrElse { exception -> - exception.printStackTrace() - if (exception is ApiException) { - when (exception.message) { - "invalid_attestation" -> "Integrity is invalid" - else -> "unknown ApiError" - } - } else { - null + fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: (Exception?) -> Unit) { + + val nonce = Base64.encodeToString(challenge.toByteArray(), Base64.DEFAULT) + + classicIntegrityTokenProvider.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build()) + ?.addOnSuccessListener { response -> onSuccess(response.token()) } + ?.addOnFailureListener(onFailure) + } + + suspend fun getApiIntegrityVerdict( + integrityToken: String, + packageName: String, + targetUrl: String, + onSuccess: (String) -> Unit, + onFailure: (Throwable) -> Unit, + ) { + runCatching { + val apiResponse = AppIntegrityRepository.getJwtToken( + integrityToken = integrityToken, + packageName = packageName, + targetUrl = targetUrl, + challengeId = challengeId, + ) + apiResponse.data?.let(onSuccess) + }.getOrElse { + it.printStackTrace() + onFailure(it) } } - private suspend fun callDemoRoute(mobileToken: String): String? = runCatching { - val apiResponse = AppIntegrityRepository.demo(mobileToken) - Log.e("TOTO", "callDemoRoute: success ${apiResponse.data}") - apiResponse.data - }.getOrElse { exception -> - if (exception is ApiException) { - when (exception.message) { - "already_used_token" -> "The JWT token has been already used" - "expired_token" -> "The JWT token has expired" - "invalid_mobile_token" -> "Mobile token is missing or invalid" - else -> "unknown ApiError" + suspend fun getChallenge(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) = runCatching { + generateChallengeId() + val apiResponse = AppIntegrityRepository.getChallenge(challengeId) + Log.d(APP_INTEGRITY_MANAGER_TAG, "challengeId : $challengeId / challenge: ${apiResponse.data}") + apiResponse.data?.let { challenge = it } + onSuccess() + }.getOrElse { + it.printStackTrace() + onFailure(it) + } + + suspend fun callDemoRoute(mobileToken: String) { + runCatching { + val apiResponse = AppIntegrityRepository.demo(mobileToken) + val logMessage = if (apiResponse.isSuccess()) { + "Success demo route response: ${apiResponse.data}" + } else { + "Error demo route : ${apiResponse.error?.errorCode}" } - } else { - null + Log.d(APP_INTEGRITY_MANAGER_TAG, logMessage) + }.getOrElse { + it.printStackTrace() } } + + private fun generateChallengeId() { + challengeId = UUID.randomUUID().toString() + } + + companion object { + const val APP_INTEGRITY_MANAGER_TAG = "App integrity manager" + } } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt index d54f365b6..b7ab7df07 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt @@ -32,24 +32,33 @@ object AppIntegrityRepository { private val apiClientProvider by lazy { ApiClientProvider() } - suspend fun getJwtToken(integrityToken: String, packageName: String, targetUrl: String): ApiResponse { - val body = mapOf("token" to integrityToken, "package_name" to packageName, "target_url" to targetUrl) - return post>( - url = Url("https://api-core.devd471.dev.infomaniak.ch:443/1/attest/integrity"), - data = body + suspend fun getChallenge(challengeId: String): ApiResponse { + val body = mapOf("challenge_id" to challengeId) + return post>(url = Url(AppIntegrityRoutes.requestChallenge), data = body) + } + + suspend fun getJwtToken( + integrityToken: String, + packageName: String, + targetUrl: String, + challengeId: String, + ): ApiResponse { + val body = mapOf( + "token" to integrityToken, + "package_name" to packageName, + "target_url" to targetUrl, + "challenge_id" to challengeId, ) + + return post>(url = Url(AppIntegrityRoutes.requestApiIntegrityCheck), data = body) } suspend fun demo(mobileToken: String): ApiResponse { val body = mapOf("mobile_token" to mobileToken) - return post>(url = Url("http://api-core.devd471.dev.infomaniak.ch/1/attest/demo"), data = body) + return post>(url = Url(AppIntegrityRoutes.demo), data = body) } - private suspend inline fun post( - url: Url, - data: Any?, - httpClient: HttpClient = apiClientProvider.httpClient - ): R { + private suspend inline fun post(url: Url, data: Any?, httpClient: HttpClient = apiClientProvider.httpClient): R { return httpClient.post(url) { contentType(ContentType.Application.Json) setBody(data) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRoutes.kt similarity index 65% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRoutes.kt index 0f797795e..5777f8546 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiRoutes.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRoutes.kt @@ -17,5 +17,13 @@ */ package com.infomaniak.appintegrity -class ApiRoutes { +internal object AppIntegrityRoutes { + + private const val PROD_URL = "https://" // TODO + private const val PREPROD_BASE_URL = "https://api-core.devd471.dev.infomaniak.ch/" + private const val BASE_URL_V1 = "$PREPROD_BASE_URL/1/attest" + + const val requestChallenge = "${BASE_URL_V1}/challenge" + const val requestApiIntegrityCheck = "${BASE_URL_V1}/integrity" + const val demo = "${BASE_URL_V1}/demo" } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt index 2898814d0..ef3733138 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt @@ -23,4 +23,7 @@ import kotlinx.serialization.Serializable data class ApiResponse( val result: ApiResponseStatus = ApiResponseStatus.UNKNOWN, val data: T? = null, -) + val error: ApiError? = null, +) { + fun isSuccess() = result == ApiResponseStatus.SUCCESS +} diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index 156c358e8..fab300961 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -22,7 +22,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.infomaniak.appintegrity.AppIntegrityManager -import com.infomaniak.swisstransfer.BuildConfig import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferScreen import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import dagger.hilt.android.AndroidEntryPoint @@ -32,7 +31,7 @@ import io.sentry.SentryLevel @AndroidEntryPoint class NewTransferActivity : ComponentActivity() { - private val appIntegrityManager by lazy { AppIntegrityManager(BuildConfig.APPLICATION_ID) } + private val appIntegrityManager by lazy { AppIntegrityManager(appContext = this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -42,7 +41,7 @@ class NewTransferActivity : ComponentActivity() { NewTransferScreen(closeActivity = { finish() }, appIntegrityManager) } } - appIntegrityManager.warmUpTokenProvider(applicationContext, appCloudNumber = 364109398419) { exception -> + appIntegrityManager.warmUpTokenProvider(appCloudNumber = 364109398419) { exception -> exception.printStackTrace() Sentry.captureMessage("Exception during AppIntegrityManager's warmup", SentryLevel.ERROR) { scope -> scope.setTag("exception", exception.message.toString()) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 7897a1ffb..1be698b77 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -19,7 +19,6 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import android.net.Uri import android.util.Log -import androidx.compose.runtime.* import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,6 +34,7 @@ import com.infomaniak.multiplatform_swisstransfer.data.NewUploadSession import com.infomaniak.multiplatform_swisstransfer.managers.AppSettingsManager import com.infomaniak.multiplatform_swisstransfer.managers.UploadManager import com.infomaniak.sentry.SentryLog +import com.infomaniak.swisstransfer.BuildConfig import com.infomaniak.swisstransfer.di.IoDispatcher import com.infomaniak.swisstransfer.ui.screen.main.settings.DownloadLimitOption import com.infomaniak.swisstransfer.ui.screen.main.settings.DownloadLimitOption.Companion.toTransferOption @@ -142,9 +142,12 @@ class ImportFilesViewModel @Inject constructor( onSuccess = { token -> viewModelScope.launch(ioDispatcher) { Log.e("TOTO", "requestIntegrityVerdictToken: m Unit, appIntegrityManager: AppIntegri @Composable private fun NewTransferPreview() { SwissTransferTheme { - NewTransferScreen({}, AppIntegrityManager(BuildConfig.APPLICATION_ID)) + NewTransferScreen({}, AppIntegrityManager(LocalContext.current)) } } From 1e518d85bc84ec6c7bef48f141243ac09b6a1430 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Fri, 15 Nov 2024 10:58:37 +0100 Subject: [PATCH 04/23] refactor(AppIntegrity): Rename app integrity package name --- Core2/AppIntegrity/build.gradle.kts | 15 ++--- .../appintegrity/ExampleInstrumentedTest.kt | 8 +-- .../appintegrity/ApiClientProvider.kt | 23 ++++--- .../appintegrity/AppIntegrityInjection.kt | 4 ++ .../appintegrity/AppIntegrityManager.kt | 10 ++-- .../appintegrity/AppIntegrityRepository.kt | 13 ++-- .../appintegrity/AppIntegrityRoutes.kt | 10 ++-- .../appintegrity/exceptions/ApiException.kt | 4 +- .../exceptions/AppIntegrityException.kt | 25 ++++++++ .../exceptions/NetworkException.kt | 2 +- .../UnexpectedApiErrorFormatException.kt | 4 +- .../exceptions/UnknownException.kt | 4 +- .../appintegrity/models/ApiError.kt | 4 +- .../appintegrity/models/ApiResponse.kt | 4 +- .../appintegrity/models/ApiResponseStatus.kt | 4 +- .../appintegrity/ExampleUnitTest.kt | 17 ------ .../core2/appintegrity/ExampleUnitTest.kt | 60 +++++++++++++++++++ Core2/gradle/core2.versions.toml | 6 ++ .../swisstransfer/ui/NewTransferActivity.kt | 4 +- .../newtransfer/ImportFilesViewModel.kt | 6 +- .../screen/newtransfer/NewTransferNavHost.kt | 2 +- .../screen/newtransfer/NewTransferScreen.kt | 2 +- .../importfiles/ImportFilesScreen.kt | 2 +- gradle/libs.versions.toml | 1 + settings.gradle.kts | 2 +- 25 files changed, 157 insertions(+), 79 deletions(-) rename Core2/AppIntegrity/src/androidTest/java/com/infomaniak/{ => core2}/appintegrity/ExampleInstrumentedTest.kt (88%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/ApiClientProvider.kt (84%) create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/AppIntegrityManager.kt (92%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/AppIntegrityRepository.kt (87%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/AppIntegrityRoutes.kt (78%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/exceptions/ApiException.kt (87%) create mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/AppIntegrityException.kt rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/exceptions/NetworkException.kt (94%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt (86%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/exceptions/UnknownException.kt (91%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/models/ApiError.kt (91%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/models/ApiResponse.kt (92%) rename Core2/AppIntegrity/src/main/java/com/infomaniak/{ => core2}/appintegrity/models/ApiResponseStatus.kt (91%) delete mode 100644 Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt create mode 100644 Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt diff --git a/Core2/AppIntegrity/build.gradle.kts b/Core2/AppIntegrity/build.gradle.kts index a0dab1074..c1a6f76b8 100644 --- a/Core2/AppIntegrity/build.gradle.kts +++ b/Core2/AppIntegrity/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "com.infomaniak.appintegrity" + namespace = "com.infomaniak.core2.appintegrity" compileSdk = 34 defaultConfig { @@ -22,11 +22,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } } @@ -39,7 +39,8 @@ dependencies { implementation(core2.ktor.client.json) implementation(core2.ktor.client.encoding) implementation(core2.ktor.client.okhttp) - api(core2.kotlinx.serialization.json) - // testImplementation(core2.junit) - // androidTestImplementation(core2.androidx.junit) + implementation(core2.kotlinx.serialization.json) + testImplementation(core2.junit) + testImplementation(core2.ktor.client.mock) + androidTestImplementation(core2.androidx.junit) } diff --git a/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt b/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt similarity index 88% rename from Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt rename to Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt index ea1c86194..82e857fb5 100644 --- a/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/appintegrity/ExampleInstrumentedTest.kt +++ b/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ -package com.infomaniak.appintegrity +package com.infomaniak.core2.appintegrity -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt similarity index 84% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt index 8a6ee8a08..a5a29bb74 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/ApiClientProvider.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt @@ -15,16 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity +package com.infomaniak.core2.appintegrity -import com.infomaniak.appintegrity.exceptions.ApiException -import com.infomaniak.appintegrity.exceptions.NetworkException -import com.infomaniak.appintegrity.exceptions.UnexpectedApiErrorFormatException -import com.infomaniak.appintegrity.exceptions.UnknownException -import com.infomaniak.appintegrity.models.ApiError +import com.infomaniak.core2.appintegrity.exceptions.ApiException +import com.infomaniak.core2.appintegrity.exceptions.NetworkException +import com.infomaniak.core2.appintegrity.exceptions.UnexpectedApiErrorFormatException +import com.infomaniak.core2.appintegrity.exceptions.UnknownException +import com.infomaniak.core2.appintegrity.models.ApiError import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.HttpClientEngineFactory +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpResponseValidator import io.ktor.client.plugins.HttpTimeout @@ -36,9 +37,7 @@ import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.errors.IOException import kotlinx.serialization.json.Json -class ApiClientProvider internal constructor(engine: HttpClientEngineFactory<*>? = null) { - - constructor() : this(null) +internal class ApiClientProvider(engine: HttpClientEngine = OkHttp.create()) { val json = Json { ignoreUnknownKeys = true @@ -49,7 +48,7 @@ class ApiClientProvider internal constructor(engine: HttpClientEngineFactory<*>? val httpClient = createHttpClient(engine) - fun createHttpClient(engine: HttpClientEngineFactory<*>?): HttpClient { + private fun createHttpClient(engine: HttpClientEngine): HttpClient { val block: HttpClientConfig<*>.() -> Unit = { expectSuccess = true install(ContentNegotiation) { @@ -94,7 +93,7 @@ class ApiClientProvider internal constructor(engine: HttpClientEngineFactory<*>? } } - return if (engine != null) HttpClient(engine, block) else HttpClient(block) + return HttpClient(engine, block) } private fun Throwable.isNetworkException() = this is IOException diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt new file mode 100644 index 000000000..3617cd550 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt @@ -0,0 +1,4 @@ +package com.infomaniak.core2.appintegrity + +class AppIntegrityInjection { +} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt similarity index 92% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index 92629fec6..348c4011a 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity +package com.infomaniak.core2.appintegrity import android.content.Context import android.util.Base64 @@ -29,6 +29,7 @@ class AppIntegrityManager(private val appContext: Context) { private var appIntegrityTokenProvider: StandardIntegrityTokenProvider? = null private val classicIntegrityTokenProvider by lazy { IntegrityManagerFactory.create(appContext) } + private val appIntegrityRepository by lazy { AppIntegrityRepository() } private var challenge = "" private var challengeId = "" @@ -61,6 +62,7 @@ class AppIntegrityManager(private val appContext: Context) { fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: (Exception?) -> Unit) { val nonce = Base64.encodeToString(challenge.toByteArray(), Base64.DEFAULT) + Log.e("TOTO", "challenge = $challenge / nonce = $nonce") classicIntegrityTokenProvider.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build()) ?.addOnSuccessListener { response -> onSuccess(response.token()) } @@ -75,7 +77,7 @@ class AppIntegrityManager(private val appContext: Context) { onFailure: (Throwable) -> Unit, ) { runCatching { - val apiResponse = AppIntegrityRepository.getJwtToken( + val apiResponse = appIntegrityRepository.getJwtToken( integrityToken = integrityToken, packageName = packageName, targetUrl = targetUrl, @@ -90,7 +92,7 @@ class AppIntegrityManager(private val appContext: Context) { suspend fun getChallenge(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) = runCatching { generateChallengeId() - val apiResponse = AppIntegrityRepository.getChallenge(challengeId) + val apiResponse = appIntegrityRepository.getChallenge(challengeId) Log.d(APP_INTEGRITY_MANAGER_TAG, "challengeId : $challengeId / challenge: ${apiResponse.data}") apiResponse.data?.let { challenge = it } onSuccess() @@ -101,7 +103,7 @@ class AppIntegrityManager(private val appContext: Context) { suspend fun callDemoRoute(mobileToken: String) { runCatching { - val apiResponse = AppIntegrityRepository.demo(mobileToken) + val apiResponse = appIntegrityRepository.demo(mobileToken) val logMessage = if (apiResponse.isSuccess()) { "Success demo route response: ${apiResponse.data}" } else { diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt similarity index 87% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index b7ab7df07..1ac729a4d 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -15,11 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity +package com.infomaniak.core2.appintegrity -import com.infomaniak.appintegrity.exceptions.UnknownException -import com.infomaniak.appintegrity.models.ApiResponse -import io.ktor.client.HttpClient +import com.infomaniak.core2.appintegrity.exceptions.UnknownException +import com.infomaniak.core2.appintegrity.models.ApiResponse import io.ktor.client.call.body import io.ktor.client.request.post import io.ktor.client.request.setBody @@ -28,7 +27,7 @@ import io.ktor.http.ContentType import io.ktor.http.Url import io.ktor.http.contentType -object AppIntegrityRepository { +internal class AppIntegrityRepository { private val apiClientProvider by lazy { ApiClientProvider() } @@ -58,8 +57,8 @@ object AppIntegrityRepository { return post>(url = Url(AppIntegrityRoutes.demo), data = body) } - private suspend inline fun post(url: Url, data: Any?, httpClient: HttpClient = apiClientProvider.httpClient): R { - return httpClient.post(url) { + private suspend inline fun post(url: Url, data: Any?): R { + return apiClientProvider.httpClient.post(url) { contentType(ContentType.Application.Json) setBody(data) }.decode() diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRoutes.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt similarity index 78% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRoutes.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt index 5777f8546..d27c93c39 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/AppIntegrityRoutes.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt @@ -15,15 +15,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity +package com.infomaniak.core2.appintegrity -internal object AppIntegrityRoutes { +object AppIntegrityRoutes { private const val PROD_URL = "https://" // TODO private const val PREPROD_BASE_URL = "https://api-core.devd471.dev.infomaniak.ch/" private const val BASE_URL_V1 = "$PREPROD_BASE_URL/1/attest" - const val requestChallenge = "${BASE_URL_V1}/challenge" - const val requestApiIntegrityCheck = "${BASE_URL_V1}/integrity" - const val demo = "${BASE_URL_V1}/demo" + internal const val requestChallenge = "$BASE_URL_V1/challenge" + internal const val requestApiIntegrityCheck = "$BASE_URL_V1/integrity" + const val demo = "$BASE_URL_V1/demo" } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/ApiException.kt similarity index 87% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/ApiException.kt index 23407fd02..deb0964bd 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/ApiException.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/ApiException.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.exceptions +package com.infomaniak.core2.appintegrity.exceptions /** * Thrown when an API call fails due to an error identified by a specific error code. @@ -26,4 +26,4 @@ package com.infomaniak.appintegrity.exceptions * @param errorCode The specific error code returned by the API. * @param errorMessage The detailed error message explaining the cause of the failure. */ -open class ApiException(val errorCode: String, errorMessage: String) : Exception(errorMessage) +internal open class ApiException(val errorCode: String, errorMessage: String) : Exception(errorMessage) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/AppIntegrityException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/AppIntegrityException.kt new file mode 100644 index 000000000..b4008f3e0 --- /dev/null +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/AppIntegrityException.kt @@ -0,0 +1,25 @@ +/* + * Infomaniak SwissTransfer - Android + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core2.appintegrity.exceptions + +/** + * Thrown when any step of the App Integrity check fails + * + * @param message A detailed message describing the error. + */ +class AppIntegrityException(message: String) : Exception(message) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/NetworkException.kt similarity index 94% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/NetworkException.kt index 074d5e68e..0062e996b 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/NetworkException.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/NetworkException.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.exceptions +package com.infomaniak.core2.appintegrity.exceptions /** * Thrown when a network-related error occurs, such as connectivity issues or timeouts. diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt similarity index 86% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt index bb87cdcd8..eb3a4aceb 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/UnexpectedApiErrorFormatException.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.exceptions +package com.infomaniak.core2.appintegrity.exceptions /** * Thrown when an API call returns an error in an unexpected format that cannot be parsed. @@ -26,4 +26,4 @@ package com.infomaniak.appintegrity.exceptions * @param statusCode The HTTP status code returned by the API. * @param bodyResponse The raw response body from the API that could not be parsed. */ -class UnexpectedApiErrorFormatException(val statusCode: Int, val bodyResponse: String) : Exception(bodyResponse) +internal class UnexpectedApiErrorFormatException(val statusCode: Int, val bodyResponse: String) : Exception(bodyResponse) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/UnknownException.kt similarity index 91% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/UnknownException.kt index 5e1be0979..1b67a5460 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/exceptions/UnknownException.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/exceptions/UnknownException.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.exceptions +package com.infomaniak.core2.appintegrity.exceptions /** * Represents an unknown exception that can occur during the execution of the application. @@ -30,6 +30,6 @@ package com.infomaniak.appintegrity.exceptions * * @param cause The underlying exception that caused this exception. */ -class UnknownException(cause: Throwable) : Exception(cause) { +internal class UnknownException(cause: Throwable) : Exception(cause) { override val message: String = cause.message ?: cause.toString() } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiError.kt similarity index 91% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiError.kt index af4310084..06a1d54e1 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiError.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiError.kt @@ -15,12 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.models +package com.infomaniak.core2.appintegrity.models import kotlinx.serialization.Serializable @Serializable -data class ApiError( +internal data class ApiError( val errorCode: String = "", val message: String = "", ) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiResponse.kt similarity index 92% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiResponse.kt index ef3733138..490c2be51 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponse.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiResponse.kt @@ -15,12 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.models +package com.infomaniak.core2.appintegrity.models import kotlinx.serialization.Serializable @Serializable -data class ApiResponse( +internal data class ApiResponse( val result: ApiResponseStatus = ApiResponseStatus.UNKNOWN, val data: T? = null, val error: ApiError? = null, diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiResponseStatus.kt similarity index 91% rename from Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt rename to Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiResponseStatus.kt index 58633448d..202a45007 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/appintegrity/models/ApiResponseStatus.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/models/ApiResponseStatus.kt @@ -15,13 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.appintegrity.models +package com.infomaniak.core2.appintegrity.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -enum class ApiResponseStatus { +internal enum class ApiResponseStatus { @SerialName("error") ERROR, diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt deleted file mode 100644 index 96857cabe..000000000 --- a/Core2/AppIntegrity/src/test/java/com/infomaniak/appintegrity/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.infomaniak.appintegrity - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt new file mode 100644 index 000000000..cdf35867b --- /dev/null +++ b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt @@ -0,0 +1,60 @@ +package com.infomaniak.core2.appintegrity + +import com.infomaniak.core2.appintegrity.exceptions.UnknownException +import com.infomaniak.core2.appintegrity.models.ApiResponse +import io.ktor.client.call.body +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.http.* +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + + private val apiClientProvider by lazy { + ApiClientProvider( + MockEngine { _ -> + respond( + content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + ) + } + + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } + + @Test + fun toto() { + runBlocking { + post>(Url("toto"), data = mapOf("toto" to 1)) + } + } + + + private suspend inline fun post(url: Url, data: Any?): R { + return apiClientProvider.httpClient.post(url) { + contentType(ContentType.Application.Json) + setBody(data) + }.decode() + } + + + private suspend inline fun HttpResponse.decode(): R { + return runCatching { body() }.getOrElse { throw UnknownException(it) } + } +} diff --git a/Core2/gradle/core2.versions.toml b/Core2/gradle/core2.versions.toml index 49156cb43..ca895c907 100644 --- a/Core2/gradle/core2.versions.toml +++ b/Core2/gradle/core2.versions.toml @@ -2,6 +2,8 @@ coreKtx = "1.15.0" composeBom = "2024.11.00" integrity = "1.4.0" +junit = "4.13.2" +junitVersion = "1.2.1" kotlinxSerializationJson = "1.7.1" ktor = "2.3.12" matomo = "4.1.4" @@ -21,6 +23,10 @@ ktor-client-content-negociation = { module = "io.ktor:ktor-client-content-negoti ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } ktor-client-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } matomo = { module = "com.github.matomo-org:matomo-sdk-android", version.ref = "matomo" } sentry-android = { module = "io.sentry:sentry-android", version.ref = "sentry-android" } +# Tests +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index fab300961..7e8d99c24 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -21,7 +21,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferScreen import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import dagger.hilt.android.AndroidEntryPoint @@ -31,7 +31,7 @@ import io.sentry.SentryLevel @AndroidEntryPoint class NewTransferActivity : ComponentActivity() { - private val appIntegrityManager by lazy { AppIntegrityManager(appContext = this) } + private val appIntegrityManager by lazy { AppIntegrityManager(applicationContext) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 1be698b77..fa52041e0 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.RemoteUploadFile import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.UploadFileSession import com.infomaniak.multiplatform_swisstransfer.common.utils.mapToList @@ -146,8 +146,8 @@ class ImportFilesViewModel @Inject constructor( integrityToken = token, packageName = BuildConfig.APPLICATION_ID, targetUrl = "http://api-core.devd471.dev.infomaniak.ch/1/attest/demo", - onSuccess = {}, - onFailure = {}, + onSuccess = { Log.e("TOTO", "sendTransfer: success") }, + onFailure = { it.printStackTrace() } ) Log.e("TOTO", "result = $result") } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt index c38cc6a3f..a679ad19e 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt @@ -22,7 +22,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation.* import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.ImportFilesScreen diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt index cf149b6c5..03853ce2c 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt @@ -20,7 +20,7 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.navigation.compose.rememberNavController -import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewAllWindows diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 1ec441307..a5b0e51e7 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.infomaniak.appintegrity.AppIntegrityManager +import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.core2.isEmail import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.FileUi import com.infomaniak.swisstransfer.R diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd20cd2f0..c16d5cff4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ hiltNavigationCompose = "1.2.0" junit = "4.13.2" junitVersion = "1.2.1" kotlin = "2.0.20" +ktorClientMock = "1.2.0" lifecycleRuntimeKtx = "2.8.7" navigation = "2.8.4" qrose = "1.0.1" diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c58b37fb..1faa412f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,5 +35,5 @@ include(":Core2:Matomo") include(":Core2:Network") include(":Core2:Onboarding") include(":Core2:Compose:Core") -include(":FileTypes") include(":Core2:AppIntegrity") +include(":FileTypes") From 05a0bcd56c70bdcb675815d0087e3f9f7dd564ff Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Fri, 15 Nov 2024 11:01:26 +0100 Subject: [PATCH 05/23] feat(AppIntegrity): Add app integrity check before starting upload --- .../appintegrity/AppIntegrityRepository.kt | 1 + .../core2/appintegrity/AppIntegrityRoutes.kt | 2 +- Core2/gradle/core2.versions.toml | 2 +- .../swisstransfer/ui/NewTransferActivity.kt | 18 ++-- .../newtransfer/ImportFilesViewModel.kt | 100 ++++++++++++------ .../screen/newtransfer/NewTransferScreen.kt | 2 +- .../importfiles/ImportFilesScreen.kt | 28 ++++- 7 files changed, 108 insertions(+), 45 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index 1ac729a4d..51dde2a27 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -47,6 +47,7 @@ internal class AppIntegrityRepository { "package_name" to packageName, "target_url" to targetUrl, "challenge_id" to challengeId, + // "force_integrity_test" to "true", ) return post>(url = Url(AppIntegrityRoutes.requestApiIntegrityCheck), data = body) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt index d27c93c39..c7758296e 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt @@ -20,7 +20,7 @@ package com.infomaniak.core2.appintegrity object AppIntegrityRoutes { private const val PROD_URL = "https://" // TODO - private const val PREPROD_BASE_URL = "https://api-core.devd471.dev.infomaniak.ch/" + private const val PREPROD_BASE_URL = "https://api-core.devd471.dev.infomaniak.ch" private const val BASE_URL_V1 = "$PREPROD_BASE_URL/1/attest" internal const val requestChallenge = "$BASE_URL_V1/challenge" diff --git a/Core2/gradle/core2.versions.toml b/Core2/gradle/core2.versions.toml index ca895c907..94686e156 100644 --- a/Core2/gradle/core2.versions.toml +++ b/Core2/gradle/core2.versions.toml @@ -5,7 +5,7 @@ integrity = "1.4.0" junit = "4.13.2" junitVersion = "1.2.1" kotlinxSerializationJson = "1.7.1" -ktor = "2.3.12" +ktor = "3.0.1" matomo = "4.1.4" sentry-android = "7.15.0" diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index 7e8d99c24..d324e14f5 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -25,13 +25,11 @@ import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferScreen import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import dagger.hilt.android.AndroidEntryPoint -import io.sentry.Sentry -import io.sentry.SentryLevel @AndroidEntryPoint class NewTransferActivity : ComponentActivity() { - private val appIntegrityManager by lazy { AppIntegrityManager(applicationContext) } + private val appIntegrityManager by lazy { AppIntegrityManager(appContext = this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,12 +39,12 @@ class NewTransferActivity : ComponentActivity() { NewTransferScreen(closeActivity = { finish() }, appIntegrityManager) } } - appIntegrityManager.warmUpTokenProvider(appCloudNumber = 364109398419) { exception -> - exception.printStackTrace() - Sentry.captureMessage("Exception during AppIntegrityManager's warmup", SentryLevel.ERROR) { scope -> - scope.setTag("exception", exception.message.toString()) - scope.setExtra("stacktrace", exception.printStackTrace().toString()) - } - } + // appIntegrityManager.warmUpTokenProvider(appCloudNumber = 364109398419) { exception -> + // exception.printStackTrace() + // Sentry.captureMessage("Exception during AppIntegrityManager's warmup", SentryLevel.ERROR) { scope -> + // scope.setTag("exception", exception.message.toString()) + // scope.setExtra("stacktrace", exception.printStackTrace().toString()) + // } + // } } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index fa52041e0..d8fc42583 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -27,6 +27,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.infomaniak.core2.appintegrity.AppIntegrityManager +import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.APP_INTEGRITY_MANAGER_TAG +import com.infomaniak.core2.appintegrity.AppIntegrityRoutes +import com.infomaniak.core2.appintegrity.exceptions.NetworkException import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.RemoteUploadFile import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.UploadFileSession import com.infomaniak.multiplatform_swisstransfer.common.utils.mapToList @@ -72,6 +75,9 @@ class ImportFilesViewModel @Inject constructor( private val _sendActionResult = MutableStateFlow(SendActionResult.NotStarted) val sendActionResult = _sendActionResult.asStateFlow() + private val _integrityCheckResult = MutableStateFlow(null) + val integrityCheckResult = _integrityCheckResult.asStateFlow() + @OptIn(FlowPreview::class) val importedFilesDebounced = importationFilesManager.importedFiles .debounce(50) @@ -137,37 +143,7 @@ class ImportFilesViewModel @Inject constructor( _sendActionResult.update { SendActionResult.Pending } viewModelScope.launch(ioDispatcher) { runCatching { - appIntegrityManager.requestIntegrityVerdictToken( - requestHash = "", - onSuccess = { token -> - viewModelScope.launch(ioDispatcher) { - Log.e("TOTO", "requestIntegrityVerdictToken: m - Sentry.captureMessage( - "Error when requiring an integrity token during account creation", - SentryLevel.ERROR, - ) { scope -> - scope.setTag("exception", exception?.message.toString()) - scope.setExtra("stacktrace", exception?.printStackTrace().toString()) - } - Log.e("TOTO", "sendTransfer: failed ${exception?.message}") - }, - onNullTokenProvider = { message -> - Sentry.captureMessage(message, SentryLevel.ERROR) - // TODO: Better error ? - Log.e("TOTO", "sendTransfer: nullTokenprovider : $message") - }, - ) + checkAppIntegrity(appIntegrityManager) val uuid = uploadManager.createAndGetUpload(generateNewUploadSession()).uuid uploadManager.initUploadSession(recaptcha = "Recaptcha")!! // TODO: Handle ContainerErrorsException here uploadWorkerScheduler.scheduleWork(uuid) @@ -186,6 +162,68 @@ class ImportFilesViewModel @Inject constructor( _sendActionResult.value = SendActionResult.NotStarted } + fun startTransfer(appIntegrityManager: AppIntegrityManager) { + runCatching { + checkAppIntegrity(appIntegrityManager) + }.onFailure { exception -> + SentryLog.e(TAG, "Failed to start the upload", exception) + _sendActionResult.update { SendActionResult.Failure } + } + } + + private fun checkAppIntegrity(appIntegrityManager: AppIntegrityManager) { + viewModelScope.launch(ioDispatcher) { + appIntegrityManager.getChallenge( + onSuccess = { requestAppIntegrityToken(appIntegrityManager) }, + onFailure = { + _integrityCheckResult.value = false + it.printStackTrace() + Log.e("TOTO", "getChallenge error") + }, + ) + } + } + + private fun requestAppIntegrityToken(appIntegrityManager: AppIntegrityManager) { + appIntegrityManager.requestClassicIntegrityVerdictToken( + onSuccess = { token -> + SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "request for app integrity token successful $token") + getApiIntegrityVerdict(appIntegrityManager, token) + }, + onFailure = { exception -> + exception?.printStackTrace() + + val errorMessage = "Error when requiring an integrity token during account creation" + Sentry.captureMessage(errorMessage, SentryLevel.ERROR) { scope -> + scope.setTag("exception", exception?.message.toString()) + scope.setExtra("stacktrace", exception?.printStackTrace().toString()) + } + _integrityCheckResult.value = false + }, + ) + } + + private fun getApiIntegrityVerdict(appIntegrityManager: AppIntegrityManager, appIntegrityToken: String) { + viewModelScope.launch(ioDispatcher) { + appIntegrityManager.getApiIntegrityVerdict( + integrityToken = appIntegrityToken, + packageName = BuildConfig.APPLICATION_ID, + targetUrl = AppIntegrityRoutes.demo, + onSuccess = { mobileToken -> + SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $mobileToken") + _integrityCheckResult.value = true + viewModelScope.launch(ioDispatcher) { + appIntegrityManager.callDemoRoute(mobileToken) + } + }, + onFailure = { + if (it !is NetworkException) Sentry.captureException(it) + _integrityCheckResult.value = false + }, + ) + } + } + private suspend fun removeOldData() { importationFilesManager.removeLocalCopyFolder() uploadManager.removeAllUploadSession() diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt index 03853ce2c..2d0698d9c 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt @@ -34,6 +34,6 @@ fun NewTransferScreen(closeActivity: () -> Unit, appIntegrityManager: AppIntegri @Composable private fun NewTransferPreview() { SwissTransferTheme { - NewTransferScreen({}, AppIntegrityManager(LocalContext.current)) + NewTransferScreen({}, AppIntegrityManager(appContext = LocalContext.current)) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index a5b0e51e7..b3776ca78 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -18,6 +18,7 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles import android.net.Uri +import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes @@ -76,6 +77,7 @@ fun ImportFilesScreen( val emailLanguageState by importFilesViewModel.selectedLanguageOption.collectAsStateWithLifecycle() val sendActionResult by importFilesViewModel.sendActionResult.collectAsStateWithLifecycle() + val integrityCheckResult by importFilesViewModel.integrityCheckResult.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } @@ -87,6 +89,16 @@ fun ImportFilesScreen( resetSendActionResult = importFilesViewModel::resetSendActionResult, ) + HandleIntegrityCheckResult( + getIntegrityCheckResult = { integrityCheckResult }, + sendTransfer = { + Log.e("TOTO", "ImportFilesScreen: sendTransfer") + importFilesViewModel.sendTransfer(appIntegrityManager) + }, + errorMessage = stringResource(R.string.uploadErrorDescription), + snackbarHostState = { snackbarHostState }, + ) + LaunchedEffect(Unit) { importFilesViewModel.initTransferOptionsValues() } val transferOptionsCallbacks = importFilesViewModel.getTransferOptionsCallbacks( @@ -136,7 +148,7 @@ fun ImportFilesScreen( transferOptionsCallbacks = transferOptionsCallbacks, addFiles = importFilesViewModel::importFiles, closeActivity = closeActivity, - sendTransfer = { importFilesViewModel.sendTransfer(appIntegrityManager) }, + sendTransfer = { importFilesViewModel.startTransfer(appIntegrityManager) }, shouldStartByPromptingUserForFiles = true, isTransferStarted = { sendActionResult != SendActionResult.NotStarted }, snackbarHostState = snackbarHostState, @@ -165,6 +177,20 @@ private fun HandleSendActionResult( } } +@Composable +private fun HandleIntegrityCheckResult( + getIntegrityCheckResult: () -> Boolean?, + sendTransfer: () -> Unit, + errorMessage: String, + snackbarHostState: () -> SnackbarHostState, +) { + LaunchedEffect(getIntegrityCheckResult() != null) { + getIntegrityCheckResult()?.let { + if (it) sendTransfer() else snackbarHostState().showSnackbar(errorMessage) + } + } +} + @Composable private fun ImportFilesScreen( files: () -> List, From 43aca9736ca59cead21b2a273d9b6f7458597b96 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Wed, 4 Dec 2024 14:26:33 +0100 Subject: [PATCH 06/23] feat(AppIntegrity): Add indeterminate progress when checking integrity --- .../newtransfer/importfiles/ImportFilesScreen.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index b3776ca78..45c3d54ab 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -226,8 +226,8 @@ private fun ImportFilesScreen( importedFiles = files, shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields }, transferAuthorEmail = transferAuthorEmail, + checkIntegrityAndNavigateToUploadProgress = sendTransfer, isTransferStarted = isTransferStarted, - navigateToUploadProgress = sendTransfer, ) }, content = { @@ -404,10 +404,11 @@ private fun SendButton( currentSessionFilesCount: () -> Int, importedFiles: () -> List, shouldShowEmailAddressesFields: () -> Boolean, + checkIntegrityAndNavigateToUploadProgress: () -> Unit, transferAuthorEmail: GetSetCallbacks, isTransferStarted: () -> Boolean, - navigateToUploadProgress: () -> Unit, ) { + var isCheckingAppIntegrity by remember { mutableStateOf(false) } val remainingFilesCount = filesToImportCount() val isImporting by remember(remainingFilesCount) { derivedStateOf { remainingFilesCount > 0 } } @@ -427,10 +428,13 @@ private fun SendButton( modifier = modifier, title = stringResource(R.string.transferSendButton), style = ButtonType.PRIMARY, - enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && !isTransferStarted() }, - showIndeterminateProgress = { isTransferStarted() }, + showIndeterminateProgress = { isCheckingAppIntegrity && isTransferStarted() }, + enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && isTransferStarted() }, progress = progress, - onClick = navigateToUploadProgress, + onClick = { + isCheckingAppIntegrity = true + checkIntegrityAndNavigateToUploadProgress() + }, ) } From c7d8bb2b4e00b1bd6fc8ad974ffcb0f4e42c65e3 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Wed, 4 Dec 2024 15:33:54 +0100 Subject: [PATCH 07/23] feat(AppIntegrity): Add sentry to the manager --- Core2/AppIntegrity/build.gradle.kts | 3 +- .../core2/appintegrity/AppIntegrityManager.kt | 45 +++++++++++++------ .../newtransfer/ImportFilesViewModel.kt | 42 ++++++----------- .../importfiles/ImportFilesScreen.kt | 14 +++--- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/Core2/AppIntegrity/build.gradle.kts b/Core2/AppIntegrity/build.gradle.kts index c1a6f76b8..6888e396b 100644 --- a/Core2/AppIntegrity/build.gradle.kts +++ b/Core2/AppIntegrity/build.gradle.kts @@ -32,7 +32,8 @@ android { dependencies { - // implementation(libs.androidx.core.ktx) + implementation(project(":Core2:Sentry")) + implementation(core2.integrity) implementation(core2.ktor.client.core) implementation(core2.ktor.client.content.negociation) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index 348c4011a..5f876f14f 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -23,6 +23,10 @@ import android.util.Log import com.google.android.play.core.integrity.IntegrityManagerFactory import com.google.android.play.core.integrity.IntegrityTokenRequest import com.google.android.play.core.integrity.StandardIntegrityManager.* +import com.infomaniak.core2.appintegrity.exceptions.NetworkException +import com.infomaniak.sentry.SentryLog +import io.sentry.Sentry +import io.sentry.SentryLevel import java.util.UUID class AppIntegrityManager(private val appContext: Context) { @@ -34,20 +38,20 @@ class AppIntegrityManager(private val appContext: Context) { private var challenge = "" private var challengeId = "" - fun warmUpTokenProvider(appCloudNumber: Long, onFailure: (Exception) -> Unit) { + fun warmUpTokenProvider(appCloudNumber: Long, onFailure: (Throwable) -> Unit) { val integrityManager = IntegrityManagerFactory.createStandard(appContext) integrityManager.prepareIntegrityToken( PrepareIntegrityTokenRequest.builder().setCloudProjectNumber(appCloudNumber).build() ).addOnSuccessListener { tokenProvider -> appIntegrityTokenProvider = tokenProvider - Log.i(APP_INTEGRITY_MANAGER_TAG, "warmUpTokenProvider: Success") - }.addOnFailureListener(onFailure) + SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "warmUpTokenProvider: Success") + }.addOnFailureListener { manageException(it, "Error during warmup", onFailure) } } fun requestIntegrityVerdictToken( requestHash: String, onSuccess: (String) -> Unit, - onFailure: (Exception?) -> Unit, + onFailure: (Throwable?) -> Unit, onNullTokenProvider: (String) -> Unit, ) { if (appIntegrityTokenProvider == null) { @@ -55,18 +59,16 @@ class AppIntegrityManager(private val appContext: Context) { } else { appIntegrityTokenProvider?.request(StandardIntegrityTokenRequest.builder().setRequestHash(requestHash).build()) ?.addOnSuccessListener { response -> onSuccess(response.token()) } - ?.addOnFailureListener(onFailure) + ?.addOnFailureListener { manageException(it, "Error when requiring a standard integrity token", onFailure) } } } - fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: (Exception?) -> Unit) { - + fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: (Throwable?) -> Unit) { val nonce = Base64.encodeToString(challenge.toByteArray(), Base64.DEFAULT) - Log.e("TOTO", "challenge = $challenge / nonce = $nonce") classicIntegrityTokenProvider.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build()) ?.addOnSuccessListener { response -> onSuccess(response.token()) } - ?.addOnFailureListener(onFailure) + ?.addOnFailureListener { manageException(it, "Error when requiring a classic integrity token", onFailure) } } suspend fun getApiIntegrityVerdict( @@ -85,22 +87,26 @@ class AppIntegrityManager(private val appContext: Context) { ) apiResponse.data?.let(onSuccess) }.getOrElse { - it.printStackTrace() - onFailure(it) + manageException(it, "Error during Integrity check by API", onFailure) } } suspend fun getChallenge(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) = runCatching { generateChallengeId() val apiResponse = appIntegrityRepository.getChallenge(challengeId) - Log.d(APP_INTEGRITY_MANAGER_TAG, "challengeId : $challengeId / challenge: ${apiResponse.data}") + SentryLog.d( + tag = APP_INTEGRITY_MANAGER_TAG, + msg = "challengeId hash : ${challengeId.hashCode()} / challenge hash: ${apiResponse.data.hashCode()}", + ) apiResponse.data?.let { challenge = it } onSuccess() }.getOrElse { - it.printStackTrace() - onFailure(it) + manageException(it, "Error fetching challenge", onFailure) } + /** + * Only used to test App Integrity in Apps before their real backend implementation + */ suspend fun callDemoRoute(mobileToken: String) { runCatching { val apiResponse = appIntegrityRepository.demo(mobileToken) @@ -119,6 +125,17 @@ class AppIntegrityManager(private val appContext: Context) { challengeId = UUID.randomUUID().toString() } + private fun manageException(exception: Throwable, errorMessage: String, onFailure: (Throwable) -> Unit) { + if (exception !is NetworkException) { + Sentry.captureMessage(errorMessage, SentryLevel.ERROR) { scope -> + scope.setTag("exception", exception.message.toString()) + scope.setExtra("stacktrace", exception.printStackTrace().toString()) + } + } + exception.printStackTrace() + onFailure(exception) + } + companion object { const val APP_INTEGRITY_MANAGER_TAG = "App integrity manager" } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index d8fc42583..f0bc76b86 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -18,7 +18,6 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import android.net.Uri -import android.util.Log import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -29,7 +28,6 @@ import androidx.lifecycle.viewModelScope import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.APP_INTEGRITY_MANAGER_TAG import com.infomaniak.core2.appintegrity.AppIntegrityRoutes -import com.infomaniak.core2.appintegrity.exceptions.NetworkException import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.RemoteUploadFile import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.UploadFileSession import com.infomaniak.multiplatform_swisstransfer.common.utils.mapToList @@ -54,8 +52,6 @@ import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks import com.infomaniak.swisstransfer.workers.UploadWorker import dagger.hilt.android.lifecycle.HiltViewModel -import io.sentry.Sentry -import io.sentry.SentryLevel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* @@ -139,11 +135,10 @@ class ImportFilesViewModel @Inject constructor( } } - fun sendTransfer(appIntegrityManager: AppIntegrityManager) { + fun sendTransfer() { _sendActionResult.update { SendActionResult.Pending } viewModelScope.launch(ioDispatcher) { runCatching { - checkAppIntegrity(appIntegrityManager) val uuid = uploadManager.createAndGetUpload(generateNewUploadSession()).uuid uploadManager.initUploadSession(recaptcha = "Recaptcha")!! // TODO: Handle ContainerErrorsException here uploadWorkerScheduler.scheduleWork(uuid) @@ -171,16 +166,17 @@ class ImportFilesViewModel @Inject constructor( } } - private fun checkAppIntegrity(appIntegrityManager: AppIntegrityManager) { + fun checkAppIntegrity(appIntegrityManager: AppIntegrityManager) { viewModelScope.launch(ioDispatcher) { - appIntegrityManager.getChallenge( - onSuccess = { requestAppIntegrityToken(appIntegrityManager) }, - onFailure = { - _integrityCheckResult.value = false - it.printStackTrace() - Log.e("TOTO", "getChallenge error") - }, - ) + runCatching { + appIntegrityManager.getChallenge( + onSuccess = { requestAppIntegrityToken(appIntegrityManager) }, + onFailure = { _integrityCheckResult.value = false }, + ) + }.onFailure { exception -> + SentryLog.e(TAG, "Failed to start the upload", exception) + _sendActionResult.update { SendActionResult.Failure } + } } } @@ -190,16 +186,7 @@ class ImportFilesViewModel @Inject constructor( SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "request for app integrity token successful $token") getApiIntegrityVerdict(appIntegrityManager, token) }, - onFailure = { exception -> - exception?.printStackTrace() - - val errorMessage = "Error when requiring an integrity token during account creation" - Sentry.captureMessage(errorMessage, SentryLevel.ERROR) { scope -> - scope.setTag("exception", exception?.message.toString()) - scope.setExtra("stacktrace", exception?.printStackTrace().toString()) - } - _integrityCheckResult.value = false - }, + onFailure = { _integrityCheckResult.value = false }, ) } @@ -216,10 +203,7 @@ class ImportFilesViewModel @Inject constructor( appIntegrityManager.callDemoRoute(mobileToken) } }, - onFailure = { - if (it !is NetworkException) Sentry.captureException(it) - _integrityCheckResult.value = false - }, + onFailure = { _integrityCheckResult.value = false }, ) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 45c3d54ab..6ccaec2bf 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -93,7 +93,7 @@ fun ImportFilesScreen( getIntegrityCheckResult = { integrityCheckResult }, sendTransfer = { Log.e("TOTO", "ImportFilesScreen: sendTransfer") - importFilesViewModel.sendTransfer(appIntegrityManager) + importFilesViewModel.sendTransfer() }, errorMessage = stringResource(R.string.uploadErrorDescription), snackbarHostState = { snackbarHostState }, @@ -148,7 +148,7 @@ fun ImportFilesScreen( transferOptionsCallbacks = transferOptionsCallbacks, addFiles = importFilesViewModel::importFiles, closeActivity = closeActivity, - sendTransfer = { importFilesViewModel.startTransfer(appIntegrityManager) }, + checkAppIntegrity = { importFilesViewModel.checkAppIntegrity(appIntegrityManager) }, shouldStartByPromptingUserForFiles = true, isTransferStarted = { sendActionResult != SendActionResult.NotStarted }, snackbarHostState = snackbarHostState, @@ -203,7 +203,7 @@ private fun ImportFilesScreen( addFiles: (List) -> Unit, closeActivity: () -> Unit, shouldStartByPromptingUserForFiles: Boolean, - sendTransfer: () -> Unit, + checkAppIntegrity: () -> Unit, isTransferStarted: () -> Boolean, snackbarHostState: SnackbarHostState? = null, ) { @@ -226,7 +226,7 @@ private fun ImportFilesScreen( importedFiles = files, shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields }, transferAuthorEmail = transferAuthorEmail, - checkIntegrityAndNavigateToUploadProgress = sendTransfer, + checkAppIntegrityBeforeSendingTransfer = checkAppIntegrity, isTransferStarted = isTransferStarted, ) }, @@ -404,8 +404,8 @@ private fun SendButton( currentSessionFilesCount: () -> Int, importedFiles: () -> List, shouldShowEmailAddressesFields: () -> Boolean, - checkIntegrityAndNavigateToUploadProgress: () -> Unit, transferAuthorEmail: GetSetCallbacks, + checkAppIntegrityBeforeSendingTransfer: () -> Unit, isTransferStarted: () -> Boolean, ) { var isCheckingAppIntegrity by remember { mutableStateOf(false) } @@ -433,7 +433,7 @@ private fun SendButton( progress = progress, onClick = { isCheckingAppIntegrity = true - checkIntegrityAndNavigateToUploadProgress() + checkAppIntegrityBeforeSendingTransfer() }, ) } @@ -500,8 +500,8 @@ private fun Preview(@PreviewParameter(FileUiListPreviewParameter::class) files: addFiles = {}, closeActivity = {}, shouldStartByPromptingUserForFiles = false, + checkAppIntegrity = {}, isTransferStarted = { false }, - sendTransfer = {}, ) } } From 2c56347d9a0f30ea9e9f67852ddceb53ebe82ffe Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Wed, 4 Dec 2024 16:49:12 +0100 Subject: [PATCH 08/23] refactor: Clean code --- Core2/AppIntegrity/.gitignore | 1 - Core2/AppIntegrity/build.gradle.kts | 16 +++++++++---- Core2/AppIntegrity/proguard-rules.pro | 2 +- .../AppIntegrity/src/main/AndroidManifest.xml | 4 ---- .../core2/appintegrity/ApiClientProvider.kt | 2 +- .../appintegrity/AppIntegrityInjection.kt | 4 ---- .../appintegrity/AppIntegrityRepository.kt | 11 +++++++-- .../core2/appintegrity/ExampleUnitTest.kt | 4 +--- .../swisstransfer/ui/NewTransferActivity.kt | 7 ------ .../newtransfer/ImportFilesViewModel.kt | 4 +++- .../importfiles/ImportFilesScreen.kt | 10 +++----- .../main/res/xml/network_security_config.xml | 23 ------------------- 12 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 Core2/AppIntegrity/.gitignore delete mode 100644 Core2/AppIntegrity/src/main/AndroidManifest.xml delete mode 100644 Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt delete mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/Core2/AppIntegrity/.gitignore b/Core2/AppIntegrity/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/Core2/AppIntegrity/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/Core2/AppIntegrity/build.gradle.kts b/Core2/AppIntegrity/build.gradle.kts index 6888e396b..1b7cce0ab 100644 --- a/Core2/AppIntegrity/build.gradle.kts +++ b/Core2/AppIntegrity/build.gradle.kts @@ -4,12 +4,16 @@ plugins { kotlin("plugin.serialization") version libs.versions.kotlin } +val sharedCompileSdk: Int by rootProject.extra +val sharedMinSdk: Int by rootProject.extra +val sharedJavaVersion: JavaVersion by rootProject.extra + android { namespace = "com.infomaniak.core2.appintegrity" - compileSdk = 34 + compileSdk = sharedCompileSdk defaultConfig { - minSdk = 21 + minSdk = sharedMinSdk testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -21,12 +25,14 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } + compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = sharedJavaVersion + targetCompatibility = sharedJavaVersion } + kotlinOptions { - jvmTarget = "17" + jvmTarget = sharedJavaVersion.toString() } } diff --git a/Core2/AppIntegrity/proguard-rules.pro b/Core2/AppIntegrity/proguard-rules.pro index 481bb4348..f1b424510 100644 --- a/Core2/AppIntegrity/proguard-rules.pro +++ b/Core2/AppIntegrity/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/Core2/AppIntegrity/src/main/AndroidManifest.xml b/Core2/AppIntegrity/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14b..000000000 --- a/Core2/AppIntegrity/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt index a5a29bb74..deac095e4 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt @@ -34,7 +34,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText import io.ktor.serialization.kotlinx.json.json -import io.ktor.utils.io.errors.IOException +import kotlinx.io.IOException import kotlinx.serialization.json.Json internal class ApiClientProvider(engine: HttpClientEngine = OkHttp.create()) { diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt deleted file mode 100644 index 3617cd550..000000000 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityInjection.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.infomaniak.core2.appintegrity - -class AppIntegrityInjection { -} diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index 51dde2a27..a058f4c39 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -17,6 +17,9 @@ */ package com.infomaniak.core2.appintegrity +import com.infomaniak.core2.appintegrity.exceptions.ApiException +import com.infomaniak.core2.appintegrity.exceptions.NetworkException +import com.infomaniak.core2.appintegrity.exceptions.UnexpectedApiErrorFormatException import com.infomaniak.core2.appintegrity.exceptions.UnknownException import com.infomaniak.core2.appintegrity.models.ApiResponse import io.ktor.client.call.body @@ -65,8 +68,12 @@ internal class AppIntegrityRepository { }.decode() } - private suspend inline fun HttpResponse.decode(): R { - return runCatching { body() }.getOrElse { throw UnknownException(it) } + return runCatching { body() }.getOrElse { exception -> + when (exception) { + is ApiException, is NetworkException, is UnexpectedApiErrorFormatException -> throw exception + else -> throw UnknownException(exception) + } + } } } diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt index cdf35867b..6e41f30f8 100644 --- a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt +++ b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt @@ -27,7 +27,7 @@ class ExampleUnitTest { respond( content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") + headers = headersOf(HttpHeaders.ContentType, "application/json"), ) } ) @@ -45,7 +45,6 @@ class ExampleUnitTest { } } - private suspend inline fun post(url: Url, data: Any?): R { return apiClientProvider.httpClient.post(url) { contentType(ContentType.Application.Json) @@ -53,7 +52,6 @@ class ExampleUnitTest { }.decode() } - private suspend inline fun HttpResponse.decode(): R { return runCatching { body() }.getOrElse { throw UnknownException(it) } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index d324e14f5..0654041a9 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -39,12 +39,5 @@ class NewTransferActivity : ComponentActivity() { NewTransferScreen(closeActivity = { finish() }, appIntegrityManager) } } - // appIntegrityManager.warmUpTokenProvider(appCloudNumber = 364109398419) { exception -> - // exception.printStackTrace() - // Sentry.captureMessage("Exception during AppIntegrityManager's warmup", SentryLevel.ERROR) { scope -> - // scope.setTag("exception", exception.message.toString()) - // scope.setExtra("stacktrace", exception.printStackTrace().toString()) - // } - // } } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index f0bc76b86..6a5a86fd6 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -18,6 +18,7 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import android.net.Uri +import android.util.Log import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -197,7 +198,8 @@ class ImportFilesViewModel @Inject constructor( packageName = BuildConfig.APPLICATION_ID, targetUrl = AppIntegrityRoutes.demo, onSuccess = { mobileToken -> - SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $mobileToken") + SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "Api verdict check") + Log.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $mobileToken") _integrityCheckResult.value = true viewModelScope.launch(ioDispatcher) { appIntegrityManager.callDemoRoute(mobileToken) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 6ccaec2bf..22193bc12 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -18,7 +18,6 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles import android.net.Uri -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes @@ -91,10 +90,7 @@ fun ImportFilesScreen( HandleIntegrityCheckResult( getIntegrityCheckResult = { integrityCheckResult }, - sendTransfer = { - Log.e("TOTO", "ImportFilesScreen: sendTransfer") - importFilesViewModel.sendTransfer() - }, + sendTransfer = { importFilesViewModel.sendTransfer() }, errorMessage = stringResource(R.string.uploadErrorDescription), snackbarHostState = { snackbarHostState }, ) @@ -185,8 +181,8 @@ private fun HandleIntegrityCheckResult( snackbarHostState: () -> SnackbarHostState, ) { LaunchedEffect(getIntegrityCheckResult() != null) { - getIntegrityCheckResult()?.let { - if (it) sendTransfer() else snackbarHostState().showSnackbar(errorMessage) + getIntegrityCheckResult()?.let { isSuccess -> + if (isSuccess) sendTransfer() else snackbarHostState().showSnackbar(errorMessage) } } } diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml deleted file mode 100644 index 14f5da1b8..000000000 --- a/app/src/main/res/xml/network_security_config.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - From f34596105f98a2b1b90f0863e325faa18f679ffd Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 5 Dec 2024 07:56:57 +0100 Subject: [PATCH 09/23] refactor(AppIntegrity): Move AppIntegrityManager in screen instead of passing it from the activity --- .../infomaniak/swisstransfer/ui/NewTransferActivity.kt | 6 +----- .../ui/screen/newtransfer/NewTransferNavHost.kt | 4 +--- .../ui/screen/newtransfer/NewTransferScreen.kt | 8 +++----- .../screen/newtransfer/importfiles/ImportFilesScreen.kt | 4 +++- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index 0654041a9..728fee5a6 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -21,22 +21,18 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferScreen import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class NewTransferActivity : ComponentActivity() { - - private val appIntegrityManager by lazy { AppIntegrityManager(appContext = this) } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { SwissTransferTheme { - NewTransferScreen(closeActivity = { finish() }, appIntegrityManager) + NewTransferScreen(closeActivity = { finish() }) } } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt index a679ad19e..6c04aad2e 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferNavHost.kt @@ -22,7 +22,6 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation.* import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.ImportFilesScreen @@ -32,7 +31,7 @@ import com.infomaniak.swisstransfer.ui.screen.newtransfer.upload.UploadProgressS import com.infomaniak.swisstransfer.ui.screen.newtransfer.upload.UploadSuccessScreen @Composable -fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Unit, appIntegrityManager: AppIntegrityManager) { +fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Unit) { NavHost(navController, NewTransferNavigation.startDestination) { composable { @@ -41,7 +40,6 @@ fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Un navigateToUploadProgress = { transferType, totalSize -> navController.navigate(UploadProgressDestination(transferType, totalSize)) }, - appIntegrityManager = appIntegrityManager ) } composable { diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt index 2d0698d9c..e12920249 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/NewTransferScreen.kt @@ -18,22 +18,20 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext import androidx.navigation.compose.rememberNavController -import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme import com.infomaniak.swisstransfer.ui.utils.PreviewAllWindows @Composable -fun NewTransferScreen(closeActivity: () -> Unit, appIntegrityManager: AppIntegrityManager) { +fun NewTransferScreen(closeActivity: () -> Unit) { val navController = rememberNavController() - NewTransferNavHost(navController, closeActivity, appIntegrityManager) + NewTransferNavHost(navController, closeActivity) } @PreviewAllWindows @Composable private fun NewTransferPreview() { SwissTransferTheme { - NewTransferScreen({}, AppIntegrityManager(appContext = LocalContext.current)) + NewTransferScreen {} } } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 22193bc12..19ee76472 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -62,8 +62,10 @@ fun ImportFilesScreen( importFilesViewModel: ImportFilesViewModel = hiltViewModel(), closeActivity: () -> Unit, navigateToUploadProgress: (transferType: TransferTypeUi, totalSize: Long) -> Unit, - appIntegrityManager: AppIntegrityManager, ) { + val appContext = LocalContext.current.applicationContext + val appIntegrityManager by lazy { AppIntegrityManager(appContext) } + val files by importFilesViewModel.importedFilesDebounced.collectAsStateWithLifecycle() val filesToImportCount by importFilesViewModel.filesToImportCount.collectAsStateWithLifecycle() val currentSessionFilesCount by importFilesViewModel.currentSessionFilesCount.collectAsStateWithLifecycle() From 1bb1fbeac36f3c45b23f478455122ab95b7a711b Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 5 Dec 2024 13:22:09 +0100 Subject: [PATCH 10/23] feat(AppIntegrity): Add mobile_token to the call header --- .../core2/appintegrity/AppIntegrityManager.kt | 1 + .../appintegrity/AppIntegrityRepository.kt | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index 5f876f14f..fb5622ab4 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -138,5 +138,6 @@ class AppIntegrityManager(private val appContext: Context) { companion object { const val APP_INTEGRITY_MANAGER_TAG = "App integrity manager" + const val MOBILE_TOKEN_HEADER = "Ik-mobile-token" } } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index a058f4c39..e5f514d0c 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -17,16 +17,19 @@ */ package com.infomaniak.core2.appintegrity +import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.MOBILE_TOKEN_HEADER import com.infomaniak.core2.appintegrity.exceptions.ApiException import com.infomaniak.core2.appintegrity.exceptions.NetworkException import com.infomaniak.core2.appintegrity.exceptions.UnexpectedApiErrorFormatException import com.infomaniak.core2.appintegrity.exceptions.UnknownException import com.infomaniak.core2.appintegrity.models.ApiResponse import io.ktor.client.call.body +import io.ktor.client.request.headers import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import io.ktor.http.ContentType +import io.ktor.http.HeadersBuilder import io.ktor.http.Url import io.ktor.http.contentType @@ -57,13 +60,21 @@ internal class AppIntegrityRepository { } suspend fun demo(mobileToken: String): ApiResponse { - val body = mapOf("mobile_token" to mobileToken) - return post>(url = Url(AppIntegrityRoutes.demo), data = body) + return post>( + url = Url(AppIntegrityRoutes.demo), + data = mapOf(), + appendHeaders = { append(MOBILE_TOKEN_HEADER, mobileToken) } + ) } - private suspend inline fun post(url: Url, data: Any?): R { + private suspend inline fun post( + url: Url, + data: Any?, + crossinline appendHeaders: HeadersBuilder.() -> Unit = {}, + ): R { return apiClientProvider.httpClient.post(url) { contentType(ContentType.Application.Json) + headers { appendHeaders() } setBody(data) }.decode() } From 758f4923f8f70a32952e01a77dac751bbcb959e1 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Fri, 6 Dec 2024 11:03:16 +0100 Subject: [PATCH 11/23] feat(AppIntegrity): Put back integrity tests on api --- .../com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index e5f514d0c..eb2148bbc 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -53,7 +53,7 @@ internal class AppIntegrityRepository { "package_name" to packageName, "target_url" to targetUrl, "challenge_id" to challengeId, - // "force_integrity_test" to "true", + "force_integrity_test" to "true", ) return post>(url = Url(AppIntegrityRoutes.requestApiIntegrityCheck), data = body) From 469dd958f60291bd8a3ce8ff385fc5602d576e1a Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 12 Dec 2024 10:42:03 +0100 Subject: [PATCH 12/23] feat(AppIntegrity): Add preprod and prod routes --- .../com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt index c7758296e..0f1a04b99 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt @@ -19,11 +19,11 @@ package com.infomaniak.core2.appintegrity object AppIntegrityRoutes { - private const val PROD_URL = "https://" // TODO - private const val PREPROD_BASE_URL = "https://api-core.devd471.dev.infomaniak.ch" + private const val PROD_URL = "https://https://api.infomaniak.com" + private const val PREPROD_BASE_URL = "https://api.preprod.dev.infomaniak.ch" private const val BASE_URL_V1 = "$PREPROD_BASE_URL/1/attest" internal const val requestChallenge = "$BASE_URL_V1/challenge" internal const val requestApiIntegrityCheck = "$BASE_URL_V1/integrity" - const val demo = "$BASE_URL_V1/demo" + const val demo = "https://api-core.devd471.dev.infomaniak.ch/1/attest/demo" } From 567ba442bce05610a70a8f5602114a93d8f2737b Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Thu, 12 Dec 2024 12:51:52 +0100 Subject: [PATCH 13/23] feat(AppIntegrity): Better state management of the AppIntegrityResult --- .../newtransfer/ImportFilesViewModel.kt | 21 ++++++++--- .../importfiles/ImportFilesScreen.kt | 37 +++++++++++-------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 6a5a86fd6..08bd4777f 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -72,7 +72,7 @@ class ImportFilesViewModel @Inject constructor( private val _sendActionResult = MutableStateFlow(SendActionResult.NotStarted) val sendActionResult = _sendActionResult.asStateFlow() - private val _integrityCheckResult = MutableStateFlow(null) + private val _integrityCheckResult = MutableStateFlow(AppIntegrityResult.Idle) val integrityCheckResult = _integrityCheckResult.asStateFlow() @OptIn(FlowPreview::class) @@ -167,12 +167,14 @@ class ImportFilesViewModel @Inject constructor( } } + //region App Integrity fun checkAppIntegrity(appIntegrityManager: AppIntegrityManager) { + _integrityCheckResult.value = AppIntegrityResult.Ongoing viewModelScope.launch(ioDispatcher) { runCatching { appIntegrityManager.getChallenge( onSuccess = { requestAppIntegrityToken(appIntegrityManager) }, - onFailure = { _integrityCheckResult.value = false }, + onFailure = { _integrityCheckResult.value = AppIntegrityResult.Fail }, ) }.onFailure { exception -> SentryLog.e(TAG, "Failed to start the upload", exception) @@ -187,7 +189,7 @@ class ImportFilesViewModel @Inject constructor( SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "request for app integrity token successful $token") getApiIntegrityVerdict(appIntegrityManager, token) }, - onFailure = { _integrityCheckResult.value = false }, + onFailure = { _integrityCheckResult.value = AppIntegrityResult.Fail }, ) } @@ -200,16 +202,21 @@ class ImportFilesViewModel @Inject constructor( onSuccess = { mobileToken -> SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "Api verdict check") Log.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $mobileToken") - _integrityCheckResult.value = true + _integrityCheckResult.value = AppIntegrityResult.Success viewModelScope.launch(ioDispatcher) { appIntegrityManager.callDemoRoute(mobileToken) } }, - onFailure = { _integrityCheckResult.value = false }, + onFailure = { _integrityCheckResult.value = AppIntegrityResult.Fail }, ) } } + fun resetIntegrityCheckResult() { + _integrityCheckResult.value = AppIntegrityResult.Idle + } + //endregion + private suspend fun removeOldData() { importationFilesManager.removeLocalCopyFolder() uploadManager.removeAllUploadSession() @@ -328,6 +335,10 @@ class ImportFilesViewModel @Inject constructor( data object Failure : SendActionResult() } + enum class AppIntegrityResult { + Idle, Ongoing, Success, Fail + } + companion object { private val TAG = ImportFilesViewModel::class.java.simpleName diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 19ee76472..bda03b766 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -48,6 +48,7 @@ import com.infomaniak.swisstransfer.ui.screen.main.settings.EmailLanguageOption import com.infomaniak.swisstransfer.ui.screen.main.settings.ValidityPeriodOption import com.infomaniak.swisstransfer.ui.screen.main.settings.components.SettingOption import com.infomaniak.swisstransfer.ui.screen.newtransfer.ImportFilesViewModel +import com.infomaniak.swisstransfer.ui.screen.newtransfer.ImportFilesViewModel.AppIntegrityResult import com.infomaniak.swisstransfer.ui.screen.newtransfer.ImportFilesViewModel.SendActionResult import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.* import com.infomaniak.swisstransfer.ui.theme.Margin @@ -82,6 +83,12 @@ fun ImportFilesScreen( val snackbarHostState = remember { SnackbarHostState() } + HandleIntegrityCheckResult( + integrityCheckResult = { integrityCheckResult }, + resetResult = { importFilesViewModel.resetIntegrityCheckResult() }, + sendTransfer = { importFilesViewModel.sendTransfer() }, + ) + HandleSendActionResult( snackbarHostState = snackbarHostState, getSendActionResult = { sendActionResult }, @@ -90,12 +97,6 @@ fun ImportFilesScreen( resetSendActionResult = importFilesViewModel::resetSendActionResult, ) - HandleIntegrityCheckResult( - getIntegrityCheckResult = { integrityCheckResult }, - sendTransfer = { importFilesViewModel.sendTransfer() }, - errorMessage = stringResource(R.string.uploadErrorDescription), - snackbarHostState = { snackbarHostState }, - ) LaunchedEffect(Unit) { importFilesViewModel.initTransferOptionsValues() } @@ -146,6 +147,7 @@ fun ImportFilesScreen( transferOptionsCallbacks = transferOptionsCallbacks, addFiles = importFilesViewModel::importFiles, closeActivity = closeActivity, + integrityCheckResult = { integrityCheckResult }, checkAppIntegrity = { importFilesViewModel.checkAppIntegrity(appIntegrityManager) }, shouldStartByPromptingUserForFiles = true, isTransferStarted = { sendActionResult != SendActionResult.NotStarted }, @@ -177,15 +179,18 @@ private fun HandleSendActionResult( @Composable private fun HandleIntegrityCheckResult( - getIntegrityCheckResult: () -> Boolean?, + integrityCheckResult: () -> AppIntegrityResult, + resetResult: () -> Unit, sendTransfer: () -> Unit, - errorMessage: String, - snackbarHostState: () -> SnackbarHostState, ) { - LaunchedEffect(getIntegrityCheckResult() != null) { - getIntegrityCheckResult()?.let { isSuccess -> - if (isSuccess) sendTransfer() else snackbarHostState().showSnackbar(errorMessage) + val result = integrityCheckResult() + LaunchedEffect(result == AppIntegrityResult.Success || result == AppIntegrityResult.Fail) { + when (integrityCheckResult()) { + AppIntegrityResult.Success -> sendTransfer() + AppIntegrityResult.Fail -> Unit // TODO: Show error + else -> Unit } + resetResult() } } @@ -201,6 +206,7 @@ private fun ImportFilesScreen( addFiles: (List) -> Unit, closeActivity: () -> Unit, shouldStartByPromptingUserForFiles: Boolean, + integrityCheckResult: () -> AppIntegrityResult, checkAppIntegrity: () -> Unit, isTransferStarted: () -> Boolean, snackbarHostState: SnackbarHostState? = null, @@ -224,6 +230,7 @@ private fun ImportFilesScreen( importedFiles = files, shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields }, transferAuthorEmail = transferAuthorEmail, + integrityCheckResult = integrityCheckResult, checkAppIntegrityBeforeSendingTransfer = checkAppIntegrity, isTransferStarted = isTransferStarted, ) @@ -403,10 +410,10 @@ private fun SendButton( importedFiles: () -> List, shouldShowEmailAddressesFields: () -> Boolean, transferAuthorEmail: GetSetCallbacks, + integrityCheckResult: () -> AppIntegrityResult, checkAppIntegrityBeforeSendingTransfer: () -> Unit, isTransferStarted: () -> Boolean, ) { - var isCheckingAppIntegrity by remember { mutableStateOf(false) } val remainingFilesCount = filesToImportCount() val isImporting by remember(remainingFilesCount) { derivedStateOf { remainingFilesCount > 0 } } @@ -426,11 +433,10 @@ private fun SendButton( modifier = modifier, title = stringResource(R.string.transferSendButton), style = ButtonType.PRIMARY, - showIndeterminateProgress = { isCheckingAppIntegrity && isTransferStarted() }, + showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing && isTransferStarted() }, enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && isTransferStarted() }, progress = progress, onClick = { - isCheckingAppIntegrity = true checkAppIntegrityBeforeSendingTransfer() }, ) @@ -498,6 +504,7 @@ private fun Preview(@PreviewParameter(FileUiListPreviewParameter::class) files: addFiles = {}, closeActivity = {}, shouldStartByPromptingUserForFiles = false, + integrityCheckResult = { AppIntegrityResult.Idle }, checkAppIntegrity = {}, isTransferStarted = { false }, ) From 0f9f7557bc21227d92526172c10d62cb2732f8d8 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Fri, 13 Dec 2024 10:44:15 +0100 Subject: [PATCH 14/23] feat(AppIntegrity): Send the token as header in initUploadSession --- .../core2/appintegrity/AppIntegrityManager.kt | 2 +- .../appintegrity/AppIntegrityRepository.kt | 4 +-- .../newtransfer/ImportFilesViewModel.kt | 33 +++++++++++-------- .../importfiles/ImportFilesScreen.kt | 8 +---- gradle/libs.versions.toml | 3 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index fb5622ab4..68df95a0d 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -138,6 +138,6 @@ class AppIntegrityManager(private val appContext: Context) { companion object { const val APP_INTEGRITY_MANAGER_TAG = "App integrity manager" - const val MOBILE_TOKEN_HEADER = "Ik-mobile-token" + const val ATTESTATION_TOKEN_HEADER = "Ik-mobile-token" } } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index eb2148bbc..e1e9828b3 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -17,7 +17,7 @@ */ package com.infomaniak.core2.appintegrity -import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.MOBILE_TOKEN_HEADER +import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.ATTESTATION_TOKEN_HEADER import com.infomaniak.core2.appintegrity.exceptions.ApiException import com.infomaniak.core2.appintegrity.exceptions.NetworkException import com.infomaniak.core2.appintegrity.exceptions.UnexpectedApiErrorFormatException @@ -63,7 +63,7 @@ internal class AppIntegrityRepository { return post>( url = Url(AppIntegrityRoutes.demo), data = mapOf(), - appendHeaders = { append(MOBILE_TOKEN_HEADER, mobileToken) } + appendHeaders = { append(ATTESTATION_TOKEN_HEADER, mobileToken) } ) } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 08bd4777f..58b25da7f 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -28,13 +28,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.APP_INTEGRITY_MANAGER_TAG -import com.infomaniak.core2.appintegrity.AppIntegrityRoutes import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.RemoteUploadFile import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.UploadFileSession import com.infomaniak.multiplatform_swisstransfer.common.utils.mapToList import com.infomaniak.multiplatform_swisstransfer.data.NewUploadSession import com.infomaniak.multiplatform_swisstransfer.managers.AppSettingsManager import com.infomaniak.multiplatform_swisstransfer.managers.UploadManager +import com.infomaniak.multiplatform_swisstransfer.network.utils.SharedApiRoutes import com.infomaniak.sentry.SentryLog import com.infomaniak.swisstransfer.BuildConfig import com.infomaniak.swisstransfer.di.IoDispatcher @@ -136,12 +136,15 @@ class ImportFilesViewModel @Inject constructor( } } - fun sendTransfer() { + private fun sendTransfer(attestationToken: String) { _sendActionResult.update { SendActionResult.Pending } viewModelScope.launch(ioDispatcher) { runCatching { val uuid = uploadManager.createAndGetUpload(generateNewUploadSession()).uuid - uploadManager.initUploadSession(recaptcha = "Recaptcha")!! // TODO: Handle ContainerErrorsException here + uploadManager.initUploadSession( + attestationHeaderName = AppIntegrityManager.ATTESTATION_TOKEN_HEADER, + attestationToken = attestationToken, + )!! // TODO: Handle ContainerErrorsException here uploadWorkerScheduler.scheduleWork(uuid) _sendActionResult.update { val totalSize = importationFilesManager.importedFiles.value.sumOf { it.fileSize } @@ -174,7 +177,7 @@ class ImportFilesViewModel @Inject constructor( runCatching { appIntegrityManager.getChallenge( onSuccess = { requestAppIntegrityToken(appIntegrityManager) }, - onFailure = { _integrityCheckResult.value = AppIntegrityResult.Fail }, + onFailure = ::setFailedIntegrityResult, ) }.onFailure { exception -> SentryLog.e(TAG, "Failed to start the upload", exception) @@ -189,7 +192,7 @@ class ImportFilesViewModel @Inject constructor( SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "request for app integrity token successful $token") getApiIntegrityVerdict(appIntegrityManager, token) }, - onFailure = { _integrityCheckResult.value = AppIntegrityResult.Fail }, + onFailure = ::setFailedIntegrityResult, ) } @@ -198,20 +201,23 @@ class ImportFilesViewModel @Inject constructor( appIntegrityManager.getApiIntegrityVerdict( integrityToken = appIntegrityToken, packageName = BuildConfig.APPLICATION_ID, - targetUrl = AppIntegrityRoutes.demo, - onSuccess = { mobileToken -> + targetUrl = SharedApiRoutes.createUploadContainer, + onSuccess = { attestationToken -> SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "Api verdict check") - Log.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $mobileToken") + Log.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $attestationToken") _integrityCheckResult.value = AppIntegrityResult.Success - viewModelScope.launch(ioDispatcher) { - appIntegrityManager.callDemoRoute(mobileToken) - } + sendTransfer(attestationToken) }, - onFailure = { _integrityCheckResult.value = AppIntegrityResult.Fail }, + onFailure = ::setFailedIntegrityResult, ) } } + private fun setFailedIntegrityResult(exception: Throwable?) { + SentryLog.e(APP_INTEGRITY_MANAGER_TAG, "Failed integrity check", exception) + _integrityCheckResult.value = AppIntegrityResult.Fail + } + fun resetIntegrityCheckResult() { _integrityCheckResult.value = AppIntegrityResult.Idle } @@ -226,6 +232,7 @@ class ImportFilesViewModel @Inject constructor( return NewUploadSession( duration = selectedValidityPeriodOption.value.apiValue, authorEmail = if (selectedTransferType.value == TransferTypeUi.MAIL) _transferAuthorEmail else "", + authorEmailToken = null, password = if (selectedPasswordOption.value == PasswordTransferOption.ACTIVATED) transferPassword else NO_PASSWORD, message = _transferMessage, numberOfDownload = selectedDownloadLimitOption.value.apiValue, @@ -240,7 +247,7 @@ class ImportFilesViewModel @Inject constructor( override val remoteUploadFile: RemoteUploadFile? = null override val size: Long = fileUi.fileSize } - } + }, ) } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index bda03b766..737e5e557 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -86,7 +86,6 @@ fun ImportFilesScreen( HandleIntegrityCheckResult( integrityCheckResult = { integrityCheckResult }, resetResult = { importFilesViewModel.resetIntegrityCheckResult() }, - sendTransfer = { importFilesViewModel.sendTransfer() }, ) HandleSendActionResult( @@ -181,15 +180,10 @@ private fun HandleSendActionResult( private fun HandleIntegrityCheckResult( integrityCheckResult: () -> AppIntegrityResult, resetResult: () -> Unit, - sendTransfer: () -> Unit, ) { val result = integrityCheckResult() LaunchedEffect(result == AppIntegrityResult.Success || result == AppIntegrityResult.Fail) { - when (integrityCheckResult()) { - AppIntegrityResult.Success -> sendTransfer() - AppIntegrityResult.Fail -> Unit // TODO: Show error - else -> Unit - } + if (integrityCheckResult() == AppIntegrityResult.Fail) Unit // TODO: Show error resetResult() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c16d5cff4..5a0e6fa3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,14 +15,13 @@ hiltNavigationCompose = "1.2.0" junit = "4.13.2" junitVersion = "1.2.1" kotlin = "2.0.20" -ktorClientMock = "1.2.0" lifecycleRuntimeKtx = "2.8.7" navigation = "2.8.4" qrose = "1.0.1" recaptcha = "18.6.1" sentry = "4.12.0" serialization = "1.7.3" -swisstransfer = "0.9.5" +swisstransfer = "0.9.6" workmanager = "2.10.0" [libraries] From 8ba9f5a3cadac18b30e5d2505ad60b52cefbcbf9 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Fri, 13 Dec 2024 14:30:04 +0100 Subject: [PATCH 15/23] refactor(AppIntegrity): Clean code --- .../core2/appintegrity/AppIntegrityManager.kt | 14 +++++++------- .../core2/appintegrity/AppIntegrityRepository.kt | 2 +- .../core2/appintegrity/ExampleUnitTest.kt | 8 +------- .../swisstransfer/ui/NewTransferActivity.kt | 1 + .../ui/screen/newtransfer/ImportFilesViewModel.kt | 3 +-- .../newtransfer/importfiles/ImportFilesScreen.kt | 4 +--- 6 files changed, 12 insertions(+), 20 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index 68df95a0d..6001d615c 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -38,7 +38,7 @@ class AppIntegrityManager(private val appContext: Context) { private var challenge = "" private var challengeId = "" - fun warmUpTokenProvider(appCloudNumber: Long, onFailure: (Throwable) -> Unit) { + fun warmUpTokenProvider(appCloudNumber: Long, onFailure: () -> Unit) { val integrityManager = IntegrityManagerFactory.createStandard(appContext) integrityManager.prepareIntegrityToken( PrepareIntegrityTokenRequest.builder().setCloudProjectNumber(appCloudNumber).build() @@ -51,7 +51,7 @@ class AppIntegrityManager(private val appContext: Context) { fun requestIntegrityVerdictToken( requestHash: String, onSuccess: (String) -> Unit, - onFailure: (Throwable?) -> Unit, + onFailure: () -> Unit, onNullTokenProvider: (String) -> Unit, ) { if (appIntegrityTokenProvider == null) { @@ -63,7 +63,7 @@ class AppIntegrityManager(private val appContext: Context) { } } - fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: (Throwable?) -> Unit) { + fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: () -> Unit) { val nonce = Base64.encodeToString(challenge.toByteArray(), Base64.DEFAULT) classicIntegrityTokenProvider.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build()) @@ -76,7 +76,7 @@ class AppIntegrityManager(private val appContext: Context) { packageName: String, targetUrl: String, onSuccess: (String) -> Unit, - onFailure: (Throwable) -> Unit, + onFailure: () -> Unit, ) { runCatching { val apiResponse = appIntegrityRepository.getJwtToken( @@ -91,7 +91,7 @@ class AppIntegrityManager(private val appContext: Context) { } } - suspend fun getChallenge(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) = runCatching { + suspend fun getChallenge(onSuccess: () -> Unit, onFailure: () -> Unit) = runCatching { generateChallengeId() val apiResponse = appIntegrityRepository.getChallenge(challengeId) SentryLog.d( @@ -125,7 +125,7 @@ class AppIntegrityManager(private val appContext: Context) { challengeId = UUID.randomUUID().toString() } - private fun manageException(exception: Throwable, errorMessage: String, onFailure: (Throwable) -> Unit) { + private fun manageException(exception: Throwable, errorMessage: String, onFailure: () -> Unit) { if (exception !is NetworkException) { Sentry.captureMessage(errorMessage, SentryLevel.ERROR) { scope -> scope.setTag("exception", exception.message.toString()) @@ -133,7 +133,7 @@ class AppIntegrityManager(private val appContext: Context) { } } exception.printStackTrace() - onFailure(exception) + onFailure() } companion object { diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index e1e9828b3..4172ee416 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -63,7 +63,7 @@ internal class AppIntegrityRepository { return post>( url = Url(AppIntegrityRoutes.demo), data = mapOf(), - appendHeaders = { append(ATTESTATION_TOKEN_HEADER, mobileToken) } + appendHeaders = { append(ATTESTATION_TOKEN_HEADER, mobileToken) }, ) } diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt index 6e41f30f8..f0791db01 100644 --- a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt +++ b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt @@ -11,7 +11,6 @@ import io.ktor.client.statement.HttpResponse import io.ktor.http.* import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals import org.junit.Test /** @@ -34,12 +33,7 @@ class ExampleUnitTest { } @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } - - @Test - fun toto() { + fun apiClientProviderTest() { runBlocking { post>(Url("toto"), data = mapOf("toto" to 1)) } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt index 728fee5a6..7621d7333 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/NewTransferActivity.kt @@ -27,6 +27,7 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class NewTransferActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 58b25da7f..dd3a26082 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -213,8 +213,7 @@ class ImportFilesViewModel @Inject constructor( } } - private fun setFailedIntegrityResult(exception: Throwable?) { - SentryLog.e(APP_INTEGRITY_MANAGER_TAG, "Failed integrity check", exception) + private fun setFailedIntegrityResult() { _integrityCheckResult.value = AppIntegrityResult.Fail } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 737e5e557..7c5b89693 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -430,9 +430,7 @@ private fun SendButton( showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing && isTransferStarted() }, enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && isTransferStarted() }, progress = progress, - onClick = { - checkAppIntegrityBeforeSendingTransfer() - }, + onClick = { checkAppIntegrityBeforeSendingTransfer() }, ) } From 60f0443a06cb9cf175ff56f382cafd999157a8f9 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Mon, 16 Dec 2024 09:37:50 +0100 Subject: [PATCH 16/23] feat(AppIntegrity): Force debug build to check app integrity on preprod --- Core2/AppIntegrity/build.gradle.kts | 11 ++++++++++- .../core2/appintegrity/AppIntegrityRepository.kt | 2 +- .../core2/appintegrity/AppIntegrityRoutes.kt | 8 +++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Core2/AppIntegrity/build.gradle.kts b/Core2/AppIntegrity/build.gradle.kts index 1b7cce0ab..d69c63c09 100644 --- a/Core2/AppIntegrity/build.gradle.kts +++ b/Core2/AppIntegrity/build.gradle.kts @@ -23,9 +23,18 @@ android { release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildConfigField("String", "APP_INTEGRITY_BASE_URL", "\"https://api.infomaniak.com\"") + } + + debug { + buildConfigField("String", "APP_INTEGRITY_BASE_URL", "\"https://api.preprod.dev.infomaniak.ch\"") } } + buildFeatures { + buildConfig = true + } + compileOptions { sourceCompatibility = sharedJavaVersion targetCompatibility = sharedJavaVersion @@ -39,7 +48,7 @@ android { dependencies { implementation(project(":Core2:Sentry")) - + implementation(core2.integrity) implementation(core2.ktor.client.core) implementation(core2.ktor.client.content.negociation) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index 4172ee416..5790132a3 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -53,7 +53,7 @@ internal class AppIntegrityRepository { "package_name" to packageName, "target_url" to targetUrl, "challenge_id" to challengeId, - "force_integrity_test" to "true", + "force_integrity_test" to "false", ) return post>(url = Url(AppIntegrityRoutes.requestApiIntegrityCheck), data = body) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt index 0f1a04b99..4de5af3da 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRoutes.kt @@ -19,11 +19,9 @@ package com.infomaniak.core2.appintegrity object AppIntegrityRoutes { - private const val PROD_URL = "https://https://api.infomaniak.com" - private const val PREPROD_BASE_URL = "https://api.preprod.dev.infomaniak.ch" - private const val BASE_URL_V1 = "$PREPROD_BASE_URL/1/attest" + private val BASE_URL_V1 = "${BuildConfig.APP_INTEGRITY_BASE_URL}/1/attest" - internal const val requestChallenge = "$BASE_URL_V1/challenge" - internal const val requestApiIntegrityCheck = "$BASE_URL_V1/integrity" + internal val requestChallenge = "$BASE_URL_V1/challenge" + internal val requestApiIntegrityCheck = "$BASE_URL_V1/integrity" const val demo = "https://api-core.devd471.dev.infomaniak.ch/1/attest/demo" } From ec85e093d7cbde150733d73f89c262027701f09d Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Mon, 16 Dec 2024 16:57:08 +0100 Subject: [PATCH 17/23] chore: Remove Useless example test and rename the Unit test file --- .../appintegrity/ExampleInstrumentedTest.kt | 22 ------------------- ...leUnitTest.kt => ApiClientProviderTest.kt} | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt rename Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/{ExampleUnitTest.kt => ApiClientProviderTest.kt} (98%) diff --git a/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt b/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt deleted file mode 100644 index 82e857fb5..000000000 --- a/Core2/AppIntegrity/src/androidTest/java/com/infomaniak/core2/appintegrity/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.infomaniak.core2.appintegrity - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.infomaniak.appintegrity.test", appContext.packageName) - } -} diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt similarity index 98% rename from Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt rename to Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt index f0791db01..e0fbc624c 100644 --- a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ExampleUnitTest.kt +++ b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt @@ -18,7 +18,7 @@ import org.junit.Test * * See [testing documentation](http://d.android.com/tools/testing). */ -class ExampleUnitTest { +class ApiClientProviderTest { private val apiClientProvider by lazy { ApiClientProvider( From 9441ab6f7685ead1b51994c426bdb90420843fda Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Tue, 17 Dec 2024 09:30:36 +0100 Subject: [PATCH 18/23] refactor(AppIntegrity): Inject manager --- .../infomaniak/core2/appintegrity/ApiClientProvider.kt | 6 ++---- .../core2/appintegrity/AppIntegrityRepository.kt | 2 +- .../core2/appintegrity/ApiClientProviderTest.kt | 5 ----- Core2/gradle/core2.versions.toml | 8 ++++---- .../swisstransfer/di/SwissTransferInjectionModule.kt | 6 ++++++ .../ui/screen/newtransfer/ImportFilesViewModel.kt | 3 ++- .../screen/newtransfer/importfiles/ImportFilesScreen.kt | 9 +++------ 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt index deac095e4..85489dbe5 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/ApiClientProvider.kt @@ -66,9 +66,7 @@ internal class ApiClientProvider(engine: HttpClientEngine = OkHttp.create()) { retryOnExceptionIf(maxRetries = MAX_RETRY) { _, cause -> cause.isNetworkException() } - delayMillis { retry -> - retry * 500L - } + delayMillis { retry -> retry * RETRY_DELAY } } HttpResponseValidator { validateResponse { response: HttpResponse -> @@ -101,6 +99,6 @@ internal class ApiClientProvider(engine: HttpClientEngine = OkHttp.create()) { companion object { private const val REQUEST_TIMEOUT = 10_000L private const val MAX_RETRY = 3 - const val REQUEST_LONG_TIMEOUT = 60_000L + private const val RETRY_DELAY = 500L } } diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index 5790132a3..4172ee416 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -53,7 +53,7 @@ internal class AppIntegrityRepository { "package_name" to packageName, "target_url" to targetUrl, "challenge_id" to challengeId, - "force_integrity_test" to "false", + "force_integrity_test" to "true", ) return post>(url = Url(AppIntegrityRoutes.requestApiIntegrityCheck), data = body) diff --git a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt index e0fbc624c..000ccfa2c 100644 --- a/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt +++ b/Core2/AppIntegrity/src/test/java/com/infomaniak/core2/appintegrity/ApiClientProviderTest.kt @@ -13,11 +13,6 @@ import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.runBlocking import org.junit.Test -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ class ApiClientProviderTest { private val apiClientProvider by lazy { diff --git a/Core2/gradle/core2.versions.toml b/Core2/gradle/core2.versions.toml index 94686e156..565124d78 100644 --- a/Core2/gradle/core2.versions.toml +++ b/Core2/gradle/core2.versions.toml @@ -2,8 +2,8 @@ coreKtx = "1.15.0" composeBom = "2024.11.00" integrity = "1.4.0" -junit = "4.13.2" -junitVersion = "1.2.1" +junitAndroidxVersion = "1.2.1" +junitVersion = "4.13.2" kotlinxSerializationJson = "1.7.1" ktor = "3.0.1" matomo = "4.1.4" @@ -28,5 +28,5 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto matomo = { module = "com.github.matomo-org:matomo-sdk-android", version.ref = "matomo" } sentry-android = { module = "io.sentry:sentry-android", version.ref = "sentry-android" } # Tests -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitAndroidxVersion" } +junit = { group = "junit", name = "junit", version.ref = "junitVersion" } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/di/SwissTransferInjectionModule.kt b/app/src/main/java/com/infomaniak/swisstransfer/di/SwissTransferInjectionModule.kt index cd9ca4fa2..d735eb5ec 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/di/SwissTransferInjectionModule.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/di/SwissTransferInjectionModule.kt @@ -17,6 +17,8 @@ */ package com.infomaniak.swisstransfer.di +import android.app.Application +import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.multiplatform_swisstransfer.SwissTransferInjection import dagger.Module import dagger.Provides @@ -51,4 +53,8 @@ object SwissTransferInjectionModule { @Provides @Singleton fun providesSharedApiUrlCreator(swissTransferInjection: SwissTransferInjection) = swissTransferInjection.sharedApiUrlCreator + + @Provides + @Singleton + fun providesAppIntegrityManger(application: Application) = AppIntegrityManager(application) } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index dd3a26082..65717ea4a 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -62,6 +62,7 @@ import javax.inject.Inject @HiltViewModel class ImportFilesViewModel @Inject constructor( private val appSettingsManager: AppSettingsManager, + private val appIntegrityManager: AppIntegrityManager, private val savedStateHandle: SavedStateHandle, private val importationFilesManager: ImportationFilesManager, private val uploadManager: UploadManager, @@ -171,7 +172,7 @@ class ImportFilesViewModel @Inject constructor( } //region App Integrity - fun checkAppIntegrity(appIntegrityManager: AppIntegrityManager) { + fun checkAppIntegrity() { _integrityCheckResult.value = AppIntegrityResult.Ongoing viewModelScope.launch(ioDispatcher) { runCatching { diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 7c5b89693..6942e6f8e 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.infomaniak.core2.appintegrity.AppIntegrityManager import com.infomaniak.core2.isEmail import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.FileUi import com.infomaniak.swisstransfer.R @@ -64,8 +63,6 @@ fun ImportFilesScreen( closeActivity: () -> Unit, navigateToUploadProgress: (transferType: TransferTypeUi, totalSize: Long) -> Unit, ) { - val appContext = LocalContext.current.applicationContext - val appIntegrityManager by lazy { AppIntegrityManager(appContext) } val files by importFilesViewModel.importedFilesDebounced.collectAsStateWithLifecycle() val filesToImportCount by importFilesViewModel.filesToImportCount.collectAsStateWithLifecycle() @@ -147,7 +144,7 @@ fun ImportFilesScreen( addFiles = importFilesViewModel::importFiles, closeActivity = closeActivity, integrityCheckResult = { integrityCheckResult }, - checkAppIntegrity = { importFilesViewModel.checkAppIntegrity(appIntegrityManager) }, + checkAppIntegrity = importFilesViewModel::checkAppIntegrity, shouldStartByPromptingUserForFiles = true, isTransferStarted = { sendActionResult != SendActionResult.NotStarted }, snackbarHostState = snackbarHostState, @@ -427,8 +424,8 @@ private fun SendButton( modifier = modifier, title = stringResource(R.string.transferSendButton), style = ButtonType.PRIMARY, - showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing && isTransferStarted() }, - enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && isTransferStarted() }, + showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing || isTransferStarted() }, + enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && !isTransferStarted() }, progress = progress, onClick = { checkAppIntegrityBeforeSendingTransfer() }, ) From 3c2957e5d1acf34fc6959c4d214c29dfa3035548 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Tue, 17 Dec 2024 09:31:25 +0100 Subject: [PATCH 19/23] feat(AppIntegrity): Add a basic error for user --- .../ui/screen/newtransfer/importfiles/ImportFilesScreen.kt | 6 +++++- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 6942e6f8e..b429c1596 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -18,6 +18,7 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles import android.net.Uri +import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes @@ -179,8 +180,11 @@ private fun HandleIntegrityCheckResult( resetResult: () -> Unit, ) { val result = integrityCheckResult() + val context = LocalContext.current LaunchedEffect(result == AppIntegrityResult.Success || result == AppIntegrityResult.Fail) { - if (integrityCheckResult() == AppIntegrityResult.Fail) Unit // TODO: Show error + if (integrityCheckResult() == AppIntegrityResult.Fail) { // TODO: Better error management + Toast.makeText(context, R.string.errorAppIntegrity, Toast.LENGTH_LONG).show() + } resetResult() } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d5faf95bb..dfe6a5a2d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -32,6 +32,7 @@ Passwort anzeigen Neuer Transfer Heruntergeladene Übertragung: %d/%d + Ihr Gerät wird nicht als sicher erkannt Das Passwort muss zwischen 6 und 25 Zeichen lang sein Ein Fehler ist aufgetreten Verfällt in %d Tagen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7e1d88827..6eab30e98 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -32,6 +32,7 @@ Mostrar contraseña Nueva transferencia Transferencia descargada: %d/%d + Su dispositivo no es reconocido como seguro La contraseña debe tener entre 6 y 25 caracteres Se ha producido un error Caduca en %d días diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5c7661b50..3f9a84ef7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -32,6 +32,7 @@ Afficher le mot de passe Nouveau transfert Transfert téléchargé : %d/%d\u0020 + Votre appareil n’est pas reconnu comme sûr Le mot de passe doit comporter entre 6 et 25 caractères Une erreur est survenue Expire dans %d jours diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 71c00a75c..182e54071 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -32,6 +32,7 @@ Mostra password Nuovo trasferimento Trasferimento scaricato: %d/%d + Il dispositivo non è riconosciuto come sicuro La password deve essere compresa tra 6 e 24 caratteri Si è verificato un errore Scade tra %d giorni diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17b259870..443a0e193 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,6 +36,7 @@ Show password New transfer Downloaded transfer: %d/%d\u0020 + Your device is not recognized as safe The password must be between 6 and 25 characters An error has occurred Expires in %d days From 5e5e9f4995f17b7ce89bb2f5756b48cb1e6bde81 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Tue, 17 Dec 2024 10:43:48 +0100 Subject: [PATCH 20/23] feat(AppIntegrity): Disable integrity check for debug --- .../infomaniak/core2/appintegrity/AppIntegrityManager.kt | 7 +++++++ .../core2/appintegrity/AppIntegrityRepository.kt | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index 6001d615c..cd72f28fe 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -64,6 +64,13 @@ class AppIntegrityManager(private val appContext: Context) { } fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: () -> Unit) { + + // You can comment this if you want to test the App Integrity (also see getJwtToken in AppIntegrityRepository) + if (BuildConfig.DEBUG) { + onSuccess("Basic app integrity token") + return + } + val nonce = Base64.encodeToString(challenge.toByteArray(), Base64.DEFAULT) classicIntegrityTokenProvider.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build()) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt index 4172ee416..31f007acf 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityRepository.kt @@ -48,14 +48,16 @@ internal class AppIntegrityRepository { targetUrl: String, challengeId: String, ): ApiResponse { - val body = mapOf( + val body = mutableMapOf( "token" to integrityToken, "package_name" to packageName, "target_url" to targetUrl, "challenge_id" to challengeId, - "force_integrity_test" to "true", ) + // Add this line to test validation by api + // body["force_integrity_test"] = "true" + return post>(url = Url(AppIntegrityRoutes.requestApiIntegrityCheck), data = body) } From 28145a7e938ab7ab5260e6498ee8fd58ac58941a Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Tue, 17 Dec 2024 11:10:33 +0100 Subject: [PATCH 21/23] chore(AppIntegrity): Rename `checkAppIntegrityBeforeSendingTransfer` to `checkAppIntegrityThenSendTransfer` --- .../ui/screen/newtransfer/importfiles/ImportFilesScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index b429c1596..9aeb9ff9e 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -226,7 +226,7 @@ private fun ImportFilesScreen( shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields }, transferAuthorEmail = transferAuthorEmail, integrityCheckResult = integrityCheckResult, - checkAppIntegrityBeforeSendingTransfer = checkAppIntegrity, + checkAppIntegrityThenSendTransfer = checkAppIntegrity, isTransferStarted = isTransferStarted, ) }, @@ -406,7 +406,7 @@ private fun SendButton( shouldShowEmailAddressesFields: () -> Boolean, transferAuthorEmail: GetSetCallbacks, integrityCheckResult: () -> AppIntegrityResult, - checkAppIntegrityBeforeSendingTransfer: () -> Unit, + checkAppIntegrityThenSendTransfer: () -> Unit, isTransferStarted: () -> Boolean, ) { val remainingFilesCount = filesToImportCount() @@ -431,7 +431,7 @@ private fun SendButton( showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing || isTransferStarted() }, enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && !isTransferStarted() }, progress = progress, - onClick = { checkAppIntegrityBeforeSendingTransfer() }, + onClick = { checkAppIntegrityThenSendTransfer() }, ) } From 6d405c08b09fd635083010607fd0a8a221116fa0 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Tue, 17 Dec 2024 11:48:57 +0100 Subject: [PATCH 22/23] docs(AppIntegrity): Add small documentation to the AppIntegrityManager --- .../core2/appintegrity/AppIntegrityManager.kt | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt index cd72f28fe..19e5b8853 100644 --- a/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt +++ b/Core2/AppIntegrity/src/main/java/com/infomaniak/core2/appintegrity/AppIntegrityManager.kt @@ -29,6 +29,13 @@ import io.sentry.Sentry import io.sentry.SentryLevel import java.util.UUID +/** + * Manager used to verify that the device used is real and doesn't have integrity problems + * + * There is 2 types of Request: + * - the standard request ([requestIntegrityVerdictToken]) that need a warm-up first ([warmUpTokenProvider]) + * - the classic request ([requestClassicIntegrityVerdictToken]) that need additional Api checks + */ class AppIntegrityManager(private val appContext: Context) { private var appIntegrityTokenProvider: StandardIntegrityTokenProvider? = null @@ -38,6 +45,10 @@ class AppIntegrityManager(private val appContext: Context) { private var challenge = "" private var challengeId = "" + /** + * This function is needed in case of standard verdict request by [requestIntegrityVerdictToken]. + * It must be called once at the initialisation because it can take a long time (up to several minutes) + */ fun warmUpTokenProvider(appCloudNumber: Long, onFailure: () -> Unit) { val integrityManager = IntegrityManagerFactory.createStandard(appContext) integrityManager.prepareIntegrityToken( @@ -48,6 +59,10 @@ class AppIntegrityManager(private val appContext: Context) { }.addOnFailureListener { manageException(it, "Error during warmup", onFailure) } } + /** + * Standard verdict request for Integrity token + * It should protect automatically from replay attack, but for now this protection seemed to not be working + */ fun requestIntegrityVerdictToken( requestHash: String, onSuccess: (String) -> Unit, @@ -63,6 +78,12 @@ class AppIntegrityManager(private val appContext: Context) { } } + /** + * Classic verdict request for Integrity token + * + * This doesn't automatically protect from replay attack, thus the use of challenge/challengeId pair with our API to add this + * layer of protection. + */ fun requestClassicIntegrityVerdictToken(onSuccess: (String) -> Unit, onFailure: () -> Unit) { // You can comment this if you want to test the App Integrity (also see getJwtToken in AppIntegrityRepository) @@ -78,6 +99,19 @@ class AppIntegrityManager(private val appContext: Context) { ?.addOnFailureListener { manageException(it, "Error when requiring a classic integrity token", onFailure) } } + suspend fun getChallenge(onSuccess: () -> Unit, onFailure: () -> Unit) = runCatching { + generateChallengeId() + val apiResponse = appIntegrityRepository.getChallenge(challengeId) + SentryLog.d( + tag = APP_INTEGRITY_MANAGER_TAG, + msg = "challengeId hash : ${challengeId.hashCode()} / challenge hash: ${apiResponse.data.hashCode()}", + ) + apiResponse.data?.let { challenge = it } + onSuccess() + }.getOrElse { + manageException(it, "Error fetching challenge", onFailure) + } + suspend fun getApiIntegrityVerdict( integrityToken: String, packageName: String, @@ -98,19 +132,6 @@ class AppIntegrityManager(private val appContext: Context) { } } - suspend fun getChallenge(onSuccess: () -> Unit, onFailure: () -> Unit) = runCatching { - generateChallengeId() - val apiResponse = appIntegrityRepository.getChallenge(challengeId) - SentryLog.d( - tag = APP_INTEGRITY_MANAGER_TAG, - msg = "challengeId hash : ${challengeId.hashCode()} / challenge hash: ${apiResponse.data.hashCode()}", - ) - apiResponse.data?.let { challenge = it } - onSuccess() - }.getOrElse { - manageException(it, "Error fetching challenge", onFailure) - } - /** * Only used to test App Integrity in Apps before their real backend implementation */ From fba120fa0ab38322821f0f2d479ff0ce5b54ce82 Mon Sep 17 00:00:00 2001 From: Fabian Devel Date: Tue, 17 Dec 2024 12:49:34 +0100 Subject: [PATCH 23/23] refactor(AppIntegrity): Use a snackbar instead of a toast to display error message --- .../ui/screen/newtransfer/ImportFilesViewModel.kt | 9 --------- .../newtransfer/importfiles/ImportFilesScreen.kt | 14 +++++++------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 65717ea4a..ba0886b86 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -162,15 +162,6 @@ class ImportFilesViewModel @Inject constructor( _sendActionResult.value = SendActionResult.NotStarted } - fun startTransfer(appIntegrityManager: AppIntegrityManager) { - runCatching { - checkAppIntegrity(appIntegrityManager) - }.onFailure { exception -> - SentryLog.e(TAG, "Failed to start the upload", exception) - _sendActionResult.update { SendActionResult.Failure } - } - } - //region App Integrity fun checkAppIntegrity() { _integrityCheckResult.value = AppIntegrityResult.Ongoing diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index 9aeb9ff9e..f8b338d51 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -18,7 +18,6 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles import android.net.Uri -import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes @@ -32,7 +31,6 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter @@ -82,6 +80,7 @@ fun ImportFilesScreen( val snackbarHostState = remember { SnackbarHostState() } HandleIntegrityCheckResult( + snackbarHostState = snackbarHostState, integrityCheckResult = { integrityCheckResult }, resetResult = { importFilesViewModel.resetIntegrityCheckResult() }, ) @@ -160,13 +159,12 @@ private fun HandleSendActionResult( navigateToUploadProgress: (transferType: TransferTypeUi, totalSize: Long) -> Unit, resetSendActionResult: () -> Unit, ) { - val context = LocalContext.current - + val errorMessage = stringResource(R.string.errorUnknown) LaunchedEffect(getSendActionResult()) { when (val actionResult = getSendActionResult()) { is SendActionResult.Success -> navigateToUploadProgress(transferType(), actionResult.totalSize) is SendActionResult.Failure -> { - snackbarHostState.showSnackbar(context.getString(R.string.errorUnknown)) + snackbarHostState.showSnackbar(errorMessage) resetSendActionResult() } else -> Unit @@ -176,14 +174,16 @@ private fun HandleSendActionResult( @Composable private fun HandleIntegrityCheckResult( + snackbarHostState: SnackbarHostState, integrityCheckResult: () -> AppIntegrityResult, resetResult: () -> Unit, ) { val result = integrityCheckResult() - val context = LocalContext.current + val errorMessage = stringResource(R.string.errorAppIntegrity) + LaunchedEffect(result == AppIntegrityResult.Success || result == AppIntegrityResult.Fail) { if (integrityCheckResult() == AppIntegrityResult.Fail) { // TODO: Better error management - Toast.makeText(context, R.string.errorAppIntegrity, Toast.LENGTH_LONG).show() + snackbarHostState.showSnackbar(errorMessage) } resetResult() }