| author | Matthew Wild
<mwild1@gmail.com> 2024-12-03 11:18:02 UTC |
| committer | Matthew Wild
<mwild1@gmail.com> 2025-04-15 16:12:15 UTC |
| parent | ee409a0d7172e3cedbbf34ade7615d34d0647f01 |
| 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 { } + }