From a8db4fcda274c1065e97b620dd502ac295a5ee40 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 18:52:35 +0000 Subject: [PATCH] Add AIDL signer service as alternative to SignerProvider Introduce an ISignerService AIDL bound-service interface so external apps can sign, encrypt/decrypt, sign PSBTs, and ping the signer over a direct binder connection instead of issuing content:// ContentProvider queries. The per-operation logic (permission lookups, history recording, error logging, NIP-42 relay whitelist handling, encrypted-data classification) is extracted into a shared SignerCore object so the ContentProvider and the new SignerService stay in sync. SignerProvider is refactored to delegate to SignerCore and map results to its existing cursor columns (PING keeps its legacy two-column shape for backwards compatibility). The service returns a Bundle mirroring the legacy cursor columns (rejected / signature / event / result); a null return signals an error, matching the ContentProvider's null-cursor behaviour. AIDL builds are enabled via the aidl build feature. --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 9 + .../nostrsigner/ISignerService.aidl | 44 ++ .../com/greenart7c3/nostrsigner/SignerCore.kt | 363 +++++++++++ .../greenart7c3/nostrsigner/SignerProvider.kt | 593 ++---------------- .../greenart7c3/nostrsigner/SignerService.kt | 107 ++++ 6 files changed, 559 insertions(+), 558 deletions(-) create mode 100644 app/src/main/aidl/com/greenart7c3/nostrsigner/ISignerService.aidl create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/SignerCore.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/SignerService.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dc166ad0..ba5b47b8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -156,6 +156,7 @@ android { compose = true buildConfig = true resValues = true + aidl = true } packaging { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5cc57e1..d81504c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,15 @@ android:exported="true" tools:ignore="ExportedContentProvider" /> + + + + + + false + relayHost in authWhitelist -> true + else -> { + recordHistory(historyDatabase, account.npub, packageName, "SIGN_EVENT", event.kind, false, event.toJson()) + return SignResult.Rejected + } + } + } 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) { + recordHistory(historyDatabase, account.npub, packageName, "SIGN_EVENT", event.kind, false, event.toJson()) + return SignResult.Rejected + } + + val signedEvent = account.signSync(event.createdAt, event.kind, event.tags, event.content) + recordHistory(historyDatabase, account.npub, packageName, "SIGN_EVENT", event.kind, true, signedEvent.toJson()) + + val signature = + if (event.kind == LnZapRequestEvent.KIND && + event.tags.any { tag -> tag.any { t -> t == "anon" } } + ) { + signedEvent.toJson() + } else { + signedEvent.sig + } + return SignResult.Reply(signature, signedEvent.toJson(), signature) + } + + fun encryptOrDecrypt( + context: Context, + packageName: String, + type: SignerType, + content: String, + pubKey: String, + npubInput: String, + ): SignResult? { + val npub = IntentUtils.parsePubKey(npubInput) ?: return null + val stringType = type.toString() + val account = LocalPreferences.loadFromEncryptedStorageSync(context, npub) ?: return null + val logDatabase = Amber.instance.getLogDatabase(account.npub) + val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) + val permDao = Amber.instance.dao(account.npub) + + val isEncrypt = type == SignerType.NIP04_ENCRYPT || type == SignerType.NIP44_ENCRYPT + + // 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) + ?: "Could not decrypt the message" + } + } catch (e: Exception) { + recordLog(logDatabase, packageName, stringType, e.message ?: "Could not decrypt the message") + "Could not decrypt the message" + } + } + + // Classify the content to determine EncryptedDataKind-based permission type + val classifyContent = if (isEncrypt) content else (result ?: content) + val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) + + var permission = permDao.getPermission(packageName, permType) + if (permission == null) { + permission = permDao.getPermission(packageName, type.toString()) + } + if (permission == null) { + val nip = when (type) { + SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT -> 4 + SignerType.NIP44_DECRYPT, SignerType.NIP44_ENCRYPT -> 44 + 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) { + recordHistory(historyDatabase, account.npub, packageName, stringType, null, false, content) + return SignResult.Rejected + } + + // For encrypt: perform the operation now after permission is confirmed + val finalResult = + result ?: try { + runBlocking { + AmberUtils.encryptOrDecryptData(content, type, account, pubKey) + ?: "Could not decrypt the message" + } + } catch (e: Exception) { + recordLog(logDatabase, packageName, stringType, e.message ?: "Could not decrypt the message") + "Could not decrypt the message" + } + + recordHistory(historyDatabase, account.npub, packageName, stringType, null, true, if (!isEncrypt) finalResult else content) + return SignResult.Reply(finalResult, finalResult, finalResult) + } + + fun signPsbt( + context: Context, + packageName: String, + psbtHex: String, + npubInput: String, + ): SignResult? { + val npub = IntentUtils.parsePubKey(npubInput) ?: run { + Log.d(Amber.TAG, "No npub") + return null + } + val account = LocalPreferences.loadFromEncryptedStorageSync(context, npub) ?: run { + Log.d(Amber.TAG, "No account from storage") + return null + } + 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) { + recordHistory(historyDatabase, account.npub, packageName, "SIGN_PSBT", null, false, psbtHex) + return SignResult.Rejected + } + + val result = try { + runBlocking { account.signPsbt(psbtHex) } + } catch (e: Exception) { + Log.d(Amber.TAG, "Failed to sign psbt", e) + recordLog(Amber.instance.getLogDatabase(account.npub), packageName, "SIGN_PSBT", e.message ?: "Could not sign the psbt") + return null + } + + recordHistory(historyDatabase, account.npub, packageName, "SIGN_PSBT", null, true, psbtHex) + return SignResult.Reply(result, result, result) + } + + fun ping( + context: Context, + packageName: String, + npubInput: String?, + ): SignResult? { + val npub = if (!npubInput.isNullOrBlank()) IntentUtils.parsePubKey(npubInput) 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 + } + } + } ?: return null + + 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) { + recordHistory(historyDatabase, account.npub, packageName, "PING", null, false) + return SignResult.Rejected + } + + recordHistory(historyDatabase, account.npub, packageName, "PING", null, true) + return SignResult.Reply("pong", null, "pong") + } + + /** Logs an unexpected error against every saved account, matching the legacy catch-all. */ + fun logError( + context: Context, + packageName: String, + type: String, + message: String, + ) { + scope.launch { + LocalPreferences.allSavedAccounts(context).forEach { accInfo -> + recordLog(Amber.instance.getLogDatabase(accInfo.npub), packageName, type, message) + } + } + } + + private fun recordHistory( + historyDatabase: HistoryDatabase, + npub: String, + packageName: String, + type: String, + kind: Int?, + accepted: Boolean, + content: String = "", + ) { + scope.launch { + historyDatabase.dao().addHistory( + listOf( + HistoryEntity( + 0, + packageName, + type, + kind, + TimeUtils.now(), + accepted, + content = content, + ), + ), + npub, + ) + } + } + + private fun recordLog( + logDatabase: LogDatabase, + packageName: String, + type: String, + message: String, + ) { + scope.launch { + logDatabase.dao().insertLog( + LogEntity( + 0, + packageName, + type, + message, + System.currentTimeMillis(), + ), + ) + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index 361b10f6..d15d9786 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -6,25 +6,10 @@ 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.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 override fun delete( uri: Uri, @@ -65,235 +50,22 @@ class SignerProvider : ContentProvider() { Log.d(Amber.TAG, "No package name") return null } + val localContext = context ?: return null return try { when (uriString) { "content://$appId.SIGN_MESSAGE" -> { - val message = projection?.first() - if (message == null) { + val message = projection?.first() ?: run { Log.d(Amber.TAG, "No message") 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 - } - val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - val permDao = Amber.instance.dao(account.npub) - val permission = - permDao - .getPermission( - packageName, - "SIGN_MESSAGE", - ) - 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 = message, - ), - ), - account.npub, - ) - } - val cursor = - MatrixCursor(arrayOf("rejected")).also { - it.addRow(arrayOf("true")) - } - - return cursor - } - - val result = runBlocking { account.signString(message) } - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - "SIGN_MESSAGE", - null, - TimeUtils.now(), - true, - content = message, - ), - ), - account.npub, - ) - } - - val localCursor = MatrixCursor(arrayOf("signature", "event", "result")).also { - it.addRow(arrayOf(result, result, result)) - } - - return localCursor + SignerCore.signMessage(localContext, packageName, message, projection[2])?.toCursor() } "content://$appId.SIGN_EVENT" -> { - val json = projection?.first() - if (json == null) { + val json = projection?.first() ?: run { 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 - } - 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 + SignerCore.signEvent(localContext, packageName, json, projection[2])?.toCursor() } "content://$appId.NIP04_DECRYPT", "content://$appId.NIP44_DECRYPT", @@ -302,349 +74,54 @@ class SignerProvider : ContentProvider() { "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 - 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 - "DECRYPT_ZAP_EVENT" -> SignerType.DECRYPT_ZAP_EVENT - else -> null - } ?: return null - - val isEncrypt = type == SignerType.NIP04_ENCRYPT || type == SignerType.NIP44_ENCRYPT - - // 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, - ) ?: "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" - } - } - - // Classify the content to determine EncryptedDataKind-based permission type - val classifyContent = if (isEncrypt) content else (result ?: content) - val permType = permissionTypeFromContent(classifyContent, isEncrypt, type) - - var permission = permDao.getPermission(packageName, permType) - if (permission == null) { - permission = permDao.getPermission( - packageName, - type.toString(), - ) - } - if (permission == null) { - 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.", ""), - null, - 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, - ) ?: "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" - } - - scope.launch { - historyDatabase.dao().addHistory( - listOf( - HistoryEntity( - 0, - packageName, - uriString.replace("content://$appId.", ""), - null, - TimeUtils.now(), - true, - content = if (!isEncrypt) finalResult else content, - ), - ), - account.npub, - ) - } - - val cursor = MatrixCursor(arrayOf("signature", "event", "result")) - cursor.addRow(arrayOf(finalResult, finalResult, finalResult)) - return cursor + val type = when (uriString.replace("content://$appId.", "")) { + "NIP04_DECRYPT" -> SignerType.NIP04_DECRYPT + "NIP44_DECRYPT" -> SignerType.NIP44_DECRYPT + "NIP04_ENCRYPT" -> SignerType.NIP04_ENCRYPT + "NIP44_ENCRYPT" -> SignerType.NIP44_ENCRYPT + "DECRYPT_ZAP_EVENT" -> SignerType.DECRYPT_ZAP_EVENT + else -> null + } ?: return null + SignerCore.encryptOrDecrypt(localContext, packageName, type, content, pubkey, projection[2])?.toCursor() } - "content://$appId.SIGN_PSBT" -> { - val psbtHex = projection?.first() - if (psbtHex == null) { + val psbtHex = projection?.first() ?: run { 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 - } - 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 + SignerCore.signPsbt(localContext, packageName, psbtHex, projection[2])?.toCursor() } "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 + val npub = if (projection != null && projection.isNotEmpty()) projection[0] else null + when (val result = SignerCore.ping(localContext, packageName, npub)) { + null -> null + is SignerCore.SignResult.Rejected -> result.toCursor() + // PING keeps its legacy two-column shape for backwards compatibility. + is SignerCore.SignResult.Reply -> + MatrixCursor(arrayOf("signature", "result")).also { + it.addRow(arrayOf(result.result, result.result)) } - } } - if (account == null) { - return null - } - 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(), - ), - ) - } - } + SignerCore.logError(localContext, packageName, uriString.replace("content://$appId.", ""), e.message ?: "Error from $callingPackage $uri") return null } } + /** Maps a [SignerCore.SignResult] onto the standard cursor columns shared by most operations. */ + private fun SignerCore.SignResult.toCursor(): Cursor = when (this) { + is SignerCore.SignResult.Rejected -> + MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) } + is SignerCore.SignResult.Reply -> + MatrixCursor(arrayOf("signature", "event", "result")).also { + it.addRow(arrayOf(signature, event, result)) + } + } + override fun update( uri: Uri, values: ContentValues?, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerService.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerService.kt new file mode 100644 index 00000000..75fbd09d --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerService.kt @@ -0,0 +1,107 @@ +package com.greenart7c3.nostrsigner + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.Bundle +import android.os.IBinder +import android.util.Log +import com.greenart7c3.nostrsigner.models.SignerType + +/** + * Bound service exposing [ISignerService] — a synchronous alternative to the + * [SignerProvider] ContentProvider. + * + * Both transports share their business logic in [SignerCore]; this class only + * resolves the calling package from the binder UID and marshals + * [SignerCore.SignResult] into the [Bundle] contract documented in the AIDL. + */ +class SignerService : Service() { + private val binder = object : ISignerService.Stub() { + override fun signMessage(message: String?, npub: String?): Bundle? = run("SIGN_MESSAGE") { pkg -> + SignerCore.signMessage(applicationContext, pkg, message ?: return@run null, npub ?: return@run null) + } + + override fun signEvent(eventJson: String?, npub: String?): Bundle? = run("SIGN_EVENT") { pkg -> + SignerCore.signEvent(applicationContext, pkg, eventJson ?: return@run null, npub ?: return@run null) + } + + override fun nip04Encrypt(plaintext: String?, pubKey: String?, npub: String?): Bundle? = encryptOrDecrypt(SignerType.NIP04_ENCRYPT, plaintext, pubKey, npub) + + override fun nip04Decrypt(ciphertext: String?, pubKey: String?, npub: String?): Bundle? = encryptOrDecrypt(SignerType.NIP04_DECRYPT, ciphertext, pubKey, npub) + + override fun nip44Encrypt(plaintext: String?, pubKey: String?, npub: String?): Bundle? = encryptOrDecrypt(SignerType.NIP44_ENCRYPT, plaintext, pubKey, npub) + + override fun nip44Decrypt(ciphertext: String?, pubKey: String?, npub: String?): Bundle? = encryptOrDecrypt(SignerType.NIP44_DECRYPT, ciphertext, pubKey, npub) + + override fun decryptZapEvent(eventJson: String?, pubKey: String?, npub: String?): Bundle? = encryptOrDecrypt(SignerType.DECRYPT_ZAP_EVENT, eventJson, pubKey, npub) + + override fun signPsbt(psbtHex: String?, npub: String?): Bundle? = run("SIGN_PSBT") { pkg -> + SignerCore.signPsbt(applicationContext, pkg, psbtHex ?: return@run null, npub ?: return@run null) + } + + override fun ping(npub: String?): Bundle? = run("PING") { pkg -> + SignerCore.ping(applicationContext, pkg, npub) + } + + private fun encryptOrDecrypt( + type: SignerType, + content: String?, + pubKey: String?, + npub: String?, + ): Bundle? = run(type.toString()) { pkg -> + SignerCore.encryptOrDecrypt( + applicationContext, + pkg, + type, + content ?: return@run null, + pubKey ?: return@run null, + npub ?: return@run null, + ) + } + } + + /** + * Resolves the caller's package, runs [block], and converts the result into + * the [Bundle] contract. Returns `null` on any error or missing caller, + * matching the ContentProvider's `null` cursor behaviour. + */ + private inline fun run( + type: String, + block: (packageName: String) -> SignerCore.SignResult?, + ): Bundle? { + val packageName = callingPackageName() ?: run { + Log.d(Amber.TAG, "No package name") + return null + } + return try { + block(packageName)?.toBundle() + } catch (e: Exception) { + SignerCore.logError(applicationContext, packageName, type, e.message ?: "Error from $packageName") + null + } + } + + private fun callingPackageName(): String? { + val uid = Binder.getCallingUid() + return packageManager.getPackagesForUid(uid)?.firstOrNull() + } + + override fun onBind(intent: Intent?): IBinder = binder + + companion object { + const val KEY_REJECTED = "rejected" + const val KEY_SIGNATURE = "signature" + const val KEY_EVENT = "event" + const val KEY_RESULT = "result" + + fun SignerCore.SignResult.toBundle(): Bundle = when (this) { + is SignerCore.SignResult.Rejected -> Bundle().apply { putBoolean(KEY_REJECTED, true) } + is SignerCore.SignResult.Reply -> Bundle().apply { + putString(KEY_SIGNATURE, signature) + putString(KEY_EVENT, event) + putString(KEY_RESULT, result) + } + } + } +}