From 12fdc2b6e014f2b3ab75c0cb1e9043af0deae846 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 22 May 2024 09:40:07 +0200 Subject: [PATCH] Add public device keys to rageshakes --- .../impl/reporter/DefaultBugReporter.kt | 17 ++++ .../impl/reporter/DefaultBugReporterTest.kt | 93 +++++++++++++++++++ .../api/encryption/EncryptionService.kt | 12 +++ .../impl/encryption/RustEncryptionService.kt | 8 ++ .../test/encryption/FakeEncryptionService.kt | 12 +++ 5 files changed, 142 insertions(+) diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 295fa17563..91a06b289d 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -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) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index f2c8d0d0a8..3aa0136f41 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -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() + 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() ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index 36c786a26f..f47487c634 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -50,4 +50,16 @@ interface EncryptionService { * Wait for backup upload steady state. */ fun waitForBackupUploadSteadyState(): Flow + + /** + * 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? } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index f5a6390989..68ab4a611e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -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() + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index cc7f53eca3..b864c69b0b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -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) }