mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Handle NIP-44 v3 plaintext like v2 (no base64 layer)
Following the v2 pattern: encrypt/decrypt operate on plaintext strings end to end, so the decrypted content flows into history naturally. This removes the Base64 wrapping introduced earlier along with the per-type decode special-casing in the history-saving switches, the encrypt payload base64 validation, and the screen's base64 decode. The bunker approval now reuses the precomputed encryptedData.result (as v2 does) instead of recomputing on accept.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
),
|
||||
|
||||
+20
-24
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
),
|
||||
|
||||
+21
-33
@@ -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
|
||||
|
||||
@@ -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 ?: ""
|
||||
}
|
||||
|
||||
@@ -655,7 +655,6 @@
|
||||
<string name="for_all_kinds">All kinds</string>
|
||||
<string name="nip44_v3_wants_to_encrypt">wants to encrypt with NIP-44 v3</string>
|
||||
<string name="nip44_v3_wants_to_decrypt">wants to read encrypted content with NIP-44 v3</string>
|
||||
<string name="nip44_v3_binary_data">Binary data (%1$s bytes)</string>
|
||||
<string name="event_kind_10086">Indexer relays</string>
|
||||
<string name="event_kind_10087">Proxy relays</string>
|
||||
<string name="event_kind_10088">Broadcast relays</string>
|
||||
|
||||
Reference in New Issue
Block a user