diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt index 50929eb851..3e6232338d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt @@ -10,12 +10,65 @@ sealed class GetTokenResponseJson { /** * Models json response of the get token request. * - * @param accessToken the access token. + * @property accessToken The user's access token. + * @property refreshToken The user's refresh token. + * @property tokenType The type of token (ex: "Bearer"). + * @property expiresInSeconds The amount of time (in seconds) before the [accessToken] expires. + * @property key The user's key. + * @property privateKey The user's private key. + * @property kdfType The KDF type. + * @property kdfIterations The number of iterations when calculating a user's password. + * @property kdfMemory The amount of memory to use when calculating a password hash (MB). + * @property kdfParallelism The number of threads to use when calculating a password hash. + * @property shouldForcePasswordReset Whether or not the app must force a password reset. + * @property shouldResetMasterPassword Whether or not the user is required to reset their + * master password. + * @property masterPasswordPolicyOptions The options available for a user's master password. + * @property userDecryptionOptions The options available to a user for decryption. */ @Serializable data class Success( @SerialName("access_token") val accessToken: String, + + @SerialName("refresh_token") + val refreshToken: String, + + @SerialName("token_type") + val tokenType: String, + + @SerialName("expires_in") + val expiresInSeconds: Int, + + @SerialName("Key") + val key: String, + + @SerialName("PrivateKey") + val privateKey: String, + + @SerialName("Kdf") + val kdfType: KdfTypeJson, + + @SerialName("KdfIterations") + val kdfIterations: Int?, + + @SerialName("KdfMemory") + val kdfMemory: Int?, + + @SerialName("KdfParallelism") + val kdfParallelism: Int?, + + @SerialName("ForcePasswordReset") + val shouldForcePasswordReset: Boolean, + + @SerialName("ResetMasterPassword") + val shouldResetMasterPassword: Boolean, + + @SerialName("MasterPasswordPolicy") + val masterPasswordPolicyOptions: MasterPasswordPolicyOptionsJson?, + + @SerialName("UserDecryptionOptions") + val userDecryptionOptions: UserDecryptionOptionsJson?, ) : GetTokenResponseJson() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/KeyConnectorUserDecryptionOptionsJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/KeyConnectorUserDecryptionOptionsJson.kt new file mode 100644 index 0000000000..221be523ee --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/KeyConnectorUserDecryptionOptionsJson.kt @@ -0,0 +1,15 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Decryption options related to a user's key connector. + * + * @property keyConnectorUrl URL to the user's key connector. + */ +@Serializable +data class KeyConnectorUserDecryptionOptionsJson( + @SerialName("KeyConnectorUrl") + val keyConnectorUrl: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/MasterPasswordPolicyOptionsJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/MasterPasswordPolicyOptionsJson.kt new file mode 100644 index 0000000000..f8c9fdba5b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/MasterPasswordPolicyOptionsJson.kt @@ -0,0 +1,39 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Policies that may be applied to a master password. + * + * @property minimumComplexity The minimum required password complexity (if applicable). + * @property minimumLength The minimum required password length (if applicable). + * @property shouldRequireUppercase Whether or not uppercase characters should be required. + * @property shouldRequireLowercase Whether or not lowercase characters should be required. + * @property shouldRequireNumbers Whether or not numbers should be required. + * @property shouldRequireSpecialCharacters Whether or not special characters should be required. + * @property shouldEnforceOnLogin Whether or not the restrictions should be enforced on login. + */ +@Serializable +data class MasterPasswordPolicyOptionsJson( + @SerialName("MinComplexity") + val minimumComplexity: Int?, + + @SerialName("MinLength") + val minimumLength: Int?, + + @SerialName("RequireUpper") + val shouldRequireUppercase: Boolean?, + + @SerialName("RequireLower") + val shouldRequireLowercase: Boolean?, + + @SerialName("RequireNumbers") + val shouldRequireNumbers: Boolean?, + + @SerialName("RequireSpecial") + val shouldRequireSpecialCharacters: Boolean?, + + @SerialName("EnforceOnLogin") + val shouldEnforceOnLogin: Boolean?, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceUserDecryptionOptionsJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceUserDecryptionOptionsJson.kt new file mode 100644 index 0000000000..42872d9c74 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TrustedDeviceUserDecryptionOptionsJson.kt @@ -0,0 +1,32 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Decryption options related to a user's trusted device. + * + * @property encryptedPrivateKey The user's encrypted private key. + * @property encryptedUserKey The user's encrypted key. + * @property hasAdminApproval Whether or not the user has admin approval. + * @property hasLoginApprovingDevice Whether or not the user has a login approving device. + * @property hasManageResetPasswordPermission Whether or not the user has manage reset password + * permission. + */ +@Serializable +data class TrustedDeviceUserDecryptionOptionsJson( + @SerialName("EncryptedPrivateKey") + val encryptedPrivateKey: String?, + + @SerialName("EncryptedUserKey") + val encryptedUserKey: String?, + + @SerialName("HasAdminApproval") + val hasAdminApproval: Boolean, + + @SerialName("HasLoginApprovingDevice") + val hasLoginApprovingDevice: Boolean, + + @SerialName("HasManageResetPasswordPermission") + val hasManageResetPasswordPermission: Boolean, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/UserDecryptionOptionsJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/UserDecryptionOptionsJson.kt new file mode 100644 index 0000000000..d4dcb29e7b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/UserDecryptionOptionsJson.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * The options available to a user for decryption. + * + * @property hasMasterPassword Whether the current user has a master password that can be used to + * decrypt their vault. + * @property trustedDeviceUserDecryptionOptions Decryption options related to a user's trusted + * device. + * @property keyConnectorUserDecryptionOptions Decryption options related to a user's key connector. + */ +@Serializable +data class UserDecryptionOptionsJson( + @SerialName("HasMasterPassword") + val hasMasterPassword: Boolean, + + @SerialName("TrustedDeviceOption") + val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?, + + @SerialName("KeyConnectorOption") + val keyConnectorUserDecryptionOptions: KeyConnectorUserDecryptionOptionsJson?, +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt index 9aae29a6b3..aea0da6009 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt @@ -2,6 +2,11 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.MasterPasswordPolicyOptionsJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson import com.x8bit.bitwarden.data.platform.base.BaseServiceTest import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider import io.mockk.every @@ -87,10 +92,79 @@ private val CAPTCHA_BODY = GetTokenResponseJson.CaptchaRequired("123") private const val LOGIN_SUCCESS_JSON = """ { - "access_token": "123" + "access_token": "accessToken", + "expires_in": 3600, + "token_type": "Bearer", + "refresh_token": "refreshToken", + "PrivateKey": "privateKey", + "Key": "key", + "MasterPasswordPolicy": { + "MinComplexity": 10, + "MinLength": 100, + "RequireUpper": true, + "RequireLower": true, + "RequireNumbers": true, + "RequireSpecial": true, + "EnforceOnLogin": true + }, + "ForcePasswordReset": true, + "ResetMasterPassword": true, + "Kdf": 1, + "KdfIterations": 600000, + "KdfMemory": 16, + "KdfParallelism": 4, + "UserDecryptionOptions": { + "HasMasterPassword": true, + "TrustedDeviceOption": { + "EncryptedPrivateKey": "encryptedPrivateKey", + "EncryptedUserKey": "encryptedUserKey", + "HasAdminApproval": true, + "HasLoginApprovingDevice": true, + "HasManageResetPasswordPermission": true + }, + "KeyConnectorOption": { + "KeyConnectorUrl": "keyConnectorUrl" + } + } } """ -private val LOGIN_SUCCESS = GetTokenResponseJson.Success("123") + +private val LOGIN_SUCCESS = GetTokenResponseJson.Success( + accessToken = "accessToken", + refreshToken = "refreshToken", + tokenType = "Bearer", + expiresInSeconds = 3600, + key = "key", + kdfType = KdfTypeJson.ARGON2_ID, + kdfIterations = 600000, + kdfMemory = 16, + kdfParallelism = 4, + privateKey = "privateKey", + shouldForcePasswordReset = true, + shouldResetMasterPassword = true, + masterPasswordPolicyOptions = MasterPasswordPolicyOptionsJson( + minimumComplexity = 10, + minimumLength = 100, + shouldRequireUppercase = true, + shouldRequireLowercase = true, + shouldRequireNumbers = true, + shouldRequireSpecialCharacters = true, + shouldEnforceOnLogin = true, + ), + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = TrustedDeviceUserDecryptionOptionsJson( + encryptedPrivateKey = "encryptedPrivateKey", + encryptedUserKey = "encryptedUserKey", + hasAdminApproval = true, + hasLoginApprovingDevice = true, + hasManageResetPasswordPermission = true, + ), + keyConnectorUserDecryptionOptions = KeyConnectorUserDecryptionOptionsJson( + keyConnectorUrl = "keyConnectorUrl", + ), + ), +) private const val INVALID_LOGIN_JSON = """ { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index ee44356f60..9d1fdc7379 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -141,6 +141,9 @@ class AuthRepositoryTest { @Test fun `login get token succeeds should return Success and update AuthState`() = runTest { + val successResponse = mockk { + every { accessToken } returns ACCESS_TOKEN + } coEvery { accountsService.preLogin(email = EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS) @@ -151,7 +154,7 @@ class AuthRepositoryTest { captchaToken = null, ) } - .returns(Result.success(GetTokenResponseJson.Success(accessToken = ACCESS_TOKEN))) + .returns(Result.success(successResponse)) every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) assertEquals(LoginResult.Success, result) @@ -205,6 +208,9 @@ class AuthRepositoryTest { @Test fun `logout should change AuthState to be Unauthenticated`() = runTest { // First login: + val successResponse = mockk { + every { accessToken } returns ACCESS_TOKEN + } coEvery { accountsService.preLogin(email = EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS) @@ -215,7 +221,8 @@ class AuthRepositoryTest { captchaToken = null, ) } - .returns(Result.success(GetTokenResponseJson.Success(accessToken = ACCESS_TOKEN))) + .returns(Result.success(successResponse)) + every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)