| author | Matthew Wild
<mwild1@gmail.com> 2025-04-22 19:24:02 UTC |
| committer | Matthew Wild
<mwild1@gmail.com> 2025-04-22 19:24:02 UTC |
| parent | f7d396def04e77f76fa22695a6ed9fca21c97277 |
| doc/OMEMO.md | +0 | -1 |
| snikket/OMEMO.hx | +87 | -20 |
| snikket/Persistence.hx | +1 | -1 |
| snikket/persistence/Dummy.hx | +1 | -1 |
| snikket/persistence/IDB.js | +7 | -2 |
| snikket/persistence/Sqlite.hx | +1 | -1 |
diff --git a/doc/OMEMO.md b/doc/OMEMO.md index edaa116..232867d 100644 --- a/doc/OMEMO.md +++ b/doc/OMEMO.md @@ -14,6 +14,5 @@ compile with the NO_OMEMO flag. - No API to determine cryptographic identity of message sender - Persistence: only IndexedDB backend is currently implemented - Encryption status reported by the API can be forged by sender -- Consumed prekeys are not removed and replaced - Outgoing messages are not encrypted to the sending account's other devices - No support for group chats diff --git a/snikket/OMEMO.hx b/snikket/OMEMO.hx index bc6568a..5113b3f 100644 --- a/snikket/OMEMO.hx +++ b/snikket/OMEMO.hx @@ -87,6 +87,11 @@ class OMEMOBundle { public function getRandomPreKey():PublicPreKey { return prekeys[Std.random(prekeys.length-1)]; } + + // Return a new bundle with an updated set of prekeys + public function withNewPreKeys(newPreKeys:Array<PublicPreKey>):OMEMOBundle { + return new OMEMOBundle(this.identity_key, this.device_id, newPreKeys, this.signed_prekey); + } } class OMEMOStore extends SignalProtocolStore { @@ -142,6 +147,7 @@ class OMEMOStore extends SignalProtocolStore { } public function removePreKey(keyId:Int):Promise<Bool> { + trace("OMEMO: Removing prekey "+keyId); persistence.removeOmemoPreKey(accountId, keyId); // FIXME: Need to signal that we need to generate a replacement // for the consumed prekey and republish our bundle @@ -444,8 +450,8 @@ class OMEMO { newBundle.prekeys = [ for(i in 0...prekeys.length) { { - keyId: i+1, - pubKey: Base64.encode(Bytes.ofData(prekeys[i].pubKey)), + keyId: prekeys[i].keyId, + pubKey: Base64.encode(Bytes.ofData(prekeys[i].keyPair.pubKey)), }; } ]; @@ -630,6 +636,8 @@ class OMEMO { // Stuff that touches libsignal // Build and store an OMEMO identity bundle + // This should only be called once, when setting up a + // new client! private function buildBundle():Promise<Bool> { final deviceId = KeyHelper.generateRegistrationId(); // FIXME: Check for collision var identityKeyPair:IdentityKeyPair; @@ -667,34 +675,75 @@ class OMEMO { }); } - private function generatePreKeys(_:Bool):Promise<Array<PublicPreKey>> { - final amount = NUM_PREKEYS; - final keys = []; + private function storePreKeys(prekeys:Array<PreKey>):Promise<Array<PublicPreKey>> { + for(prekey in prekeys) { + // Store the full keypair + persistence.storeOmemoPreKey(client.accountId(), prekey.keyId, prekey.keyPair); + } + return Promise.resolve([ + for(prekey in prekeys) { + // Emit the base64 public part for the application to publish + { + keyId: prekey.keyId, + pubKey: Base64.encode(Bytes.ofData(prekey.keyPair.pubKey)) + }; + } + ]); + } + // Generate new prekeys + private function generatePreKeys(_:Bool):Promise<Array<PublicPreKey>> { final generatedPreKeys:Promise<Array<PreKey>> = PromiseTools.all([ - for(i in 1...(amount+1)) { + for(i in 1...(NUM_PREKEYS+1)) { trace("Generating prekey "+Std.string(i)); KeyHelper.generatePreKey(i); } ]); - final publicStoredPreKeys:Promise<Array<PublicPreKey>> = cast generatedPreKeys.then(cast function (prekeys:Array<PreKey>):Array<PublicPreKey> { - for(prekey in prekeys) { - // Store the full keypair - persistence.storeOmemoPreKey(client.accountId(), prekey.keyId, prekey.keyPair); - } - return [ - for(prekey in prekeys) { - // Emit the base64 public part for the application to publish - { - keyId: prekey.keyId, - pubKey: Base64.encode(Bytes.ofData(prekey.keyPair.pubKey)) - }; + return generatedPreKeys.then(storePreKeys); + } + + // Generate only prekeys which are "missing" (i.e. consumed) + // Returns an array of all prekeys, which can be used to update the + // published bundle. + private function generateMissingPreKeys():Promise<Array<PublicPreKey>> { + return new Promise((resolve, reject) -> { + persistence.getOmemoPreKeys(client.accountId(), function (prekeys:Array<PreKey>) { + // Generate an array of all keyIds we currently have in storage + final currentKeyIds:Array<Int> = prekeys.map(function (prekey) { + return prekey.keyId; + }); + + currentKeyIds.sort(function (a:Int, b:Int):Int { + if(a == b) { + return 0; + } + return a < b ? -1 : 1; + }); + + final generatedKeys:Array<Promise<PreKey>> = []; + var idx = 0; + for(keyId in 1...(NUM_PREKEYS+1)) { + if(currentKeyIds[idx] == keyId) { + // Key already present + idx++; + } else { + trace("Generating replacement prekey "+Std.string(keyId)); + generatedKeys.push(KeyHelper.generatePreKey(keyId)); + } } - ]; + + PromiseTools.all(generatedKeys).then(storePreKeys).then((storedPreKeys) -> { + resolve(prekeys.map(preKeyToPublicPreKey).concat(storedPreKeys)); + }); + }); }); + } - return publicStoredPreKeys; + private function publishNewPreKeys(newPreKeys:Array<PublicPreKey>):Bool { + this.bundle = this.bundle.withNewPreKeys(newPreKeys); + publishBundle(); + return true; } public function getDeviceId():Promise<Int> { @@ -748,6 +797,17 @@ class OMEMO { return cipher.decryptWhisperMessage(deviceKey.getRawKey()); } }); + + if(deviceKey.prekey) { + promRawKeyWithTag.then((cipher) -> { + // Now it has been used, we need to replace the prekey that + // was used for this incoming message. libsignal has already + // removed it from the store, so we just need to regenerate + // any missing keys + generateMissingPreKeys().then(publishNewPreKeys); + }); + } + final promPayload = promRawKeyWithTag.then((rawKeyWithTag) -> { return decryptPayloadWithKey(payload.getRawPayload(), rawKeyWithTag, payload.getRawIv()); }); @@ -1074,4 +1134,11 @@ class OMEMO { return header.toXml(); }); } + + static private function preKeyToPublicPreKey(prekey:PreKey):PublicPreKey { + return { + keyId: prekey.keyId, + pubKey: Base64.encode(Bytes.ofData(prekey.keyPair.pubKey)), + }; + } } diff --git a/snikket/Persistence.hx b/snikket/Persistence.hx index 65f5398..6801072 100644 --- a/snikket/Persistence.hx +++ b/snikket/Persistence.hx @@ -51,7 +51,7 @@ interface Persistence { public function removeOmemoPreKey(identifier:String, keyId:Int):Void; public function storeOmemoSignedPreKey(login:String, signedPreKey:SignedPreKey):Void; public function getOmemoSignedPreKey(login:String, keyId:Int, callback: (SignedPreKey)->Void):Void; - public function getOmemoPreKeys(login:String, callback: (Array<PreKeyPair>)->Void):Void; + public function getOmemoPreKeys(login:String, callback: (Array<PreKey>)->Void):Void; public function storeOmemoContactIdentityKey(account:String, address:String, identityKey:IdentityPublicKey):Void; public function getOmemoContactIdentityKey(account:String, address:String, callback:(IdentityPublicKey)->Void):Void; public function getOmemoSession(account:String, address:String, callback:(SignalSession)->Void):Void; diff --git a/snikket/persistence/Dummy.hx b/snikket/persistence/Dummy.hx index ebad151..aa9ddc9 100644 --- a/snikket/persistence/Dummy.hx +++ b/snikket/persistence/Dummy.hx @@ -174,7 +174,7 @@ class Dummy implements Persistence { public function getOmemoSignedPreKey(login:String, keyId:Int, callback: (SignedPreKey)->Void):Void { } @HaxeCBridge.noemit - public function getOmemoPreKeys(login:String, callback: (Array<PreKeyPair>)->Void):Void { } + public function getOmemoPreKeys(login:String, callback: (Array<PreKey>)->Void):Void { } @HaxeCBridge.noemit public function storeOmemoContactIdentityKey(account:String, address:String, identityKey:IdentityPublicKey):Void { } diff --git a/snikket/persistence/IDB.js b/snikket/persistence/IDB.js index d2e8f9d..9b27982 100644 --- a/snikket/persistence/IDB.js +++ b/snikket/persistence/IDB.js @@ -735,9 +735,14 @@ export default (dbname, media, tokenize, stemmer) => { req.onsuccess = (event) => { const cursor = event.target.result; if(cursor) { + const splitDbKey = cursor.key.split(":"); + const keyId = parseInt(splitDbKey[splitDbKey.length - 1], 10); prekeys.push({ - "privKey": base64ToArrayBuffer(cursor.value.privKey), - "pubKey": base64ToArrayBuffer(cursor.value.pubKey), + keyId: keyId, + keyPair: { + "privKey": base64ToArrayBuffer(cursor.value.privKey), + "pubKey": base64ToArrayBuffer(cursor.value.pubKey), + }, }); cursor.continue(); } else { diff --git a/snikket/persistence/Sqlite.hx b/snikket/persistence/Sqlite.hx index f1910db..6b548f9 100644 --- a/snikket/persistence/Sqlite.hx +++ b/snikket/persistence/Sqlite.hx @@ -865,7 +865,7 @@ class Sqlite implements Persistence implements KeyValueStore { public function getOmemoSignedPreKey(login:String, keyId:Int, callback: (SignedPreKey)->Void):Void { } @HaxeCBridge.noemit - public function getOmemoPreKeys(login:String, callback: (Array<PreKeyPair>)->Void):Void { } + public function getOmemoPreKeys(login:String, callback: (Array<PreKey>)->Void):Void { } @HaxeCBridge.noemit public function storeOmemoContactIdentityKey(account:String, address:String, identityKey:IdentityPublicKey):Void { }