From 7b491d3c3c7c0235a9543bdb4c560ce0b1d83f35 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:32:54 -0500 Subject: [PATCH] [PM-28157] Add string extension to prefix URIs with www (#6183) Co-authored-by: Claude --- .../credentials/manager/OriginManagerImpl.kt | 9 ++- .../credentials/manager/OriginManagerTest.kt | 58 ++++++++++++++++++ .../ui/platform/base/util/StringExtensions.kt | 28 +++++++++ .../base/util/StringExtensionsTest.kt | 61 +++++++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerImpl.kt index 7db20e9516..def2f8e013 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerImpl.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.credentials.manager import androidx.credentials.provider.CallingAppInfo import com.bitwarden.network.service.DigitalAssetLinkService import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary +import com.bitwarden.ui.platform.base.util.prefixWwwIfNecessary import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository import com.x8bit.bitwarden.data.platform.manager.AssetManager @@ -40,7 +41,13 @@ class OriginManagerImpl( ): ValidateOriginResult { return digitalAssetLinkService .checkDigitalAssetLinksRelations( - sourceWebSite = relyingPartyId.prefixHttpsIfNecessary(), + sourceWebSite = relyingPartyId + // The DAL API does not allow redirects, so we add `www.` to prevent redirects + // when it is absent from the `relyingPartyId`. This ensures that relying + // parties storing their `assetlinks.json` at the `www.` subdomain do not fail + // verification checks. + .prefixWwwIfNecessary() + .prefixHttpsIfNecessary(), targetPackageName = callingAppInfo.packageName, targetCertificateFingerprint = callingAppInfo .getSignatureFingerprintAsHexString() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerTest.kt index 9cbe70d40d..1420342c24 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/credentials/manager/OriginManagerTest.kt @@ -242,6 +242,64 @@ class OriginManagerTest { ), ) } + + @Test + fun `validateOrigin should prefix www to rpId without www before checking asset links`() = + runTest { + coEvery { + mockDigitalAssetLinkService.checkDigitalAssetLinksRelations( + sourceWebSite = "https://www.example.com", + targetPackageName = DEFAULT_PACKAGE_NAME, + targetCertificateFingerprint = DEFAULT_CERT_FINGERPRINT, + relations = listOf("delegate_permission/common.handle_all_urls"), + ) + } returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE.asSuccess() + + val result = originManager.validateOrigin( + relyingPartyId = "example.com", + callingAppInfo = mockNonPrivilegedAppInfo, + ) + + assertEquals(ValidateOriginResult.Success(null), result) + } + + @Test + fun `validateOrigin should preserve existing www prefix when present`() = runTest { + coEvery { + mockDigitalAssetLinkService.checkDigitalAssetLinksRelations( + sourceWebSite = "https://www.example.com", + targetPackageName = DEFAULT_PACKAGE_NAME, + targetCertificateFingerprint = DEFAULT_CERT_FINGERPRINT, + relations = listOf("delegate_permission/common.handle_all_urls"), + ) + } returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE.asSuccess() + + val result = originManager.validateOrigin( + relyingPartyId = "www.example.com", + callingAppInfo = mockNonPrivilegedAppInfo, + ) + + assertEquals(ValidateOriginResult.Success(null), result) + } + + @Test + fun `validateOrigin should handle rpId with https scheme correctly`() = runTest { + coEvery { + mockDigitalAssetLinkService.checkDigitalAssetLinksRelations( + sourceWebSite = "https://www.example.com", + targetPackageName = DEFAULT_PACKAGE_NAME, + targetCertificateFingerprint = DEFAULT_CERT_FINGERPRINT, + relations = listOf("delegate_permission/common.handle_all_urls"), + ) + } returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE.asSuccess() + + val result = originManager.validateOrigin( + relyingPartyId = "https://example.com", + callingAppInfo = mockNonPrivilegedAppInfo, + ) + + assertEquals(ValidateOriginResult.Success(null), result) + } } private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden" diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringExtensions.kt index 00ff9947e9..3871d44f7f 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringExtensions.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringExtensions.kt @@ -239,6 +239,34 @@ fun String.prefixHttpsIfNecessaryOrNull(): String? = fun String.prefixHttpsIfNecessary(): String = prefixHttpsIfNecessaryOrNull() ?: this +/** + * If the given [String] is a valid URI, "www." will be prepended (or inserted after the scheme + * if present) if it is not already present. Otherwise `null` will be returned. + * + * Examples: + * - "example.com" -> "www.example.com" + * - "www.example.com" -> "www.example.com" + * - "https://example.com" -> "https://www.example.com" + * - "https://www.example.com" -> "https://www.example.com" + */ +fun String.prefixWwwIfNecessaryOrNull(): String? = + when { + this.isBlank() || !this.isValidUri() -> null + this.startsWith("www.") -> this + this.startsWith("http://") || this.startsWith("https://") -> { + if ("://www." in this) this else this.replaceFirst("://", "://www.") + } + + else -> "www.$this" + } + +/** + * If the given [String] is a valid URI, "www." will be prepended (or inserted after the scheme + * if present) if it is not already present. Otherwise the original [String] will be returned. + */ +fun String.prefixWwwIfNecessary(): String = + prefixWwwIfNecessaryOrNull() ?: this + /** * Checks if a string is using base32 digits. */ diff --git a/ui/src/test/kotlin/com/bitwarden/ui/platform/base/util/StringExtensionsTest.kt b/ui/src/test/kotlin/com/bitwarden/ui/platform/base/util/StringExtensionsTest.kt index 3a9de66085..5d4ad58c0c 100644 --- a/ui/src/test/kotlin/com/bitwarden/ui/platform/base/util/StringExtensionsTest.kt +++ b/ui/src/test/kotlin/com/bitwarden/ui/platform/base/util/StringExtensionsTest.kt @@ -281,4 +281,65 @@ class StringExtensionsTest { fun `orZeroWidthSpace returns the original value for a non-blank string`() { assertEquals("test", "test".orZeroWidthSpace()) } + + @Suppress("MaxLineLength") + @Test + fun `prefixWwwIfNecessaryOrNull should prefix www when URI is valid and no scheme and no www`() { + val uri = "example.com" + val expected = "www.$uri" + val actual = uri.prefixWwwIfNecessaryOrNull() + + assertEquals(expected, actual) + } + + @Test + fun `prefixWwwIfNecessaryOrNull should return URI unchanged when starts with www`() { + val uri = "www.example.com" + val actual = uri.prefixWwwIfNecessaryOrNull() + assertEquals(uri, actual) + } + + @Test + fun `prefixWwwIfNecessaryOrNull should prefix www when scheme is http and no www`() { + val uri = "http://example.com" + val expected = "http://www.example.com" + val actual = uri.prefixWwwIfNecessaryOrNull() + assertEquals(expected, actual) + } + + @Test + fun `prefixWwwIfNecessaryOrNull should prefix www when scheme is https and no www`() { + val uri = "https://example.com" + val expected = "https://www.example.com" + val actual = uri.prefixWwwIfNecessaryOrNull() + assertEquals(expected, actual) + } + + @Suppress("MaxLineLength") + @Test + fun `prefixWwwIfNecessaryOrNull should return URI unchanged when scheme is http and www is present`() { + val uri = "http://www.example.com" + val actual = uri.prefixWwwIfNecessaryOrNull() + assertEquals(uri, actual) + } + + @Suppress("MaxLineLength") + @Test + fun `prefixWwwIfNecessaryOrNull should return URI unchanged when scheme is https and www is present`() { + val uri = "https://www.example.com" + val actual = uri.prefixWwwIfNecessaryOrNull() + assertEquals(uri, actual) + } + + @Test + fun `prefixWwwIfNecessaryOrNull should return null when URI is empty string`() { + val uri = "" + assertNull(uri.prefixWwwIfNecessaryOrNull()) + } + + @Test + fun `prefixWwwIfNecessaryOrNull should return null when URI is invalid`() { + val invalidUri = "invalid uri" + assertNull(invalidUri.prefixWwwIfNecessaryOrNull()) + } }