Merge branch 'fix/off-the-record-outbox' into 'main'
Keep off-the-record messages out of the outbox on disk See merge request paulhitt/flamenet-messenger-android!143
This commit was merged in pull request #170.
This commit is contained in:
@@ -1304,7 +1304,33 @@ class Vault(
|
||||
*/
|
||||
fun outbox(): JSONArray = readJson(OUTBOX)?.optJSONArray("pending") ?: JSONArray()
|
||||
|
||||
fun saveOutbox(pending: JSONArray) = writeJson(OUTBOX, JSONObject().put("pending", pending))
|
||||
/**
|
||||
* Write the queue, minus anything for an off-the-record conversation.
|
||||
*
|
||||
* An entry carries `to` and `text`, so this is the message and not a note
|
||||
* about it. Every enqueue writes here directly, so an off-the-record send
|
||||
* queued for later reached disk in plaintext the moment it was typed, for
|
||||
* a conversation §6.28 says is gone when the app closes.
|
||||
*
|
||||
* The filter is here rather than at the four call sites for the same
|
||||
* reason it is on [saveUnreadThreads]: a later caller cannot forget it.
|
||||
* A group entry carries `thread` and no user-thread setting, so it passes
|
||||
* through.
|
||||
*
|
||||
* Dropping a queued send is the promise, not the cost of it. The thread
|
||||
* it belongs to is not kept either, so a message that survived would
|
||||
* arrive into a conversation the app no longer has.
|
||||
*/
|
||||
fun saveOutbox(pending: JSONArray) {
|
||||
val kept = JSONArray()
|
||||
for (i in 0 until pending.length()) {
|
||||
val entry = pending.optJSONObject(i) ?: continue
|
||||
val to = entry.optLong("to", 0L)
|
||||
if (to != 0L && offTheRecord(threadForUser(to))) continue
|
||||
kept.put(entry)
|
||||
}
|
||||
writeJson(OUTBOX, JSONObject().put("pending", kept))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls placed and received, newest last.
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* A queued send for an off-the-record conversation never reaches the vault.
|
||||
*
|
||||
* This is the message, not a note about it: an entry carries `to` and `text`.
|
||||
* Every enqueue writes the whole queue back, so an off-the-record send queued
|
||||
* for later was on disk in plaintext the moment it was typed.
|
||||
*/
|
||||
class OffTheRecordOutboxTest {
|
||||
private class MapStorage : Vault.Storage {
|
||||
val records = LinkedHashMap<String, ByteArray>()
|
||||
|
||||
override fun read(name: String) = records[name]
|
||||
|
||||
override fun write(
|
||||
name: String,
|
||||
bytes: ByteArray,
|
||||
) {
|
||||
records[name] = bytes
|
||||
}
|
||||
|
||||
override fun delete(name: String) {
|
||||
records.remove(name)
|
||||
}
|
||||
|
||||
override fun names() = records.keys.toList()
|
||||
}
|
||||
|
||||
private fun key(): SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
||||
|
||||
private fun entry(
|
||||
to: Long,
|
||||
text: String,
|
||||
) = JSONObject().put("to", to).put("text", text).put("at", 1L)
|
||||
|
||||
private fun texts(array: JSONArray): List<String> =
|
||||
(0 until array.length()).mapNotNull { array.optJSONObject(it)?.optString("text") }
|
||||
|
||||
@Test
|
||||
fun `a send queued in an off the record thread is not written`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
Vault(storage, k).apply {
|
||||
saveOffTheRecord(Vault.threadForUser(7L), true)
|
||||
saveOutbox(JSONArray().put(entry(7L, "meet me at six")).put(entry(8L, "ordinary")))
|
||||
}
|
||||
|
||||
val restored = texts(Vault(storage, k).outbox())
|
||||
assertFalse("meet me at six" in restored, "the off-the-record message text was written to the vault")
|
||||
assertTrue("ordinary" in restored)
|
||||
assertEquals(1, restored.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nothing is dropped when no thread is off the record`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
Vault(storage, k).saveOutbox(JSONArray().put(entry(7L, "one")).put(entry(8L, "two")))
|
||||
assertEquals(listOf("one", "two"), texts(Vault(storage, k).outbox()))
|
||||
}
|
||||
|
||||
/**
|
||||
* A group entry names the group thread and carries the member it is going
|
||||
* to, and a group has no off-the-record setting of its own.
|
||||
*/
|
||||
@Test
|
||||
fun `a group entry passes through`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
Vault(storage, k).saveOutbox(
|
||||
JSONArray().put(entry(8L, "to the group").put("thread", "group:g-1")),
|
||||
)
|
||||
assertEquals(listOf("to the group"), texts(Vault(storage, k).outbox()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Turning it on does not reach back into what was already written, but the
|
||||
* next save must drop it: the queue is rewritten whole every time.
|
||||
*/
|
||||
@Test
|
||||
fun `the next save after turning it on clears the queue`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
val v = Vault(storage, k)
|
||||
v.saveOutbox(JSONArray().put(entry(7L, "still here")))
|
||||
assertEquals(listOf("still here"), texts(Vault(storage, k).outbox()))
|
||||
|
||||
v.saveOffTheRecord(Vault.threadForUser(7L), true)
|
||||
v.saveOutbox(v.outbox())
|
||||
assertTrue(texts(Vault(storage, k).outbox()).isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user