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) } + } +}