git » sdk » commit 5d32596

OMEMO: Bundle creation, storage and publication

author Matthew Wild
2024-12-03 11:18:02 UTC
committer Matthew Wild
2025-04-15 16:12:15 UTC
parent ee409a0d7172e3cedbbf34ade7615d34d0647f01

OMEMO: Bundle creation, storage and publication

snikket/Client.hx +41 -17
snikket/OMEMO.hx +0 -1
snikket/Persistence.hx +16 -0
snikket/persistence/Custom.hx +23 -0
snikket/persistence/Dummy.hx +33 -0
snikket/persistence/IDB.js +150 -0
snikket/persistence/Sqlite.hx +13 -0

diff --git a/snikket/Client.hx b/snikket/Client.hx
index ecb2c90..29acb75 100644
--- a/snikket/Client.hx
+++ b/snikket/Client.hx
@@ -13,6 +13,7 @@ import snikket.ChatMessage;
 import snikket.Message;
 import snikket.EventEmitter;
 import snikket.EventHandler;
+import snikket.OMEMO;
 import snikket.PubsubEvent;
 import snikket.Stream;
 import snikket.jingle.Session;
@@ -77,13 +78,17 @@ class Client extends EventEmitter {
 			"urn:xmpp:jingle:apps:rtp:1",
 			"urn:xmpp:jingle:apps:rtp:audio",
 			"urn:xmpp:jingle:apps:rtp:video",
-			"urn:xmpp:jingle:transports:ice-udp:1"
+			"urn:xmpp:jingle:transports:ice-udp:1",
+			"eu.siacs.conversations.axolotl.devicelist+notify"
 		]
 	);
 	private var _displayName: String;
 	private var fastMechanism: Null<String> = null;
 	private var token: Null<String> = null;
 	private final pendingCaps: Map<String, Array<(Null<Caps>)->Chat>> = [];
+
+	private final omemo: OMEMO;
+
 	@:allow(snikket)
 	private var inSync(default, null) = false;
 
