mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Store encrypt/decrypt payloads as ciphertext, decrypt on demand
History/activity rows previously persisted the plaintext of every NIP-04/NIP-44(/v3) encrypt and decrypt operation (encrypt = plaintext input, decrypt = plaintext output) and the decrypted private zap. That left sensitive cleartext at rest in history_db_$npub. Now the encrypted form is stored instead and the plaintext is recovered on demand only when the activity/history screen renders a row: - Encrypt requests store their ciphertext output; decrypt requests store the ciphertext input that arrived in the request. Rejected encrypts store nothing (no ciphertext exists, so no plaintext is leaked). - HistoryEntity gains encryptionPubKey + encryptionScope; the existing kind column carries the NIP-44 v3 kind. ActivityRow decrypts via account.decrypt / nip44v3Decrypt / decryptZapEvent, falling back to the stored value for pre-migration rows or missing context. - Ciphertext rows are exempted from content truncation so they remain decryptable. Covers all three ingestion paths (ContentProvider/relay via SignerProviderQuery, nostrsigner:// intents, NIP-46 bunker approvals) and both accept and reject flows. Adds HistoryDatabase migration 3->4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WABMihCLr9XN51uLWj93oX
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "9c7e0cf73a7c502f89cfc6dc97b0b56c",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pkKey` TEXT NOT NULL, `type` TEXT NOT NULL, `kind` INTEGER, `time` INTEGER NOT NULL, `accepted` INTEGER NOT NULL, `translatedPermission` TEXT NOT NULL, `content` TEXT NOT NULL, `encryptionPubKey` TEXT NOT NULL, `encryptionScope` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pkKey",
|
||||
"columnName": "pkKey",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "kind",
|
||||
"columnName": "kind",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accepted",
|
||||
"columnName": "accepted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translatedPermission",
|
||||
"columnName": "translatedPermission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "encryptionPubKey",
|
||||
"columnName": "encryptionPubKey",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "encryptionScope",
|
||||
"columnName": "encryptionScope",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "history_by_pk_key",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"pkKey"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `history_by_pk_key` ON `${TABLE_NAME}` (`pkKey`)"
|
||||
},
|
||||
{
|
||||
"name": "history_by_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `history_by_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "history_by_time",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"time"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `history_by_time` ON `${TABLE_NAME}` (`time`)"
|
||||
},
|
||||
{
|
||||
"name": "history_by_key_and_time",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"pkKey",
|
||||
"time"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `history_by_key_and_time` ON `${TABLE_NAME}` (`pkKey`, `time`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c7e0cf73a7c502f89cfc6dc97b0b56c')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.greenart7c3.nostrsigner.database.HistoryEntity
|
||||
import com.greenart7c3.nostrsigner.database.LogEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.encryptDecryptSignerTypes
|
||||
import com.greenart7c3.nostrsigner.models.kindToNip
|
||||
import com.greenart7c3.nostrsigner.models.permissionTypeFromContent
|
||||
import com.greenart7c3.nostrsigner.service.AmberUtils
|
||||
@@ -50,14 +51,6 @@ object SignerProviderQuery {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a signer operation and returns its result as a [Cursor], mirroring the
|
||||
* column layout the [SignerProvider] ContentProvider exposes to external apps.
|
||||
@@ -367,6 +360,11 @@ object SignerProviderQuery {
|
||||
val signPolicy = permDao.getSignPolicy(requesterId)
|
||||
val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null
|
||||
if (!isRemembered) {
|
||||
// A rejected encrypt request was never performed, so no
|
||||
// ciphertext exists — store nothing rather than leaking the
|
||||
// plaintext input. Decrypt requests carry their ciphertext as
|
||||
// input, so persist it and decrypt on demand.
|
||||
val rejectedContent = if (isEncrypt) "" else content
|
||||
scope.launch {
|
||||
historyDatabase.dao().addHistory(
|
||||
listOf(
|
||||
@@ -377,7 +375,9 @@ object SignerProviderQuery {
|
||||
v3Kind,
|
||||
TimeUtils.now(),
|
||||
false,
|
||||
content = content,
|
||||
content = rejectedContent,
|
||||
encryptionPubKey = if (rejectedContent.isNotEmpty() && type in encryptDecryptSignerTypes && type != SignerType.DECRYPT_ZAP_EVENT) pubkey else "",
|
||||
encryptionScope = if (isV3) v3Scope else "",
|
||||
),
|
||||
),
|
||||
account.npub,
|
||||
@@ -420,13 +420,11 @@ object SignerProviderQuery {
|
||||
"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
|
||||
}
|
||||
// Persist the encrypted form (ciphertext), never the plaintext:
|
||||
// encrypt requests store their ciphertext output (`finalResult`),
|
||||
// decrypt requests store the ciphertext input (`content`). The
|
||||
// plaintext is recovered on demand in the activity/history UI.
|
||||
val historyContent = if (isEncrypt) finalResult else content
|
||||
scope.launch {
|
||||
historyDatabase.dao().addHistory(
|
||||
listOf(
|
||||
@@ -438,6 +436,8 @@ object SignerProviderQuery {
|
||||
TimeUtils.now(),
|
||||
true,
|
||||
content = historyContent,
|
||||
encryptionPubKey = if (type in encryptDecryptSignerTypes && type != SignerType.DECRYPT_ZAP_EVENT) pubkey else "",
|
||||
encryptionScope = if (isV3) v3Scope else "",
|
||||
),
|
||||
),
|
||||
account.npub,
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.room.Transaction
|
||||
import com.greenart7c3.nostrsigner.Amber
|
||||
import com.greenart7c3.nostrsigner.AmberLog
|
||||
import com.greenart7c3.nostrsigner.models.Permission
|
||||
import com.greenart7c3.nostrsigner.models.encryptDecryptHistoryTypeNames
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
private const val MAX_CONTENT_LENGTH = 500
|
||||
@@ -110,9 +111,12 @@ interface HistoryDao {
|
||||
try {
|
||||
val localEntities = entities.map { entity ->
|
||||
val permission = Permission(entity.type.toLowerCase(Locale.current), entity.kind)
|
||||
// Ciphertext stored for encrypt/decrypt rows must be kept whole so it
|
||||
// can still be decrypted on demand; only truncate plaintext content.
|
||||
val isEncryptedContent = entity.type in encryptDecryptHistoryTypeNames
|
||||
entity.copy(
|
||||
translatedPermission = permission.toLocalizedString(Amber.instance, true),
|
||||
content = if (entity.content.length > MAX_CONTENT_LENGTH) entity.content.take(MAX_CONTENT_LENGTH) else entity.content,
|
||||
content = if (!isEncryptedContent && entity.content.length > MAX_CONTENT_LENGTH) entity.content.take(MAX_CONTENT_LENGTH) else entity.content,
|
||||
)
|
||||
}
|
||||
innerAddHistory(localEntities)
|
||||
|
||||
@@ -21,11 +21,18 @@ val migration_2_3 = object : Migration(2, 3) {
|
||||
}
|
||||
}
|
||||
|
||||
val migration_3_4 = object : Migration(3, 4) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE history ADD COLUMN encryptionPubKey TEXT NOT NULL DEFAULT ''")
|
||||
db.execSQL("ALTER TABLE history ADD COLUMN encryptionScope TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
HistoryEntity::class,
|
||||
],
|
||||
version = 3,
|
||||
version = 4,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class HistoryDatabase : RoomDatabase() {
|
||||
@@ -47,7 +54,7 @@ abstract class HistoryDatabase : RoomDatabase() {
|
||||
)
|
||||
.setQueryExecutor(executor)
|
||||
.setTransactionExecutor(transactionExecutor)
|
||||
.addMigrations(migration_1_2, migration_2_3)
|
||||
.addMigrations(migration_1_2, migration_2_3, migration_3_4)
|
||||
.build()
|
||||
|
||||
instance
|
||||
|
||||
@@ -34,5 +34,11 @@ data class HistoryEntity(
|
||||
val time: Long,
|
||||
val accepted: Boolean,
|
||||
val translatedPermission: String = "",
|
||||
// For encrypt/decrypt requests `content` holds the ciphertext (the encrypted
|
||||
// form that was sent or received), never the plaintext. The plaintext is
|
||||
// recovered on demand in the activity/history UI via [encryptionPubKey] (the
|
||||
// counterparty key) and, for NIP-44 v3, [kind] + [encryptionScope].
|
||||
val content: String = "",
|
||||
val encryptionPubKey: String = "",
|
||||
val encryptionScope: String = "",
|
||||
)
|
||||
|
||||
@@ -48,6 +48,19 @@ val encryptDecryptSignerTypes = setOf(
|
||||
SignerType.DECRYPT_ZAP_EVENT,
|
||||
)
|
||||
|
||||
val encryptSignerTypes = setOf(
|
||||
SignerType.NIP04_ENCRYPT,
|
||||
SignerType.NIP44_ENCRYPT,
|
||||
SignerType.NIP44_V3_ENCRYPT,
|
||||
)
|
||||
|
||||
/**
|
||||
* History rows for these request types store the ciphertext (encrypted form) in
|
||||
* `content` rather than the plaintext, so the value must never be truncated —
|
||||
* a partial ciphertext can no longer be decrypted on demand.
|
||||
*/
|
||||
val encryptDecryptHistoryTypeNames: Set<String> = encryptDecryptSignerTypes.mapTo(mutableSetOf()) { it.name }
|
||||
|
||||
/**
|
||||
* Determines the permission type string based on content string and operation direction.
|
||||
* For ENCRYPT: pass the plaintext to classify what kind of data is being encrypted.
|
||||
|
||||
@@ -16,7 +16,8 @@ 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.models.encryptDecryptSignerTypes
|
||||
import com.greenart7c3.nostrsigner.models.encryptSignerTypes
|
||||
import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.ui.RememberType
|
||||
@@ -455,28 +456,35 @@ object BunkerRequestUtils {
|
||||
// assume that everything worked and try to revert it if it fails
|
||||
EventNotificationConsumer(context).notificationManager().cancelAll()
|
||||
dao.insertApplicationWithPermissions(application)
|
||||
val isV3 = type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT
|
||||
// Persist the encrypted form (ciphertext), never the plaintext: encrypt
|
||||
// requests store their ciphertext output (`response`), decrypt requests
|
||||
// store the ciphertext input that arrived in the request. The plaintext
|
||||
// is recovered on demand in the activity/history UI.
|
||||
val historyContent = when (type) {
|
||||
SignerType.NIP04_ENCRYPT,
|
||||
SignerType.NIP44_ENCRYPT,
|
||||
SignerType.NIP44_V3_ENCRYPT,
|
||||
SignerType.SIGN_EVENT,
|
||||
-> response
|
||||
else -> getDataFromBunker(bunkerRequest.request)
|
||||
}
|
||||
historyDatabase.dao().addHistory(
|
||||
listOf(
|
||||
HistoryEntity(
|
||||
0,
|
||||
key,
|
||||
type.toString(),
|
||||
kind,
|
||||
if (isV3) getNip44v3Kind(bunkerRequest.request) else kind,
|
||||
TimeUtils.now(),
|
||||
true,
|
||||
content = when (type) {
|
||||
SignerType.SIGN_EVENT,
|
||||
SignerType.NIP04_DECRYPT,
|
||||
SignerType.NIP44_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)
|
||||
content = historyContent,
|
||||
encryptionPubKey = if (type in encryptDecryptSignerTypes && type != SignerType.DECRYPT_ZAP_EVENT) {
|
||||
bunkerRequest.request.params.firstOrNull() ?: ""
|
||||
} else {
|
||||
""
|
||||
},
|
||||
encryptionScope = if (isV3) getNip44v3Scope(bunkerRequest.request) else "",
|
||||
),
|
||||
),
|
||||
account.npub,
|
||||
@@ -630,16 +638,28 @@ object BunkerRequestUtils {
|
||||
|
||||
if (bunkerRequest.request !is BunkerRequestConnect) {
|
||||
Amber.instance.dao(account.npub).insertApplicationWithPermissions(application)
|
||||
val isV3 = signerType == SignerType.NIP44_V3_ENCRYPT || signerType == SignerType.NIP44_V3_DECRYPT
|
||||
// A rejected encrypt request was never performed, so no ciphertext
|
||||
// exists — store nothing rather than leaking the plaintext input.
|
||||
// Decrypt requests carry their ciphertext in the request, so it is
|
||||
// safe to persist and decrypt on demand.
|
||||
val historyContent = if (signerType in encryptSignerTypes) "" else getDataFromBunker(bunkerRequest.request)
|
||||
Amber.instance.getHistoryDatabase(account.npub).dao().addHistory(
|
||||
listOf(
|
||||
HistoryEntity(
|
||||
0,
|
||||
key,
|
||||
signerType.toString(),
|
||||
null,
|
||||
if (isV3) getNip44v3Kind(bunkerRequest.request) else null,
|
||||
TimeUtils.now(),
|
||||
false,
|
||||
content = getDataFromBunker(bunkerRequest.request),
|
||||
content = historyContent,
|
||||
encryptionPubKey = if (historyContent.isNotEmpty() && signerType in encryptDecryptSignerTypes && signerType != SignerType.DECRYPT_ZAP_EVENT) {
|
||||
bunkerRequest.request.params.firstOrNull() ?: ""
|
||||
} else {
|
||||
""
|
||||
},
|
||||
encryptionScope = if (isV3) getNip44v3Scope(bunkerRequest.request) else "",
|
||||
),
|
||||
),
|
||||
account.npub,
|
||||
|
||||
@@ -35,7 +35,8 @@ 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.models.encryptDecryptSignerTypes
|
||||
import com.greenart7c3.nostrsigner.models.encryptSignerTypes
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.ui.RememberType
|
||||
import com.greenart7c3.nostrsigner.ui.components.DecryptTypeScope
|
||||
@@ -900,6 +901,19 @@ object IntentUtils {
|
||||
// immediately, not only on a later request.
|
||||
persistNativeAppMetadata(context, account, packageName)
|
||||
val historyDatabase = Amber.instance.getHistoryDatabase(account.npub)
|
||||
val isV3 = intentData.type == SignerType.NIP44_V3_ENCRYPT || intentData.type == SignerType.NIP44_V3_DECRYPT
|
||||
// Persist the encrypted form (ciphertext), never the plaintext:
|
||||
// encrypt requests store their ciphertext output (`value`), decrypt
|
||||
// requests store the ciphertext input (`intentData.data`). The
|
||||
// plaintext is recovered on demand in the activity/history UI.
|
||||
val historyContent = when (intentData.type) {
|
||||
SignerType.SIGN_EVENT -> event
|
||||
SignerType.NIP04_ENCRYPT,
|
||||
SignerType.NIP44_ENCRYPT,
|
||||
SignerType.NIP44_V3_ENCRYPT,
|
||||
-> value
|
||||
else -> intentData.data
|
||||
}
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
historyDatabase.dao().addHistory(
|
||||
listOf(
|
||||
@@ -907,24 +921,16 @@ object IntentUtils {
|
||||
0,
|
||||
key,
|
||||
intentData.type.toString(),
|
||||
kind,
|
||||
if (isV3) intentData.nip44v3Kind else kind,
|
||||
TimeUtils.now(),
|
||||
true,
|
||||
content = when (intentData.type) {
|
||||
SignerType.SIGN_EVENT -> event
|
||||
SignerType.NIP04_DECRYPT,
|
||||
SignerType.NIP44_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
|
||||
content = historyContent,
|
||||
encryptionPubKey = if (intentData.type in encryptDecryptSignerTypes && intentData.type != SignerType.DECRYPT_ZAP_EVENT) {
|
||||
intentData.pubKey
|
||||
} else {
|
||||
""
|
||||
},
|
||||
encryptionScope = if (isV3) intentData.nip44v3Scope else "",
|
||||
),
|
||||
),
|
||||
account.npub,
|
||||
@@ -1058,16 +1064,27 @@ object IntentUtils {
|
||||
}
|
||||
|
||||
Amber.instance.dao(account.npub).insertApplicationWithPermissions(application)
|
||||
val isV3 = intentData.type == SignerType.NIP44_V3_ENCRYPT || intentData.type == SignerType.NIP44_V3_DECRYPT
|
||||
// A rejected encrypt request was never performed, so no ciphertext exists —
|
||||
// store nothing rather than leaking the plaintext input. Decrypt requests
|
||||
// carry their ciphertext in the request, so persist it and decrypt on demand.
|
||||
val historyContent = if (intentData.type in encryptSignerTypes) "" else intentData.data
|
||||
Amber.instance.getHistoryDatabase(account.npub).dao().addHistory(
|
||||
listOf(
|
||||
HistoryEntity(
|
||||
0,
|
||||
key,
|
||||
intentData.type.toString(),
|
||||
kind,
|
||||
if (isV3) intentData.nip44v3Kind else kind,
|
||||
TimeUtils.now(),
|
||||
false,
|
||||
content = intentData.data,
|
||||
content = historyContent,
|
||||
encryptionPubKey = if (historyContent.isNotEmpty() && intentData.type in encryptDecryptSignerTypes && intentData.type != SignerType.DECRYPT_ZAP_EVENT) {
|
||||
intentData.pubKey
|
||||
} else {
|
||||
""
|
||||
},
|
||||
encryptionScope = if (isV3) intentData.nip44v3Scope else "",
|
||||
),
|
||||
),
|
||||
account.npub,
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -51,6 +52,7 @@ import com.greenart7c3.nostrsigner.AmberLog
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.database.HistoryEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.models.supportedKindNumbers
|
||||
import com.greenart7c3.nostrsigner.service.ApplicationNameCache
|
||||
@@ -188,14 +190,52 @@ fun ActivitiesScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovers the readable plaintext for a history row on demand. Encrypt/decrypt
|
||||
* rows persist only the ciphertext, so the plaintext is derived here using the
|
||||
* stored counterparty key (and, for NIP-44 v3, the kind + scope). Falls back to
|
||||
* the stored content if the row is not encrypted or decryption fails (e.g. old
|
||||
* rows created before ciphertext-at-rest, or a missing counterparty key).
|
||||
*/
|
||||
suspend fun decryptHistoryContent(activity: HistoryEntity, account: Account): String {
|
||||
if (activity.content.isBlank()) return ""
|
||||
return try {
|
||||
when (activity.type) {
|
||||
SignerType.NIP04_ENCRYPT.name, SignerType.NIP44_ENCRYPT.name,
|
||||
SignerType.NIP04_DECRYPT.name, SignerType.NIP44_DECRYPT.name,
|
||||
->
|
||||
if (activity.encryptionPubKey.isBlank()) {
|
||||
activity.content
|
||||
} else {
|
||||
account.decrypt(activity.content, activity.encryptionPubKey)
|
||||
}
|
||||
SignerType.NIP44_V3_ENCRYPT.name, SignerType.NIP44_V3_DECRYPT.name -> {
|
||||
val kind = activity.kind
|
||||
if (activity.encryptionPubKey.isBlank() || kind == null) {
|
||||
activity.content
|
||||
} else {
|
||||
account.nip44v3Decrypt(activity.content, activity.encryptionPubKey, kind, activity.encryptionScope).decodeToString()
|
||||
}
|
||||
}
|
||||
SignerType.DECRYPT_ZAP_EVENT.name -> account.decryptZapEvent(activity.content) ?: activity.content
|
||||
else -> activity.content
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
activity.content
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActivityRow(activity: HistoryEntity, account: Account) {
|
||||
val clipboard = LocalClipboard.current
|
||||
val parsedEvent = remember(activity.content) {
|
||||
if (activity.content.isBlank()) {
|
||||
val displayContent by produceState(initialValue = "", activity.id, activity.content) {
|
||||
value = withContext(Dispatchers.IO) { decryptHistoryContent(activity, account) }
|
||||
}
|
||||
val parsedEvent = remember(displayContent) {
|
||||
if (displayContent.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
runCatching { AmberEvent.fromJson(activity.content).toEvent() }.getOrNull()
|
||||
runCatching { AmberEvent.fromJson(displayContent).toEvent() }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,11 +290,11 @@ fun ActivityRow(activity: HistoryEntity, account: Account) {
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if (activity.content.isNotBlank()) {
|
||||
} else if (displayContent.isNotBlank()) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
text = activity.content,
|
||||
text = displayContent,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp,
|
||||
|
||||
Reference in New Issue
Block a user