mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Setup for generator policy implementation (#888)
This commit is contained in:
parent
d9d5eaeea2
commit
5ceec9b2f7
6 changed files with 252 additions and 0 deletions
|
@ -42,4 +42,60 @@ sealed class PolicyInformation {
|
|||
@SerialName("enforceOnLogin")
|
||||
val enforceOnLogin: Boolean?,
|
||||
) : PolicyInformation()
|
||||
|
||||
/**
|
||||
* Represents a policy enforcing rules on the password generator.
|
||||
*
|
||||
* @property defaultType The default type of password to be generated.
|
||||
* @property minLength The minimum length of the password.
|
||||
* @property useUpper Whether the password requires upper case letters.
|
||||
* @property useLower Whether the password requires lower case letters.
|
||||
* @property useNumbers Whether the password requires numbers.
|
||||
* @property useSpecial Whether the password requires special characters.
|
||||
* @property minNumbers The minimum number of digits in the password.
|
||||
* @property minSpecial The minimum number of special characters in the password.
|
||||
* @property minNumberWords The minimum number of words in a passphrase.
|
||||
* @property capitalize Whether to capitalize the first character of each word in a passphrase.
|
||||
* @property includeNumber Whether to include a number at the end of a passphrase.
|
||||
*/
|
||||
@Serializable
|
||||
data class PasswordGenerator(
|
||||
@SerialName("defaultType")
|
||||
val defaultType: String?,
|
||||
|
||||
@SerialName("minLength")
|
||||
val minLength: Int?,
|
||||
|
||||
@SerialName("useUpper")
|
||||
val useUpper: Boolean?,
|
||||
|
||||
@SerialName("useLower")
|
||||
val useLower: Boolean?,
|
||||
|
||||
@SerialName("useNumbers")
|
||||
val useNumbers: Boolean?,
|
||||
|
||||
@SerialName("useSpecial")
|
||||
val useSpecial: Boolean?,
|
||||
|
||||
@SerialName("minNumbers")
|
||||
val minNumbers: Int?,
|
||||
|
||||
@SerialName("minSpecial")
|
||||
val minSpecial: Int?,
|
||||
|
||||
@SerialName("minNumberWords")
|
||||
val minNumberWords: Int?,
|
||||
|
||||
@SerialName("capitalize")
|
||||
val capitalize: Boolean?,
|
||||
|
||||
@SerialName("includeNumber")
|
||||
val includeNumber: Boolean?,
|
||||
) : PolicyInformation() {
|
||||
companion object {
|
||||
const val TYPE_PASSWORD: String = "password"
|
||||
const val TYPE_PASSPHRASE: String = "passphrase"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,9 @@ val SyncResponseJson.Policy.policyInformation: PolicyInformation?
|
|||
PolicyTypeJson.MASTER_PASSWORD -> {
|
||||
Json.decodeFromString<PolicyInformation.MasterPassword>(it)
|
||||
}
|
||||
PolicyTypeJson.PASSWORD_GENERATOR -> {
|
||||
Json.decodeFromString<PolicyInformation.PasswordGenerator>(it)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.bitwarden.core.PasswordHistoryView
|
|||
import com.bitwarden.generators.PassphraseGeneratorRequest
|
||||
import com.bitwarden.generators.PasswordGeneratorRequest
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
|
||||
|
@ -84,6 +85,11 @@ interface GeneratorRepository {
|
|||
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
|
||||
): GeneratedForwardedServiceUsernameResult
|
||||
|
||||
/**
|
||||
* Get the policy for password generation.
|
||||
*/
|
||||
fun getPasswordGeneratorPolicy(): PolicyInformation.PasswordGenerator?
|
||||
|
||||
/**
|
||||
* Get the [PasscodeGenerationOptions] for the current user.
|
||||
*/
|
||||
|
|
|
@ -7,6 +7,8 @@ import com.bitwarden.generators.PassphraseGeneratorRequest
|
|||
import com.bitwarden.generators.PasswordGeneratorRequest
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
|
@ -24,6 +26,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandom
|
|||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
@ -39,6 +42,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Default implementation of [GeneratorRepository].
|
||||
|
@ -197,6 +201,75 @@ class GeneratorRepositoryImpl(
|
|||
},
|
||||
)
|
||||
|
||||
@Suppress("LongMethod", "ReturnCount", "CyclomaticComplexMethod")
|
||||
override fun getPasswordGeneratorPolicy(): PolicyInformation.PasswordGenerator? {
|
||||
val userId = authDiskSource.userState?.activeUserId ?: return null
|
||||
val policies = authDiskSource.getPolicies(userId) ?: return null
|
||||
|
||||
var minLength: Int? = null
|
||||
var useUpper = false
|
||||
var useLower = false
|
||||
var useNumbers = false
|
||||
var useSpecial = false
|
||||
var minNumbers: Int? = null
|
||||
var minSpecial: Int? = null
|
||||
var minNumberWords: Int? = null
|
||||
var capitalize = false
|
||||
var includeNumber = false
|
||||
|
||||
var isPassphrasePresent = false
|
||||
policies.filter { it.type == PolicyTypeJson.PASSWORD_GENERATOR && it.isEnabled }
|
||||
.mapNotNull { it.policyInformation as? PolicyInformation.PasswordGenerator }
|
||||
.forEach { policy ->
|
||||
if (policy.defaultType == PolicyInformation.PasswordGenerator.TYPE_PASSPHRASE) {
|
||||
isPassphrasePresent = true
|
||||
}
|
||||
minLength = max(minLength ?: 0, policy.minLength ?: 0)
|
||||
useUpper = useUpper || policy.useUpper == true
|
||||
useLower = useLower || policy.useLower == true
|
||||
useNumbers = useNumbers || policy.useNumbers == true
|
||||
useSpecial = useSpecial || policy.useSpecial == true
|
||||
minNumbers = max(minNumbers ?: 0, policy.minNumbers ?: 0)
|
||||
minSpecial = max(minSpecial ?: 0, policy.minSpecial ?: 0)
|
||||
minNumberWords = max(minNumberWords ?: 0, policy.minNumberWords ?: 0)
|
||||
capitalize = capitalize || policy.capitalize == true
|
||||
includeNumber = includeNumber || policy.includeNumber == true
|
||||
}
|
||||
|
||||
// Only return a new policy if any policy settings were actually provided
|
||||
return PolicyInformation.PasswordGenerator(
|
||||
defaultType = if (isPassphrasePresent) {
|
||||
PolicyInformation.PasswordGenerator.TYPE_PASSPHRASE
|
||||
} else {
|
||||
PolicyInformation.PasswordGenerator.TYPE_PASSWORD
|
||||
},
|
||||
minLength = minLength,
|
||||
useUpper = useUpper,
|
||||
useLower = useLower,
|
||||
useNumbers = useNumbers,
|
||||
useSpecial = useSpecial,
|
||||
minNumbers = minNumbers,
|
||||
minSpecial = minSpecial,
|
||||
minNumberWords = minNumberWords,
|
||||
capitalize = capitalize,
|
||||
includeNumber = includeNumber,
|
||||
).takeIf {
|
||||
listOf(
|
||||
minLength,
|
||||
useUpper,
|
||||
useLower,
|
||||
useNumbers,
|
||||
useSpecial,
|
||||
minNumbers,
|
||||
minSpecial,
|
||||
minNumberWords,
|
||||
capitalize,
|
||||
includeNumber,
|
||||
)
|
||||
.any { it != null }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
|
||||
val userId = authDiskSource.userState?.activeUserId
|
||||
return userId?.let { generatorDiskSource.getPasscodeGenerationOptions(it) }
|
||||
|
|
|
@ -17,6 +17,7 @@ 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.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
|
@ -32,6 +33,8 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAd
|
|||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
@ -44,8 +47,12 @@ import io.mockk.unmockkStatic
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -759,6 +766,102 @@ class GeneratorRepositoryTest {
|
|||
generatorDiskSource.storeUsernameGenerationOptions(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPasswordGeneratorPolicy returns default settings when no policies are present`() = runTest {
|
||||
val userId = "testUserId"
|
||||
coEvery { authDiskSource.userState?.activeUserId } returns userId
|
||||
coEvery { authDiskSource.getPolicies(userId) } returns emptyList()
|
||||
|
||||
val policy = repository.getPasswordGeneratorPolicy()
|
||||
|
||||
val expectedPolicy = PolicyInformation.PasswordGenerator(
|
||||
defaultType = "password",
|
||||
minLength = null,
|
||||
useUpper = false,
|
||||
useLower = false,
|
||||
useNumbers = false,
|
||||
useSpecial = false,
|
||||
minNumbers = null,
|
||||
minSpecial = null,
|
||||
minNumberWords = null,
|
||||
capitalize = false,
|
||||
includeNumber = false,
|
||||
)
|
||||
|
||||
assertNotNull(policy)
|
||||
assertEquals(expectedPolicy, policy)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPasswordGeneratorPolicy applies strictest settings from multiple policies`() = runTest {
|
||||
val userId = "testUserId"
|
||||
val policy1 = PolicyInformation.PasswordGenerator(
|
||||
defaultType = "password",
|
||||
minLength = 8,
|
||||
useUpper = true,
|
||||
useLower = true,
|
||||
useNumbers = true,
|
||||
useSpecial = false,
|
||||
minNumbers = 1,
|
||||
minSpecial = 1,
|
||||
minNumberWords = 3,
|
||||
capitalize = true,
|
||||
includeNumber = true,
|
||||
)
|
||||
val policy2 = PolicyInformation.PasswordGenerator(
|
||||
defaultType = "passphrase", // Different type, more specific in this context
|
||||
minLength = 12, // More strict
|
||||
useUpper = true,
|
||||
useLower = true,
|
||||
useNumbers = true,
|
||||
useSpecial = true, // More strict
|
||||
minNumbers = 2, // More strict
|
||||
minSpecial = 2, // More strict
|
||||
minNumberWords = 4, // More strict
|
||||
capitalize = false, // Different, less strict, should not override
|
||||
includeNumber = false, // Different, less strict, should not override
|
||||
)
|
||||
val policies = listOf(
|
||||
SyncResponseJson.Policy(
|
||||
id = "1",
|
||||
type = PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
isEnabled = true,
|
||||
data = Json.encodeToJsonElement(policy1).jsonObject,
|
||||
organizationId = "id1",
|
||||
),
|
||||
SyncResponseJson.Policy(
|
||||
id = "2",
|
||||
type = PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
isEnabled = true,
|
||||
data = Json.encodeToJsonElement(policy2).jsonObject,
|
||||
organizationId = "id2",
|
||||
),
|
||||
)
|
||||
coEvery { authDiskSource.userState?.activeUserId } returns userId
|
||||
coEvery { authDiskSource.getPolicies(userId) } returns policies
|
||||
|
||||
val resultPolicy = repository.getPasswordGeneratorPolicy()
|
||||
|
||||
// The expected combined policy
|
||||
val expectedPolicy = PolicyInformation.PasswordGenerator(
|
||||
defaultType = "passphrase",
|
||||
minLength = 12,
|
||||
useUpper = true,
|
||||
useLower = true,
|
||||
useNumbers = true,
|
||||
useSpecial = true,
|
||||
minNumbers = 2,
|
||||
minSpecial = 2,
|
||||
minNumberWords = 4,
|
||||
capitalize = true,
|
||||
includeNumber = true,
|
||||
)
|
||||
|
||||
assertEquals(expectedPolicy, resultPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
private val USER_STATE = UserStateJson(
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.bitwarden.core.PasswordHistoryView
|
|||
import com.bitwarden.generators.PassphraseGeneratorRequest
|
||||
import com.bitwarden.generators.PasswordGeneratorRequest
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
|
@ -62,6 +63,8 @@ class FakeGeneratorRepository : GeneratorRepository {
|
|||
generatedEmailAddress = "updatedUsername",
|
||||
)
|
||||
|
||||
private var passwordGeneratorPolicy: PolicyInformation.PasswordGenerator? = null
|
||||
|
||||
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
||||
get() = mutablePasswordHistoryStateFlow
|
||||
|
||||
|
@ -135,6 +138,10 @@ class FakeGeneratorRepository : GeneratorRepository {
|
|||
mutablePasswordHistoryStateFlow.value = LocalDataState.Loaded(emptyList())
|
||||
}
|
||||
|
||||
override fun getPasswordGeneratorPolicy(): PolicyInformation.PasswordGenerator? {
|
||||
return passwordGeneratorPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mock result for the generatePassword function.
|
||||
*/
|
||||
|
@ -184,4 +191,8 @@ class FakeGeneratorRepository : GeneratorRepository {
|
|||
fun setMockRandomWordResult(result: GeneratedRandomWordUsernameResult) {
|
||||
generateRandomWordUsernameResult = result
|
||||
}
|
||||
|
||||
fun setMockPasswordGeneratorPolicy(policy: PolicyInformation.PasswordGenerator?) {
|
||||
this.passwordGeneratorPolicy = policy
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue