git » sdk » commit 3e7119c

Implement get/store/removeOmemoSession

author Eric Roberts
2026-08-26 18:14:54 UTC
committer Stephen Paul Weber
2026-08-26 18:16:17 UTC
parent de56f6215065867c95081c1abc50b98e981d39cb

Implement get/store/removeOmemoSession

libsignal passes SessionRecord.serialize() strings to the storage API[^1], so
we can just put it in the db. There was a Dynamic type that is now known so I
changed it.

libsignal also expects loadSession to return undefined if there's no session,
but I updated getOmemoSession to return null so that it would be in sync with
the actual Persistence interface. So I had to add a cast to undefined in
loadSession for this to work as expected.

[^1]: https://github.com/privacyresearchgroup/libsignal-protocol-typescript/blob/c1a336f/src/session-cipher.ts#L96

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();
+	});
 }