Browse Source

Merge pull request #2893 from element-hq/feature/valere/add_device_keys_to_rs

Add public device keys to rageshakes
pull/2927/head
Valere 4 months ago committed by GitHub
parent
commit
544d064706
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      changelog.d/2893.misc
  2. 18
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
  3. 206
      features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt
  4. 12
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
  5. 8
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
  6. 12
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt

1
changelog.d/2893.misc

@ -0,0 +1 @@ @@ -0,0 +1 @@
BugReporting | Add public device keys to rageshakes

18
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt

@ -36,7 +36,9 @@ import io.element.android.libraries.core.mimetype.MimeTypes @@ -36,7 +36,9 @@ 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.UserId
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException
@ -79,6 +81,7 @@ class DefaultBugReporter @Inject constructor( @@ -79,6 +81,7 @@ class DefaultBugReporter @Inject constructor(
private val buildMeta: BuildMeta,
private val bugReporterUrlProvider: BugReporterUrlProvider,
private val sdkMetadata: SdkMetadata,
private val matrixClientProvider: MatrixClientProvider,
) : BugReporter {
companion object {
// filenames
@ -145,7 +148,7 @@ class DefaultBugReporter @Inject constructor( @@ -145,7 +148,7 @@ class DefaultBugReporter @Inject constructor(
val sessionData = sessionStore.getLatestSession()
val deviceId = sessionData?.deviceId ?: "undefined"
val userId = sessionData?.userId ?: "undefined"
val userId = sessionData?.userId?.let { UserId(it) }
if (!isCancelled) {
// build the multi part request
@ -153,9 +156,20 @@ class DefaultBugReporter @Inject constructor( @@ -153,9 +156,20 @@ class DefaultBugReporter @Inject constructor(
.addFormDataPart("text", bugDescription)
.addFormDataPart("app", context.getString(R.string.bug_report_app_name))
.addFormDataPart("user_agent", userAgentProvider.provide())
.addFormDataPart("user_id", userId)
.addFormDataPart("user_id", userId?.toString() ?: "undefined")
.addFormDataPart("can_contact", canContact.toString())
.addFormDataPart("device_id", deviceId)
.apply {
userId?.let {
matrixClientProvider.getOrNull(it)?.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)

206
features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt

@ -20,16 +20,25 @@ import com.google.common.truth.Truth.assertThat @@ -20,16 +20,25 @@ 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 okhttp3.mockwebserver.RecordedRequest
import okio.buffer
import okio.source
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@ -84,6 +93,202 @@ class DefaultBugReporterTest { @@ -84,6 +93,202 @@ 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(mockSessionData("@foo:eample.com", "ABCDEFGH"))
}
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"),
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
)
val progressValues = mutableListOf<Int>()
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
override fun onUploadCancelled() {}
override fun onUploadFailed(reason: String?) {}
override fun onProgress(progress: Int) {
progressValues.add(progress)
}
override fun onUploadSucceed() {}
},
)
val request = server.takeRequest()
val foundValues = collectValuesFromFormData(request)
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")
// device_key now added given they are not null
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE + 1)
server.shutdown()
}
@Test
fun `test sendBugReport should not report device_keys if not known`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(200)
)
server.start()
val mockSessionStore = InMemorySessionStore().apply {
storeData(mockSessionData("@foo:eample.com", "ABCDEFGH"))
}
val buildMeta = aBuildMeta()
val fakeEncryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService)
fakeEncryptionService.givenDeviceKeys(null, null)
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"),
matrixClientProvider = 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 foundValues = collectValuesFromFormData(request)
assertThat(foundValues["device_keys"]).isNull()
server.shutdown()
}
@Test
fun `test sendBugReport no client provider no session data`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(200)
)
server.start()
val buildMeta = aBuildMeta()
val fakeEncryptionService = FakeEncryptionService()
fakeEncryptionService.givenDeviceKeys(null, null)
val sut = DefaultBugReporter(
context = RuntimeEnvironment.getApplication(),
screenshotHolder = FakeScreenshotHolder(),
crashDataStore = FakeCrashDataStore("I did crash", true),
coroutineDispatchers = testCoroutineDispatchers(),
okHttpClient = { OkHttpClient.Builder().build() },
userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")),
sessionStore = InMemorySessionStore(),
buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") },
sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(Exception("Mock no client")) })
)
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = null
)
val request = server.takeRequest()
val foundValues = collectValuesFromFormData(request)
println("## FOUND VALUES $foundValues")
assertThat(foundValues["device_keys"]).isNull()
assertThat(foundValues["device_id"]).isEqualTo("undefined")
assertThat(foundValues["user_id"]).isEqualTo("undefined")
assertThat(foundValues["label"]).isEqualTo("crash")
}
private fun collectValuesFromFormData(request: RecordedRequest): HashMap<String, String> {
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 {
var part = multipartReader.nextPart()
while (part != null) {
part.headers["Content-Disposition"]?.let { contentDisposition ->
regex.find(contentDisposition)?.groupValues?.get(1)?.let { name ->
foundValues.put(name, part!!.body.readUtf8())
}
}
part = multipartReader.nextPart()
}
}
}
return foundValues
}
private fun mockSessionData(userId: String, deviceId: String) = SessionData(
userId = userId,
deviceId = deviceId,
homeserverUrl = "example.com",
accessToken = "AA",
isTokenValid = true,
loginType = LoginType.DIRECT,
loginTimestamp = null,
oidcData = null,
refreshToken = null,
slidingSyncProxy = null,
passphrase = null
)
@Test
fun `test sendBugReport error`() = runTest {
val server = MockWebServer()
@ -150,6 +355,7 @@ class DefaultBugReporterTest { @@ -150,6 +355,7 @@ class DefaultBugReporterTest {
buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") },
sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientProvider = FakeMatrixClientProvider()
)
}

12
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt

@ -50,4 +50,16 @@ interface EncryptionService { @@ -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?
}

8
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt

@ -190,4 +190,12 @@ internal class RustEncryptionService( @@ -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()
}
}

12
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt

@ -39,6 +39,9 @@ class FakeEncryptionService : EncryptionService { @@ -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 { @@ -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)
}

Loading…
Cancel
Save