Merge pull request #2893 from element-hq/feature/valere/add_device_keys_to_rs
Add public device keys to rageshakes
This commit is contained in:
commit
544d064706
6 changed files with 255 additions and 2 deletions
1
changelog.d/2893.misc
Normal file
1
changelog.d/2893.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
BugReporting | Add public device keys to rageshakes
|
||||
|
|
@ -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(
|
|||
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(
|
|||
|
||||
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(
|
|||
.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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
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 {
|
|||
buildMeta = buildMeta,
|
||||
bugReporterUrlProvider = { server.url("/") },
|
||||
sdkMetadata = FakeSdkMetadata("123456789"),
|
||||
matrixClientProvider = FakeMatrixClientProvider()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue