From 2e10e0ebe06e552b0dfb609acb999443060cf426 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 20:27:37 +0000 Subject: [PATCH 1/2] Extract SignerProvider query logic into SignerProviderQuery helper Move the signing/encryption logic out of the SignerProvider ContentProvider into a reusable SignerProviderQuery helper. SignerProvider now just adapts the binder call (context + calling package) and delegates. The NIP-46 relay path (EventNotificationConsumer) previously reached this logic through a same-app ContentResolver.query() round-trip; it now calls SignerProviderQuery.query() directly, avoiding the IPC. The helper's parameters are named for intent instead of mirroring the ContentProvider signature, and the sortOrder-based package-name override is removed. The requester identity is now always derived by Amber (the calling package for IPC, the client pubkey for NIP-46) and never read from caller-supplied query arguments, so external apps can no longer impersonate another requester. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AhvBy73LEpCFpDR7aQNBDn --- .../greenart7c3/nostrsigner/SignerProvider.kt | 623 +---------------- .../nostrsigner/SignerProviderQuery.kt | 643 ++++++++++++++++++ .../service/EventNotificationConsumer.kt | 15 +- 3 files changed, 668 insertions(+), 613 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index abd855a6..efd360a6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -3,53 +3,10 @@ package com.greenart7c3.nostrsigner import android.content.ContentProvider import android.content.ContentValues import android.database.Cursor -import android.database.MatrixCursor import android.net.Uri import android.util.Log -import com.greenart7c3.nostrsigner.database.HistoryEntity -import com.greenart7c3.nostrsigner.database.LogEntity -import com.greenart7c3.nostrsigner.models.Account -import com.greenart7c3.nostrsigner.models.SignerType -import com.greenart7c3.nostrsigner.models.kindToNip -import com.greenart7c3.nostrsigner.models.permissionTypeFromContent -import com.greenart7c3.nostrsigner.service.AmberUtils -import com.greenart7c3.nostrsigner.service.IntentUtils -import com.greenart7c3.nostrsigner.service.RelayUrlUtils -import com.greenart7c3.nostrsigner.service.model.AmberEvent -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent -import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.TimeUtils -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking class SignerProvider : ContentProvider() { - private val scope get() = Amber.instance.applicationIOScope - - private fun rejectedCursor(): Cursor = MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) } - - /** - * Capture the calling app's launcher icon/name onto its saved app while it is - * visible to us. Dispatched off the binder thread so it adds no latency to the - * signing response, and gated to run once per app (see [IntentUtils.persistNativeAppMetadata]). - */ - private fun captureCallerMetadata(account: Account) { - val pkg = callingPackage ?: return - val ctx = context ?: return - scope.launch { - IntentUtils.persistNativeAppMetadata(ctx, account, pkg) - } - } - - // Decodes the Base64 v3 wire value to readable plaintext for history. - @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) - private fun nip44v3Plaintext(wireValue: String): String = try { - kotlin.io.encoding.Base64.decode(wireValue).toString(Charsets.UTF_8) - } catch (_: Exception) { - wireValue - } - override fun delete( uri: Uri, selection: String?, @@ -72,574 +29,26 @@ class SignerProvider : ContentProvider() { selectionArgs: Array?, sortOrder: String?, ): Cursor? { - Log.d(Amber.TAG, "Querying $uri has context ${context != null}") - - val appId = BuildConfig.APPLICATION_ID - val uriString = uri.toString() - val packageName = if (sortOrder.isNullOrBlank()) { - callingPackage - } else { - if (Hex.isHex(sortOrder)) { - sortOrder - } else { - null - } - } - if (packageName == null) { - Log.d(Amber.TAG, "No package name") + val ctx = context + if (ctx == null) { + Log.d(Amber.TAG, "No context") return null } - return try { - when (uriString) { - "content://$appId.SIGN_EVENT" -> { - val json = projection?.first() - if (json == null) { - Log.d(Amber.TAG, "No json") - return null - } - val npub = IntentUtils.parsePubKey(projection[2]) - if (npub == null) { - Log.d(Amber.TAG, "No npub") - return null - } - val account = LocalPreferences.loadFromEncryptedStorageSync(context!!, npub) - if (account == null) { - Log.d(Amber.TAG, "No account from storage") - return null - } - captureCallerMetadata(account) - val event = try { - IntentUtils.getUnsignedEvent(json, account) - } catch (e: Exception) { - Log.d(Amber.TAG, "Failed to parse event from $packageName", e) - return null - } - - val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - val permDao = Amber.instance.dao(account.npub) - - // For kind 22242 (NIP-42 relay auth), extract relay host once for both whitelist and permission checks - val relayHost = if (event.kind == 22242) { - RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event)) - } else { - "" - } - - val whitelistAutoAccept = if (event.kind == 22242) { - val authWhitelist = Amber.instance.settings.authWhitelist - when { - authWhitelist.isEmpty() -> false - relayHost in authWhitelist -> true - else -> { - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - event.kind, - TimeUtils.now(), - false, - content = event.toJson(), - ), - ), - account.npub, - ) - } - return MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) } - } - } - } else { - false - } - - var permission = if (event.kind == 22242) { - // Kind 22242 = relay client auth (NIP-42): check relay-specific permission first - permDao.getPermissionForRelay(packageName, "SIGN_EVENT", 22242, relayHost) - ?: permDao.getWildcardRelayPermission(packageName, "SIGN_EVENT", 22242) - } else { - permDao - .getPermission( - packageName, - "SIGN_EVENT", - event.kind, - ) - } - if (permission == null && event.kind != 22242) { - event.kind.kindToNip()?.let { - val nipNumber = it.toIntOrNull() - permission = if (nipNumber == null) { - null - } else { - permDao - .getPermission( - packageName, - "NIP", - nipNumber, - ) - } - } - } - val signPolicy = permDao.getSignPolicy(packageName) - val isRemembered = whitelistAutoAccept || IntentUtils.isRemembered(signPolicy, permission) ?: return null - if (!isRemembered) { - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - event.kind, - TimeUtils.now(), - false, - content = event.toJson(), - ), - ), - account.npub, - ) - } - - val cursor = - MatrixCursor(arrayOf("rejected")).also { - it.addRow(arrayOf("true")) - } - - return cursor - } - - val signedEvent = account.signSync(event.createdAt, event.kind, event.tags, event.content) - - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - "SIGN_EVENT", - event.kind, - TimeUtils.now(), - true, - content = signedEvent.toJson(), - ), - ), - account.npub, - ) - } - - val cursor = - MatrixCursor(arrayOf("signature", "event", "result")).also { - val signature = - if (event.kind == LnZapRequestEvent.KIND && - event.tags.any { tag -> - tag.any { t -> t == "anon" } - } - ) { - signedEvent.toJson() - } else { - signedEvent.sig - } - it.addRow(arrayOf(signature, signedEvent.toJson(), signature)) - } - - return cursor - } - "content://$appId.NIP04_DECRYPT", - "content://$appId.NIP44_DECRYPT", - "content://$appId.NIP04_ENCRYPT", - "content://$appId.NIP44_ENCRYPT", - "content://$appId.NIP44_V3_DECRYPT", - "content://$appId.NIP44_V3_ENCRYPT", - "content://$appId.DECRYPT_ZAP_EVENT", - -> { - val content = projection?.first() ?: return null - val npub = IntentUtils.parsePubKey(projection[2]) ?: return null - val stringType = uriString.replace("content://$appId.", "") - val pubkey = projection[1] - val account = LocalPreferences.loadFromEncryptedStorageSync(context!!, npub) ?: return null - captureCallerMetadata(account) - val logDatabase = Amber.instance.getLogDatabase(account.npub) - val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - val permDao = Amber.instance.dao(account.npub) - val type = - when (stringType) { - "NIP04_DECRYPT" -> SignerType.NIP04_DECRYPT - "NIP44_DECRYPT" -> SignerType.NIP44_DECRYPT - "NIP04_ENCRYPT" -> SignerType.NIP04_ENCRYPT - "NIP44_ENCRYPT" -> SignerType.NIP44_ENCRYPT - "NIP44_V3_DECRYPT" -> SignerType.NIP44_V3_DECRYPT - "NIP44_V3_ENCRYPT" -> SignerType.NIP44_V3_ENCRYPT - "DECRYPT_ZAP_EVENT" -> SignerType.DECRYPT_ZAP_EVENT - else -> null - } ?: return null - - val isEncrypt = type == SignerType.NIP04_ENCRYPT || - type == SignerType.NIP44_ENCRYPT || - type == SignerType.NIP44_V3_ENCRYPT - val isV3 = type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT - - // V3 carries kind and scope as projection[3] and projection[4]. - // A missing/invalid kind is a malformed request: auto-reject - // instead of prompting the user. - val (v3Kind, v3Scope) = if (isV3) { - val kindStr = projection.getOrNull(3) - val scopeStr = projection.getOrNull(4) ?: "" - val parsedKind = kindStr?.toIntOrNull() - if (parsedKind == null) { - Log.d(Amber.TAG, "NIP-44 v3 request missing/invalid kind") - return rejectedCursor() - } - parsedKind to scopeStr - } else { - null to "" - } - - // For ENCRYPT: classify plaintext input; for DECRYPT: perform operation first then classify result - val result = - if (isEncrypt) { - null // defer until after permission check - } else { - try { - runBlocking { - AmberUtils.encryptOrDecryptData( - content, - type, - account, - pubkey, - v3Kind, - v3Scope, - ) ?: "Could not decrypt the message" - } - } catch (e: Exception) { - scope.launch { - logDatabase.dao().insertLog( - LogEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - e.message ?: "Could not decrypt the message", - System.currentTimeMillis(), - ), - ) - } - // A V3 decrypt that throws cannot succeed (wrong - // version/context, bad MAC, corrupt padding, ...): - // reject it rather than prompting the user. - if (isV3) return rejectedCursor() - "Could not decrypt the message" - } - } - - // Permission lookup. V3 grants are scoped by (packageName, - // SignerType, kind); fall back to a kind=null "all kinds" - // grant. V3 grants do NOT satisfy V2 requests and vice versa. - var permission = if (isV3) { - // V3 grants are kind-scoped; fall back to the explicit - // "all kinds" (kind IS NULL) grant only, never to any - // other kind — otherwise e.g. a kind-A reject would - // leak to a kind-B request. - permDao.getPermission(packageName, type.toString(), v3Kind!!) - ?: permDao.getPermissionAllKinds(packageName, type.toString()) - } else { - // Classify the content to determine EncryptedDataKind-based permission type - val classifyContent = if (isEncrypt) content else (result ?: content) - val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) - permDao.getPermission(packageName, permType) - ?: permDao.getPermission(packageName, type.toString()) - } - if (permission == null && !isV3) { - val nip = when (stringType) { - "NIP04_DECRYPT" -> 4 - "NIP44_DECRYPT" -> 44 - "NIP04_ENCRYPT" -> 4 - "NIP44_ENCRYPT" -> 44 - "DECRYPT_ZAP_EVENT" -> null - else -> null - } - nip?.let { - permission = - permDao - .getPermission( - packageName, - "NIP", - it, - ) - } - } - val signPolicy = permDao.getSignPolicy(packageName) - val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null - if (!isRemembered) { - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - v3Kind, - TimeUtils.now(), - false, - content = content, - ), - ), - account.npub, - ) - } - - val cursor = - MatrixCursor(arrayOf("rejected")).also { - it.addRow(arrayOf("true")) - } - - return cursor - } - - // For encrypt: perform the operation now after permission is confirmed - val finalResult = - result ?: try { - runBlocking { - AmberUtils.encryptOrDecryptData( - content, - type, - account, - pubkey, - v3Kind, - v3Scope, - ) ?: "Could not decrypt the message" - } - } catch (e: Exception) { - scope.launch { - logDatabase.dao().insertLog( - LogEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - e.message ?: "Could not decrypt the message", - System.currentTimeMillis(), - ), - ) - } - "Could not decrypt the message" - } - - // For v3 the wire value is Base64; this auto-accept path has no - // EncryptedDataKind, so decode it once for the readable log. - val historyContent = if (isV3) { - nip44v3Plaintext(if (!isEncrypt) finalResult else content) - } else { - if (!isEncrypt) finalResult else content - } - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - v3Kind, - TimeUtils.now(), - true, - content = historyContent, - ), - ), - account.npub, - ) - } - - val cursor = MatrixCursor(arrayOf("signature", "event", "result")) - cursor.addRow(arrayOf(finalResult, finalResult, finalResult)) - return cursor - } - - "content://$appId.SIGN_PSBT" -> { - val psbtHex = projection?.first() - if (psbtHex == null) { - Log.d(Amber.TAG, "No psbt") - return null - } - val npub = IntentUtils.parsePubKey(projection[2]) - if (npub == null) { - Log.d(Amber.TAG, "No npub") - return null - } - val account = LocalPreferences.loadFromEncryptedStorageSync(context!!, npub) - if (account == null) { - Log.d(Amber.TAG, "No account from storage") - return null - } - captureCallerMetadata(account) - val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - val permDao = Amber.instance.dao(account.npub) - val permission = - permDao - .getPermission( - packageName, - "SIGN_PSBT", - ) - val signPolicy = permDao.getSignPolicy(packageName) - val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null - if (!isRemembered) { - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - null, - TimeUtils.now(), - false, - content = psbtHex, - ), - ), - account.npub, - ) - } - val cursor = - MatrixCursor(arrayOf("rejected")).also { - it.addRow(arrayOf("true")) - } - - return cursor - } - - val result = try { - runBlocking { account.signPsbt(psbtHex) } - } catch (e: Exception) { - Log.d(Amber.TAG, "Failed to sign psbt", e) - scope.launch { - val logDb = Amber.instance.getLogDatabase(account.npub) - logDb.dao().insertLog( - LogEntity( - 0, - packageName, - "SIGN_PSBT", - e.message ?: "Could not sign the psbt", - System.currentTimeMillis(), - ), - ) - } - return null - } - - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - "SIGN_PSBT", - null, - TimeUtils.now(), - true, - content = psbtHex, - ), - ), - account.npub, - ) - } - - val localCursor = MatrixCursor(arrayOf("signature", "event", "result")).also { - it.addRow(arrayOf(result, result, result)) - } - - return localCursor - } - "content://$appId.PING" -> { - val npub = if (projection != null && projection.isNotEmpty()) IntentUtils.parsePubKey(projection[0]) else null - val account = if (npub != null) { - LocalPreferences.loadFromEncryptedStorageSync(context!!, npub) - } else { - LocalPreferences.allSavedAccounts(context!!).firstNotNullOfOrNull { accountInfo -> - val localDatabase = Amber.instance.getDatabase(accountInfo.npub) - val hasAccount = localDatabase.dao().getByKeySync(packageName) != null - if (hasAccount) { - LocalPreferences.loadFromEncryptedStorageSync(context!!, accountInfo.npub) - } else { - null - } - } - } - if (account == null) { - return null - } - captureCallerMetadata(account) - val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - val permDao = Amber.instance.dao(account.npub) - val permission = - permDao - .getPermission( - packageName, - "PING", - ) - - val signPolicy = permDao.getSignPolicy(packageName) - val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null - if (!isRemembered) { - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - null, - TimeUtils.now(), - false, - ), - ), - account.npub, - ) - } - - val cursor = - MatrixCursor(arrayOf("rejected")).also { - it.addRow(arrayOf("true")) - } - - return cursor - } - - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - null, - TimeUtils.now(), - true, - ), - ), - account.npub, - ) - } - - val cursor = MatrixCursor(arrayOf("signature", "result")) - cursor.addRow(arrayOf("pong", "pong")) - return cursor - } - else -> null - } - } catch (e: Exception) { - scope.launch { - LocalPreferences.allSavedAccounts(context!!).forEach { accInfo -> - val database = Amber.instance.getLogDatabase(accInfo.npub) - database.dao().insertLog( - LogEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - e.message ?: "Error from $callingPackage $uri", - System.currentTimeMillis(), - ), - ) - } - } + // External apps are always attributed to their own Android package. The + // requester identity is taken from the binder's calling package, never from + // caller-supplied query arguments, so an app cannot impersonate another one. + val caller = callingPackage + if (caller == null) { + Log.d(Amber.TAG, "No calling package") return null } + return SignerProviderQuery.query( + context = ctx, + requesterId = caller, + callerPackageName = caller, + operationUri = uri, + arguments = projection, + ) } override fun update( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt new file mode 100644 index 00000000..08a1a040 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt @@ -0,0 +1,643 @@ +package com.greenart7c3.nostrsigner + +import android.content.Context +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.util.Log +import com.greenart7c3.nostrsigner.database.HistoryEntity +import com.greenart7c3.nostrsigner.database.LogEntity +import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.models.SignerType +import com.greenart7c3.nostrsigner.models.kindToNip +import com.greenart7c3.nostrsigner.models.permissionTypeFromContent +import com.greenart7c3.nostrsigner.service.AmberUtils +import com.greenart7c3.nostrsigner.service.IntentUtils +import com.greenart7c3.nostrsigner.service.RelayUrlUtils +import com.greenart7c3.nostrsigner.service.model.AmberEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +/** + * Holds the signing/encryption logic that backs [SignerProvider]. + * + * The same logic is reached two ways: + * - [SignerProvider] (a [android.content.ContentProvider]) for external IPC callers, and + * - in-process callers such as [com.greenart7c3.nostrsigner.service.EventNotificationConsumer] + * (the NIP-46 relay path), which call [query] directly instead of going through + * a same-app [android.content.ContentResolver] round-trip. + */ +object SignerProviderQuery { + private val scope get() = Amber.instance.applicationIOScope + + private fun rejectedCursor(): Cursor = MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) } + + /** + * Capture the calling app's launcher icon/name onto its saved app while it is + * visible to us. Dispatched off the binder thread so it adds no latency to the + * signing response, and gated to run once per app (see [IntentUtils.persistNativeAppMetadata]). + */ + private fun captureCallerMetadata( + context: Context, + callerPackageName: String?, + account: Account, + ) { + val pkg = callerPackageName ?: return + scope.launch { + IntentUtils.persistNativeAppMetadata(context, account, pkg) + } + } + + // Decodes the Base64 v3 wire value to readable plaintext for history. + @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) + private fun nip44v3Plaintext(wireValue: String): String = try { + kotlin.io.encoding.Base64.decode(wireValue).toString(Charsets.UTF_8) + } catch (_: Exception) { + wireValue + } + + /** + * Runs a signer operation and returns its result as a [Cursor], mirroring the + * column layout the [SignerProvider] ContentProvider exposes to external apps. + * + * @param context an application [Context] used for storage and PackageManager lookups. + * @param requesterId who the request is attributed to for permission checks, + * history and logs. For IPC requests this is the caller's Android package name; + * for NIP-46 relay requests it is the client's pubkey (hex). This is always + * derived by Amber, never taken from caller-supplied arguments, so external + * apps cannot impersonate another requester. + * @param callerPackageName the installed Android package whose launcher icon/name + * should be captured onto the saved app, or null when there is no installed + * caller (e.g. NIP-46 relay requests). + * @param operationUri identifies the operation to run, e.g. `content://.SIGN_EVENT`. + * @param arguments the operation's input values; their meaning depends on + * [operationUri] (e.g. for SIGN_EVENT: [0] = event json, [2] = npub). See callers. + */ + fun query( + context: Context, + requesterId: String, + callerPackageName: String?, + operationUri: Uri, + arguments: Array?, + ): Cursor? { + Log.d(Amber.TAG, "Querying $operationUri") + + val appId = BuildConfig.APPLICATION_ID + val uriString = operationUri.toString() + val packageName = requesterId + return try { + when (uriString) { + "content://$appId.SIGN_EVENT" -> { + val json = arguments?.first() + if (json == null) { + Log.d(Amber.TAG, "No json") + return null + } + val npub = IntentUtils.parsePubKey(arguments[2]) + if (npub == null) { + Log.d(Amber.TAG, "No npub") + return null + } + val account = LocalPreferences.loadFromEncryptedStorageSync(context, npub) + if (account == null) { + Log.d(Amber.TAG, "No account from storage") + return null + } + captureCallerMetadata(context, callerPackageName, account) + val event = try { + IntentUtils.getUnsignedEvent(json, account) + } catch (e: Exception) { + Log.d(Amber.TAG, "Failed to parse event from $packageName", e) + return null + } + + val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) + val permDao = Amber.instance.dao(account.npub) + + // For kind 22242 (NIP-42 relay auth), extract relay host once for both whitelist and permission checks + val relayHost = if (event.kind == 22242) { + RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event)) + } else { + "" + } + + val whitelistAutoAccept = if (event.kind == 22242) { + val authWhitelist = Amber.instance.settings.authWhitelist + when { + authWhitelist.isEmpty() -> false + relayHost in authWhitelist -> true + else -> { + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + event.kind, + TimeUtils.now(), + false, + content = event.toJson(), + ), + ), + account.npub, + ) + } + return MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) } + } + } + } else { + false + } + + var permission = if (event.kind == 22242) { + // Kind 22242 = relay client auth (NIP-42): check relay-specific permission first + permDao.getPermissionForRelay(packageName, "SIGN_EVENT", 22242, relayHost) + ?: permDao.getWildcardRelayPermission(packageName, "SIGN_EVENT", 22242) + } else { + permDao + .getPermission( + packageName, + "SIGN_EVENT", + event.kind, + ) + } + if (permission == null && event.kind != 22242) { + event.kind.kindToNip()?.let { + val nipNumber = it.toIntOrNull() + permission = if (nipNumber == null) { + null + } else { + permDao + .getPermission( + packageName, + "NIP", + nipNumber, + ) + } + } + } + val signPolicy = permDao.getSignPolicy(packageName) + val isRemembered = whitelistAutoAccept || IntentUtils.isRemembered(signPolicy, permission) ?: return null + if (!isRemembered) { + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + event.kind, + TimeUtils.now(), + false, + content = event.toJson(), + ), + ), + account.npub, + ) + } + + val cursor = + MatrixCursor(arrayOf("rejected")).also { + it.addRow(arrayOf("true")) + } + + return cursor + } + + val signedEvent = account.signSync(event.createdAt, event.kind, event.tags, event.content) + + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + "SIGN_EVENT", + event.kind, + TimeUtils.now(), + true, + content = signedEvent.toJson(), + ), + ), + account.npub, + ) + } + + val cursor = + MatrixCursor(arrayOf("signature", "event", "result")).also { + val signature = + if (event.kind == LnZapRequestEvent.KIND && + event.tags.any { tag -> + tag.any { t -> t == "anon" } + } + ) { + signedEvent.toJson() + } else { + signedEvent.sig + } + it.addRow(arrayOf(signature, signedEvent.toJson(), signature)) + } + + return cursor + } + "content://$appId.NIP04_DECRYPT", + "content://$appId.NIP44_DECRYPT", + "content://$appId.NIP04_ENCRYPT", + "content://$appId.NIP44_ENCRYPT", + "content://$appId.NIP44_V3_DECRYPT", + "content://$appId.NIP44_V3_ENCRYPT", + "content://$appId.DECRYPT_ZAP_EVENT", + -> { + val content = arguments?.first() ?: return null + val npub = IntentUtils.parsePubKey(arguments[2]) ?: return null + val stringType = uriString.replace("content://$appId.", "") + val pubkey = arguments[1] + val account = LocalPreferences.loadFromEncryptedStorageSync(context, npub) ?: return null + captureCallerMetadata(context, callerPackageName, account) + val logDatabase = Amber.instance.getLogDatabase(account.npub) + val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) + val permDao = Amber.instance.dao(account.npub) + val type = + when (stringType) { + "NIP04_DECRYPT" -> SignerType.NIP04_DECRYPT + "NIP44_DECRYPT" -> SignerType.NIP44_DECRYPT + "NIP04_ENCRYPT" -> SignerType.NIP04_ENCRYPT + "NIP44_ENCRYPT" -> SignerType.NIP44_ENCRYPT + "NIP44_V3_DECRYPT" -> SignerType.NIP44_V3_DECRYPT + "NIP44_V3_ENCRYPT" -> SignerType.NIP44_V3_ENCRYPT + "DECRYPT_ZAP_EVENT" -> SignerType.DECRYPT_ZAP_EVENT + else -> null + } ?: return null + + val isEncrypt = type == SignerType.NIP04_ENCRYPT || + type == SignerType.NIP44_ENCRYPT || + type == SignerType.NIP44_V3_ENCRYPT + val isV3 = type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT + + // V3 carries kind and scope as arguments[3] and arguments[4]. + // A missing/invalid kind is a malformed request: auto-reject + // instead of prompting the user. + val (v3Kind, v3Scope) = if (isV3) { + val kindStr = arguments.getOrNull(3) + val scopeStr = arguments.getOrNull(4) ?: "" + val parsedKind = kindStr?.toIntOrNull() + if (parsedKind == null) { + Log.d(Amber.TAG, "NIP-44 v3 request missing/invalid kind") + return rejectedCursor() + } + parsedKind to scopeStr + } else { + null to "" + } + + // For ENCRYPT: classify plaintext input; for DECRYPT: perform operation first then classify result + val result = + if (isEncrypt) { + null // defer until after permission check + } else { + try { + runBlocking { + AmberUtils.encryptOrDecryptData( + content, + type, + account, + pubkey, + v3Kind, + v3Scope, + ) ?: "Could not decrypt the message" + } + } catch (e: Exception) { + scope.launch { + logDatabase.dao().insertLog( + LogEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + e.message ?: "Could not decrypt the message", + System.currentTimeMillis(), + ), + ) + } + // A V3 decrypt that throws cannot succeed (wrong + // version/context, bad MAC, corrupt padding, ...): + // reject it rather than prompting the user. + if (isV3) return rejectedCursor() + "Could not decrypt the message" + } + } + + // Permission lookup. V3 grants are scoped by (packageName, + // SignerType, kind); fall back to a kind=null "all kinds" + // grant. V3 grants do NOT satisfy V2 requests and vice versa. + var permission = if (isV3) { + // V3 grants are kind-scoped; fall back to the explicit + // "all kinds" (kind IS NULL) grant only, never to any + // other kind — otherwise e.g. a kind-A reject would + // leak to a kind-B request. + permDao.getPermission(packageName, type.toString(), v3Kind!!) + ?: permDao.getPermissionAllKinds(packageName, type.toString()) + } else { + // Classify the content to determine EncryptedDataKind-based permission type + val classifyContent = if (isEncrypt) content else (result ?: content) + val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) + permDao.getPermission(packageName, permType) + ?: permDao.getPermission(packageName, type.toString()) + } + if (permission == null && !isV3) { + val nip = when (stringType) { + "NIP04_DECRYPT" -> 4 + "NIP44_DECRYPT" -> 44 + "NIP04_ENCRYPT" -> 4 + "NIP44_ENCRYPT" -> 44 + "DECRYPT_ZAP_EVENT" -> null + else -> null + } + nip?.let { + permission = + permDao + .getPermission( + packageName, + "NIP", + it, + ) + } + } + val signPolicy = permDao.getSignPolicy(packageName) + val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null + if (!isRemembered) { + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + v3Kind, + TimeUtils.now(), + false, + content = content, + ), + ), + account.npub, + ) + } + + val cursor = + MatrixCursor(arrayOf("rejected")).also { + it.addRow(arrayOf("true")) + } + + return cursor + } + + // For encrypt: perform the operation now after permission is confirmed + val finalResult = + result ?: try { + runBlocking { + AmberUtils.encryptOrDecryptData( + content, + type, + account, + pubkey, + v3Kind, + v3Scope, + ) ?: "Could not decrypt the message" + } + } catch (e: Exception) { + scope.launch { + logDatabase.dao().insertLog( + LogEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + e.message ?: "Could not decrypt the message", + System.currentTimeMillis(), + ), + ) + } + "Could not decrypt the message" + } + + // For v3 the wire value is Base64; this auto-accept path has no + // EncryptedDataKind, so decode it once for the readable log. + val historyContent = if (isV3) { + nip44v3Plaintext(if (!isEncrypt) finalResult else content) + } else { + if (!isEncrypt) finalResult else content + } + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + v3Kind, + TimeUtils.now(), + true, + content = historyContent, + ), + ), + account.npub, + ) + } + + val cursor = MatrixCursor(arrayOf("signature", "event", "result")) + cursor.addRow(arrayOf(finalResult, finalResult, finalResult)) + return cursor + } + + "content://$appId.SIGN_PSBT" -> { + val psbtHex = arguments?.first() + if (psbtHex == null) { + Log.d(Amber.TAG, "No psbt") + return null + } + val npub = IntentUtils.parsePubKey(arguments[2]) + if (npub == null) { + Log.d(Amber.TAG, "No npub") + return null + } + val account = LocalPreferences.loadFromEncryptedStorageSync(context, npub) + if (account == null) { + Log.d(Amber.TAG, "No account from storage") + return null + } + captureCallerMetadata(context, callerPackageName, account) + val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) + val permDao = Amber.instance.dao(account.npub) + val permission = + permDao + .getPermission( + packageName, + "SIGN_PSBT", + ) + val signPolicy = permDao.getSignPolicy(packageName) + val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null + if (!isRemembered) { + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + null, + TimeUtils.now(), + false, + content = psbtHex, + ), + ), + account.npub, + ) + } + val cursor = + MatrixCursor(arrayOf("rejected")).also { + it.addRow(arrayOf("true")) + } + + return cursor + } + + val result = try { + runBlocking { account.signPsbt(psbtHex) } + } catch (e: Exception) { + Log.d(Amber.TAG, "Failed to sign psbt", e) + scope.launch { + val logDb = Amber.instance.getLogDatabase(account.npub) + logDb.dao().insertLog( + LogEntity( + 0, + packageName, + "SIGN_PSBT", + e.message ?: "Could not sign the psbt", + System.currentTimeMillis(), + ), + ) + } + return null + } + + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + "SIGN_PSBT", + null, + TimeUtils.now(), + true, + content = psbtHex, + ), + ), + account.npub, + ) + } + + val localCursor = MatrixCursor(arrayOf("signature", "event", "result")).also { + it.addRow(arrayOf(result, result, result)) + } + + return localCursor + } + "content://$appId.PING" -> { + val npub = if (arguments != null && arguments.isNotEmpty()) IntentUtils.parsePubKey(arguments[0]) else null + val account = if (npub != null) { + LocalPreferences.loadFromEncryptedStorageSync(context, npub) + } else { + LocalPreferences.allSavedAccounts(context).firstNotNullOfOrNull { accountInfo -> + val localDatabase = Amber.instance.getDatabase(accountInfo.npub) + val hasAccount = localDatabase.dao().getByKeySync(packageName) != null + if (hasAccount) { + LocalPreferences.loadFromEncryptedStorageSync(context, accountInfo.npub) + } else { + null + } + } + } + if (account == null) { + return null + } + captureCallerMetadata(context, callerPackageName, account) + val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) + val permDao = Amber.instance.dao(account.npub) + val permission = + permDao + .getPermission( + packageName, + "PING", + ) + + val signPolicy = permDao.getSignPolicy(packageName) + val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null + if (!isRemembered) { + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + null, + TimeUtils.now(), + false, + ), + ), + account.npub, + ) + } + + val cursor = + MatrixCursor(arrayOf("rejected")).also { + it.addRow(arrayOf("true")) + } + + return cursor + } + + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + null, + TimeUtils.now(), + true, + ), + ), + account.npub, + ) + } + + val cursor = MatrixCursor(arrayOf("signature", "result")) + cursor.addRow(arrayOf("pong", "pong")) + return cursor + } + else -> null + } + } catch (e: Exception) { + scope.launch { + LocalPreferences.allSavedAccounts(context).forEach { accInfo -> + val database = Amber.instance.getLogDatabase(accInfo.npub) + database.dao().insertLog( + LogEntity( + 0, + packageName, + uriString.replace("content://$appId.", ""), + e.message ?: "Error from ${callerPackageName ?: packageName} $operationUri", + System.currentTimeMillis(), + ), + ) + } + } + return null + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt index a8dfefc5..f758778a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt @@ -31,6 +31,7 @@ import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.BuildConfig import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.SignerProviderQuery import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions import com.greenart7c3.nostrsigner.database.HistoryEntity import com.greenart7c3.nostrsigner.database.LogEntity @@ -532,12 +533,14 @@ class EventNotificationConsumer(private val applicationContext: Context) { else -> arrayOf(data, pubKey, acc.npub) } val cursor = - applicationContext.contentResolver.query( - "content://${BuildConfig.APPLICATION_ID}.$type".toUri(), - projection, - "Amber", - null, - event.pubKey, + SignerProviderQuery.query( + context = applicationContext, + // NIP-46 requests are attributed to the client's pubkey, not an + // installed app. There is no installed caller, so nothing to capture. + requesterId = event.pubKey, + callerPackageName = null, + operationUri = "content://${BuildConfig.APPLICATION_ID}.$type".toUri(), + arguments = projection, ) cursor.use { localCursor -> From 7ad501fb9efdcac57dafb927ed542f771e656d43 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 19 Jun 2026 05:47:43 -0300 Subject: [PATCH 2/2] Use requesterId directly in SignerProviderQuery This change removes the redundant `packageName` local variable and replaces its occurrences with `requesterId` throughout the `SignerProviderQuery` class. The refactoring affects: - Permission and sign policy lookups in `permDao`. - History and log entry creation for `HistoryEntity` and `LogEntity`. - Account existence checks and logging of operation errors. --- .../nostrsigner/SignerProviderQuery.kt | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt index 08a1a040..c81331ab 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProviderQuery.kt @@ -87,7 +87,6 @@ object SignerProviderQuery { val appId = BuildConfig.APPLICATION_ID val uriString = operationUri.toString() - val packageName = requesterId return try { when (uriString) { "content://$appId.SIGN_EVENT" -> { @@ -110,7 +109,7 @@ object SignerProviderQuery { val event = try { IntentUtils.getUnsignedEvent(json, account) } catch (e: Exception) { - Log.d(Amber.TAG, "Failed to parse event from $packageName", e) + Log.d(Amber.TAG, "Failed to parse event from $requesterId", e) return null } @@ -135,7 +134,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), event.kind, TimeUtils.now(), @@ -155,12 +154,12 @@ object SignerProviderQuery { var permission = if (event.kind == 22242) { // Kind 22242 = relay client auth (NIP-42): check relay-specific permission first - permDao.getPermissionForRelay(packageName, "SIGN_EVENT", 22242, relayHost) - ?: permDao.getWildcardRelayPermission(packageName, "SIGN_EVENT", 22242) + permDao.getPermissionForRelay(requesterId, "SIGN_EVENT", 22242, relayHost) + ?: permDao.getWildcardRelayPermission(requesterId, "SIGN_EVENT", 22242) } else { permDao .getPermission( - packageName, + requesterId, "SIGN_EVENT", event.kind, ) @@ -173,14 +172,14 @@ object SignerProviderQuery { } else { permDao .getPermission( - packageName, + requesterId, "NIP", nipNumber, ) } } } - val signPolicy = permDao.getSignPolicy(packageName) + val signPolicy = permDao.getSignPolicy(requesterId) val isRemembered = whitelistAutoAccept || IntentUtils.isRemembered(signPolicy, permission) ?: return null if (!isRemembered) { scope.launch { @@ -188,7 +187,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), event.kind, TimeUtils.now(), @@ -215,7 +214,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, "SIGN_EVENT", event.kind, TimeUtils.now(), @@ -315,7 +314,7 @@ object SignerProviderQuery { logDatabase.dao().insertLog( LogEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), e.message ?: "Could not decrypt the message", System.currentTimeMillis(), @@ -338,14 +337,14 @@ object SignerProviderQuery { // "all kinds" (kind IS NULL) grant only, never to any // other kind — otherwise e.g. a kind-A reject would // leak to a kind-B request. - permDao.getPermission(packageName, type.toString(), v3Kind!!) - ?: permDao.getPermissionAllKinds(packageName, type.toString()) + permDao.getPermission(requesterId, type.toString(), v3Kind!!) + ?: permDao.getPermissionAllKinds(requesterId, type.toString()) } else { // Classify the content to determine EncryptedDataKind-based permission type val classifyContent = if (isEncrypt) content else (result ?: content) val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) - permDao.getPermission(packageName, permType) - ?: permDao.getPermission(packageName, type.toString()) + permDao.getPermission(requesterId, permType) + ?: permDao.getPermission(requesterId, type.toString()) } if (permission == null && !isV3) { val nip = when (stringType) { @@ -360,13 +359,13 @@ object SignerProviderQuery { permission = permDao .getPermission( - packageName, + requesterId, "NIP", it, ) } } - val signPolicy = permDao.getSignPolicy(packageName) + val signPolicy = permDao.getSignPolicy(requesterId) val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null if (!isRemembered) { scope.launch { @@ -374,7 +373,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), v3Kind, TimeUtils.now(), @@ -412,7 +411,7 @@ object SignerProviderQuery { logDatabase.dao().insertLog( LogEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), e.message ?: "Could not decrypt the message", System.currentTimeMillis(), @@ -434,7 +433,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), v3Kind, TimeUtils.now(), @@ -473,10 +472,10 @@ object SignerProviderQuery { val permission = permDao .getPermission( - packageName, + requesterId, "SIGN_PSBT", ) - val signPolicy = permDao.getSignPolicy(packageName) + val signPolicy = permDao.getSignPolicy(requesterId) val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null if (!isRemembered) { scope.launch { @@ -484,7 +483,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), null, TimeUtils.now(), @@ -512,7 +511,7 @@ object SignerProviderQuery { logDb.dao().insertLog( LogEntity( 0, - packageName, + requesterId, "SIGN_PSBT", e.message ?: "Could not sign the psbt", System.currentTimeMillis(), @@ -527,7 +526,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, "SIGN_PSBT", null, TimeUtils.now(), @@ -552,7 +551,7 @@ object SignerProviderQuery { } else { LocalPreferences.allSavedAccounts(context).firstNotNullOfOrNull { accountInfo -> val localDatabase = Amber.instance.getDatabase(accountInfo.npub) - val hasAccount = localDatabase.dao().getByKeySync(packageName) != null + val hasAccount = localDatabase.dao().getByKeySync(requesterId) != null if (hasAccount) { LocalPreferences.loadFromEncryptedStorageSync(context, accountInfo.npub) } else { @@ -569,11 +568,11 @@ object SignerProviderQuery { val permission = permDao .getPermission( - packageName, + requesterId, "PING", ) - val signPolicy = permDao.getSignPolicy(packageName) + val signPolicy = permDao.getSignPolicy(requesterId) val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null if (!isRemembered) { scope.launch { @@ -581,7 +580,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), null, TimeUtils.now(), @@ -605,7 +604,7 @@ object SignerProviderQuery { listOf( HistoryEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), null, TimeUtils.now(), @@ -629,9 +628,9 @@ object SignerProviderQuery { database.dao().insertLog( LogEntity( 0, - packageName, + requesterId, uriString.replace("content://$appId.", ""), - e.message ?: "Error from ${callerPackageName ?: packageName} $operationUri", + e.message ?: "Error from ${callerPackageName ?: requesterId} $operationUri", System.currentTimeMillis(), ), )