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