Skip to content

Commit

Permalink
BIT-102: Create account functionality (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
ramsey-livefront authored and vvolkgang committed Jun 20, 2024
1 parent 6f21206 commit 79c953b
Show file tree
Hide file tree
Showing 35 changed files with 1,134 additions and 114 deletions.
2 changes: 1 addition & 1 deletion app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.x8bit.bitwarden

import android.content.Intent
import androidx.lifecycle.ViewModel
import com.x8bit.bitwarden.data.auth.datasource.network.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api

import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import retrofit2.http.Body
import retrofit2.http.POST

Expand All @@ -12,4 +14,7 @@ interface AccountsApi {

@POST("/accounts/prelogin")
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>

@POST("/accounts/register")
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ object NetworkModule {
@Singleton
fun providesAccountService(
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
): AccountsService = AccountsServiceImpl(retrofit.create())
json: Json,
): AccountsService = AccountsServiceImpl(retrofit.create(), json)

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model

import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson.Keys
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Request body for register.
*
* @param email the email to be registered.
* @param masterPasswordHash the master password (encrypted).
* @param masterPasswordHint the hint for the master password (nullable).
* @param captchaResponse the captcha bypass token.
* @param key the user key for the request (encrypted).
* @param keys a [Keys] object containing public and private keys.
* @param kdfType the kdf type represented as an [Int].
* @param kdfIterations the number of kdf iterations.
*/
@Serializable
data class RegisterRequestJson(
@SerialName("email")
val email: String,

@SerialName("masterPasswordHash")
val masterPasswordHash: String,

@SerialName("masterPasswordHint")
val masterPasswordHint: String?,

@SerialName("captchaResponse")
val captchaResponse: String?,

@SerialName("key")
val key: String,

@SerialName("keys")
val keys: Keys,

@SerialName("kdf")
val kdfType: KdfTypeJson,

@SerialName("kdfIterations")
val kdfIterations: UInt,
) {

/**
* A keys object containing public and private keys.
*
* @param publicKey the public key (encrypted).
* @param encryptedPrivateKey the private key (encrypted).
*/
@Serializable
data class Keys(
@SerialName("publicKey")
val publicKey: String,

@SerialName("encryptedPrivateKey")
val encryptedPrivateKey: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Models response bodies for the register request.
*/
@Serializable
sealed class RegisterResponseJson {

/**
* Models a successful json response of the register request.
*
* @param captchaBypassToken the bypass token.
*/
@Serializable
data class Success(
@SerialName("captchaBypassToken")
val captchaBypassToken: String,
) : RegisterResponseJson()

/**
* Models a json body of a captcha error.
*
* @param validationErrors object containing error validations of the response.
*/
@Serializable
data class CaptchaRequired(
@SerialName("validationErrors")
val validationErrors: ValidationErrors,
) : RegisterResponseJson() {

/**
* Error validations containing a HCaptcha Site Key.
*
* @param captchaKeys keys for attempting captcha verification.
*/
@Serializable
data class ValidationErrors(
@SerialName("HCaptcha_SiteKey")
val captchaKeys: List<String>,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service

import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson

/**
* Provides an API for querying accounts endpoints.
Expand All @@ -11,4 +13,9 @@ interface AccountsService {
* Make pre login request to get KDF params.
*/
suspend fun preLogin(email: String): Result<PreLoginResponseJson>

/**
* Register a new account to Bitwarden.
*/
suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,31 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import kotlinx.serialization.json.Json
import java.net.HttpURLConnection

class AccountsServiceImpl constructor(
private val accountsApi: AccountsApi,
private val json: Json,
) : AccountsService {

override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
accountsApi.preLogin(PreLoginRequestJson(email = email))

// TODO add error parsing and pass along error message for validations BIT-763
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
accountsApi
.register(body)
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
code = HttpURLConnection.HTTP_BAD_REQUEST,
json = json,
) ?: throw throwable
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.sdk.util

import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.ARGON2_ID
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256

/**
* Convert a [Kdf] to a [KdfTypeJson].
*/
fun Kdf.toKdfTypeJson(): KdfTypeJson =
when (this) {
is Kdf.Argon2id -> ARGON2_ID
is Kdf.Pbkdf2 -> PBKDF2_SHA256
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.x8bit.bitwarden.data.auth.repository

import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -26,6 +27,12 @@ interface AuthRepository {
*/
var rememberedEmailAddress: String?

/**
* The currently selected region label (`null` if not set).
*/
// TODO replace this with a more robust selected region object BIT-725
var selectedRegionLabel: String

/**
* Attempt to login with the given email and password. Updated access token will be reflected
* in [authStateFlow].
Expand All @@ -41,6 +48,16 @@ interface AuthRepository {
*/
fun logout()

/**
* Attempt to register a new account with the given parameters.
*/
suspend fun register(
email: String,
masterPassword: String,
masterPasswordHint: String?,
captchaToken: String?,
): RegisterResult

/**
* Set the value of [captchaTokenResultFlow].
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.x8bit.bitwarden.data.auth.repository

import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.CaptchaRequired
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.Success
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.util.flatMap
Expand All @@ -23,6 +28,8 @@ import kotlinx.coroutines.flow.update
import javax.inject.Inject
import javax.inject.Singleton

private const val DEFAULT_KDF_ITERATIONS = 600000

/**
* Default implementation of [AuthRepository].
*/
Expand All @@ -49,6 +56,9 @@ class AuthRepositoryImpl @Inject constructor(
authDiskSource.rememberedEmailAddress = value
}

// TODO Handle selected region functionality BIT-725
override var selectedRegionLabel: String = "bitwarden.us"

override suspend fun login(
email: String,
password: String,
Expand Down Expand Up @@ -93,6 +103,56 @@ class AuthRepositoryImpl @Inject constructor(
mutableAuthStateFlow.update { AuthState.Unauthenticated }
}

override suspend fun register(
email: String,
masterPassword: String,
masterPasswordHint: String?,
captchaToken: String?,
): RegisterResult {
val kdf = Kdf.Pbkdf2(DEFAULT_KDF_ITERATIONS.toUInt())
return authSdkSource
.makeRegisterKeys(
email = email,
password = masterPassword,
kdf = kdf,
)
.flatMap { registerKeyResponse ->
accountsService.register(
body = RegisterRequestJson(
email = email,
masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint,
captchaResponse = captchaToken,
key = registerKeyResponse.encryptedUserKey,
keys = RegisterRequestJson.Keys(
publicKey = registerKeyResponse.keys.public,
encryptedPrivateKey = registerKeyResponse.keys.private,
),
kdfType = kdf.toKdfTypeJson(),
kdfIterations = kdf.iterations,
),
)
}
.fold(
onSuccess = {
when (it) {
is RegisterResponseJson.CaptchaRequired -> {
it.validationErrors.captchaKeys.firstOrNull()?.let { key ->
RegisterResult.CaptchaRequired(captchaId = key)
} ?: RegisterResult.Error(errorMessage = null)
}

is RegisterResponseJson.Success -> {
RegisterResult.Success(captchaToken = it.captchaBypassToken)
}
}
},
onFailure = {
RegisterResult.Error(errorMessage = null)
},
)
}

override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
mutableCaptchaTokenFlow.tryEmit(tokenResult)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
package com.x8bit.bitwarden.data.auth.repository.model

/**
* Models high level auth state for the application.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
package com.x8bit.bitwarden.data.auth.repository.model

/**
* Models result of logging in.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.data.auth.repository.model

/**
* Models result of registering a new account.
*/
sealed class RegisterResult {
/**
* Register succeeded.
*
* @param captchaToken the captcha bypass token to bypass future captcha verifications.
*/
data class Success(val captchaToken: String) : RegisterResult()

/**
* Captcha verification is required.
*
* @param captchaId the captcha id for performing the captcha verification.
*/
data class CaptchaRequired(val captchaId: String) : RegisterResult()

/**
* There was an error logging in.
*
* @param errorMessage a message describing the error.
*/
data class Error(val errorMessage: String?) : RegisterResult()
}
Loading

0 comments on commit 79c953b

Please sign in to comment.