diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt
index 74c8cf30..efbe0472 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt
@@ -28,14 +28,6 @@ class SignerProvider : ContentProvider() {
private fun rejectedCursor(): Cursor = MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) }
- @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
- private fun isValidBase64(value: String): Boolean = try {
- kotlin.io.encoding.Base64.decode(value)
- true
- } catch (_: IllegalArgumentException) {
- false
- }
-
override fun delete(
uri: Uri,
selection: String?,
@@ -279,14 +271,6 @@ class SignerProvider : ContentProvider() {
null to ""
}
- // For V3 encrypt the wire payload is base64-encoded plaintext.
- // Validate it up-front (no secret material involved) so a
- // malformed request is rejected without bothering the user.
- if (isV3 && isEncrypt && !isValidBase64(content)) {
- Log.d(Amber.TAG, "NIP-44 v3 encrypt payload is not valid base64")
- return rejectedCursor()
- }
-
// For ENCRYPT: classify plaintext input; for DECRYPT: perform operation first then classify result
val result =
if (isEncrypt) {
@@ -411,12 +395,6 @@ class SignerProvider : ContentProvider() {
"Could not decrypt the message"
}
- // For v3 the wire payload is Base64; store the readable plaintext.
- val historyContent = if (isV3) {
- AmberUtils.decodeNip44v3LogContent(if (!isEncrypt) finalResult else content)
- } else {
- if (!isEncrypt) finalResult else content
- }
scope.launch {
historyDatabase.dao().addHistory(
listOf(
@@ -427,7 +405,7 @@ class SignerProvider : ContentProvider() {
v3Kind,
TimeUtils.now(),
true,
- content = historyContent,
+ content = if (!isEncrypt) finalResult else content,
),
),
account.npub,
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt
index 29b16842..097b1f99 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt
@@ -25,16 +25,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
object AmberUtils {
/**
- * For NIP-44 v3 callers, [data] follows the NIP-46 wire format:
- * - V3 encrypt: [data] is base64-encoded plaintext bytes; the result is the
- * v3 ciphertext string.
- * - V3 decrypt: [data] is the v3 ciphertext string; the result is the
- * base64-encoded plaintext bytes.
- *
- * [nip44v3Kind] and [nip44v3Scope] are required for the V3 SignerType variants
- * (kind comes from the request; scope defaults to empty per spec).
+ * Like v2, NIP-44 v3 plaintext is handled as a UTF-8 string here: encrypt
+ * takes the plaintext and returns the ciphertext; decrypt returns the
+ * plaintext. [nip44v3Kind] and [nip44v3Scope] are required for the V3
+ * SignerType variants (kind comes from the request; scope defaults to empty).
*/
- @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
suspend fun encryptOrDecryptData(
data: String,
type: SignerType,
@@ -57,31 +52,17 @@ object AmberUtils {
}
SignerType.NIP44_V3_ENCRYPT -> {
requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_ENCRYPT" }
- val plaintextBytes = kotlin.io.encoding.Base64.decode(data)
- account.nip44v3Encrypt(plaintextBytes, pubKey, nip44v3Kind, nip44v3Scope)
+ account.nip44v3Encrypt(data.toByteArray(Charsets.UTF_8), pubKey, nip44v3Kind, nip44v3Scope)
}
SignerType.NIP44_V3_DECRYPT -> {
requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_DECRYPT" }
- val plaintextBytes = account.nip44v3Decrypt(data, pubKey, nip44v3Kind, nip44v3Scope)
- kotlin.io.encoding.Base64.encode(plaintextBytes)
+ account.nip44v3Decrypt(data, pubKey, nip44v3Kind, nip44v3Scope).toString(Charsets.UTF_8)
}
else -> {
account.nip44Decrypt(data, pubKey)
}
}
- /**
- * NIP-44 v3 wire payloads are Base64-encoded plaintext bytes. History logs
- * should record the readable plaintext, so decode it here; if the bytes
- * aren't valid base64/UTF-8 (e.g. binary data) keep the original string.
- */
- @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
- fun decodeNip44v3LogContent(content: String): String = try {
- kotlin.io.encoding.Base64.decode(content).decodeToString(throwOnInvalidSequence = true)
- } catch (_: Exception) {
- content
- }
-
suspend fun sendBunkerError(
account: Account,
bunkerRequest: AmberBunkerRequest,
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt
index d4f544d2..31de2e7d 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt
@@ -430,11 +430,9 @@ object BunkerRequestUtils {
SignerType.SIGN_EVENT,
SignerType.NIP04_DECRYPT,
SignerType.NIP44_DECRYPT,
+ SignerType.NIP44_V3_DECRYPT,
SignerType.DECRYPT_ZAP_EVENT,
-> response
- // v3 payloads are Base64; log the readable plaintext.
- SignerType.NIP44_V3_DECRYPT -> AmberUtils.decodeNip44v3LogContent(response)
- SignerType.NIP44_V3_ENCRYPT -> AmberUtils.decodeNip44v3LogContent(getDataFromBunker(bunkerRequest.request))
else -> getDataFromBunker(bunkerRequest.request)
},
),
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt
index 069dbac8..bb0b8c69 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt
@@ -608,8 +608,8 @@ class EventNotificationConsumer(private val applicationContext: Context) {
acc: Account,
url: String,
): EncryptedDataKind? = if (bunkerRequest != null && (bunkerRequest.method == "nip44v3_encrypt" || bunkerRequest.method == "nip44v3_decrypt")) {
- // V3 arrives as a generic BunkerRequest; build a preview the dedicated
- // approval screen can render (it Base64-decodes the stored payload).
+ // V3 arrives as a generic BunkerRequest; build the preview the dedicated
+ // approval screen renders (plaintext, like the v2 path).
nip44v3EncryptedDataKind(bunkerRequest, acc)
} else if (bunkerRequest is BunkerRequestNip44Decrypt) {
val result = acc.nip44Decrypt(bunkerRequest.ciphertext, bunkerRequest.pubKey)
@@ -782,19 +782,12 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
/**
- * Validates a NIP-44 v3 bunker request. Returns null when the request can
- * proceed, or an error message when it is malformed and must be rejected
- * without prompting the user (missing/invalid kind, context mismatch on
- * decrypt, bad MAC/padding, or a non-base64 encrypt payload).
+ * Builds the preview [EncryptedDataKind] for a NIP-44 v3 bunker request,
+ * mirroring v2: encrypt stores the plaintext as `text` and the ciphertext
+ * as `result`; decrypt stores the ciphertext as `text` and the decrypted
+ * plaintext as `result`. Returns null if it can't be produced (the request
+ * is auto-rejected elsewhere in that case).
*/
- /**
- * Builds the preview [EncryptedDataKind] for a NIP-44 v3 bunker request.
- * For encrypt, the wire payload is Base64 plaintext (shown decoded). For
- * decrypt, the payload is decrypted and the plaintext stored Base64-encoded
- * — matching what the approval screen expects. Returns null if it can't be
- * produced (the request is auto-rejected elsewhere in that case).
- */
- @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
private fun nip44v3EncryptedDataKind(
bunkerRequest: BunkerRequest,
acc: Account,
@@ -805,10 +798,11 @@ class EventNotificationConsumer(private val applicationContext: Context) {
val pubKey = bunkerRequest.params.firstOrNull() ?: return null
return try {
if (bunkerRequest.method == "nip44v3_encrypt") {
- ClearTextEncryptedDataKind(data, "")
+ val ciphertext = acc.nip44v3Encrypt(data.toByteArray(Charsets.UTF_8), pubKey, kind, scope)
+ ClearTextEncryptedDataKind(data, ciphertext)
} else {
- val plaintext = acc.nip44v3Decrypt(data, pubKey, kind, scope)
- ClearTextEncryptedDataKind("", kotlin.io.encoding.Base64.encode(plaintext))
+ val plaintext = acc.nip44v3Decrypt(data, pubKey, kind, scope).toString(Charsets.UTF_8)
+ ClearTextEncryptedDataKind(data, plaintext)
}
} catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) throw e
@@ -816,7 +810,12 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
- @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
+ /**
+ * Validates a NIP-44 v3 bunker request. Returns null when it can proceed, or
+ * a generic error when it is malformed and must be rejected without
+ * prompting the user (missing kind, or a decrypt whose context/MAC/padding
+ * doesn't check out).
+ */
private fun validateNip44v3Request(
type: SignerType,
kind: Int?,
@@ -826,18 +825,15 @@ class EventNotificationConsumer(private val applicationContext: Context) {
acc: Account,
): String? {
if (kind == null) return "kind is required for nip44v3"
+ if (type != SignerType.NIP44_V3_DECRYPT) return null
return try {
- if (type == SignerType.NIP44_V3_DECRYPT) {
- acc.nip44v3Decrypt(data, pubKey, kind, scope)
- } else {
- kotlin.io.encoding.Base64.decode(data)
- }
+ acc.nip44v3Decrypt(data, pubKey, kind, scope)
null
} catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) throw e
// Keep the response generic so we don't leak the ciphertext's
// embedded context back to the requester.
- if (type == SignerType.NIP44_V3_DECRYPT) "could not decrypt the message" else "invalid encrypted payload"
+ "could not decrypt the message"
}
}
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt
index 9c392ea9..8e7964e3 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt
@@ -854,13 +854,10 @@ object IntentUtils {
SignerType.SIGN_EVENT -> event
SignerType.NIP04_DECRYPT,
SignerType.NIP44_DECRYPT,
+ SignerType.NIP44_V3_DECRYPT,
SignerType.DECRYPT_ZAP_EVENT,
-> value
- // v3 payloads are Base64; log the readable plaintext.
- SignerType.NIP44_V3_DECRYPT -> AmberUtils.decodeNip44v3LogContent(value)
- SignerType.NIP44_V3_ENCRYPT -> AmberUtils.decodeNip44v3LogContent(intentData.data)
-
else -> intentData.data
},
),
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt
index 92843cee..b6442d4c 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt
@@ -36,7 +36,6 @@ import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.kindToNip
import com.greenart7c3.nostrsigner.models.toPermissionType
-import com.greenart7c3.nostrsigner.service.AmberUtils
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.service.PsbtDecoder
@@ -840,38 +839,27 @@ fun BunkerSingleEventHomeScreen(
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
onAccept = { rememberType, scope ->
- Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
- val result = try {
- AmberUtils.encryptOrDecryptData(
- BunkerRequestUtils.getDataFromBunker(bunkerRequest.request),
- type,
- account,
- bunkerRequest.request.params.first(),
- v3Kind,
- v3Scope,
- ) ?: ""
- } catch (e: Exception) {
- ""
- }
-
- // SPECIFIC ⇒ kind-scoped grant; ALL ⇒ broad grant (kind=null).
- val grantedKind = if (scope == DecryptTypeScope.SPECIFIC) v3Kind else null
- BunkerRequestUtils.sendResult(
- context = context,
- account = account,
- key = key,
- response = result,
- bunkerRequest = bunkerRequest,
- kind = grantedKind,
- onLoading = onLoading,
- permissions = null,
- appName = appName,
- signPolicy = null,
- shouldCloseApplication = bunkerRequest.closeApplication,
- rememberType = rememberType,
- decryptTypeScope = scope,
- )
- }
+ // encryptedData.result holds the ciphertext (encrypt) or
+ // decrypted plaintext (decrypt), computed when the request
+ // arrived — same as the v2 path.
+ val result = bunkerRequest.encryptedData?.result ?: ""
+ // SPECIFIC ⇒ kind-scoped grant; ALL ⇒ broad grant (kind=null).
+ val grantedKind = if (scope == DecryptTypeScope.SPECIFIC) v3Kind else null
+ BunkerRequestUtils.sendResult(
+ context = context,
+ account = account,
+ key = key,
+ response = result,
+ bunkerRequest = bunkerRequest,
+ kind = grantedKind,
+ onLoading = onLoading,
+ permissions = null,
+ appName = appName,
+ signPolicy = null,
+ shouldCloseApplication = bunkerRequest.closeApplication,
+ rememberType = rememberType,
+ decryptTypeScope = scope,
+ )
},
onReject = { rememberType, scope ->
val grantedKind = if (scope == DecryptTypeScope.SPECIFIC) v3Kind else null
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Nip44v3ApprovalData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Nip44v3ApprovalData.kt
index 8fb9f577..82c07a08 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Nip44v3ApprovalData.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Nip44v3ApprovalData.kt
@@ -32,17 +32,13 @@ import com.greenart7c3.nostrsigner.models.EncryptedDataKind
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.ui.RememberType
-import java.nio.charset.CharacterCodingException
-import kotlin.io.encoding.Base64
-import kotlin.io.encoding.ExperimentalEncodingApi
/**
* Dedicated approval screen for NIP-44 v3 encrypt/decrypt requests.
*
* Unlike the v2/v4 [EncryptDecryptData] screen, v3 authenticates an event
- * `kind` and a `scope`, and its wire payloads are Base64-encoded bytes — so
- * this screen surfaces that context explicitly and decodes the payload for
- * display. The scope toggle grants either a single kind or all kinds.
+ * `kind` and a `scope`, so this screen surfaces that context explicitly. The
+ * scope toggle grants either a single kind or all kinds.
*/
@Composable
fun Nip44v3ApprovalData(
@@ -187,45 +183,11 @@ fun Nip44v3ContextBox(
}
/**
- * The v3 payload travels Base64-encoded. Decode it for display, falling back to
- * a "binary data" placeholder when the bytes aren't valid UTF-8 text.
+ * The plaintext to show: encrypt stores it as `text`, decrypt as `result`
+ * (populated when the request was parsed) — same shape as the v2 screen.
*/
-@Composable
-private fun nip44v3DisplayContent(encryptedData: EncryptedDataKind?, isEncrypt: Boolean): String {
- val raw = if (isEncrypt) {
- (encryptedData as? ClearTextEncryptedDataKind)?.text ?: encryptedData?.result ?: ""
- } else {
- encryptedData?.result ?: ""
- }
- if (raw.isEmpty()) return ""
- val binaryLabelSize = decodeBinarySizeOrNull(raw)
- return binaryLabelSize?.let { stringResource(R.string.nip44_v3_binary_data, it.toString()) }
- ?: decodeTextOrRaw(raw)
-}
-
-@OptIn(ExperimentalEncodingApi::class)
-private fun decodeBytesOrNull(value: String): ByteArray? = try {
- Base64.decode(value)
-} catch (_: IllegalArgumentException) {
- null
-}
-
-/** @return the byte count when [value] is Base64 of non-UTF-8 bytes, else null. */
-private fun decodeBinarySizeOrNull(value: String): Int? {
- val bytes = decodeBytesOrNull(value) ?: return null
- return try {
- bytes.decodeToString(throwOnInvalidSequence = true)
- null
- } catch (_: CharacterCodingException) {
- bytes.size
- }
-}
-
-private fun decodeTextOrRaw(value: String): String {
- val bytes = decodeBytesOrNull(value) ?: return value
- return try {
- bytes.decodeToString(throwOnInvalidSequence = true)
- } catch (_: CharacterCodingException) {
- value
- }
+private fun nip44v3DisplayContent(encryptedData: EncryptedDataKind?, isEncrypt: Boolean): String = if (isEncrypt) {
+ (encryptedData as? ClearTextEncryptedDataKind)?.text ?: encryptedData?.result ?: ""
+} else {
+ encryptedData?.result ?: ""
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4c4e9f7c..5a2d627f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -655,7 +655,6 @@
All kinds
wants to encrypt with NIP-44 v3
wants to read encrypted content with NIP-44 v3
- Binary data (%1$s bytes)
Indexer relays
Proxy relays
Broadcast relays