Skip to content

Commit 2fc12ad

Browse files
committed
Expose service to let Element X access some internal data
Note: Using permission "${applicationId}.READ_DATA" with android:protectionLevel="normal" does not work in release mode.
1 parent 9dd2f43 commit 2fc12ad

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

vector/build.gradle

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ android {
7171
if (project.hasProperty("coverage")) {
7272
testCoverageEnabled = project.properties["coverage"] == "true"
7373
}
74+
buildConfigField(
75+
"String",
76+
"ELEMENT_X_APP_ID",
77+
"\"io.element.android.x.debug\"",
78+
)
79+
buildConfigField(
80+
"String",
81+
"ELEMENT_X_FINGERPRINT",
82+
"\"B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E\"",
83+
)
84+
}
85+
release {
86+
buildConfigField(
87+
"String",
88+
"ELEMENT_X_APP_ID",
89+
"\"io.element.android.x\"",
90+
)
91+
buildConfigField(
92+
"String",
93+
"ELEMENT_X_FINGERPRINT",
94+
"\"C6:DB:9B:9C:8C:BD:D6:5D:16:E8:EC:8C:8B:91:C8:31:B9:EF:C9:5C:BF:98:AE:41:F6:A9:D8:35:15:1A:7E:16\"",
95+
)
7496
}
7597
}
7698

vector/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,12 @@
415415
android:foregroundServiceType="microphone"
416416
android:permission="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
417417

418+
<service
419+
android:name=".features.importer.ImporterService"
420+
android:exported="true"
421+
android:process=":remote"
422+
tools:ignore="ExportedService" />
423+
418424
<!-- Receivers -->
419425

