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.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn 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.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.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -79,6 +82,7 @@ class DefaultBugReporter @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val bugReporterUrlProvider: BugReporterUrlProvider, private val bugReporterUrlProvider: BugReporterUrlProvider,
private val sdkMetadata: SdkMetadata, private val sdkMetadata: SdkMetadata,
private val matrixClientsProvider: MatrixClientProvider,
) : BugReporter { ) : BugReporter {
companion object { companion object {
// filenames // filenames
@ -156,6 +160,19 @@ class DefaultBugReporter @Inject constructor(
.addFormDataPart("user_id", userId) .addFormDataPart("user_id", userId)
.addFormDataPart("can_contact", canContact.toString()) .addFormDataPart("can_contact", canContact.toString())
.addFormDataPart("device_id", deviceId) .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("device", Build.MODEL.trim())
.addFormDataPart("locale", Locale.getDefault().toString()) .addFormDataPart("locale", Locale.getDefault().toString())
.addFormDataPart("sdk_sha", sdkMetadata.sdkGitSha) .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.api.reporter.BugReporterListener
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder 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.FakeSdkMetadata
import io.element.android.libraries.matrix.test.core.aBuildMeta 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.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.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.MultipartReader
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
@ -84,6 +92,90 @@ class DefaultBugReporterTest {
assertThat(onUploadSucceedCalled).isTrue() 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 @Test
fun `test sendBugReport error`() = runTest { fun `test sendBugReport error`() = runTest {
val server = MockWebServer() val server = MockWebServer()
@ -150,6 +242,7 @@ class DefaultBugReporterTest {
buildMeta = buildMeta, buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") }, bugReporterUrlProvider = { server.url("/") },
sdkMetadata = FakeSdkMetadata("123456789"), sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientsProvider = FakeMatrixClientProvider()
) )
} }

View file

@ -50,4 +50,16 @@ interface EncryptionService {
* Wait for backup upload steady state. * Wait for backup upload steady state.
*/ */
fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> 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() 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 enableBackupsFailure: Exception? = null
private var curve25519: String? = null
private var ed25519: String? = null
fun givenEnableBackupsFailure(exception: Exception?) { fun givenEnableBackupsFailure(exception: Exception?) {
enableBackupsFailure = exception enableBackupsFailure = exception
} }
@ -94,6 +97,15 @@ class FakeEncryptionService : EncryptionService {
return waitForBackupUploadSteadyStateFlow 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) { suspend fun emitBackupState(state: BackupState) {
backupStateStateFlow.emit(state) backupStateStateFlow.emit(state)
} }