diff --git a/vector/build.gradle b/vector/build.gradle
index 561da835788..680622e77f4 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -71,6 +71,28 @@ android {
if (project.hasProperty("coverage")) {
testCoverageEnabled = project.properties["coverage"] == "true"
}
+ buildConfigField(
+ "String",
+ "ELEMENT_X_APP_ID",
+ "\"io.element.android.x.debug\"",
+ )
+ buildConfigField(
+ "String",
+ "ELEMENT_X_FINGERPRINT",
+ "\"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\"",
+ )
+ }
+ release {
+ buildConfigField(
+ "String",
+ "ELEMENT_X_APP_ID",
+ "\"io.element.android.x\"",
+ )
+ buildConfigField(
+ "String",
+ "ELEMENT_X_FINGERPRINT",
+ "\"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\"",
+ )
}
}
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index afad474d7e4..8aa2e46fd1b 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -415,6 +415,12 @@
android:foregroundServiceType="microphone"
android:permission="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
+
+
bundle.putSessionData()
+ else -> bundle.putString(KEY_ERROR_STR, "Unknown command ${msg.what}")
+ }
+ } else {
+ Timber.w("ImporterService: Unauthorized caller")
+ bundle.putString(KEY_ERROR_STR, "Unauthorized")
+ }
+ replyTo.sendResponse(msg.what, bundle)
+ }
+ }
+ }
+
+ private fun Bundle.putSessionData() {
+ val session = activeSessionHolder.getSafeActiveSession()
+ if (session == null) {
+ // Keep an empty Bundle to indicate that there is no session
+ } else {
+ putString(KEY_USER_ID_STR, session.myUserId)
+ val crossSigningService = session.cryptoService().crossSigningService()
+ val keysBackupService = session.cryptoService().keysBackupService()
+ runBlocking {
+ // TODO Use method exposed by the SDK (see https://github.com/matrix-org/matrix-rust-sdk/issues/6037)
+ putBoolean(KEY_IS_VERIFIED_BOOLEAN, crossSigningService.isCrossSigningVerified())
+ crossSigningService.getCrossSigningPrivateKeys()?.let { privateKeys ->
+ putString(KEY_MASTER_PRIVATE_KEY_STR, privateKeys.master)
+ putString(KEY_SELF_SIGNING_PRIVATE_KEY_STR, privateKeys.selfSigned)
+ putString(KEY_USER_PRIVATE_KEY_STR, privateKeys.user)
+ }
+ val keyBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey?.toBase64()
+ putString(KEY_KEY_BACKUP_KEY_STR, keyBackupKey)
+ }
+ }
+ }
+
+ private fun Messenger.sendResponse(what: Int, bundle: Bundle) {
+ Timber.d("ImporterService: send response to client")
+ try {
+ val message = Message.obtain(null, what).also {
+ it.data = bundle
+ }
+ send(message)
+ } catch (e: RemoteException) {
+ // The client is dead.
+ Timber.e(e, "ImporterService: The client is dead.")
+ }
+ }
+
+ /**
+ * When binding to the service, we return an interface to our messenger
+ * for sending messages to the service.
+ */
+ override fun onBind(intent: Intent): IBinder? {
+ Timber.w("ImporterService: onBind")
+ val messenger = Messenger(IncomingHandler())
+ return messenger.binder
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ Timber.w("ImporterService: onUnbind")
+ return super.onUnbind(intent)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/importer/SignaturePermissionChecker.kt b/vector/src/main/java/im/vector/app/features/importer/SignaturePermissionChecker.kt
new file mode 100644
index 00000000000..90e1d5d0da3
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/importer/SignaturePermissionChecker.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2026 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package im.vector.app.features.importer
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import im.vector.app.BuildConfig
+import timber.log.Timber
+import java.security.MessageDigest
+
+class SignaturePermissionChecker {
+ /**
+ * Check if the calling UID is allowed to access the service.
+ * @param sendingUid The UID of the calling process.
+ * @param pm The PackageManager to use to get package info.
+ * @return True if the calling UID is allowed, false otherwise.
+ */
+ fun check(sendingUid: Int, pm: PackageManager): Boolean {
+ Timber.w("ImporterService: callingUid: $sendingUid")
+ val pkgs = pm.getPackagesForUid(sendingUid) ?: return false
+ for (pkg in pkgs) {
+ Timber.w("ImporterService: checking package: $pkg")
+ if (pkg == BuildConfig.ELEMENT_X_APP_ID && isSignatureAllowed(pkg, pm)) {
+ return true
+ }
+ }
+ Timber.e("ImporterService: Unauthorized attempt, denying")
+ return false
+ }
+
+ private fun isSignatureAllowed(packageName: String, pm: PackageManager): Boolean {
+ try {
+ val fingerprints = getSignatureFingerprints(pm, packageName)
+ if (fingerprints.any { fingerprint ->
+ Timber.d("isSignatureAllowed: checking fingerprint $fingerprint")
+ fingerprint == BuildConfig.ELEMENT_X_FINGERPRINT
+ }
+ ) {
+ return true
+ }
+ } catch (e: Exception) {
+ Timber.w(e, "signature check failed for $packageName")
+ }
+ Timber.w("isSignatureAllowed: not allowed")
+ return false
+ }
+
+ private fun getSignatureFingerprints(pm: PackageManager, packageName: String): List {
+ val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val pkgInfo: PackageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
+ if (pkgInfo.signingInfo?.hasMultipleSigners() == true) {
+ pkgInfo.signingInfo?.apkContentsSigners
+ } else {
+ pkgInfo.signingInfo?.signingCertificateHistory
+ }
+ } else {
+ @Suppress("DEPRECATION")
+ val pkgInfo: PackageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
+ @Suppress("DEPRECATION")
+ pkgInfo.signatures
+ }
+ if (signatures.isNullOrEmpty()) {
+ Timber.w("ImporterService: isSignatureAllowed: no signatures found for package $packageName")
+ }
+ return signatures.orEmpty().map { sig ->
+ sha256Hex(sig.toByteArray())
+ }
+ }
+
+ private fun sha256Hex(data: ByteArray): String {
+ val md = MessageDigest.getInstance("SHA-256")
+ val digest = md.digest(data)
+ return digest.joinToString(":") { "%02X".format(it) }
+ }
+}