git » sdk » commit 0a22cea

OMEMO: Encrypt to sending account's other devices

author Matthew Wild
2025-05-09 09:48:52 UTC
committer Stephen Paul Weber
2025-09-29 13:43:04 UTC
parent 63389c56375e8459d83625b2da62bf6dbfa21803

OMEMO: Encrypt to sending account's other devices

doc/OMEMO.md +13 -7
snikket/OMEMO.hx +38 -19

diff --git a/doc/OMEMO.md b/doc/OMEMO.md
index 232867d..48412bd 100644
--- a/doc/OMEMO.md
+++ b/doc/OMEMO.md
@@ -9,10 +9,16 @@ compile with the NO_OMEMO flag.
 
 ## TODO / known issues
 
-- No caching of remote contact devices
-- No API to control encryption of outgoing messages
-- 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
-- Outgoing messages are not encrypted to the sending account's other devices
-- No support for group chats
+- [x] One-to-one bidirectional OMEMO
+- [x] Healing of broken sessions
+- [x] Remove and replace consumed prekeys
+- [x] Allow non-OMEMO messages to recipients with no published keys when policy allows
+- [x] Encrypt outgoing messages to the sending account's other devices
+- [x] Persistence: IndexedDB (for web)
+- [ ] Use cache for remote contact devices
+- [ ] Persistence: SQLite backend (for native)
+- [ ] API to control encryption of outgoing messages
+- [ ] API to determine cryptographic identity of message sender
+- [ ] Fix that encryption status reported by the API can be forged by sender
+- [ ] Group chat support
+
diff --git a/snikket/OMEMO.hx b/snikket/OMEMO.hx
index 5113b3f..d31c786 100644
--- a/snikket/OMEMO.hx
+++ b/snikket/OMEMO.hx
@@ -1093,31 +1093,50 @@ class OMEMO {
 		return sessionCipher.encrypt(keyWithTag);
 	}
 
+	// Convert a key from a string of raw bytes to base64
+	private static function b64EncodeKey(keyStr:String) {
+		#if js
+			// Haxe cannot natively convert this string to a byte array. It only supports two
+			// encodings - 'UTF8' and 'RawNative'. The former wrongly tries to interpret
+			// the binary data as UTF-8 sequences, and the latter translates each character
+			// to a pair of bytes (since JS uses UTF-16).
+			return Browser.window.btoa(keyStr);
+		#else
+			return Base64.encode(Bytes.ofString(keyStr, RawNative));
+		#end
+	}
+
+	private function encryptForDevice(sid:Int, jid:String, rid:Int, encryptionResult:OMEMOEncryptionResult):Promise<OMEMOPayloadKey> {
+		final promSessionCipher = getSessionCipher(sid, jid, rid);
+		return promSessionCipher.then((sessionCipher) -> {
+			return encryptPayloadKeyForSession(encryptionResult, sessionCipher).then((encryptedKey) -> {
+				final payloadKey:OMEMOPayloadKey = {
+					rid: rid,
+					prekey: encryptedKey.type == 3,
+					encodedKey: b64EncodeKey(encryptedKey.body),
+				};
+				return payloadKey;
+			});
+		});
+	}
+
 	private function buildOMEMOHeader(encryptionResult:OMEMOEncryptionResult, sid:Int, jid:String, deviceList:Array<Int>):Promise<Stanza> {
 		final promKeys = [
 			for(rid in deviceList) {
-				final promSessionCipher = getSessionCipher(sid, jid, rid);
-				promSessionCipher.then((sessionCipher) -> {
-					return encryptPayloadKeyForSession(encryptionResult, sessionCipher).then((encryptedKey) -> {
-						final payloadKey:OMEMOPayloadKey = {
-							rid: rid,
-							prekey: encryptedKey.type == 3,
-#if js
-							// Haxe cannot natively convert this string to a byte array. It only supports two
-							// encodings - 'UTF8' and 'RawNative'. The former wrongly tries to interpret
-							// the binary data as UTF-8 sequences, and the latter translates each character
-							// to a pair of bytes (since JS uses UTF-16).
-							encodedKey: Browser.window.btoa(encryptedKey.body)
-#else
-							encodedKey: Base64.encode(Bytes.ofString(encryptedKey.body, RawNative))
-#end
-						};
-						return payloadKey;
-					});
-				});
+				encryptForDevice(sid, jid, rid, encryptionResult);
 			}
 		];
 
+		// We've included keys for our contact's devices, now we need
+		// to include any of our own devices, so they can read the outgoing
+		// message also.
+		for(rid in this.deviceList) {
+			// Don't encrypt to our own device (we already have the original message locally)
+			if(sid != rid) {
+				promKeys.push(encryptForDevice(sid, this.client.accountId(), rid, encryptionResult));
+			}
+		}
+
 		final promHeader = new Promise((resolve, reject) -> {
 			PromiseTools.all(promKeys).then((recipientKeys) -> {
 				final header:OMEMOPayload = {