[PM-31982] Add CookieDiskSource for cookie persistence (#6504)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <SaintPatrck@users.noreply.github.com>
This commit is contained in:
Patrick Honkonen
2026-02-10 10:39:16 -05:00
committed by GitHub
parent f0837f7668
commit d8c69a3243
5 changed files with 228 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.datasource.disk.model.CookieConfigurationData
/**
* Disk source for cookie persistence.
*/
interface CookieDiskSource {
/**
* Gets cookie configuration for a specific [hostname].
*
* @param hostname The server hostname to retrieve configuration for.
* @return The [CookieConfigurationData] if found, or null if no cookies stored.
*/
fun getCookieConfig(hostname: String): CookieConfigurationData?
/**
* Stores cookie [config] for the given [hostname].
*
* @param hostname The server hostname to associate with this configuration.
* @param config The [CookieConfigurationData] to persist.
*/
fun storeCookieConfig(hostname: String, config: CookieConfigurationData)
}

View File

@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.model.CookieConfigurationData
import kotlinx.serialization.json.Json
private const val CONFIG_PREFIX = "elb_cookie_config"
/**
* Implementation of [CookieDiskSource] using encrypted SharedPreferences.
*
* Simple storage layer for cookies.
*/
class CookieDiskSourceImpl(
sharedPreferences: SharedPreferences,
encryptedSharedPreferences: SharedPreferences,
private val json: Json,
) : CookieDiskSource,
BaseEncryptedDiskSource(
sharedPreferences = sharedPreferences,
encryptedSharedPreferences = encryptedSharedPreferences,
) {
override fun getCookieConfig(hostname: String): CookieConfigurationData? {
val key = CONFIG_PREFIX.appendIdentifier(hostname)
return getEncryptedString(key)
?.let { json.decodeFromStringOrNull<CookieConfigurationData>(it) }
}
override fun storeCookieConfig(hostname: String, config: CookieConfigurationData) {
val key = CONFIG_PREFIX.appendIdentifier(hostname)
putEncryptedString(key, json.encodeToString(config))
}
}

View File

@@ -8,6 +8,8 @@ import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
@@ -155,4 +157,16 @@ object PlatformDiskModule {
): FeatureFlagOverrideDiskSource = FeatureFlagOverrideDiskSourceImpl(
sharedPreferences = sharedPreferences,
)
@Provides
@Singleton
fun provideCookieDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
json: Json,
): CookieDiskSource = CookieDiskSourceImpl(
sharedPreferences = sharedPreferences,
encryptedSharedPreferences = encryptedSharedPreferences,
json = json,
)
}

View File

@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.model
import kotlinx.serialization.Serializable
/**
* Simple domain model for cookie storage.
*
* @property hostname The server hostname this configuration applies to.
* @property cookies The list of cookies for this server configuration.
*/
@Serializable
data class CookieConfigurationData(
val hostname: String,
val cookies: List<Cookie>,
) {
/**
* Simple domain model for a cookie.
*
* @property name The cookie name.
* @property value The cookie value.
*/
@Serializable
data class Cookie(
val name: String,
val value: String,
)
}

View File

@@ -0,0 +1,126 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.bitwarden.core.di.CoreModule
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.model.CookieConfigurationData
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class CookieDiskSourceTest {
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
private val fakeSharedPreferences = FakeSharedPreferences()
private val json = CoreModule.providesJson()
private val cookieDiskSource: CookieDiskSource = CookieDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
json = json,
)
@Test
fun `getCookieConfig should return null when no config exists`() {
assertNull(cookieDiskSource.getCookieConfig("example.com"))
}
@Test
fun `storeCookieConfig should persist config and getCookieConfig should retrieve it`() {
val hostname = "vault.bitwarden.com"
val config = CookieConfigurationData(
hostname = hostname,
cookies = listOf(
CookieConfigurationData.Cookie(
name = "BW_SESSION",
value = "encrypted_cookie_value",
),
),
)
cookieDiskSource.storeCookieConfig(hostname, config)
val retrieved = cookieDiskSource.getCookieConfig(hostname)
assertEquals(config, retrieved)
}
@Test
fun `storeCookieConfig should update existing config`() {
val hostname = "vault.bitwarden.com"
val initialConfig = CookieConfigurationData(
hostname = hostname,
cookies = listOf(
CookieConfigurationData.Cookie(
name = "SESSION",
value = "initial_value",
),
),
)
val updatedConfig = CookieConfigurationData(
hostname = hostname,
cookies = listOf(
CookieConfigurationData.Cookie(
name = "SESSION",
value = "updated_value",
),
),
)
cookieDiskSource.storeCookieConfig(hostname, initialConfig)
cookieDiskSource.storeCookieConfig(hostname, updatedConfig)
val retrieved = cookieDiskSource.getCookieConfig(hostname)
assertEquals(updatedConfig, retrieved)
}
@Test
fun `storage should handle cookies with multiple values`() {
val hostname = "vault.bitwarden.com"
val config = CookieConfigurationData(
hostname = hostname,
cookies = listOf(
CookieConfigurationData.Cookie(
name = "BW_SESSION",
value = "session_value",
),
CookieConfigurationData.Cookie(
name = "BW_REFRESH",
value = "refresh_value",
),
),
)
cookieDiskSource.storeCookieConfig(hostname, config)
assertEquals(config, cookieDiskSource.getCookieConfig(hostname))
}
@Test
fun `storage should isolate configs by hostname`() {
val hostname1 = "vault.bitwarden.com"
val hostname2 = "other.bitwarden.com"
val config1 = CookieConfigurationData(
hostname = hostname1,
cookies = listOf(
CookieConfigurationData.Cookie(
name = "A",
value = "1",
),
),
)
val config2 = CookieConfigurationData(
hostname = hostname2,
cookies = listOf(
CookieConfigurationData.Cookie(
name = "B",
value = "2",
),
),
)
cookieDiskSource.storeCookieConfig(hostname1, config1)
cookieDiskSource.storeCookieConfig(hostname2, config2)
assertEquals(config1, cookieDiskSource.getCookieConfig(hostname1))
assertEquals(config2, cookieDiskSource.getCookieConfig(hostname2))
}
}