| author | Eric Roberts
<eric@devl.me> 2026-08-26 18:14:54 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-08-26 18:16:17 UTC |
| parent | de56f6215065867c95081c1abc50b98e981d39cb |
| borogove/OMEMO.hx | +11 | -4 |
| borogove/Persistence.hx | +3 | -3 |
| borogove/SignalProtocol.hx | +7 | -2 |
| borogove/persistence/Dummy.hx | +8 | -4 |
| borogove/persistence/IDB.js | +8 | -7 |
| borogove/persistence/Sqlite.hx | +33 | -4 |
| test/TestSqlite.hx | +38 | -0 |
| test/persistence-tests.ts | +42 | -0 |
diff --git a/borogove/OMEMO.hx b/borogove/OMEMO.hx index d372fd8..f5c6e07 100644 --- a/borogove/OMEMO.hx +++ b/borogove/OMEMO.hx @@ -166,12 +166,19 @@ class OMEMOStore extends SignalProtocolStore { } public function loadSession(identifier:SignalProtocolAddress):Promise<SignalSession> { - return persistence.getOmemoSession(accountId, identifier.toString()); + return persistence.getOmemoSession(accountId, identifier.toString()).then(session -> { + #if js + // libsignal assumes a missing session will be undefined rather than null + // Passing null makes it try to deserialize null. + return session == null ? cast js.Syntax.code("undefined") : session; + #else + return session; + #end + }); } public function storeSession(identifier:SignalProtocolAddress, session:SignalSession):Promise<Bool> { - persistence.storeOmemoSession(accountId, identifier.toString(), session); - return Promise.resolve(true); + return persistence.storeOmemoSession(accountId, identifier.toString(), session).then(_ -> true); } public function removeSession(identifier:SignalProtocolAddress):Promise<Bool> { @@ -1191,7 +1198,7 @@ class OMEMO { trace("OMEMO: No session for "+addr.toString()); return buildSession(sid, addr, "new"); } - return session; + return Promise.resolve(session); }).then((session) -> { return new SessionCipher(signalStore, addr); }); diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx index 94ca911..894a06f 100644 --- a/borogove/Persistence.hx +++ b/borogove/Persistence.hx @@ -410,17 +410,17 @@ interface Persistence { /** Load a stored OMEMO session for a remote device **/ - public function getOmemoSession(account:String, address:String): Promise<SignalSession>; + public function getOmemoSession(account:String, address:String): Promise<Null<SignalSession>>; /** Store an OMEMO session for a remote device **/ - public function storeOmemoSession(account:String, address:String, session:SignalSession):Void; + public function storeOmemoSession(account:String, address:String, session:SignalSession):Promise<SignalSession>; /** Remove a stored OMEMO session for a remote device **/ - public function removeOmemoSession(account:String, address:String):Void; + public function removeOmemoSession(account:String, address:String):Promise<Bool>; /** Store extra metadata associated with an OMEMO session diff --git a/borogove/SignalProtocol.hx b/borogove/SignalProtocol.hx index 112bbda..d20afc3 100644 --- a/borogove/SignalProtocol.hx +++ b/borogove/SignalProtocol.hx @@ -101,8 +101,13 @@ extern class SessionCipher { public function encrypt(plaintext:BytesData):Promise<SignalCipherText>; } -// Not sure what the fields are for this one -typedef SignalSession = Dynamic; +// libsignal serializes SessionRecord before passing it to the storage API: +// https://github.com/privacyresearchgroup/libsignal-protocol-typescript/blob/c1a336f/src/session-cipher.ts +abstract SignalSession(String) { + public inline function new(value:String) { + this = value; + } +} @:native("libsignal.KeyHelper") extern class KeyHelper { diff --git a/borogove/persistence/Dummy.hx b/borogove/persistence/Dummy.hx index 8f04b33..a86d289 100644 --- a/borogove/persistence/Dummy.hx +++ b/borogove/persistence/Dummy.hx @@ -254,15 +254,19 @@ class Dummy implements Persistence { } @HaxeCBridge.noemit - public function getOmemoSession(account:String, address:String): Promise<SignalSession> { - return Promise.reject("Not found"); + public function getOmemoSession(account:String, address:String): Promise<Null<SignalSession>> { + return Promise.resolve(null); } @HaxeCBridge.noemit - public function storeOmemoSession(account:String, address:String, session:SignalSession):Void { } + public function storeOmemoSession(account:String, address:String, session:SignalSession):Promise<SignalSession> { + return Promise.resolve(session); + } @HaxeCBridge.noemit - public function removeOmemoSession(account:String, address:String):Void { } + public function removeOmemoSession(account:String, address:String):Promise<Bool> { + return Promise.resolve(true); + } @HaxeCBridge.noemit public function storeOmemoMetadata(account:String, address:String, metadata:OMEMOSessionMetadata):Void { } diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js index 98f668e..f90bad4 100644 --- a/borogove/persistence/IDB.js +++ b/borogove/persistence/IDB.js @@ -1458,19 +1458,18 @@ tx.onerror = console.error; const tx = db.transaction(["omemo_sessions"], "readonly"); const store = tx.objectStore("omemo_sessions"); const result = await promisifyRequest(store.get([account, address])); - return result?.session; + return result?.session ?? null; }, - storeOmemoSession(account, address, session) { + async storeOmemoSession(account, address, session) { const tx = db.transaction(["omemo_sessions"], "readwrite"); const store = tx.objectStore("omemo_sessions"); - promisifyRequest(store.put({ + await promisifyRequest(store.put({ account: account, address: address, session: session, - })).catch((e) => { - console.error("Failed to store OMEMO session: " + e); - }); + })); + return session; }, storeOmemoMetadata(account, address, metadata) { @@ -1492,12 +1491,14 @@ tx.onerror = console.error; return result?.metadata; }, - removeOmemoSession(account, address) { + async removeOmemoSession(account, address) { // Remove session and any stored metadata const tx = db.transaction(["omemo_sessions", "omemo_sessions_meta"], "readwrite"); const path = [account, address]; tx.objectStore("omemo_sessions").delete(path); tx.objectStore("omemo_sessions_meta").delete(path); + await promisifyRequest(tx); + return true; }, get(k) { diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx index 3d3886c..d3809c2 100644 --- a/borogove/persistence/Sqlite.hx +++ b/borogove/persistence/Sqlite.hx @@ -324,6 +324,17 @@ class Sqlite implements Persistence implements KeyValueStore { "PRAGMA user_version = 18"]); } return Promise.resolve(null); + }).then(_ -> { + if (version < 19) { + return exec(["CREATE TABLE omemo_sessions ( + account_id TEXT NOT NULL, + address TEXT NOT NULL, + session TEXT NOT NULL, + PRIMARY KEY (account_id, address) + ) STRICT", + "PRAGMA user_version = 19"]); + } + return Promise.resolve(null); }); }); }); @@ -1592,15 +1603,33 @@ class Sqlite implements Persistence implements KeyValueStore { } @HaxeCBridge.noemit - public function getOmemoSession(account:String, address:String): Promise<SignalSession> { - return Promise.reject("TODO"); + public function getOmemoSession(account:String, address:String): Promise<Null<SignalSession>> { + return db.exec( + "SELECT session FROM omemo_sessions WHERE account_id=? AND address=? LIMIT 1", + [account, address], + ).then(result -> { + for (row in result) { + return row.session; + } + return null; + }); } @HaxeCBridge.noemit - public function storeOmemoSession(account:String, address:String, session:SignalSession):Void { } + public function storeOmemoSession(account:String, address:String, session:SignalSession):Promise<SignalSession> { + return db.exec( + "INSERT OR REPLACE INTO omemo_sessions VALUES (?,?,?)", + [account, address, session], + ).then(_ -> session); + } @HaxeCBridge.noemit - public function removeOmemoSession(account:String, address:String):Void { } + public function removeOmemoSession(account:String, address:String):Promise<Bool> { + return db.exec( + "DELETE FROM omemo_sessions WHERE account_id=? AND address=?", + [account, address], + ).then(_ -> true); + } @HaxeCBridge.noemit public function storeOmemoMetadata(account:String, address:String, metadata:OMEMOSessionMetadata):Void { } diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx index 58da533..e3b2211 100644 --- a/test/TestSqlite.hx +++ b/test/TestSqlite.hx @@ -28,6 +28,8 @@ import borogove.Role; import borogove.Stanza; import borogove.Source; import borogove.SignalProtocol.PreKeyPair; +import borogove.SignalProtocol.SignalSession; +import borogove.OMEMO.OMEMOSessionMetadata; using Lambda; using thenshim.PromiseTools; @@ -1512,6 +1514,42 @@ class TestSqlite extends utest.Test { }); } + public function testGetOmemoSessionNotFound(async: Async) { + persistence + .getOmemoSession("session-notfound@example.com", "contact@example.com/1") + .then(result -> { + Assert.equals(null, result); + async.done(); + }) + .catchError(e -> { + Assert.fail(Std.string(e)); + async.done(); + }); + } + + public function testOmemoSession(async: Async) { + final account = "session-existing@example.com"; + final address = "contact@example.com/1"; + final session = new SignalSession('{"sessions":{},"version":"v1"}'); + + persistence + .storeOmemoSession(account, address, session) + .then(_ -> persistence.getOmemoSession(account, address)) + .then(result -> { + Assert.equals(session, result); + return persistence.removeOmemoSession(account, address); + }) + .then(_ -> persistence.getOmemoSession(account, address)) + .then(result -> { + Assert.equals(null, result); + async.done(); + }) + .catchError(e -> { + Assert.fail(Std.string(e)); + async.done(); + }); + } + public function testOmemoSignedPreKey(async: Async) { final login = "signed-prekey@example.com"; final signedPreKey = { diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts index 33cf4b8..3cac885 100644 --- a/test/persistence-tests.ts +++ b/test/persistence-tests.ts @@ -1789,4 +1789,46 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result).toEqual(identityKey); }); + + test("getOmemoSession returns null when none is stored", async ({ + page, + persistence, + }) => { + const result = await page.evaluate( + async ({ persistence }) => + persistence.getOmemoSession( + "omemo-session-not-found@example.com", + "contact@example.com/1", + ), + { persistence }, + ); + + expect(result).toBeNull(); + }); + + test("storeOmemoSession, getOmemoSession, and removeOmemoSession", async ({ + page, + persistence, + }) => { + const account = "omemo-session-existing@example.com"; + const address = "contact@example.com/1"; + const session = '{"sessions":{},"version":"v1"}'; + const result = await page.evaluate( + async ({ persistence, account, address, session }) => { + await persistence.storeOmemoSession(account, address, session); + const loaded = await persistence.getOmemoSession(account, address); + await persistence.removeOmemoSession(account, address); + const afterRemove = await persistence.getOmemoSession(account, address); + + return { + loaded, + afterRemove, + }; + }, + { persistence, account, address, session }, + ); + + expect(result.loaded).toEqual(session); + expect(result.afterRemove).toBeNull(); + }); }