@@ -99,6 +104,7 @@ class Client extends EventEmitter {
 		this.jid = JID.parse(address);
 		this._displayName = this.jid.node;
 		this.persistence = persistence;
+		this.omemo = new OMEMO(this, persistence);
 		stream = new Stream();
 		stream.on("status/online", this.onConnected);
 		stream.on("status/offline", (data) -> {
@@ -312,25 +318,43 @@ class Client extends EventEmitter {
 				}
 			}
 
-			if (pubsubEvent != null && pubsubEvent.getFrom() != null && JID.parse(pubsubEvent.getFrom()).asBare().asString() == accountId() && pubsubEvent.getNode() == "http://jabber.org/protocol/nick" && pubsubEvent.getItems().length > 0) {
-				updateDisplayName(pubsubEvent.getItems()[0].getChildText("nick", "http://jabber.org/protocol/nick"));
-			}
-
-			if (pubsubEvent != null && pubsubEvent.getFrom() != null && JID.parse(pubsubEvent.getFrom()).asBare().asString() == accountId() && pubsubEvent.getNode() == "urn:xmpp:mds:displayed:0" && pubsubEvent.getItems().length > 0) {
-				for (item in pubsubEvent.getItems()) {
-					if (item.attr.get("id") != null) {
-						final upTo = item.getChild("displayed", "urn:xmpp:mds:displayed:0")?.getChild("stanza-id", "urn:xmpp:sid:0");
-						final chat = getChat(item.attr.get("id"));
-						if (chat == null) {
-							startChatWith(item.attr.get("id"), (caps) -> Closed, (chat) -> chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by")));
-						} else {
-							chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by"), () -> {
-								persistence.storeChats(accountId(), [chat]);
-								this.trigger("chats/update", [chat]);
-							});
+			trace("pubsubEvent "+Std.string(pubsubEvent!=null));
+			if (pubsubEvent != null && pubsubEvent.getFrom() != null {
+			if (pubsubEvent != null && pubsubEvent.getFrom() != null) {
+				final fromBare = JID.parse(pubsubEvent.getFrom()).asBare();
+				final isOwnAccount = fromBare.asString() == accountId();
+				final pubsubNode = pubsubEvent.getNode();
+
+				if(isOwnAccount && pubsubEvent.getNode() == "urn:xmpp:mds:displayed:0" && pubsubEvent.getItems().length > 0) {
+					for (item in pubsubEvent.getItems()) {
+						if (item.attr.get("id") != null) {
+							final upTo = item.getChild("displayed", "urn:xmpp:mds:displayed:0")?.getChild("stanza-id", "urn:xmpp:sid:0");
+							final chat = getChat(item.attr.get("id"));
+							if (chat == null) {
+								startChatWith(item.attr.get("id"), (caps) -> Closed, (chat) -> chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by")));
+							} else {
+								chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by"), () -> {
+									persistence.storeChats(accountId(), [chat]);
+									this.trigger("chats/update", [chat]);
+								});
+							}
 						}
 					}
 				}
+						
+				if (isOwnAccount && pubsubNode == "http://jabber.org/protocol/nick" && pubsubEvent.getItems().length > 0) {
+					updateDisplayName(pubsubEvent.getItems()[0].getChildText("nick", "http://jabber.org/protocol/nick"));
+				}
+
+				trace("pubsubNode == "+pubsubNode);
+
+				if(pubsubNode == "eu.siacs.conversations.axolotl.devicelist") {
+					if(isOwnAccount) {
+						omemo.onAccountUpdatedDeviceList(pubsubEvent.getItems());
+					} else {
+						omemo.onContactUpdatedDeviceList(fromBare, pubsubEvent.getItems());
+					}
+				}
 			}
 
 			return EventUnhandled; // Allow others to get this event as well
diff --git a/snikket/OMEMO.hx b/snikket/OMEMO.hx
index 5bc2de6..0c52123 100644
--- a/snikket/OMEMO.hx
+++ b/snikket/OMEMO.hx
@@ -164,7 +164,6 @@ class OMEMO {
 			persistence.getOmemoPreKeys(client.accountId(), resolve);
 		}).then(function (prekeys) {
 			// Always an array (just empty if no keys)
-			// FIXME: bundle should contain Array<PublicPreKey> rather then PreKeyPair!!
 			newBundle.prekeys = [
 				for(i in 0...prekeys.length) {
 					{
diff --git a/snikket/Persistence.hx b/snikket/Persistence.hx
index 18b774b..7672d65 100644
--- a/snikket/Persistence.hx
+++ b/snikket/Persistence.hx
@@ -5,6 +5,10 @@ import snikket.Chat;
 import snikket.ChatMessage;
 import snikket.Message;
 
+import snikket.OMEMO;
+
+using snikket.SignalProtocol;
+
 #if cpp
 @:build(HaxeSwiftBridge.expose())
 #end
@@ -34,6 +38,18 @@ interface Persistence {
 	public function storeStreamManagement(accountId:String, data:Null<BytesData>):Void;
 	public function getStreamManagement(accountId:String, callback: (Null<BytesData>)->Void):Void;
 	public function storeService(accountId:String, serviceId:String, name:Null<String>, node:Null<String>, caps:Caps):Void;
+	public function getOmemoId(login:String, callback:(omemoId:Null<Int>)->Void):Void;
+	public function storeOmemoId(login:String, omemoId:Int):Void;
+	public function storeOmemoIdentityKey(login:String, keypair:IdentityKeyPair):Void;
+	public function getOmemoIdentityKey(login:String, callback: (IdentityKeyPair)->Void):Void;
+	public function getOmemoDeviceList(identifier:String, callback: (Array<Int>)->Void):Void;
+	public function storeOmemoDeviceList(identifier:String, deviceIds:Array<Int>):Void;
+	public function storeOmemoPreKey(identifier:String, keyId:Int, keyPair:PreKeyPair):Void;
+	public function getOmemoPreKey(identifier:String, keyId:Int, callback: (PreKeyPair)->Void):Void;
+	public function storeOmemoSignedPreKey(login:String, signedPreKey:OMEMOBundleSignedPreKey):Void;
+	public function getOmemoSignedPreKey(login:String, keyId:Int, callback: (OMEMOBundleSignedPreKey)->Void):Void;
+	public function getOmemoPreKeys(login:String, callback: (Array<PreKeyPair>)->Void):Void;
+
 	@HaxeCBridge.noemit
 	public function findServicesWithFeature(accountId:String, feature:String, callback:(Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>)->Void):Void;
 }
diff --git a/snikket/persistence/Custom.hx b/snikket/persistence/Custom.hx
index d6d0772..ed1f1c6 100644
--- a/snikket/persistence/Custom.hx
+++ b/snikket/persistence/Custom.hx
@@ -157,6 +157,29 @@ class Custom implements Persistence {
 	public function findServicesWithFeature(accountId:String, feature:String, callback:(Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>)->Void) {
 		backing.findServicesWithFeature(accountId, feature, callback);
 	}
+
+
+	// OMEMO
+	// TODO
+	@HaxeCBridge.noemit
+	public function getOmemoId(login:String, callback:(omemoId:Null<Int>)->Void):Void {
+		backing.getOmemoId(login, callback);
+	}
+
+	@HaxeCBridge.noemit
+	public function storeOmemoId(login:String, omemoId:Int):Void {
+		backing.storeOmemoId(login, omemoId);
+	}
+
+	@HaxeCBridge.noemit
+	public function getOMEMODeviceList(identifier:String, callback: (Array<Int>)->Void) {
+		return backing.getOMEMODeviceList(identifier, callback);
+	}
+
+	@HaxeCBridge.noemit
+	public function storeOMEMODeviceList(identifier:String, deviceIds:Array<Int>) {
+		backing.storeOMEMODeviceList(identifier, deviceIds);
+	}
 }
 
 @:expose
diff --git a/snikket/persistence/Dummy.hx b/snikket/persistence/Dummy.hx
index 39ba123..79c05b0 100644
--- a/snikket/persistence/Dummy.hx
+++ b/snikket/persistence/Dummy.hx
@@ -7,6 +7,9 @@ import haxe.io.BytesData;
 import snikket.Caps;
 import snikket.Chat;
 import snikket.Message;
+import snikket.OMEMO;
+
+using snikket.SignalProtocol;
 
 // TODO: consider doing background threads for operations
 
@@ -138,4 +141,34 @@ class Dummy implements Persistence {
 	public function findServicesWithFeature(accountId:String, feature:String, callback:(Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>)->Void) {
 		callback([]);
 	}
+
+	@HaxeCBridge.noemit
+	public function getOmemoId(login:String, callback:(omemoId:Null<Int>)->Void):Void { }
+
+	@HaxeCBridge.noemit
+	public function storeOmemoId(login:String, omemoId:Int):Void { }
+
+	@HaxeCBridge.noemit
+	public function getOmemoDeviceList(identifier:String, callback: (Array<Int>)->Void) { }
+	@HaxeCBridge.noemit
+	public function storeOmemoDeviceList(identifier:String, deviceIds:Array<Int>):Void { }
+
+	@HaxeCBridge.noemit
+	public function storeOmemoPreKey(identifier:String, keyId:Int, keyPair:PreKeyPair):Void { }
+	@HaxeCBridge.noemit
+	public function getOmemoPreKey(identifier:String, keyId:Int, callback: (PreKeyPair)->Void):Void { }
+
+	@HaxeCBridge.noemit
+	public function storeOmemoIdentityKey(login:String, keypair:IdentityKeyPair):Void { }
+	@HaxeCBridge.noemit
+	public function getOmemoIdentityKey(login:String, callback: (IdentityKeyPair)->Void):Void { }
+
+	@HaxeCBridge.noemit
+	public function storeOmemoSignedPreKey(login:String, signedPreKey:OMEMOBundleSignedPreKey):Void { }
+	@HaxeCBridge.noemit
+	public function getOmemoSignedPreKey(login:String, keyId:Int, callback: (OMEMOBundleSignedPreKey)->Void):Void { }
+
+	@HaxeCBridge.noemit
+	public function getOmemoPreKeys(login:String, callback: (Array<PreKeyPair>)->Void):Void { }
+
 }
diff --git a/snikket/persistence/IDB.js b/snikket/persistence/IDB.js
index 0cd4f1e..9b3aed9 100644
--- a/snikket/persistence/IDB.js
+++ b/snikket/persistence/IDB.js
@@ -8,6 +8,23 @@ export default (dbname, media, tokenize, stemmer) => {
 	if (!tokenize) tokenize = function(s) { return s.split(" "); }
 	if (!stemmer) stemmer = function(s) { return s; }
 
+	// Helper functions to convert binary data to storage-safe strings
+	// Uint8Array.to/fromBase64() is not yet widely available
+	function arrayBufferToBase64 (ab) {
+		return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
+	}
+
+	function base64ToArrayBuffer (b64) {
+		const binary_string = atob(b64);
+		const len = binary_string.length;
+		const bytes = new Uint8Array(len);
+
+		for (let i = 0; i < len; i++) {
+			bytes[i] = binary_string.charCodeAt(i);
+		}
+		return bytes.buffer;
+	}
+
 	var db = null;
 	function openDb(version) {
 		var dbOpenReq = indexedDB.open(dbname, version);
@@ -613,6 +630,100 @@ export default (dbname, media, tokenize, stemmer) => {
 			}
 		},
 
+		storeOmemoId: function(login, omemoId) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			store.put(omemoId, "omemo:id:" + login).onerror = console.error;
+		},
+
+		storeOmemoIdentityKey: function (login, keypair) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			store.put(keypair, "omemo:key:" + login).onerror = console.error;
+		},
+
+		storeOmemoDeviceList: function (identifier, deviceIds) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			const key = "omemo:devices:"+identifier;
+			if(deviceIds.length>0) {
+				store.put(deviceIds, key);
+			} else {
+				store.delete(key);
+			}
+		},
+
+		getOmemoDeviceList: function (identifier, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			promisifyRequest(store.get("omemo:devices:"+identifier)).then((result) => {
+				if (result === undefined) {
+					callback([]);
+				} else {
+					callback(result);
+				}
+			}).catch((e) => {
+				console.error(e);
+				callback([]);
+			});
+		},
+
+		storeOmemoPreKey: function (login, keyId, keyPair) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			const storedKeyPair = {
+				"privKey": arrayBufferToBase64(keyPair.privKey),
+				"pubKey": arrayBufferToBase64(keyPair.pubKey),
+			};
+			store.put(storedKeyPair, "omemo:prekeys:"+login+":"+keyId.toString());
+		},
+
+		getOmemoPreKey: function (login, keyId, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			promisifyRequest(store.get("omemo:prekeys:"+login+":"+keyId.toString())).then((result) => {
+				if(result === undefined) {
+					callback(null);
+				} else {
+					callback({
+						"privKey": base64ToArrayBuffer(result.privKey),
+						"pubKey": base64ToArrayBuffer(result.pubKey),
+					});
+				}
+			}).catch((e) => {
+				console.error(e);
+				callback(null);
+			});
+		},
+
+		getOmemoPreKeys: function (login, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			const prefix = "omemo:prekeys:"+login+":";
+			const keyRange = IDBKeyRange.bound(prefix, prefix + '\uffff');
+
+			const prekeys = [];
+			const req = store.openCursor(keyRange);
+
+			req.onsuccess = (event) => {
+				const cursor = event.target.result;
+				if(cursor) {
+					prekeys.push({
+						"privKey": base64ToArrayBuffer(cursor.value.privKey),
+						"pubKey": base64ToArrayBuffer(cursor.value.pubKey),
+					});
+					cursor.continue();
+				} else {
+					callback(prekeys);
+				}
+			}
+
+			req.onerror = (e) => {
+				console.error(e);
+				callback(null);
+			};
+		},
+
 		storeStreamManagement: function(account, sm) {
 			// Don't bother on ios, the indexeddb is too broken
 			// https://bugs.webkit.org/show_bug.cgi?id=287876
@@ -663,6 +774,45 @@ export default (dbname, media, tokenize, stemmer) => {
 			});
 		},
 
+		getOmemoId: function(login, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			promisifyRequest(store.get("omemo:id:"+login)).then((result) => {
+				callback(result);
+			}).catch((e) => {
+				console.error(e);
+				callback(null);
+			});
+		},
+
+		getOmemoIdentityKey: function(login, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			promisifyRequest(store.get("omemo:key:"+login)).then((result) => {
+				callback(result);
+			}).catch((e) => {
+				console.error(e);
+				callback(null);
+			});
+		},
+
+		getOmemoSignedPreKey: function(login, keyId, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			promisifyRequest(store.get("omemo:signed-prekey:"+login+":"+keyId.toString())).then((result) => {
+				callback(result);
+			}).catch((e) => {
+				console.error(e);
+				callback(null);
+			});
+		},
+
+		storeOmemoSignedPreKey: function (login, signedKey) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			store.put(signedKey, "omemo:signed-prekey:"+login+":"+signedKey.id.toString());
+		},
+
 		removeAccount(account, completely) {
 			const tx = db.transaction(["keyvaluepairs", "services", "messages", "chats", "reactions"], "readwrite");
 			const store = tx.objectStore("keyvaluepairs");
diff --git a/snikket/persistence/Sqlite.hx b/snikket/persistence/Sqlite.hx
index 2e78f88..405affd 100644
--- a/snikket/persistence/Sqlite.hx
+++ b/snikket/persistence/Sqlite.hx
@@ -14,8 +14,12 @@ import snikket.Chat;
 import snikket.Message;
 import snikket.Reaction;
 import snikket.ReactionUpdate;
+import snikket.OMEMO;
+
 using Lambda;
 
+// TODO: consider doing background threads for operations
+
 @:expose
 #if cpp
 @:build(HaxeCBridge.expose())
@@ -819,4 +823,13 @@ class Sqlite implements Persistence implements KeyValueStore {
 			return builder;
 		}));
 	}
+
+	// OMEMO
+	// TODO
+	@HaxeCBridge.noemit
+	public function getOmemoId(login:String, callback:(omemoId:Null<Int>)->Void):Void { }
+
+	@HaxeCBridge.noemit
+	public function storeOmemoId(login:String, omemoId:Int):Void { }
+
 }