Add public device keys to rageshakes

This commit is contained in:
Valere
2024-05-22 09:40:07 +02:00
parent 180c300a28
commit 12fdc2b6e0
5 changed files with 142 additions and 0 deletions

View File

@@ -36,7 +36,10 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException
@@ -79,6 +82,7 @@ class DefaultBugReporter @Inject constructor(
private val buildMeta: BuildMeta,
private val bugReporterUrlProvider: BugReporterUrlProvider,
private val sdkMetadata: SdkMetadata,
private val matrixClientsProvider: MatrixClientProvider,
) : BugReporter {
companion object {
// filenames
@@ -156,6 +160,19 @@ class DefaultBugReporter @Inject constructor(
.addFormDataPart("user_id", userId)
.addFormDataPart("can_contact", canContact.toString())
.addFormDataPart("device_id", deviceId)
.apply {
userId.takeIf { MatrixPatterns.isUserId(it) }?.let {
SessionId(it)
}?.let { sessionId ->
matrixClientsProvider.getOrNull(sessionId)?.let { client ->
val curveKey = client.encryptionService().deviceCurve25519()
val edKey = client.encryptionService().deviceEd25519()
if (curveKey != null && edKey != null) {
addFormDataPart("device_keys", "curve25519:$curveKey, ed25519:$edKey")
}
}
}
}
.addFormDataPart("device", Build.MODEL.trim())
.addFormDataPart("locale", Locale.getDefault().toString())
.addFormDataPart("sdk_sha", sdkMetadata.sdkGitSha)

View File

@@ -20,16 +20,24 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import okhttp3.MultipartReader
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -84,6 +92,90 @@ class DefaultBugReporterTest {
assertThat(onUploadSucceedCalled).isTrue()
}
@Test
fun `test sendBugReport form data`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(200)
)
server.start()
val mockSessionStore = InMemorySessionStore().apply {
storeData(
SessionData(
userId = "@foo:eample.com",
deviceId = "ABCDEFGH",
homeserverUrl = "example.com",
accessToken = "AA",
isTokenValid = true,
loginType = LoginType.DIRECT,
loginTimestamp = null,
oidcData = null,
refreshToken = null,
slidingSyncProxy = null,
passphrase = null
)
)
}
val buildMeta = aBuildMeta()
val fakeEncryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService)
fakeEncryptionService.givenDeviceKeys("CURVECURVECURVE", "EDKEYEDKEYEDKY")
val sut = DefaultBugReporter(
context = RuntimeEnvironment.getApplication(),
screenshotHolder = FakeScreenshotHolder(),
crashDataStore = FakeCrashDataStore(),
coroutineDispatchers = testCoroutineDispatchers(),
okHttpClient = { OkHttpClient.Builder().build() },
userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")),
sessionStore = mockSessionStore,
buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") },
sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
)
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = null
)
val request = server.takeRequest()
val boundary = request.headers["Content-Type"]!!.split("=").last()
val foundValues = HashMap<String, String>()
request.body.inputStream().source().buffer().use {
val multipartReader = MultipartReader(it, boundary)
// Just use simple parsing to detect basic properties
val regex = "form-data; name=\"(\\w*)\".*".toRegex()
multipartReader.use {
while (true) {
val part = multipartReader.nextPart() ?: break
val contentDisposition = part.headers["Content-Disposition"] ?: continue
regex.find(contentDisposition)?.groupValues?.get(1)?.let { name ->
foundValues.put(name, part.body.readUtf8())
}
}
}
}
assertThat(foundValues["app"]).isEqualTo("element-x-android")
assertThat(foundValues["can_contact"]).isEqualTo("true")
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
assertThat(foundValues["user_id"]).isEqualTo("@foo:eample.com")
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
server.shutdown()
}
@Test
fun `test sendBugReport error`() = runTest {
val server = MockWebServer()
@@ -150,6 +242,7 @@ class DefaultBugReporterTest {
buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") },
sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientsProvider = FakeMatrixClientProvider()
)
}

View File

@@ -50,4 +50,16 @@ interface EncryptionService {
* Wait for backup upload steady state.
*/
fun waitForBackupUploadSteadyState(): Flow<BackupUploadState>
/**
* Get the public curve25519 key of our own device in base64. This is usually what is
* called the identity key of the device.
*/
suspend fun deviceCurve25519(): String?
/**
* Get the public ed25519 key of our own device. This is usually what is
* called the fingerprint of the device.
*/
suspend fun deviceEd25519(): String?
}

View File

@@ -190,4 +190,12 @@ internal class RustEncryptionService(
it.mapRecoveryException()
}
}
override suspend fun deviceCurve25519(): String? {
return service.curve25519Key()
}
override suspend fun deviceEd25519(): String? {
return service.ed25519Key()
}
}

View File

@@ -39,6 +39,9 @@ class FakeEncryptionService : EncryptionService {
private var enableBackupsFailure: Exception? = null
private var curve25519: String? = null
private var ed25519: String? = null
fun givenEnableBackupsFailure(exception: Exception?) {
enableBackupsFailure = exception
}
@@ -94,6 +97,15 @@ class FakeEncryptionService : EncryptionService {
return waitForBackupUploadSteadyStateFlow
}
fun givenDeviceKeys(curve25519: String?, ed25519: String?) {
this.curve25519 = curve25519
this.ed25519 = ed25519
}
override suspend fun deviceCurve25519(): String? = curve25519
override suspend fun deviceEd25519(): String? = ed25519
suspend fun emitBackupState(state: BackupState) {
backupStateStateFlow.emit(state)
}