diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index efbe0472..1da97b10 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -28,6 +28,14 @@ class SignerProvider : ContentProvider() { private fun rejectedCursor(): Cursor = MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) } + // Decodes the Base64 v3 wire value to readable plaintext for history. + @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) + private fun nip44v3Plaintext(wireValue: String): String = try { + kotlin.io.encoding.Base64.decode(wireValue).toString(Charsets.UTF_8) + } catch (_: Exception) { + wireValue + } + override fun delete( uri: Uri, selection: String?, @@ -395,6 +403,13 @@ class SignerProvider : ContentProvider() { "Could not decrypt the message" } + // For v3 the wire value is Base64; this auto-accept path has no + // EncryptedDataKind, so decode it once for the readable log. + val historyContent = if (isV3) { + nip44v3Plaintext(if (!isEncrypt) finalResult else content) + } else { + if (!isEncrypt) finalResult else content + } scope.launch { historyDatabase.dao().addHistory( listOf( @@ -405,7 +420,7 @@ class SignerProvider : ContentProvider() { v3Kind, TimeUtils.now(), true, - content = if (!isEncrypt) finalResult else content, + content = historyContent, ), ), account.npub, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt index 8763a198..b82cf9c5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/EncryptedDataKind.kt @@ -31,6 +31,13 @@ fun SignerType.toPermissionTypeString(encryptedData: EncryptedDataKind?): String else -> this.toString() } +/** + * The readable NIP-44 v3 plaintext. For v3 the data kind stores the real + * plaintext in `text` and the Base64 wire value in `result`, so history/display + * read `text` and never touch the wire encoding. + */ +fun EncryptedDataKind?.nip44v3Plaintext(): String = (this as? ClearTextEncryptedDataKind)?.text ?: this?.result ?: "" + val encryptDecryptSignerTypes = setOf( SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, 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 097b1f99..92bcf476 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt @@ -25,11 +25,13 @@ import com.vitorpamplona.quartz.utils.TimeUtils object AmberUtils { /** - * 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). + * Produces the wire value for a request. Per the nip44v3 NIP-46 draft, v3 + * plaintext travels Base64-encoded: encrypt receives base64(plaintext) and + * returns the ciphertext; decrypt returns base64(plaintext). The readable + * plaintext for logs/display comes from the EncryptedDataKind instead (see + * [nip44v3Plaintext]). [nip44v3Kind]/[nip44v3Scope] are required for V3. */ + @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) suspend fun encryptOrDecryptData( data: String, type: SignerType, @@ -52,11 +54,11 @@ object AmberUtils { } SignerType.NIP44_V3_ENCRYPT -> { requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_ENCRYPT" } - account.nip44v3Encrypt(data.toByteArray(Charsets.UTF_8), pubKey, nip44v3Kind, nip44v3Scope) + account.nip44v3Encrypt(kotlin.io.encoding.Base64.decode(data), pubKey, nip44v3Kind, nip44v3Scope) } SignerType.NIP44_V3_DECRYPT -> { requireNotNull(nip44v3Kind) { "kind is required for NIP44_V3_DECRYPT" } - account.nip44v3Decrypt(data, pubKey, nip44v3Kind, nip44v3Scope).toString(Charsets.UTF_8) + kotlin.io.encoding.Base64.encode(account.nip44v3Decrypt(data, pubKey, nip44v3Kind, nip44v3Scope)) } else -> { account.nip44Decrypt(data, pubKey) 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 31de2e7d..ba2bb3fa 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -16,6 +16,7 @@ import com.greenart7c3.nostrsigner.models.AmberBunkerRequest import com.greenart7c3.nostrsigner.models.EncryptionType import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.SignerType +import com.greenart7c3.nostrsigner.models.nip44v3Plaintext import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.ui.RememberType @@ -430,9 +431,13 @@ object BunkerRequestUtils { SignerType.SIGN_EVENT, SignerType.NIP04_DECRYPT, SignerType.NIP44_DECRYPT, - SignerType.NIP44_V3_DECRYPT, SignerType.DECRYPT_ZAP_EVENT, -> response + // v3 wire values are Base64; log the readable plaintext + // already decoded into encryptedData. + SignerType.NIP44_V3_ENCRYPT, + SignerType.NIP44_V3_DECRYPT, + -> bunkerRequest.encryptedData.nip44v3Plaintext() 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 bb0b8c69..46489a54 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt @@ -785,9 +785,11 @@ class EventNotificationConsumer(private val applicationContext: Context) { * 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). + * plaintext as `result`. The Base64 wire payload is decoded here once so + * history/display can use the readable plaintext. 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, @@ -797,12 +799,14 @@ class EventNotificationConsumer(private val applicationContext: Context) { val data = BunkerRequestUtils.getDataFromBunker(bunkerRequest) val pubKey = bunkerRequest.params.firstOrNull() ?: return null return try { + // text = readable plaintext (for display/history); result = wire value. if (bunkerRequest.method == "nip44v3_encrypt") { - val ciphertext = acc.nip44v3Encrypt(data.toByteArray(Charsets.UTF_8), pubKey, kind, scope) - ClearTextEncryptedDataKind(data, ciphertext) + val plainBytes = kotlin.io.encoding.Base64.decode(data) + val ciphertext = acc.nip44v3Encrypt(plainBytes, pubKey, kind, scope) + ClearTextEncryptedDataKind(plainBytes.toString(Charsets.UTF_8), ciphertext) } else { - val plaintext = acc.nip44v3Decrypt(data, pubKey, kind, scope).toString(Charsets.UTF_8) - ClearTextEncryptedDataKind(data, plaintext) + val plainBytes = acc.nip44v3Decrypt(data, pubKey, kind, scope) + ClearTextEncryptedDataKind(plainBytes.toString(Charsets.UTF_8), kotlin.io.encoding.Base64.encode(plainBytes)) } } catch (e: Exception) { if (e is kotlinx.coroutines.CancellationException) throw e 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 8e7964e3..14c29baf 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -35,6 +35,7 @@ import com.greenart7c3.nostrsigner.models.ReturnType import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind import com.greenart7c3.nostrsigner.models.containsNip +import com.greenart7c3.nostrsigner.models.nip44v3Plaintext import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.components.DecryptTypeScope @@ -545,6 +546,7 @@ object IntentUtils { } } + @OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) private suspend fun getEncryptedDataKind( type: SignerType, result: String, @@ -555,6 +557,25 @@ object IntentUtils { PrivateZapEncryptedDataKind(result) } + // v3 wire values are Base64; decode once so display/history can read the + // plaintext from `text`, while `result` keeps the wire value. + SignerType.NIP44_V3_ENCRYPT -> { + val plaintext = try { + kotlin.io.encoding.Base64.decode(data).toString(Charsets.UTF_8) + } catch (_: Exception) { + data + } + ClearTextEncryptedDataKind(plaintext, result) + } + SignerType.NIP44_V3_DECRYPT -> { + val plaintext = try { + kotlin.io.encoding.Base64.decode(result).toString(Charsets.UTF_8) + } catch (_: Exception) { + result + } + ClearTextEncryptedDataKind(plaintext, result) + } + else -> { if (type.name.contains("ENCRYPT")) { if (data.startsWith("{")) { @@ -854,10 +875,15 @@ object IntentUtils { SignerType.SIGN_EVENT -> event SignerType.NIP04_DECRYPT, SignerType.NIP44_DECRYPT, - SignerType.NIP44_V3_DECRYPT, SignerType.DECRYPT_ZAP_EVENT, -> value + // v3 wire values are Base64; log the readable + // plaintext already decoded into encryptedData. + SignerType.NIP44_V3_ENCRYPT, + SignerType.NIP44_V3_DECRYPT, + -> intentData.encryptedData.nip44v3Plaintext() + 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 b6442d4c..421aee03 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 @@ -839,9 +839,9 @@ fun BunkerSingleEventHomeScreen( encryptedData = bunkerRequest.encryptedData, shouldRunOnAccept = acceptOrReject, onAccept = { rememberType, scope -> - // encryptedData.result holds the ciphertext (encrypt) or - // decrypted plaintext (decrypt), computed when the request - // arrived — same as the v2 path. + // encryptedData.result holds the Base64 wire value (ciphertext + // for encrypt, base64 plaintext for decrypt), computed when the + // request arrived — same shape 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 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 82c07a08..12812e7f 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 @@ -27,10 +27,10 @@ import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account -import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind import com.greenart7c3.nostrsigner.models.EncryptedDataKind import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.SignerType +import com.greenart7c3.nostrsigner.models.nip44v3Plaintext import com.greenart7c3.nostrsigner.ui.RememberType /** @@ -61,7 +61,7 @@ fun Nip44v3ApprovalData( val isEncrypt = type == SignerType.NIP44_V3_ENCRYPT val messageRes = if (isEncrypt) R.string.nip44_v3_wants_to_encrypt else R.string.nip44_v3_wants_to_decrypt - val displayContent = nip44v3DisplayContent(encryptedData, isEncrypt) + val displayContent = encryptedData.nip44v3Plaintext() Column(modifier) { if (isBunker) { @@ -181,13 +181,3 @@ fun Nip44v3ContextBox( } } } - -/** - * The plaintext to show: encrypt stores it as `text`, decrypt as `result` - * (populated when the request was parsed) — same shape as the v2 screen. - */ -private fun nip44v3DisplayContent(encryptedData: EncryptedDataKind?, isEncrypt: Boolean): String = if (isEncrypt) { - (encryptedData as? ClearTextEncryptedDataKind)?.text ?: encryptedData?.result ?: "" -} else { - encryptedData?.result ?: "" -}