From b298a45f9ea8d6c5d2d3b2d259b9b0a7ea7ae4e1 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 21 Dec 2021 09:37:19 +0200 Subject: [PATCH 1/3] Prepared Retrofit realization for sending encrypted messages through the web portal.| #1615 --- .../java/com/flowcrypt/email/Constants.kt | 1 + .../email/api/retrofit/ApiRepository.kt | 41 +++++++++++-- .../email/api/retrofit/ApiService.kt | 31 +++++++++- .../api/retrofit/FlowcryptApiRepository.kt | 60 +++++++++++++++++-- .../request/model/MessageUploadRequest.kt | 20 +++++++ .../response/api/MessageReplyTokenResponse.kt | 44 ++++++++++++++ .../response/api/MessageUploadResponse.kt | 44 ++++++++++++++ .../email/jetpack/viewmodel/EkmViewModel.kt | 4 +- .../email/jetpack/viewmodel/LoginViewModel.kt | 4 +- 9 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/model/MessageUploadRequest.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageReplyTokenResponse.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageUploadResponse.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt index 690a29396f..aa47da66f8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt @@ -38,6 +38,7 @@ class Constants { * The MIME type of PGP keys. */ const val MIME_TYPE_PGP_KEY = "application/pgp-keys" + const val MIME_TYPE_JSON = "application/json" const val MIME_TYPE_BINARY_DATA = "application/octet-stream" const val MIME_TYPE_RFC822 = "message/rfc822" diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt index 0d2a906d74..d58495c35d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt @@ -9,10 +9,13 @@ import android.content.Context import com.flowcrypt.email.api.retrofit.base.BaseApiRepository import com.flowcrypt.email.api.retrofit.request.model.InitialLegacySubmitModel import com.flowcrypt.email.api.retrofit.request.model.LoginModel +import com.flowcrypt.email.api.retrofit.request.model.MessageUploadRequest import com.flowcrypt.email.api.retrofit.request.model.TestWelcomeModel import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse import com.flowcrypt.email.api.retrofit.response.api.FesServerResponse import com.flowcrypt.email.api.retrofit.response.api.LoginResponse +import com.flowcrypt.email.api.retrofit.response.api.MessageReplyTokenResponse +import com.flowcrypt.email.api.retrofit.response.api.MessageUploadResponse import com.flowcrypt.email.api.retrofit.response.attester.InitialLegacySubmitResponse import com.flowcrypt.email.api.retrofit.response.attester.PubResponse import com.flowcrypt.email.api.retrofit.response.attester.TestWelcomeResponse @@ -34,12 +37,12 @@ interface ApiRepository : BaseApiRepository { /** * @param context Interface to global information about an application environment. * @param loginModel An instance of [LoginModel]. - * @param tokenId OIDC token. + * @param idToken OIDC token. */ suspend fun login( context: Context, loginModel: LoginModel, - tokenId: String + idToken: String ): Result /** @@ -121,10 +124,10 @@ interface ApiRepository : BaseApiRepository { * * @param context Interface to global information about an application environment. * @param ekmUrl key_manager_url from [OrgRules]. - * @param tokenId OIDC token. + * @param idToken OIDC token. */ suspend fun getPrivateKeysViaEkm( - context: Context, ekmUrl: String, tokenId: String + context: Context, ekmUrl: String, idToken: String ): Result /** @@ -134,4 +137,34 @@ interface ApiRepository : BaseApiRepository { * @param domain A company domain. */ suspend fun checkFes(context: Context, domain: String): Result + + /** + * Grab a reply token before uploading a password protected message + * + * @param context Interface to global information about an application environment. + * @param domain A company domain. + * @param domain OIDC token. + */ + suspend fun getReplyTokenForPasswordProtectedMsg( + context: Context, + domain: String, + idToken: String + ): Result + + /** + * Upload a password protected message to a web portal + * + * @param context Interface to global information about an application environment. + * @param domain A company domain. + * @param idToken OIDC token. + * @param messageUploadRequest an instance of [MessageUploadRequest] + * @param msg an encrypted message that will be sent + */ + suspend fun uploadPasswordProtectedMsgToWebPortal( + context: Context, + domain: String, + idToken: String, + messageUploadRequest: MessageUploadRequest, + msg: String + ): Result } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt index 0535328d1f..ca56e176a7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt @@ -16,11 +16,15 @@ import com.flowcrypt.email.api.retrofit.response.api.DomainOrgRulesResponse import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse import com.flowcrypt.email.api.retrofit.response.api.FesServerResponse import com.flowcrypt.email.api.retrofit.response.api.LoginResponse +import com.flowcrypt.email.api.retrofit.response.api.MessageReplyTokenResponse +import com.flowcrypt.email.api.retrofit.response.api.MessageUploadResponse import com.flowcrypt.email.api.retrofit.response.api.PostHelpFeedbackResponse import com.flowcrypt.email.api.retrofit.response.attester.InitialLegacySubmitResponse import com.flowcrypt.email.api.retrofit.response.attester.TestWelcomeResponse import com.flowcrypt.email.api.retrofit.response.oauth2.MicrosoftOAuth2TokenResponse import com.google.gson.JsonObject +import okhttp3.MultipartBody +import okhttp3.RequestBody import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Response @@ -29,7 +33,9 @@ import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Path import retrofit2.http.Query import retrofit2.http.Streaming @@ -143,7 +149,7 @@ interface ApiService { * @param body POJO model for requests */ @POST(BuildConfig.API_URL + "account/login") - suspend fun postLogin(@Body body: LoginModel, @Header("Authorization") tokenId: String): + suspend fun postLogin(@Body body: LoginModel, @Header("Authorization") idToken: String): Response /** @@ -200,7 +206,7 @@ interface ApiService { @GET suspend fun getPrivateKeysViaEkm( @Url ekmUrl: String, - @Header("Authorization") tokenId: String + @Header("Authorization") idToken: String ): Response /** @@ -214,4 +220,25 @@ interface ApiService { */ @GET() suspend fun isAvailable(@Url url: String): Response + + /** + * This method grabs a reply token before uploading a password protected message + */ + @POST("https://fes.{domain}/api/v1/message/new-reply-token") + suspend fun getReplyTokenForPasswordProtectedMsg( + @Path("domain") domain: String, + @Header("Authorization") idToken: String + ): Response + + /** + * This method uploads a password protected message to a web portal + */ + @Multipart + @POST("https://fes.{domain}/api/v1/message") + suspend fun uploadPasswordProtectedMsgToWebPortal( + @Path("domain") domain: String, + @Header("Authorization") idToken: String, + @Part("details") details: RequestBody, + @Part content: MultipartBody.Part + ): Response } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt index 69d0c4b053..498080be3c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt @@ -6,13 +6,17 @@ package com.flowcrypt.email.api.retrofit import android.content.Context +import com.flowcrypt.email.Constants import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.request.model.InitialLegacySubmitModel import com.flowcrypt.email.api.retrofit.request.model.LoginModel +import com.flowcrypt.email.api.retrofit.request.model.MessageUploadRequest import com.flowcrypt.email.api.retrofit.request.model.TestWelcomeModel import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse import com.flowcrypt.email.api.retrofit.response.api.FesServerResponse import com.flowcrypt.email.api.retrofit.response.api.LoginResponse +import com.flowcrypt.email.api.retrofit.response.api.MessageReplyTokenResponse +import com.flowcrypt.email.api.retrofit.response.api.MessageUploadResponse import com.flowcrypt.email.api.retrofit.response.attester.InitialLegacySubmitResponse import com.flowcrypt.email.api.retrofit.response.attester.PubResponse import com.flowcrypt.email.api.retrofit.response.attester.TestWelcomeResponse @@ -24,10 +28,14 @@ import com.flowcrypt.email.api.wkd.WkdClient import com.flowcrypt.email.extensions.kotlin.isValidEmail import com.flowcrypt.email.extensions.kotlin.isValidLocalhostEmail import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.armor +import com.google.gson.GsonBuilder import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.toResponseBody import org.pgpainless.algorithm.EncryptionPurpose import org.pgpainless.key.info.KeyRingInfo @@ -49,14 +57,14 @@ class FlowcryptApiRepository : ApiRepository { override suspend fun login( context: Context, loginModel: LoginModel, - tokenId: String + idToken: String ): Result = withContext(Dispatchers.IO) { val apiService = ApiHelper.getInstance(context).retrofit.create(ApiService::class.java) getResult( context = context, expectedResultClass = LoginResponse::class.java - ) { apiService.postLogin(loginModel, "Bearer $tokenId") } + ) { apiService.postLogin(loginModel, "Bearer $idToken") } } override suspend fun getDomainOrgRules( @@ -205,7 +213,7 @@ class FlowcryptApiRepository : ApiRepository { override suspend fun getPrivateKeysViaEkm( context: Context, ekmUrl: String, - tokenId: String + idToken: String ): Result = withContext(Dispatchers.IO) { val apiService = ApiHelper.getInstance(context).retrofit.create(ApiService::class.java) @@ -213,7 +221,7 @@ class FlowcryptApiRepository : ApiRepository { getResult( context = context, expectedResultClass = EkmPrivateKeysResponse::class.java - ) { apiService.getPrivateKeysViaEkm("${url}v1/keys/private", "Bearer $tokenId") } + ) { apiService.getPrivateKeysViaEkm("${url}v1/keys/private", "Bearer $idToken") } } override suspend fun checkFes(context: Context, domain: String): Result = @@ -238,4 +246,48 @@ class FlowcryptApiRepository : ApiRepository { expectedResultClass = FesServerResponse::class.java ) { apiService.checkFes(domain) } } + + override suspend fun getReplyTokenForPasswordProtectedMsg( + context: Context, + domain: String, + idToken: String + ): Result = + withContext(Dispatchers.IO) { + val apiService = ApiHelper.getInstance(context).retrofit.create(ApiService::class.java) + getResult( + context = context, + expectedResultClass = MessageReplyTokenResponse::class.java + ) { apiService.getReplyTokenForPasswordProtectedMsg(domain, "Bearer $idToken") } + } + + override suspend fun uploadPasswordProtectedMsgToWebPortal( + context: Context, + domain: String, + idToken: String, + messageUploadRequest: MessageUploadRequest, + msg: String + ): Result = + withContext(Dispatchers.IO) { + val apiService = ApiHelper.getInstance(context).retrofit.create(ApiService::class.java) + getResult( + context = context, + expectedResultClass = MessageUploadResponse::class.java + ) { + val details = GsonBuilder().create().toJson(messageUploadRequest) + .toRequestBody(Constants.MIME_TYPE_JSON.toMediaTypeOrNull()) + + val content = MultipartBody.Part.createFormData( + "content", + "content", + msg.toByteArray().toRequestBody(Constants.MIME_TYPE_BINARY_DATA.toMediaTypeOrNull()) + ) + + apiService.uploadPasswordProtectedMsgToWebPortal( + domain = domain, + idToken = "Bearer $idToken", + details = details, + content = content + ) + } + } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/model/MessageUploadRequest.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/model/MessageUploadRequest.kt new file mode 100644 index 0000000000..0f1e066e3f --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/model/MessageUploadRequest.kt @@ -0,0 +1,20 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.api.retrofit.request.model + +/** + * @author Denis Bondarenko + * Date: 12/20/21 + * Time: 12:19 PM + * E-mail: DenBond7@gmail.com + */ +data class MessageUploadRequest( + val associateReplyToken: String, + val from: String, + val to: List, + val cc: List = emptyList(), + val bcc: List = emptyList() +) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageReplyTokenResponse.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageReplyTokenResponse.kt new file mode 100644 index 0000000000..a0d7a2db8a --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageReplyTokenResponse.kt @@ -0,0 +1,44 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.api.retrofit.response.api + +import android.os.Parcel +import android.os.Parcelable +import com.flowcrypt.email.api.retrofit.response.base.ApiError +import com.flowcrypt.email.api.retrofit.response.base.ApiResponse +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +/** + * @author Denis Bondarenko + * Date: 12/20/21 + * Time: 12:10 PM + * E-mail: DenBond7@gmail.com + */ +data class MessageReplyTokenResponse( + @SerializedName("error") + @Expose override val apiError: ApiError? = null, + @Expose val replyToken: String? = null +) : ApiResponse { + constructor(parcel: Parcel) : this( + parcel.readParcelable(ApiError::class.java.classLoader), + parcel.readString() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(apiError, flags) + parcel.writeString(replyToken) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = MessageReplyTokenResponse(parcel) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageUploadResponse.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageUploadResponse.kt new file mode 100644 index 0000000000..0c7242dfa2 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/api/MessageUploadResponse.kt @@ -0,0 +1,44 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.api.retrofit.response.api + +import android.os.Parcel +import android.os.Parcelable +import com.flowcrypt.email.api.retrofit.response.base.ApiError +import com.flowcrypt.email.api.retrofit.response.base.ApiResponse +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +/** + * @author Denis Bondarenko + * Date: 12/20/21 + * Time: 12:34 PM + * E-mail: DenBond7@gmail.com + */ +data class MessageUploadResponse( + @SerializedName("error") + @Expose override val apiError: ApiError? = null, + @Expose val url: String? = null +) : ApiResponse { + constructor(parcel: Parcel) : this( + parcel.readParcelable(ApiError::class.java.classLoader), + parcel.readString() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(apiError, flags) + parcel.writeString(url) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = MessageUploadResponse(parcel) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EkmViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EkmViewModel.kt index f8a4fc2d1d..14b63ca72f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EkmViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EkmViewModel.kt @@ -35,7 +35,7 @@ class EkmViewModel(application: Application) : BaseAndroidViewModel(application) private val repository = FlowcryptApiRepository() val ekmLiveData: MutableLiveData> = MutableLiveData(Result.none()) - fun fetchPrvKeys(orgRules: OrgRules, tokenId: String) { + fun fetchPrvKeys(orgRules: OrgRules, idToken: String) { viewModelScope.launch { val context: Context = getApplication() ekmLiveData.value = Result.loading(progressMsg = context.getString(R.string.fetching_keys)) @@ -57,7 +57,7 @@ class EkmViewModel(application: Application) : BaseAndroidViewModel(application) context = context, ekmUrl = orgRules.keyManagerUrl ?: throw IllegalArgumentException("key_manager_url is empty"), - tokenId = tokenId + idToken = idToken ) if (ekmPrivateResult.status != Result.Status.SUCCESS) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoginViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoginViewModel.kt index 9d8f9d61a6..9477026b46 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoginViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoginViewModel.kt @@ -31,7 +31,7 @@ class LoginViewModel(application: Application) : BaseAndroidViewModel(applicatio private val repository = FlowcryptApiRepository() val loginLiveData: MutableLiveData> = MutableLiveData(Result.none()) - fun login(account: String, uuid: String, tokenId: String) { + fun login(account: String, uuid: String, idToken: String) { viewModelScope.launch { val context: Context = getApplication() loginLiveData.value = Result.loading(progressMsg = context.getString(R.string.loading)) @@ -39,7 +39,7 @@ class LoginViewModel(application: Application) : BaseAndroidViewModel(applicatio loginLiveData.value = repository.login( context = context, loginModel = LoginModel(account, uuid), - tokenId = tokenId + idToken = idToken ) } catch (e: Exception) { loginLiveData.value = Result.exception(e) From 6559c9d385cdde733a4776aca0337fb038fbceb6 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 21 Dec 2021 16:47:57 +0200 Subject: [PATCH 2/3] Fixed names.| #1615 --- .../java/com/flowcrypt/email/api/retrofit/ApiService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt index ca56e176a7..2dce1741a9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiService.kt @@ -149,7 +149,7 @@ interface ApiService { * @param body POJO model for requests */ @POST(BuildConfig.API_URL + "account/login") - suspend fun postLogin(@Body body: LoginModel, @Header("Authorization") idToken: String): + suspend fun postLogin(@Body body: LoginModel, @Header("Authorization") authorization: String): Response /** @@ -206,7 +206,7 @@ interface ApiService { @GET suspend fun getPrivateKeysViaEkm( @Url ekmUrl: String, - @Header("Authorization") idToken: String + @Header("Authorization") authorization: String ): Response /** @@ -227,7 +227,7 @@ interface ApiService { @POST("https://fes.{domain}/api/v1/message/new-reply-token") suspend fun getReplyTokenForPasswordProtectedMsg( @Path("domain") domain: String, - @Header("Authorization") idToken: String + @Header("Authorization") authorization: String ): Response /** @@ -237,7 +237,7 @@ interface ApiService { @POST("https://fes.{domain}/api/v1/message") suspend fun uploadPasswordProtectedMsgToWebPortal( @Path("domain") domain: String, - @Header("Authorization") idToken: String, + @Header("Authorization") authorization: String, @Part("details") details: RequestBody, @Part content: MultipartBody.Part ): Response From e53cb4b0d4c169a72e20cb34d172fa9d01009093 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 21 Dec 2021 19:01:54 +0200 Subject: [PATCH 3/3] Fixed renaming bug.| #1615 --- .../com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt index 498080be3c..446fb899f7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt @@ -284,7 +284,7 @@ class FlowcryptApiRepository : ApiRepository { apiService.uploadPasswordProtectedMsgToWebPortal( domain = domain, - idToken = "Bearer $idToken", + authorization = "Bearer $idToken", details = details, content = content )