fix: Avoid being flagged as a bot by using desktop devices' personas #324

This commit is contained in:
Artem Chepurnoy 2024-05-05 15:59:08 +03:00
parent b5a8e01e1c
commit 1bae2319e3
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
2 changed files with 70 additions and 13 deletions

View File

@ -1,6 +1,8 @@
package com.artemchep.keyguard.provider.bitwarden.api.builder
import com.artemchep.keyguard.platform.CurrentPlatform
import com.artemchep.keyguard.provider.bitwarden.ServerEnv
import com.artemchep.keyguard.provider.bitwarden.api.BitwardenPersona
import com.artemchep.keyguard.provider.bitwarden.api.entity.SyncResponse
import com.artemchep.keyguard.provider.bitwarden.entity.AttachmentEntity
import com.artemchep.keyguard.provider.bitwarden.entity.AvatarRequestEntity
@ -551,10 +553,14 @@ private suspend inline fun String.delete(
.bodyOrApiException<HttpResponse>()
fun HttpRequestBuilder.headers(env: ServerEnv) {
// Let Bitwarden know who we are.
header("Keyguard-Client", "1")
// Seems like now Bitwarden now requires you to specify
// the client name and version.
header("Bitwarden-Client-Name", "web")
header("Bitwarden-Client-Version", "2024.03.0")
val persona = CurrentPlatform
.let(BitwardenPersona::of)
header("Bitwarden-Client-Name", persona.clientName)
header("Bitwarden-Client-Version", persona.clientVersion)
// App does not work if hidden behind reverse-proxy under
// a subdirectory. We should specify the 'referer' so the server
// generates correct urls for us.

View File

@ -9,6 +9,8 @@ import com.artemchep.keyguard.common.service.text.url
import com.artemchep.keyguard.common.usecase.DeviceIdUseCase
import com.artemchep.keyguard.common.util.int
import com.artemchep.keyguard.core.store.bitwarden.BitwardenToken
import com.artemchep.keyguard.platform.CurrentPlatform
import com.artemchep.keyguard.platform.Platform
import com.artemchep.keyguard.provider.bitwarden.ServerEnv
import com.artemchep.keyguard.provider.bitwarden.ServerTwoFactorToken
import com.artemchep.keyguard.provider.bitwarden.api.builder.api
@ -41,8 +43,52 @@ import java.util.Locale
private const val PBKDF2_KEY_LENGTH = 32
private const val CLIENT_ID = "web"
private const val DEVICE_TYPE = "9"
data class BitwardenPersona(
val clientId: String,
val clientName: String,
val clientVersion: String,
val deviceType: String,
val deviceName: String,
) {
companion object {
private const val CLIENT_VERSION = "2024.4.0"
fun of(platform: Platform) = when (platform) {
is Platform.Mobile -> desktopLinux()
is Platform.Desktop -> when (platform) {
is Platform.Desktop.Windows -> desktopWindows()
is Platform.Desktop.MacOS -> desktopMacOs()
is Platform.Desktop.Other,
is Platform.Desktop.Linux,
-> desktopLinux()
}
}
private fun desktopLinux() = BitwardenPersona(
clientId = "desktop",
clientName = "desktop",
clientVersion = CLIENT_VERSION,
deviceType = "8",
deviceName = "linux",
)
private fun desktopMacOs() = BitwardenPersona(
clientId = "desktop",
clientName = "desktop",
clientVersion = CLIENT_VERSION,
deviceType = "7",
deviceName = "macos",
)
private fun desktopWindows() = BitwardenPersona(
clientId = "desktop",
clientName = "desktop",
clientVersion = CLIENT_VERSION,
deviceType = "6",
deviceName = "windows",
)
}
}
suspend fun login(
deviceIdUseCase: DeviceIdUseCase,
@ -91,9 +137,12 @@ private suspend fun internalLogin(
passwordKey: ByteArray,
): Login = httpClient
.post(env.identity.connect.token) {
val persona = CurrentPlatform
.let(BitwardenPersona::of)
headers(env)
header("device-type", DEVICE_TYPE)
header("dnt", "1")
header("device-type", persona.deviceType)
header("cache-control", "no-store")
// Official Bitwarden backend specifically checks for this header,
// which is just a base-64 string of an email.
val emailBase64 = base64Service
@ -108,7 +157,7 @@ private suspend fun internalLogin(
append("username", email)
append("password", passwordBase64)
append("scope", "api offline_access")
append("client_id", CLIENT_ID)
append("client_id", persona.clientId)
// As per
// https://github.com/bitwarden/cli/issues/383#issuecomment-937819752
// the backdoor to a captcha is a client secret.
@ -116,8 +165,8 @@ private suspend fun internalLogin(
append("captchaResponse", clientSecret)
}
append("deviceIdentifier", deviceId)
append("deviceType", DEVICE_TYPE)
append("deviceName", "chrome")
append("deviceType", persona.deviceType)
append("deviceName", persona.deviceName)
if (twoFactorToken != null) {
val providerEntity = TwoFactorProviderTypeEntity.of(twoFactorToken.provider)
@ -150,10 +199,12 @@ suspend fun refresh(
token: BitwardenToken.Token,
): Login = httpClient
.post(env.identity.connect.token) {
headers(env)
header("device-type", DEVICE_TYPE)
header("dnt", "1")
val persona = CurrentPlatform
.let(BitwardenPersona::of)
headers(env)
header("device-type", persona.deviceType)
header("cache-control", "no-store")
val clientId = kotlin.runCatching {
val jwtData = parseAccessTokenData(
base64Service = base64Service,
@ -162,7 +213,7 @@ suspend fun refresh(
)
jwtData["client_id"]?.jsonPrimitive?.content
}.getOrNull()
?: CLIENT_ID
?: persona.clientId
body = FormDataContent(
Parameters.build {
append("grant_type", "refresh_token")