mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Merge upstream/master and resolve database version conflict by bumping to 20
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 19,
|
"version": 19,
|
||||||
"identityHash": "4d6bba48f6829e611cf02157dcc20cf5",
|
"identityHash": "a56de62f406b4670ae2cbaf4225fdc6e",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "application",
|
"tableName": "application",
|
||||||
@@ -225,11 +225,61 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "bunker_event",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` TEXT NOT NULL, `time` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "eventId",
|
||||||
|
"columnName": "eventId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "time",
|
||||||
|
"columnName": "time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_bunker_event_id",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"eventId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bunker_event_id` ON `${TABLE_NAME}` (`eventId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_bunker_event_time",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bunker_event_time` ON `${TABLE_NAME}` (`time`)"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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, '4d6bba48f6829e611cf02157dcc20cf5')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a56de62f406b4670ae2cbaf4225fdc6e')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 20,
|
||||||
|
"identityHash": "a56de62f406b4670ae2cbaf4225fdc6e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "application",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `name` TEXT NOT NULL, `relays` TEXT NOT NULL, `url` TEXT NOT NULL, `icon` TEXT NOT NULL, `description` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `isConnected` INTEGER NOT NULL, `secret` TEXT NOT NULL, `useSecret` INTEGER NOT NULL, `signPolicy` INTEGER NOT NULL, `closeApplication` INTEGER NOT NULL, `deleteAfter` INTEGER NOT NULL, `lastUsed` INTEGER NOT NULL, `localKey` TEXT NOT NULL, PRIMARY KEY(`key`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "relays",
|
||||||
|
"columnName": "relays",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pubKey",
|
||||||
|
"columnName": "pubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isConnected",
|
||||||
|
"columnName": "isConnected",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "secret",
|
||||||
|
"columnName": "secret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "useSecret",
|
||||||
|
"columnName": "useSecret",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "signPolicy",
|
||||||
|
"columnName": "signPolicy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeApplication",
|
||||||
|
"columnName": "closeApplication",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deleteAfter",
|
||||||
|
"columnName": "deleteAfter",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "lastUsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localKey",
|
||||||
|
"columnName": "localKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_key",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_key` ON `${TABLE_NAME}` (`key`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "applicationPermission",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `pkKey` TEXT NOT NULL, `type` TEXT NOT NULL, `kind` INTEGER, `acceptable` INTEGER NOT NULL, `rememberType` INTEGER NOT NULL, `acceptUntil` INTEGER NOT NULL, `rejectUntil` INTEGER NOT NULL, `relay` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`pkKey`) REFERENCES `application`(`key`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pkKey",
|
||||||
|
"columnName": "pkKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "kind",
|
||||||
|
"columnName": "kind",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "acceptable",
|
||||||
|
"columnName": "acceptable",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rememberType",
|
||||||
|
"columnName": "rememberType",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "acceptUntil",
|
||||||
|
"columnName": "acceptUntil",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rejectUntil",
|
||||||
|
"columnName": "rejectUntil",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "relay",
|
||||||
|
"columnName": "relay",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "permissions_by_pk_key",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"pkKey"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `permissions_by_pk_key` ON `${TABLE_NAME}` (`pkKey`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "permissions_unique",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"pkKey",
|
||||||
|
"type",
|
||||||
|
"kind",
|
||||||
|
"relay"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `permissions_unique` ON `${TABLE_NAME}` (`pkKey`, `type`, `kind`, `relay`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "application",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"pkKey"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "bunker_event",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` TEXT NOT NULL, `time` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "eventId",
|
||||||
|
"columnName": "eventId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "time",
|
||||||
|
"columnName": "time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_bunker_event_id",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"eventId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bunker_event_id` ON `${TABLE_NAME}` (`eventId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_bunker_event_time",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bunker_event_time` ON `${TABLE_NAME}` (`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, 'a56de62f406b4670ae2cbaf4225fdc6e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,40 +108,40 @@ object SignerProviderQuery {
|
|||||||
val historyDatabase = Amber.instance.getHistoryDatabase(account.npub)
|
val historyDatabase = Amber.instance.getHistoryDatabase(account.npub)
|
||||||
val permDao = Amber.instance.dao(account.npub)
|
val permDao = Amber.instance.dao(account.npub)
|
||||||
|
|
||||||
// For kind 22242 (NIP-42 relay auth), extract relay host once for both whitelist and permission checks
|
// For kind 22242 (NIP-42 relay auth), extract relay host once for the
|
||||||
|
// whitelist gate and the requester-scoped permission checks below.
|
||||||
val relayHost = if (event.kind == 22242) {
|
val relayHost = if (event.kind == 22242) {
|
||||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event))
|
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event))
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
val whitelistAutoAccept = if (event.kind == 22242) {
|
// The auth whitelist is only a relay constraint: when non-empty, relays
|
||||||
|
// outside the list are auto-rejected. Membership never authorizes signing
|
||||||
|
// by itself — a requester-scoped permission (or user prompt) is still
|
||||||
|
// required, otherwise any app/NIP-46 client could silently obtain
|
||||||
|
// relay-auth signatures (GHSA-vx4h-56qj-wcp7).
|
||||||
|
if (event.kind == 22242) {
|
||||||
val authWhitelist = Amber.instance.settings.authWhitelist
|
val authWhitelist = Amber.instance.settings.authWhitelist
|
||||||
when {
|
if (authWhitelist.isNotEmpty() && relayHost !in authWhitelist) {
|
||||||
authWhitelist.isEmpty() -> false
|
scope.launch {
|
||||||
relayHost in authWhitelist -> true
|
historyDatabase.dao().addHistory(
|
||||||
else -> {
|
listOf(
|
||||||
scope.launch {
|
HistoryEntity(
|
||||||
historyDatabase.dao().addHistory(
|
0,
|
||||||
listOf(
|
requesterId,
|
||||||
HistoryEntity(
|
uriString.replace("content://$appId.", ""),
|
||||||
0,
|
event.kind,
|
||||||
requesterId,
|
TimeUtils.now(),
|
||||||
uriString.replace("content://$appId.", ""),
|
false,
|
||||||
event.kind,
|
content = event.toJson(),
|
||||||
TimeUtils.now(),
|
|
||||||
false,
|
|
||||||
content = event.toJson(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
account.npub,
|
),
|
||||||
)
|
account.npub,
|
||||||
}
|
)
|
||||||
return MatrixCursor(arrayOf("rejected")).also { it.addRow(arrayOf("true")) }
|
|
||||||
}
|
}
|
||||||
|
return rejectedCursor()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var permission = if (event.kind == 22242) {
|
var permission = if (event.kind == 22242) {
|
||||||
@@ -172,7 +172,7 @@ object SignerProviderQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val signPolicy = permDao.getSignPolicy(requesterId)
|
val signPolicy = permDao.getSignPolicy(requesterId)
|
||||||
val isRemembered = whitelistAutoAccept || IntentUtils.isRemembered(signPolicy, permission) ?: return null
|
val isRemembered = IntentUtils.isRemembered(signPolicy, permission) ?: return null
|
||||||
if (!isRemembered) {
|
if (!isRemembered) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
historyDatabase.dao().addHistory(
|
historyDatabase.dao().addHistory(
|
||||||
|
|||||||
@@ -166,6 +166,14 @@ val MIGRATION_17_18 = object : Migration(17, 18) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_18_19 = object : Migration(18, 19) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `bunker_event` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` TEXT NOT NULL, `time` INTEGER NOT NULL)")
|
||||||
|
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bunker_event_id` ON `bunker_event` (`eventId`)")
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS `index_bunker_event_time` ON `bunker_event` (`time`)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GHSA-5fjp-ghh8-wch8 (CWE-312): envelope-encrypt the two NIP-46 secret columns
|
* GHSA-5fjp-ghh8-wch8 (CWE-312): envelope-encrypt the two NIP-46 secret columns
|
||||||
* of the `application` table — `secret` (bunker shared secret) and `localKey`
|
* of the `application` table — `secret` (bunker shared secret) and `localKey`
|
||||||
@@ -180,9 +188,9 @@ val MIGRATION_17_18 = object : Migration(17, 18) {
|
|||||||
*
|
*
|
||||||
* Idempotency: Room migrations run exactly once per version bump. The
|
* Idempotency: Room migrations run exactly once per version bump. The
|
||||||
* migration is not safe to re-run on already-encrypted data (it would
|
* migration is not safe to re-run on already-encrypted data (it would
|
||||||
* double-encrypt), so it must only run between schema 18 and 19.
|
* double-encrypt), so it must only run between schema 19 and 20.
|
||||||
*/
|
*/
|
||||||
val MIGRATION_18_19 = object : Migration(18, 19) {
|
val MIGRATION_19_20 = object : Migration(19, 20) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
db.beginTransaction()
|
db.beginTransaction()
|
||||||
try {
|
try {
|
||||||
@@ -207,7 +215,7 @@ val MIGRATION_18_19 = object : Migration(18, 19) {
|
|||||||
}
|
}
|
||||||
db.setTransactionSuccessful()
|
db.setTransactionSuccessful()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
AmberLog.e(Amber.TAG, "MIGRATION_18_19: failed to envelope-encrypt application secrets", e)
|
AmberLog.e(Amber.TAG, "MIGRATION_19_20: failed to envelope-encrypt application secrets", e)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction()
|
db.endTransaction()
|
||||||
@@ -219,13 +227,16 @@ val MIGRATION_18_19 = object : Migration(18, 19) {
|
|||||||
entities = [
|
entities = [
|
||||||
ApplicationEntity::class,
|
ApplicationEntity::class,
|
||||||
ApplicationPermissionsEntity::class,
|
ApplicationPermissionsEntity::class,
|
||||||
|
BunkerEventEntity::class,
|
||||||
],
|
],
|
||||||
version = 19,
|
version = 20,
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun dao(): ApplicationDao
|
abstract fun dao(): ApplicationDao
|
||||||
|
|
||||||
|
abstract fun bunkerEventDao(): BunkerEventDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getDatabase(
|
fun getDatabase(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -260,6 +271,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
.addMigrations(MIGRATION_16_17)
|
.addMigrations(MIGRATION_16_17)
|
||||||
.addMigrations(MIGRATION_17_18)
|
.addMigrations(MIGRATION_17_18)
|
||||||
.addMigrations(MIGRATION_18_19)
|
.addMigrations(MIGRATION_18_19)
|
||||||
|
.addMigrations(MIGRATION_19_20)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
instance
|
instance
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.database
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface BunkerEventDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
@Transaction
|
||||||
|
suspend fun insert(bunkerEvent: BunkerEventEntity): Long?
|
||||||
|
|
||||||
|
@Query("SELECT EXISTS(SELECT 1 FROM bunker_event WHERE eventId = :eventId)")
|
||||||
|
suspend fun exists(eventId: String): Boolean
|
||||||
|
|
||||||
|
@Query("DELETE FROM bunker_event WHERE time < :time")
|
||||||
|
@Transaction
|
||||||
|
suspend fun deleteOld(time: Long): Int
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.database
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "bunker_event",
|
||||||
|
indices = [
|
||||||
|
Index(
|
||||||
|
value = ["eventId"],
|
||||||
|
name = "index_bunker_event_id",
|
||||||
|
unique = true,
|
||||||
|
),
|
||||||
|
Index(
|
||||||
|
value = ["time"],
|
||||||
|
name = "index_bunker_event_time",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class BunkerEventEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int,
|
||||||
|
val eventId: String,
|
||||||
|
val time: Long,
|
||||||
|
)
|
||||||
@@ -47,6 +47,12 @@ class ClearLogsWorker(appContext: Context, workerParams: WorkerParameters) : Cor
|
|||||||
AmberLog.d(Amber.TAG, "Trimmed $excessLogs excess log entries (cap: $MAX_LOG_ENTRIES)")
|
AmberLog.d(Amber.TAG, "Trimmed $excessLogs excess log entries (cap: $MAX_LOG_ENTRIES)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bunkerEventDao = Amber.instance.getDatabase(it.npub).bunkerEventDao()
|
||||||
|
val deletedBunkerEvents = bunkerEventDao.deleteOld(threeDaysAgo / 1000)
|
||||||
|
if (deletedBunkerEvents > 0) {
|
||||||
|
AmberLog.d(Amber.TAG, "Deleted $deletedBunkerEvents old bunker event entries")
|
||||||
|
}
|
||||||
|
|
||||||
val dao = Amber.instance.dao(it.npub)
|
val dao = Amber.instance.dao(it.npub)
|
||||||
dao.updateExpiredPermissions(TimeUtils.now())
|
dao.updateExpiredPermissions(TimeUtils.now())
|
||||||
val deleted = dao.deleteOldApplications(now / 1000)
|
val deleted = dao.deleteOldApplications(now / 1000)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.greenart7c3.nostrsigner.LocalPreferences
|
|||||||
import com.greenart7c3.nostrsigner.R
|
import com.greenart7c3.nostrsigner.R
|
||||||
import com.greenart7c3.nostrsigner.SignerProviderQuery
|
import com.greenart7c3.nostrsigner.SignerProviderQuery
|
||||||
import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions
|
import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions
|
||||||
|
import com.greenart7c3.nostrsigner.database.BunkerEventEntity
|
||||||
import com.greenart7c3.nostrsigner.database.HistoryEntity
|
import com.greenart7c3.nostrsigner.database.HistoryEntity
|
||||||
import com.greenart7c3.nostrsigner.database.LogEntity
|
import com.greenart7c3.nostrsigner.database.LogEntity
|
||||||
import com.greenart7c3.nostrsigner.models.Account
|
import com.greenart7c3.nostrsigner.models.Account
|
||||||
@@ -56,6 +57,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
|||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
||||||
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
|
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
|
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
|
||||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
|
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
|
||||||
@@ -130,6 +132,22 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = TimeUtils.now()
|
||||||
|
if (event.createdAt < now - TimeUtils.FIVE_MINUTES) {
|
||||||
|
saveLog("Event ${event.id} is too old: ${now - event.createdAt}s ago", relay.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.createdAt > now + TimeUtils.FIVE_MINUTES) {
|
||||||
|
saveLog("Event ${event.id} is in the future: ${event.createdAt - now}s ahead", relay.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val expiration = event.expiration()
|
||||||
|
if (expiration != null && expiration < now) {
|
||||||
|
saveLog("Event ${event.id} has expired", relay.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
NotificationUtils.getOrCreateBunkerChannel(applicationContext)
|
NotificationUtils.getOrCreateBunkerChannel(applicationContext)
|
||||||
NotificationUtils.getOrCreateErrorsChannel(applicationContext)
|
NotificationUtils.getOrCreateErrorsChannel(applicationContext)
|
||||||
|
|
||||||
@@ -167,6 +185,14 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
|||||||
saveLog("Tagged account ${taggedKey.toNPub()} not logged in", relay.url)
|
saveLog("Tagged account ${taggedKey.toNPub()} not logged in", relay.url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bunkerEventDao = Amber.instance.getDatabase(acc.npub).bunkerEventDao()
|
||||||
|
if (bunkerEventDao.exists(event.id)) {
|
||||||
|
saveLog("Event ${event.id} already processed (persistent cache)", relay.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bunkerEventDao.insert(BunkerEventEntity(0, event.id, event.createdAt))
|
||||||
|
|
||||||
notify(event, acc, relay, connectionPrivKey)
|
notify(event, acc, relay, connectionPrivKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package com.greenart7c3.nostrsigner
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.greenart7c3.nostrsigner.database.ApplicationDao
|
||||||
|
import com.greenart7c3.nostrsigner.database.ApplicationPermissionsEntity
|
||||||
|
import com.greenart7c3.nostrsigner.database.HistoryDao
|
||||||
|
import com.greenart7c3.nostrsigner.database.HistoryDatabase
|
||||||
|
import com.greenart7c3.nostrsigner.models.Account
|
||||||
|
import com.greenart7c3.nostrsigner.models.AmberSettings
|
||||||
|
import com.greenart7c3.nostrsigner.service.installAmberInstance
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.unmockkObject
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression tests for GHSA-vx4h-56qj-wcp7: the NIP-42 relay-auth whitelist
|
||||||
|
* must act only as a relay constraint (non-whitelisted relays auto-reject),
|
||||||
|
* never as an authorization grant. Signing a kind-22242 event always requires
|
||||||
|
* a requester-scoped permission (or a user prompt).
|
||||||
|
*
|
||||||
|
* MatrixCursor is an android.jar stub in local unit tests, so cursor contents
|
||||||
|
* cannot be inspected here; instead these tests verify whether signing was
|
||||||
|
* reached at all ([Account.signSync]) and which permission lookups ran.
|
||||||
|
*/
|
||||||
|
class SignerProviderQueryTest {
|
||||||
|
private val context = mockk<Context>(relaxed = true)
|
||||||
|
private lateinit var amber: Amber
|
||||||
|
private lateinit var dao: ApplicationDao
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REQUESTER = "com.attacker.app"
|
||||||
|
private const val NPUB = "npub1test000000000000000000000000000000000000000000000000000000"
|
||||||
|
private const val WHITELISTED_HOST = "relay.example.com"
|
||||||
|
private const val WHITELISTED_URL = "wss://relay.example.com"
|
||||||
|
private const val OTHER_URL = "wss://evil.example.com"
|
||||||
|
private val HEX = "ab".repeat(32)
|
||||||
|
|
||||||
|
private fun relayAuthJson(relayUrl: String) = """{"kind":22242,"tags":[["relay","$relayUrl"]],"content":""}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkObject(AmberLog)
|
||||||
|
every { AmberLog.d(any(), any<String>()) } returns Unit
|
||||||
|
every { AmberLog.d(any(), any(), any()) } returns Unit
|
||||||
|
|
||||||
|
val historyDao = mockk<HistoryDao>(relaxed = true)
|
||||||
|
val historyDatabase = mockk<HistoryDatabase>()
|
||||||
|
every { historyDatabase.dao() } returns historyDao
|
||||||
|
|
||||||
|
dao = mockk()
|
||||||
|
amber = mockk()
|
||||||
|
every { amber.applicationIOScope } returns CoroutineScope(Dispatchers.Unconfined)
|
||||||
|
every { amber.getHistoryDatabase(any()) } returns historyDatabase
|
||||||
|
every { amber.dao(any()) } returns dao
|
||||||
|
installAmberInstance(amber)
|
||||||
|
|
||||||
|
account = mockk()
|
||||||
|
every { account.npub } returns NPUB
|
||||||
|
every { account.hexKey } returns HEX
|
||||||
|
|
||||||
|
mockkObject(LocalPreferences)
|
||||||
|
every { LocalPreferences.loadFromEncryptedStorageSync(any(), any()) } returns account
|
||||||
|
every { LocalPreferences.allSavedAccounts(any()) } returns emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkObject(LocalPreferences)
|
||||||
|
unmockkObject(AmberLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun signEventUri(): Uri {
|
||||||
|
val uri = mockk<Uri>()
|
||||||
|
every { uri.toString() } returns "content://${BuildConfig.APPLICATION_ID}.SIGN_EVENT"
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryRelayAuth(relayUrl: String) = SignerProviderQuery.query(
|
||||||
|
context = context,
|
||||||
|
requesterId = REQUESTER,
|
||||||
|
callerPackageName = null,
|
||||||
|
operationUri = signEventUri(),
|
||||||
|
arguments = arrayOf(relayAuthJson(relayUrl), "", NPUB),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun acceptedPermission(relay: String) = ApplicationPermissionsEntity(
|
||||||
|
id = 1,
|
||||||
|
pkKey = REQUESTER,
|
||||||
|
type = "SIGN_EVENT",
|
||||||
|
kind = 22242,
|
||||||
|
acceptable = true,
|
||||||
|
rememberType = 0,
|
||||||
|
acceptUntil = TimeUtils.now() + 3600,
|
||||||
|
rejectUntil = 0,
|
||||||
|
relay = relay,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun stubNoGrants() {
|
||||||
|
every { dao.getPermissionForRelay(REQUESTER, "SIGN_EVENT", 22242, WHITELISTED_HOST) } returns null
|
||||||
|
every { dao.getWildcardRelayPermission(REQUESTER, "SIGN_EVENT", 22242) } returns null
|
||||||
|
every { dao.getSignPolicy(REQUESTER) } returns null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stubSigning() {
|
||||||
|
val signed = Event("cd".repeat(32), HEX, TimeUtils.now(), 22242, arrayOf(arrayOf("relay", WHITELISTED_URL)), "", "ef".repeat(64))
|
||||||
|
every { account.signSync<Event>(any(), 22242, any(), any()) } returns signed
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `whitelisted relay does not auto-accept a requester with no permission`() {
|
||||||
|
every { amber.settings } returns AmberSettings(authWhitelist = listOf(WHITELISTED_HOST))
|
||||||
|
stubNoGrants()
|
||||||
|
|
||||||
|
val cursor = queryRelayAuth(WHITELISTED_URL)
|
||||||
|
|
||||||
|
// null = no silent signing; the caller must surface a user prompt.
|
||||||
|
assertNull(cursor)
|
||||||
|
verify { dao.getPermissionForRelay(REQUESTER, "SIGN_EVENT", 22242, WHITELISTED_HOST) }
|
||||||
|
verify(exactly = 0) { account.signSync<Event>(any(), any(), any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `whitelisted relay signs silently when the requester holds an accepted relay grant`() {
|
||||||
|
every { amber.settings } returns AmberSettings(authWhitelist = listOf(WHITELISTED_HOST))
|
||||||
|
every { dao.getPermissionForRelay(REQUESTER, "SIGN_EVENT", 22242, WHITELISTED_HOST) } returns acceptedPermission(WHITELISTED_HOST)
|
||||||
|
every { dao.getSignPolicy(REQUESTER) } returns null
|
||||||
|
stubSigning()
|
||||||
|
|
||||||
|
queryRelayAuth(WHITELISTED_URL)
|
||||||
|
|
||||||
|
verify { account.signSync<Event>(any(), 22242, any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `whitelisted relay signs silently when the requester holds a wildcard relay grant`() {
|
||||||
|
every { amber.settings } returns AmberSettings(authWhitelist = listOf(WHITELISTED_HOST))
|
||||||
|
every { dao.getPermissionForRelay(REQUESTER, "SIGN_EVENT", 22242, WHITELISTED_HOST) } returns null
|
||||||
|
every { dao.getWildcardRelayPermission(REQUESTER, "SIGN_EVENT", 22242) } returns acceptedPermission("*")
|
||||||
|
every { dao.getSignPolicy(REQUESTER) } returns null
|
||||||
|
stubSigning()
|
||||||
|
|
||||||
|
queryRelayAuth(WHITELISTED_URL)
|
||||||
|
|
||||||
|
verify { account.signSync<Event>(any(), 22242, any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-whitelisted relay is auto-rejected before any permission lookup`() {
|
||||||
|
every { amber.settings } returns AmberSettings(authWhitelist = listOf(WHITELISTED_HOST))
|
||||||
|
|
||||||
|
queryRelayAuth(OTHER_URL)
|
||||||
|
|
||||||
|
verify(exactly = 0) { dao.getPermissionForRelay(any(), any(), any(), any()) }
|
||||||
|
verify(exactly = 0) { dao.getWildcardRelayPermission(any(), any(), any()) }
|
||||||
|
verify(exactly = 0) { account.signSync<Event>(any(), any(), any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty whitelist keeps the normal permission flow`() {
|
||||||
|
every { amber.settings } returns AmberSettings(authWhitelist = emptyList())
|
||||||
|
stubNoGrants()
|
||||||
|
|
||||||
|
val cursor = queryRelayAuth(WHITELISTED_URL)
|
||||||
|
|
||||||
|
assertNull(cursor)
|
||||||
|
verify { dao.getPermissionForRelay(REQUESTER, "SIGN_EVENT", 22242, WHITELISTED_HOST) }
|
||||||
|
verify(exactly = 0) { account.signSync<Event>(any(), any(), any(), any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.service
|
||||||
|
|
||||||
|
import com.greenart7c3.nostrsigner.Amber
|
||||||
|
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||||
|
import com.greenart7c3.nostrsigner.database.AppDatabase
|
||||||
|
import com.greenart7c3.nostrsigner.database.ApplicationDao
|
||||||
|
import com.greenart7c3.nostrsigner.database.BunkerEventDao
|
||||||
|
import com.greenart7c3.nostrsigner.models.Account
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
|
||||||
|
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.unmockkObject
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class EventNotificationConsumerTest {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
private lateinit var consumer: EventNotificationConsumer
|
||||||
|
private lateinit var bunkerEventDao: BunkerEventDao
|
||||||
|
private lateinit var amber: Amber
|
||||||
|
private lateinit var account: Account
|
||||||
|
private lateinit var dao: ApplicationDao
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic("com.vitorpamplona.quartz.nip01Core.crypto.EventKt")
|
||||||
|
mockkStatic("com.vitorpamplona.quartz.nip01Core.tags.people.EventExtKt")
|
||||||
|
mockkStatic("com.vitorpamplona.quartz.nip40Expiration.EventExtKt")
|
||||||
|
|
||||||
|
bunkerEventDao = mockk(relaxed = true)
|
||||||
|
dao = mockk(relaxed = true)
|
||||||
|
val database = mockk<AppDatabase>(relaxed = true)
|
||||||
|
every { database.bunkerEventDao() } returns bunkerEventDao
|
||||||
|
every { database.dao() } returns dao
|
||||||
|
|
||||||
|
amber = mockk(relaxed = true)
|
||||||
|
every { amber.getDatabase(any()) } returns database
|
||||||
|
every { amber.dao(any()) } returns dao
|
||||||
|
installAmberInstance(amber)
|
||||||
|
|
||||||
|
mockkObject(LocalPreferences)
|
||||||
|
account = newTestAccount(scope)
|
||||||
|
|
||||||
|
consumer = spyk(EventNotificationConsumer(mockk(relaxed = true)))
|
||||||
|
every { consumer["notificationManager"]() } returns mockk(relaxed = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkObject(LocalPreferences)
|
||||||
|
unmockkStatic("com.vitorpamplona.quartz.nip01Core.crypto.EventKt")
|
||||||
|
unmockkStatic("com.vitorpamplona.quartz.nip01Core.tags.people.EventExtKt")
|
||||||
|
unmockkStatic("com.vitorpamplona.quartz.nip40Expiration.EventExtKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `consume rejects old events`() = runBlocking {
|
||||||
|
val event = mockk<Event>()
|
||||||
|
every { event.verify() } returns true
|
||||||
|
every { event.kind } returns NostrConnectEvent.KIND
|
||||||
|
every { event.createdAt } returns (TimeUtils.now() - TimeUtils.FIVE_MINUTES - 1)
|
||||||
|
|
||||||
|
consumer.consume(event, NormalizedRelayUrl("wss://relay.com"))
|
||||||
|
|
||||||
|
verify(exactly = 0) { event.tags }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `consume rejects future events`() = runBlocking {
|
||||||
|
val event = mockk<Event>()
|
||||||
|
every { event.verify() } returns true
|
||||||
|
every { event.kind } returns NostrConnectEvent.KIND
|
||||||
|
every { event.createdAt } returns (TimeUtils.now() + TimeUtils.FIVE_MINUTES + 1)
|
||||||
|
|
||||||
|
consumer.consume(event, NormalizedRelayUrl("wss://relay.com"))
|
||||||
|
|
||||||
|
verify(exactly = 0) { event.tags }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `consume rejects expired events`() = runBlocking {
|
||||||
|
val event = mockk<Event>()
|
||||||
|
every { event.verify() } returns true
|
||||||
|
every { event.kind } returns NostrConnectEvent.KIND
|
||||||
|
every { event.createdAt } returns TimeUtils.now()
|
||||||
|
every { event.tags } returns arrayOf(arrayOf(ExpirationTag.TAG_NAME, (TimeUtils.now() - 10).toString()))
|
||||||
|
|
||||||
|
consumer.consume(event, NormalizedRelayUrl("wss://relay.com"))
|
||||||
|
|
||||||
|
// If it passed expiration, it would call taggedUsers()
|
||||||
|
verify(exactly = 0) { event.taggedUsers() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `consume rejects duplicate events persistently`() = runBlocking {
|
||||||
|
val event = mockk<Event>()
|
||||||
|
every { event.id } returns "event1"
|
||||||
|
every { event.verify() } returns true
|
||||||
|
every { event.kind } returns NostrConnectEvent.KIND
|
||||||
|
every { event.createdAt } returns TimeUtils.now()
|
||||||
|
every { event.tags } returns arrayOf(arrayOf("p", account.hexKey))
|
||||||
|
|
||||||
|
coEvery { LocalPreferences.loadFromEncryptedStorageSync(any(), any()) } returns account
|
||||||
|
coEvery { bunkerEventDao.exists("event1") } returns true
|
||||||
|
|
||||||
|
consumer.consume(event, NormalizedRelayUrl("wss://relay.com"))
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { bunkerEventDao.exists("event1") }
|
||||||
|
coVerify(exactly = 0) { bunkerEventDao.insert(any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user