mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
The per-account Room database (amber_db_<npub>) stored two NIP-46 secret values as cleartext TEXT columns: the bunker connection `secret` and the `localKey` — the latter being a full Nostr private key. Anyone with access to the app's internal storage (rooted device, privilege-escalating malware, or a future bug exporting the DB) could recover these values and bypass the Keystore protection the main account nsec enjoys (CWE-312). Fix: envelope-encrypt both columns with the existing Keystore-backed AES-256-GCM key (SecureCryptoHelper) before Room persistence, and decrypt on read, so every existing consumer continues to see the plaintext values it already expects. - SecureCryptoHelper: add non-suspend encryptBlocking/decryptBlocking so Migration.migrate() and getByKeySync() can call them without a runBlocking bridge; suspend variants now delegate to the blocking implementations. - ApplicationEntityCrypto.kt (new): encryptForStorage/decryptFromStorage mappers + DecryptingPagingSource. Sentinel rule: empty values stay "" at rest (matches the WebDAV password idiom in LocalPreferences.kt:661-681), preserving `WHERE localKey != ''` enumeration in NotificationSubscription and the `localPubKey` derivation on empty localKey. - ApplicationDao: split methods touching `secret`/`localKey` into Room- generated `*Raw` (encrypted columns) and default-method wrappers that apply the mappers. `getBySecret` rewritten to decrypt and filter in Kotlin (random GCM IV breaks `WHERE secret = :secret`). - CachingApplicationDao: add delegating `*Raw` overrides so the decorator still instantiates; cache logic unchanged. - AppDatabase: add MIGRATION_18_19 (in-place envelope-encrypt of existing plaintext rows via compiled statement + transaction; empty values stay empty). Bump @Database version to 19. - Backup/restore: no changes — ApplicationBackup.buildPayload reads via the wrapped DAO (plaintext) and the JSON is already NIP-44 encrypted by the account key; restore goes through the wrapped insert (auto-encrypts). - Tests: new androidTest ApplicationEntityCryptoTest covers round-trip, raw-column-ciphertext assertion, empty sentinel, localPubKey derivation, getAllWithLocalKey filter, getBySecret (hit/miss/empty), insertApplicationWithPermissions, getAll, and the MIGRATION_18_19 row re-encryption. Requires a device/emulator (AndroidKeyStore unavailable under JVM test). - New room-testing androidTestImplementation dependency. Verified: ktlintCheck, lint (no issues), testFreeDebugUnitTest (0 failures), compileFreeDebugAndroidTestKotlin, assembleFreeDebug, assembleOfflineDebug, and the offline merged manifest check (no INTERNET/ACCESS_NETWORK_STATE/ CHANGE_NETWORK_STATE permissions leaked).
285 lines
8.4 KiB
JSON
285 lines
8.4 KiB
JSON
{
|
|
"formatVersion": 1,
|
|
"database": {
|
|
"version": 19,
|
|
"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')"
|
|
]
|
|
}
|
|
} |