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