feat: Add support for NIP-55 #18
@@ -11,6 +11,14 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="nostrsigner" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
|
||||||
|
class AndroidExternalSigner(
|
||||||
|
private val context: Context,
|
||||||
|
private val launcher: ExternalSignerLauncher,
|
||||||
|
) : ExternalSignerHandler {
|
||||||
|
private var cachedPackageName: String? = null
|
||||||
|
private var cachedPublicKey: PublicKey? = null
|
||||||
|
|
||||||
|
private data class ContentResolverResult(
|
||||||
|
val result: String,
|
||||||
|
val event: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun queryContentResolver(
|
||||||
|
type: String,
|
||||||
|
payload: String,
|
||||||
|
pubkey: PublicKey? = null,
|
||||||
|
currentUser: PublicKey? = null,
|
||||||
|
): ContentResolverResult? {
|
||||||
|
val uri = "content://$cachedPackageName.${type.uppercase()}".toUri()
|
||||||
|
val cursor = context.contentResolver.query(
|
||||||
|
uri,
|
||||||
|
listOf(payload, pubkey, currentUser) as Array<out String?>?,
|
||||||
|
null, null, null,
|
||||||
|
) ?: return null
|
||||||
|
|
||||||
|
return cursor.use {
|
||||||
|
if (it.getColumnIndex("rejected") > -1) return null
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val resultIndex = it.getColumnIndex("result")
|
||||||
|
val result = if (resultIndex > -1) it.getString(resultIndex) else null
|
||||||
|
|
||||||
|
val eventIndex = it.getColumnIndex("event")
|
||||||
|
val event = if (eventIndex > -1) it.getString(eventIndex) else null
|
||||||
|
|
||||||
|
ContentResolverResult(result = result!!, event = event)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun request(
|
||||||
|
type: String,
|
||||||
|
payload: String,
|
||||||
|
pubkey: PublicKey? = null,
|
||||||
|
currentUser: PublicKey? = null,
|
||||||
|
): String? {
|
||||||
|
// Try Content Resolver first
|
||||||
|
queryContentResolver(type, payload, pubkey, currentUser)?.let {
|
||||||
|
return it.result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to Intent
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$payload".toUri()).apply {
|
||||||
|
`package` = cachedPackageName
|
||||||
|
putExtra("type", type)
|
||||||
|
if (pubkey != null) putExtra("pubkey", pubkey.toHex())
|
||||||
|
if (currentUser != null) putExtra("current_user", currentUser.toHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = launcher.launch(intent)
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return null
|
||||||
|
|
||||||
|
val data = result.data ?: return null
|
||||||
|
if (data.getBooleanExtra("rejected", false)) return null
|
||||||
|
|
||||||
|
return data.getStringExtra("result")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri())
|
||||||
|
return context.packageManager.queryIntentActivities(intent, 0).isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPublicKey(permissions: String?): ExternalSignerResult? {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri()).apply {
|
||||||
|
putExtra("type", "get_public_key")
|
||||||
|
if (permissions != null) putExtra("permissions", permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = launcher.launch(intent)
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return null
|
||||||
|
|
||||||
|
val data = result.data ?: return null
|
||||||
|
if (data.getBooleanExtra("rejected", false)) return null
|
||||||
|
|
||||||
|
val pubkey = data.getStringExtra("result") ?: return null
|
||||||
|
val packageName = data.getStringExtra("package") ?: return null
|
||||||
|
|
||||||
|
return ExternalSignerResult(pubkey, packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun signEvent(event: UnsignedEvent, currentUser: PublicKey): String? {
|
||||||
|
// Try Content Resolver first
|
||||||
|
queryContentResolver(
|
||||||
|
"SIGN_EVENT",
|
||||||
|
event.asJson(),
|
||||||
|
null,
|
||||||
|
currentUser
|
||||||
|
)?.let { result ->
|
||||||
|
if (result.event != null) return result.event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to Intent
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:${event.asJson()}".toUri()).apply {
|
||||||
|
`package` = cachedPackageName
|
||||||
|
putExtra("type", "sign_event")
|
||||||
|
putExtra("current_user", currentUser.toHex())
|
||||||
|
if (event.id() != null) putExtra("id", event.id()!!.toHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = launcher.launch(intent)
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return null
|
||||||
|
|
||||||
|
val data = result.data ?: return null
|
||||||
|
if (data.getBooleanExtra("rejected", false)) return null
|
||||||
|
|
||||||
|
return data.getStringExtra("event")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip04Encrypt(plaintext: String, pubkey: PublicKey): String? {
|
||||||
|
return request("nip04_encrypt", plaintext, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip04Decrypt(ciphertext: String, pubkey: PublicKey): String? {
|
||||||
|
return request("nip04_decrypt", ciphertext, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip44Encrypt(
|
||||||
|
plaintext: String,
|
||||||
|
pubkey: PublicKey,
|
||||||
|
currentUser: PublicKey
|
||||||
|
): String? {
|
||||||
|
return request("nip44_encrypt", plaintext, pubkey, currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip44Decrypt(
|
||||||
|
ciphertext: String,
|
||||||
|
pubkey: PublicKey,
|
||||||
|
currentUser: PublicKey
|
||||||
|
): String? {
|
||||||
|
return request("nip44_decrypt", ciphertext, pubkey, currentUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
166
composeApp/src/androidMain/kotlin/su/reya/coop/ExternalSigner.kt
Normal file
166
composeApp/src/androidMain/kotlin/su/reya/coop/ExternalSigner.kt
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
|
import rust.nostr.sdk.Event
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
|
||||||
|
data class SignerResult(
|
||||||
|
val result: String,
|
||||||
|
val event: Event? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ExternalSigner(
|
||||||
|
private val context: Context,
|
||||||
|
private val packageName: String,
|
||||||
|
private val currentUser: PublicKey,
|
||||||
|
private val launcher: ExternalSignerLauncher,
|
||||||
|
) : AsyncNostrSigner {
|
||||||
|
private fun queryContentResolver(
|
||||||
|
type: String,
|
||||||
|
payload: String,
|
||||||
|
pubkey: PublicKey? = null
|
||||||
|
): SignerResult? {
|
||||||
|
val uri = "content://$packageName.${type.uppercase()}".toUri()
|
||||||
|
val projection = mutableListOf<String>()
|
||||||
|
projection.add(payload)
|
||||||
|
if (pubkey != null) projection.add(pubkey.toHex())
|
||||||
|
projection.add(currentUser.toHex())
|
||||||
|
|
||||||
|
val cursor = context.contentResolver.query(
|
||||||
|
uri,
|
||||||
|
projection.toList() as Array<out String?>?,
|
||||||
|
null, null, null,
|
||||||
|
) ?: return null
|
||||||
|
|
||||||
|
return cursor.use {
|
||||||
|
if (it.getColumnIndex("rejected") > -1) return null
|
||||||
|
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val resultIndex = it.getColumnIndex(payload)
|
||||||
|
val result = if (resultIndex > -1) it.getString(resultIndex) else null
|
||||||
|
|
||||||
|
val eventIndex = it.getColumnIndex("event")
|
||||||
|
val event = if (eventIndex > -1) it.getString(eventIndex) else null
|
||||||
|
|
||||||
|
SignerResult(result = result!!, event = Event.fromJson(event!!))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestViaIntent(
|
||||||
|
type: String,
|
||||||
|
payload: String,
|
||||||
|
extras: Map<String, String> = emptyMap()
|
||||||
|
): String {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$payload".toUri()).apply {
|
||||||
|
`package` = packageName
|
||||||
|
putExtra("type", type)
|
||||||
|
putExtra("current_user", currentUser.toHex())
|
||||||
|
extras.forEach { (k, v) -> putExtra(k, v) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = launcher.launch(intent)
|
||||||
|
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) {
|
||||||
|
throw Exception("Signer returned error (resultCode=${result.resultCode})")
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = result.data ?: throw Exception("Signer returned no data")
|
||||||
|
|
||||||
|
if (data.getBooleanExtra("rejected", false)) {
|
||||||
|
throw Exception("User rejected the request")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.getStringExtra("result") ?: throw Exception("Signer returned no result")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun request(
|
||||||
|
type: String,
|
||||||
|
payload: String,
|
||||||
|
pubkey: PublicKey? = null,
|
||||||
|
currentUser: PublicKey? = null,
|
||||||
|
extras: Map<String, String> = emptyMap()
|
||||||
|
): String {
|
||||||
|
// Try silent Content Resolver first
|
||||||
|
queryContentResolver(type, payload, pubkey)?.let { return it.result }
|
||||||
|
|
||||||
|
// Fall back to Intent
|
||||||
|
val allExtras = extras.toMutableMap().apply {
|
||||||
|
if (pubkey != null) put("pubkey", pubkey.toHex())
|
||||||
|
if (currentUser != null) put("current_user", currentUser.toHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestViaIntent(type, payload, allExtras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPublicKeyAsync(): PublicKey {
|
||||||
|
return currentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun signEventAsync(unsignedEvent: UnsignedEvent): Event? {
|
||||||
|
val eventJson = unsignedEvent.asJson()
|
||||||
|
|
||||||
|
// Try Content Resolver first
|
||||||
|
val contentResult = queryContentResolver("sign_event", eventJson)
|
||||||
|
contentResult?.event?.let { return it }
|
||||||
|
|
||||||
|
// Fall back to Intent
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$eventJson".toUri()).apply {
|
||||||
|
`package` = packageName
|
||||||
|
putExtra("type", "sign_event")
|
||||||
|
putExtra("current_user", currentUser.toHex())
|
||||||
|
putExtra("id", unsignedEvent.id()?.toHex() ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = launcher.launch(intent)
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return null
|
||||||
|
|
||||||
|
val data = result.data ?: return null
|
||||||
|
if (data.getBooleanExtra("rejected", false)) return null
|
||||||
|
|
||||||
|
val signedEventJson = data.getStringExtra("event")
|
||||||
|
|
||||||
|
return signedEventJson?.let { Event.fromJson(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip04EncryptAsync(publicKey: PublicKey, content: String): String {
|
||||||
|
return request(
|
||||||
|
"nip04_encrypt",
|
||||||
|
payload = content,
|
||||||
|
pubkey = publicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip04DecryptAsync(publicKey: PublicKey, encryptedContent: String): String {
|
||||||
|
return request(
|
||||||
|
"nip04_decrypt",
|
||||||
|
payload = encryptedContent,
|
||||||
|
pubkey = publicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip44EncryptAsync(publicKey: PublicKey, content: String): String {
|
||||||
|
return request(
|
||||||
|
"nip44_encrypt",
|
||||||
|
payload = content,
|
||||||
|
pubkey = publicKey,
|
||||||
|
currentUser = currentUser
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip44DecryptAsync(publicKey: PublicKey, payload: String): String {
|
||||||
|
return request(
|
||||||
|
"nip44_decrypt",
|
||||||
|
payload = payload,
|
||||||
|
pubkey = publicKey,
|
||||||
|
currentUser = currentUser
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
|
||||||
|
class ExternalSignerLauncher {
|
||||||
|
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||||
|
private var pendingResult: CompletableDeferred<ActivityResult>? = null
|
||||||
|
|
||||||
|
fun register(launcher: ActivityResultLauncher<Intent>) {
|
||||||
|
this.launcher = launcher
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister() {
|
||||||
|
launcher = null
|
||||||
|
pendingResult?.cancel()
|
||||||
|
pendingResult = null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun launch(intent: Intent): ActivityResult {
|
||||||
|
val deferred = CompletableDeferred<ActivityResult>()
|
||||||
|
pendingResult = deferred
|
||||||
|
launcher?.launch(intent)
|
||||||
|
?: throw IllegalStateException("ExternalSignerLauncher not registered")
|
||||||
|
return deferred.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResult(result: ActivityResult) {
|
||||||
|
pendingResult?.complete(result)
|
||||||
|
pendingResult = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ package su.reya.coop
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
@@ -14,11 +17,14 @@ import su.reya.coop.coop.storage.SecretStore
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private val externalSignerLauncher = ExternalSignerLauncher()
|
||||||
|
|
||||||
private val viewModel: NostrViewModel by viewModels {
|
private val viewModel: NostrViewModel by viewModels {
|
||||||
object : ViewModelProvider.Factory {
|
object : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
val secretStore = SecretStore(this@MainActivity)
|
val secretStore = SecretStore(this@MainActivity)
|
||||||
return NostrViewModel(NostrManager.instance, secretStore) as T
|
val androidSigner = AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||||
|
return NostrViewModel(NostrManager.instance, secretStore, androidSigner) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,10 +32,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
throwable.printStackTrace()
|
throwable.printStackTrace()
|
||||||
android.util.Log.e(
|
|
||||||
"CoopCrash",
|
Log.e(
|
||||||
"Uncaught exception in thread ${thread.name}",
|
"CoopCrash", "Uncaught exception in thread ${thread.name}", throwable
|
||||||
throwable
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start the Crash Activity
|
// Start the Crash Activity
|
||||||
@@ -40,10 +45,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|
||||||
// Exit
|
// Exit
|
||||||
android.os.Process.killProcess(android.os.Process.myPid())
|
Process.killProcess(Process.myPid())
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val resultLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
externalSignerLauncher.onResult(result)
|
||||||
|
}
|
||||||
|
externalSignerLauncher.register(resultLauncher)
|
||||||
|
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform interface for NIP-55 external signer communication.
|
||||||
|
* Implemented on Android; no-op/null on other platforms.
|
||||||
|
*/
|
||||||
|
interface ExternalSignerHandler {
|
||||||
|
fun isAvailable(): Boolean
|
||||||
|
|
||||||
|
suspend fun getPublicKey(permissions: String? = null): ExternalSignerResult?
|
||||||
|
suspend fun signEvent(event: UnsignedEvent, currentUser: PublicKey): String?
|
||||||
|
suspend fun nip04Encrypt(plaintext: String, pubkey: PublicKey): String?
|
||||||
|
suspend fun nip04Decrypt(ciphertext: String, pubkey: PublicKey): String?
|
||||||
|
suspend fun nip44Encrypt(plaintext: String, pubkey: PublicKey, currentUser: PublicKey): String?
|
||||||
|
suspend fun nip44Decrypt(ciphertext: String, pubkey: PublicKey, currentUser: PublicKey): String?
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ExternalSignerResult(
|
||||||
|
val pubkey: String,
|
||||||
|
val packageName: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
|
import rust.nostr.sdk.Event
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
|
||||||
|
class ExternalSignerProxy(
|
||||||
|
private val handler: ExternalSignerHandler,
|
||||||
|
private val packageName: String,
|
||||||
|
private val currentUser: PublicKey,
|
||||||
|
) : AsyncNostrSigner {
|
||||||
|
override suspend fun getPublicKeyAsync(): PublicKey {
|
||||||
|
return currentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun signEventAsync(unsignedEvent: UnsignedEvent): Event? {
|
||||||
|
val signedJson = handler.signEvent(unsignedEvent, currentUser) ?: return null
|
||||||
|
return Event.fromJson(signedJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip04EncryptAsync(publicKey: PublicKey, content: String): String {
|
||||||
|
return handler.nip04Encrypt(content, publicKey)
|
||||||
|
?: throw Exception("NIP-04 encrypt rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip04DecryptAsync(publicKey: PublicKey, encryptedContent: String): String {
|
||||||
|
return handler.nip04Decrypt(encryptedContent, publicKey)
|
||||||
|
?: throw Exception("NIP-04 decrypt rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip44EncryptAsync(publicKey: PublicKey, content: String): String {
|
||||||
|
return handler.nip44Encrypt(content, publicKey, currentUser)
|
||||||
|
?: throw Exception("NIP-44 encrypt rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nip44DecryptAsync(publicKey: PublicKey, payload: String): String {
|
||||||
|
return handler.nip44Decrypt(payload, publicKey, currentUser)
|
||||||
|
?: throw Exception("NIP-44 decrypt rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,8 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
class NostrViewModel(
|
class NostrViewModel(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
private val secretStore: SecretStorage
|
private val secretStore: SecretStorage,
|
||||||
|
private val externalSignerHandler: ExternalSignerHandler? = null,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _isNotificationBannerDismissed = MutableStateFlow(false)
|
private val _isNotificationBannerDismissed = MutableStateFlow(false)
|
||||||
val isNotificationBannerDismissed = _isNotificationBannerDismissed.asStateFlow()
|
val isNotificationBannerDismissed = _isNotificationBannerDismissed.asStateFlow()
|
||||||
@@ -374,6 +375,7 @@ class NostrViewModel(
|
|||||||
private suspend fun createSigner(secret: String): AsyncNostrSigner {
|
private suspend fun createSigner(secret: String): AsyncNostrSigner {
|
||||||
return when {
|
return when {
|
||||||
secret.startsWith("nsec1") -> Keys.parse(secret)
|
secret.startsWith("nsec1") -> Keys.parse(secret)
|
||||||
|
|
||||||
secret.startsWith("bunker://") -> {
|
secret.startsWith("bunker://") -> {
|
||||||
val appKeys = getOrInitAppKeys()
|
val appKeys = getOrInitAppKeys()
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
val bunker = NostrConnectUri.parse(secret)
|
||||||
@@ -381,6 +383,18 @@ class NostrViewModel(
|
|||||||
NostrConnect(uri = bunker, appKeys, timeout, null)
|
NostrConnect(uri = bunker, appKeys, timeout, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secret.startsWith("nip55://") -> {
|
||||||
|
val handler = externalSignerHandler
|
||||||
|
?: throw IllegalStateException("External signer not available on this platform")
|
||||||
|
|
||||||
|
// Format: nip55://packageName/hexPubkey
|
||||||
|
val parts = secret.removePrefix("nip55://").split("/", limit = 2)
|
||||||
|
val packageName = parts[0]
|
||||||
|
val pubkey = PublicKey.parse(parts[1])
|
||||||
|
|
||||||
|
ExternalSignerProxy(handler, packageName, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("Invalid secret format")
|
else -> throw IllegalArgumentException("Invalid secret format")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user