420426
<receiver
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2026 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package im.vector.app.features.importer
9+
10+
import android.app.Service
11+
import android.content.Intent
12+
import android.os.Bundle
13+
import android.os.Handler
14+
import android.os.IBinder
15+
import android.os.Looper
16+
import android.os.Message
17+
import android.os.Messenger
18+
import android.os.RemoteException
19+
import dagger.hilt.android.AndroidEntryPoint
20+
import im.vector.app.core.di.ActiveSessionHolder
21+
import kotlinx.coroutines.runBlocking
22+
import timber.log.Timber
23+
import javax.inject.Inject
24+
25+
@AndroidEntryPoint
26+
class ImporterService : Service() {
27+
private companion object {
28+
/**
29+
* Command to the service to get the data.
30+
*/
31+
const val MSG_GET_DATA = 1
32+
33+
const val KEY_ERROR_STR = "error"
34+
const val KEY_USER_ID_STR = "userId"
35+
const val KEY_IS_VERIFIED_BOOLEAN = "isVerified"
36+
const val KEY_MASTER_PRIVATE_KEY_STR = "masterPrivateKey"
37+
const val KEY_USER_PRIVATE_KEY_STR = "userPrivateKey"
38+
const val KEY_SELF_SIGNING_PRIVATE_KEY_STR = "selfSigningPrivateKey"
39+
const val KEY_KEY_BACKUP_KEY_STR = "keyBackupKey"
40+
}
41+
42+
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
43+
private val signaturePermissionChecker = SignaturePermissionChecker()
44+
45+
/**
46+
* Handler of incoming messages from clients.
47+
*/
48+
private inner class IncomingHandler : Handler(Looper.getMainLooper()) {
49+
override fun handleMessage(msg: Message) {
50+
Timber.w("ImporterService: handling message ${msg.what}")
51+
val replyTo = msg.replyTo
52+
if (replyTo == null) {
53+
Timber.e("ImporterService: no replyTo in the message, cannot answer")
54+
} else {
55+
val bundle = Bundle()
56+
if (signaturePermissionChecker.check(msg.sendingUid, packageManager)) {
57+
Timber.w("ImporterService: Authorized caller")
58+
when (msg.what) {
59+
MSG_GET_DATA -> bundle.putSessionData()
60+
else -> bundle.putString(KEY_ERROR_STR, "Unknown command ${msg.what}")
61+
}
62+
} else {
63+
Timber.w("ImporterService: Unauthorized caller")
64+
bundle.putString(KEY_ERROR_STR, "Unauthorized")
65+
}
66+
replyTo.sendResponse(msg.what, bundle)
67+
}
68+
}
69+
}
70+
71+
private fun Bundle.putSessionData() {
72+
val session = activeSessionHolder.getSafeActiveSession()
73+
if (session == null) {
74+
// Keep an empty Bundle to indicate that there is no session
75+
} else {
76+
putString(KEY_USER_ID_STR, session.myUserId)
77+
val crossSigningService = session.cryptoService().crossSigningService()
78+
val keysBackupService = session.cryptoService().keysBackupService()
79+
runBlocking {
80+
// TODO Use method exposed by the SDK (see https://github.com/matrix-org/matrix-rust-sdk/issues/6037)
81+
putBoolean(KEY_IS_VERIFIED_BOOLEAN, crossSigningService.isCrossSigningVerified())
82+
crossSigningService.getCrossSigningPrivateKeys()?.let { privateKeys ->
83+
putString(KEY_MASTER_PRIVATE_KEY_STR, privateKeys.master)
84+
putString(KEY_SELF_SIGNING_PRIVATE_KEY_STR, privateKeys.selfSigned)
85+
putString(KEY_USER_PRIVATE_KEY_STR, privateKeys.user)
86+
}
87+
val keyBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey?.toBase64()
88+
putString(KEY_KEY_BACKUP_KEY_STR, keyBackupKey)
89+
}
90+
}
91+
}
92+
93+
private fun Messenger.sendResponse(what: Int, bundle: Bundle) {
94+
Timber.d("ImporterService: send response to client")
95+
try {
96+
val message = Message.obtain(null, what).also {
97+
it.data = bundle
98+
}
99+
send(message)
100+
} catch (e: RemoteException) {
101+
// The client is dead.
102+
Timber.e(e, "ImporterService: The client is dead.")
103+
}
104+
}
105+
106+
/**
107+
* When binding to the service, we return an interface to our messenger
108+
* for sending messages to the service.
109+
*/
110+
override fun onBind(intent: Intent): IBinder? {
111+
Timber.w("ImporterService: onBind")
112+
val messenger = Messenger(IncomingHandler())
113+
return messenger.binder
114+
}
115+
116+
override fun onUnbind(intent: Intent?): Boolean {
117+
Timber.w("ImporterService: onUnbind")
118+
return super.onUnbind(intent)
119+
}
120+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2026 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package im.vector.app.features.importer
9+
10+
import android.content.pm.PackageInfo
11+
import android.content.pm.PackageManager
12+
import android.os.Build
13+
import im.vector.app.BuildConfig
14+
import timber.log.Timber
15+
import java.security.MessageDigest
16+
17+
class SignaturePermissionChecker {
18+
/**
19+
* Check if the calling UID is allowed to access the service.
20+
* @param sendingUid The UID of the calling process.
21+
* @param pm The PackageManager to use to get package info.
22+
* @return True if the calling UID is allowed, false otherwise.
23+
*/
24+
fun check(sendingUid: Int, pm: PackageManager): Boolean {
25+
Timber.w("ImporterService: callingUid: $sendingUid")
26+
val pkgs = pm.getPackagesForUid(sendingUid) ?: return false
27+
for (pkg in pkgs) {
28+
Timber.w("ImporterService: checking package: $pkg")
29+
if (pkg == BuildConfig.ELEMENT_X_APP_ID && isSignatureAllowed(pkg, pm)) {
30+
return true
31+
}
32+
}
33+
Timber.e("ImporterService: Unauthorized attempt, denying")
34+
return false
35+
}
36+
37+
private fun isSignatureAllowed(packageName: String, pm: PackageManager): Boolean {
38+
try {
39+
val fingerprints = getSignatureFingerprints(pm, packageName)
40+
if (fingerprints.any { fingerprint ->
41+
Timber.d("isSignatureAllowed: checking fingerprint $fingerprint")
42+
fingerprint == BuildConfig.ELEMENT_X_FINGERPRINT
43+
}
44+
) {
45+
return true
46+
}
47+
} catch (e: Exception) {
48+
Timber.w(e, "signature check failed for $packageName")
49+
}
50+
Timber.w("isSignatureAllowed: not allowed")
51+
return false
52+
}
53+
54+
private fun getSignatureFingerprints(pm: PackageManager, packageName: String): List<String> {
55+
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
56+
val pkgInfo: PackageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
57+
if (pkgInfo.signingInfo?.hasMultipleSigners() == true) {
58+
pkgInfo.signingInfo?.apkContentsSigners
59+
} else {
60+
pkgInfo.signingInfo?.signingCertificateHistory
61+
}
62+
} else {
63+
@Suppress("DEPRECATION")
64+
val pkgInfo: PackageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
65+
@Suppress("DEPRECATION")
66+
pkgInfo.signatures
67+
}
68+
if (signatures.isNullOrEmpty()) {
69+
Timber.w("ImporterService: isSignatureAllowed: no signatures found for package $packageName")
70+
}
71+
return signatures.orEmpty().map { sig ->
72+
sha256Hex(sig.toByteArray())
73+
}
74+
}
75+
76+
private fun sha256Hex(data: ByteArray): String {
77+
val md = MessageDigest.getInstance("SHA-256")
78+
val digest = md.digest(data)
79+
return digest.joinToString(":") { "%02X".format(it) }
80+
}
81+
}

0 commit comments

Comments
 (0)