From e3f1db403b2b23ee52f114c60e5af1b0ecb28b39 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Mon, 7 Apr 2025 14:37:54 +0200 Subject: [PATCH 01/62] Update file upload extension to be compatible with current XEP --- Extensions/XEP-0363/XMPPHTTPFileUpload.m | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Extensions/XEP-0363/XMPPHTTPFileUpload.m b/Extensions/XEP-0363/XMPPHTTPFileUpload.m index 6c82170e72..fc7a05a261 100644 --- a/Extensions/XEP-0363/XMPPHTTPFileUpload.m +++ b/Extensions/XEP-0363/XMPPHTTPFileUpload.m @@ -19,7 +19,7 @@ static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; #endif -NSString *const XMPPHTTPFileUploadNamespace = @"urn:xmpp:http:upload"; +NSString *const XMPPHTTPFileUploadNamespace = @"urn:xmpp:http:upload:0"; NSString *const XMPPHTTPFileUploadErrorDomain = @"XMPPHTTPFileUploadErrorDomain"; NSString* StringForXMPPHTTPFileUploadErrorCode(XMPPHTTPFileUploadErrorCode errorCode) { @@ -129,11 +129,10 @@ - (void)requestSlotFromService:(XMPPJID*)serviceJID // - // - // my_juliet.png - // 23456 - // image/jpeg - // + // // NSString *iqID = [XMPPStream generateUUID]; @@ -142,11 +141,11 @@ - (void)requestSlotFromService:(XMPPJID*)serviceJID XMPPElement *request = [XMPPElement elementWithName:@"request"]; [request setXmlns:XMPPHTTPFileUploadNamespace]; if (filename) { - [request addChild:[XMPPElement elementWithName:@"filename" stringValue:filename]]; + [request addAttributeWithName:@"filename" stringValue:filename]; } - [request addChild:[XMPPElement elementWithName:@"size" numberValue:[NSNumber numberWithUnsignedInteger:size]]]; + [request addAttributeWithName:@"size" unsignedIntegerValue:size]; if (contentType) { - [request addChild:[XMPPElement elementWithName:@"content-type" stringValue:contentType]]; + [request addAttributeWithName:@"content-type" stringValue:contentType]; } [iq addChild:request]; From 0ec523a2badb90ea7cd5c34174398a295ac740c4 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Mon, 7 Apr 2025 14:40:31 +0200 Subject: [PATCH 02/62] Fix out of band data extension to inject full URLs into outgoing messages --- Extensions/XEP-0066/XMPPMessage+XEP_0066.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Extensions/XEP-0066/XMPPMessage+XEP_0066.m b/Extensions/XEP-0066/XMPPMessage+XEP_0066.m index 13e78db538..91a87fcce4 100644 --- a/Extensions/XEP-0066/XMPPMessage+XEP_0066.m +++ b/Extensions/XEP-0066/XMPPMessage+XEP_0066.m @@ -14,9 +14,9 @@ - (void)addOutOfBandURL:(NSURL *)URL desc:(NSString *)desc { NSXMLElement *outOfBand = [NSXMLElement elementWithName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; - if([[URL path] length]) + if([[URL absoluteString] length]) { - NSXMLElement *URLElement = [NSXMLElement elementWithName:@"url" stringValue:[URL path]]; + NSXMLElement *URLElement = [NSXMLElement elementWithName:@"url" stringValue:[URL absoluteString]]; [outOfBand addChild:URLElement]; } From 74c121c89fb2a3a80f4e1412e870b2f0b79fc35f Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 5 Jun 2025 15:14:52 +0200 Subject: [PATCH 03/62] Implement in-band account registration request in registration module --- Extensions/XEP-0077/XMPPRegistration.h | 5 ++ Extensions/XEP-0077/XMPPRegistration.m | 76 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/Extensions/XEP-0077/XMPPRegistration.h b/Extensions/XEP-0077/XMPPRegistration.h index 6233b7d1b4..3e5b44e6d1 100644 --- a/Extensions/XEP-0077/XMPPRegistration.h +++ b/Extensions/XEP-0077/XMPPRegistration.h @@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN XMPPIDTracker *xmppIDTracker; } +- (BOOL)registerWithFields:(NSDictionary *)fields; + /** * This method will attempt to change the current user's password to the new one provided. The * user *MUST* be authenticated for this to work successfully. @@ -54,6 +56,9 @@ NS_ASSUME_NONNULL_BEGIN @protocol XMPPRegistrationDelegate @optional +- (void)registrationSuccesful:(XMPPRegistration *)sender; +- (void)registrationFailed:(XMPPRegistration *)sender withError:(nullable NSError *)error; + /** * Implement this method when calling [regInstance changePassword:]. It will be invoked * if the request for changing the user's password is successfully executed and receives a diff --git a/Extensions/XEP-0077/XMPPRegistration.m b/Extensions/XEP-0077/XMPPRegistration.m index 9367a75a65..bca39c8d2a 100644 --- a/Extensions/XEP-0077/XMPPRegistration.m +++ b/Extensions/XEP-0077/XMPPRegistration.m @@ -28,6 +28,44 @@ - (void)willDeactivate #pragma mark Public API //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)registerWithFields:(NSDictionary *)fields +{ + if ([xmppStream isAuthenticated]) + return NO; + + dispatch_block_t block = ^{ + @autoreleasepool { + NSString *toStr = self->xmppStream.myJID.domain; + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + + for (NSString *fieldName in fields) { + NSXMLElement *field = [NSXMLElement elementWithName:fieldName + stringValue:fields[fieldName]]; + [query addChild:field]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" + to:[XMPPJID jidWithString:toStr] + elementID:[self->xmppStream generateUUID] + child:query]; + + [self->xmppIDTracker addID:[iq elementID] + target:self + selector:@selector(handleRegistrationQueryIQ:withInfo:) + timeout:60]; + + [self->xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + + return YES; +} + /** * This method provides functionality of XEP-0077 3.3 User Changes Password. * @@ -137,6 +175,44 @@ - (BOOL)cancelRegistrationUsingPassword:(NSString *)password #pragma mark - XMPPIDTracker //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)handleRegistrationQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info +{ + dispatch_block_t block = ^{ + @autoreleasepool { + NSXMLElement *errorElem = [iq elementForName:@"error"]; + + if (errorElem) { + NSString *errMsg = [[errorElem children] componentsJoinedByString:@", "]; + NSInteger errCode = [errorElem attributeIntegerValueForName:@"code" + withDefaultValue:-1]; + NSDictionary *errInfo = @{NSLocalizedDescriptionKey : errMsg}; + NSError *err = [NSError errorWithDomain:XMPPRegistrationErrorDomain + code:errCode + userInfo:errInfo]; + + [self->multicastDelegate registrationFailed:self + withError:err]; + return; + } + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"]) { + [self->multicastDelegate registrationSuccesful:self]; + } else { + // this should be impossible to reach, but just for safety's sake... + [self->multicastDelegate registrationFailed:self + withError:nil]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + /** * This method handles the response received (or not received) after calling changePassword. */ From 8b8ec5e7424b2d9224c00c37e612ee76bcb54db8 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 11:04:25 +0200 Subject: [PATCH 04/62] Implement SCE envelope content manipulation helpers --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Swift/XEP-0420/XMLElement+XEP_0420.swift diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift new file mode 100644 index 0000000000..b1f7b1ca78 --- /dev/null +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -0,0 +1,31 @@ +// +// XMLElement+XEP_0420.swift +// XMPPFramework +// +// Created by Piotr Wegrzynek on 16/07/2025. +// + +#if canImport(XMPPFramework) +import XMPPFramework +#endif + +extension XMLElement { + public static func makeStanzaContentEncryptionEnvelope() -> XMPPElement { + XMPPElement(name: "envelope", xmlns: "urn:xmpp:sce:1") + } + + public var isStanzaContentEncryptionEnvelope: Bool { + name == "envelope" && xmlns == "urn:xmpp:sce:1" + } + + public func withStanzaContentEncryptionEnvelopeContent(_ body: (_ content: XMLElement) throws -> T) rethrows -> T { + let content: XMLElement + if let existingContent = element(forName: "content") { + content = existingContent + } else { + content = XMLElement(name: "content") + addChild(content) + } + return try body(content) + } +} From 0228959d60dd69729decb0b610d7121d0ae51877 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 11:08:42 +0200 Subject: [PATCH 05/62] Implement SCE affix element helpers --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index b1f7b1ca78..dd85642bb4 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -29,3 +29,94 @@ extension XMLElement { return try body(content) } } + +// In order to prevent certain attacks, different affix elements MAY be added as direct child elements of the element. +extension XMLElement { + // Prevent known ciphertext and message length correlation attacks. + public func addStanzaContentEncryptionRandomPaddingAffix() { + addChild(XMLElement(name: "rpad", stringValue: String(StanzaContentEncryptionPaddingGenerator()))) + } + + // Prevent replay attacks using old messages. + public func addStanzaContentEncryptionTimestampAffix(with date: Date) { + let affix = XMLElement(name: "time") + affix.addAttribute(withName: "stamp", stringValue: date.xmppDateTimeString) + addChild(affix) + } + + // Prevent spoofing of the recipient. + public func addStanzaContentEncryptionRecipientAffix(with jid: XMPPJID) { + let affix = XMLElement(name: "to") + affix.addAttribute(withName: "jid", stringValue: jid.bare) + addChild(affix) + } + + // Prevent spoofing of the sender. + public func addStanzaContentEncryptionSenderAffix(with jid: XMPPJID) { + let affix = XMLElement(name: "from") + affix.addAttribute(withName: "jid", stringValue: jid.bare) + addChild(affix) + } +} + +extension XMLElement { + // Receiving clients MUST check whether the difference between the timestamp and the sending time derived from the stanza itself lays within a reasonable margin. + // The client SHOULD use the content of the timestamp element when displaying the send date of the message + public func verifyStanzaContentEncryptionTimestamp(expecting expectedDate: Date, withMargin verificationMargin: TimeInterval = 10) -> Bool { + for affix in elements(forName: "time") { + guard let stamp = affix.attributeStringValue(forName: "stamp"), + let actualDate = Date.from(xmppDateTimeString: stamp), + abs(actualDate.timeIntervalSince(expectedDate)) <= verificationMargin else { + return false + } + } + return true + } + + // Receiving clients MUST check if the JID matches the to attribute of the enclosing stanza and otherwise alert the user/reject the message + public func verifyStanzaContentEncryptionRecipient(expecting expectedJID: XMPPJID) -> Bool { + for affix in elements(forName: "to") { + guard let jid = affix.attributeStringValue(forName: "jid"), + let actualJID = XMPPJID(string: jid), + actualJID.isEqual(to: expectedJID, options: .bare) else { + return false + } + } + return true + } + + // Receiving clients MUST check if the value matches the from attribute of the enclosing stanza and otherwise alert the user/reject the message + public func verifyStanzaContentEncryptionSender(expecting expectedJID: XMPPJID) -> Bool { + for affix in elements(forName: "from") { + guard let jid = affix.attributeStringValue(forName: "jid"), + let actualJID = XMPPJID(string: jid), + actualJID.isEqual(to: expectedJID, options: .bare) else { + return false + } + } + return true + } +} + +private struct StanzaContentEncryptionPaddingGenerator: Sequence, IteratorProtocol { + private static let alphabet: [Character] = { + let printableASCIICharacterCodes = 33...126 + let xmlUnsafeCharacters = CharacterSet(charactersIn: #""'<>&"#) + return printableASCIICharacterCodes.compactMap { code in + guard let scalar = UnicodeScalar(code), !xmlUnsafeCharacters.contains(scalar) else { + return nil + } + return Character(scalar) + } + }() + + private var remaining = Int.random(in: 0...200) + + mutating func next() -> Character? { + guard remaining > 0, let randomCharacter = Self.alphabet.randomElement() else { + return nil + } + remaining -= 1 + return randomCharacter + } +} From 459350034400373ba1b375e49b42ee9e23a5f0f6 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 11:10:52 +0200 Subject: [PATCH 06/62] Define stanza content encryption handling module API --- .../XMPPStanzaContentEncryption.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Swift/XEP-0420/XMPPStanzaContentEncryption.swift diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift new file mode 100644 index 0000000000..50bd298763 --- /dev/null +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -0,0 +1,42 @@ +// +// XMPPStanzaContentEncryption.swift +// XMPPFramework +// +// Created by Piotr Wegrzynek on 18/07/2025. +// + +#if canImport(XMPPFramework) +import XMPPFramework +#endif + +public protocol XMPPStanzaContentEncryptionProfile { + func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement + func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMPPMessage, XMLElement?) -> Void) + func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) + func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool +} + +@objc public protocol XMPPStanzaContentEncryptionDelegate: NSObjectProtocol { + @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEnvelope: XMLElement, in message: XMPPMessage) + @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToReceiveEnvelopeIn message: XMPPMessage) +} + +extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} + +/// A module implementing XMPP stanza content encryption specification as defined in [XEP-0420 version 0.4.1](https://xmpp.org/extensions/attic/xep-0420-0.4.1.html). +public class XMPPStanzaContentEncryption: XMPPModule { + private let profile: XMPPStanzaContentEncryptionProfile + + public init(profile: XMPPStanzaContentEncryptionProfile, dispatchQueue: DispatchQueue? = nil) { + self.profile = profile + super.init(dispatchQueue: dispatchQueue) + } + + public func sendEncryptedMessage(withSensitiveContent sensitiveContent: [XMLElement], to: XMPPJID, messageType: XMPPMessage.MessageType? = nil, elementId: String? = nil) { + + } +} + +extension XMPPStanzaContentEncryption: XMPPStreamDelegate { + +} From d49f93a141dac36c137cdbb9ce3b2e6be674d950 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 11:34:18 +0200 Subject: [PATCH 07/62] Implement encrypted message sending method --- .../XMPPStanzaContentEncryption.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 50bd298763..e83c700790 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -33,10 +33,51 @@ public class XMPPStanzaContentEncryption: XMPPModule { } public func sendEncryptedMessage(withSensitiveContent sensitiveContent: [XMLElement], to: XMPPJID, messageType: XMPPMessage.MessageType? = nil, elementId: String? = nil) { + performBlock { + let outgoingMessage = XMPPMessage(messageType: messageType, to: to, elementID: elementId) + // TODO: Allow modifying sensitiveContent via multidelegation + + // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. + let envelope = XMPPElement.makeStanzaContentEncryptionEnvelope() + envelope.withStanzaContentEncryptionEnvelopeContent { content in + for sensitiveElement in sensitiveContent { + guard let name = sensitiveElement.name, sensitiveElement.xmlns != nil else { + // Elements in the element MUST be identified using an element name and namespace. + assertionFailure("Encountered element without name or namespace in element") + continue + } + content.addChild(sensitiveElement) + } + } + + // Depending on the encryption-specific SCE-profile, some affix elements are added as child elements of the element. + let finalEnvelope = self.profile.addAffixElemenets(to: envelope, for: outgoingMessage) + + // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. + self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: outgoingMessage) { finalOutgoingMessage, encrypted in + guard let encrypted else { return } + + // The result is appended to the message. + finalOutgoingMessage.addChild(encrypted) + + // Since the outer message element does not contain a element the sender appends an unencrypted hint as specified in Message Processing Hints (XEP-0334) [7]. + finalOutgoingMessage.addStorageHint(.store) + + // The message can then be sent to the recipient. + self.performBlock { self.xmppStream?.send(finalOutgoingMessage) } + } + } } } extension XMPPStanzaContentEncryption: XMPPStreamDelegate { + public func xmppStream(_ sender: XMPPStream, willSend message: XMPPMessage) -> XMPPMessage? { + // Unencrypted elements are NOT ALLOWED as child elements of the stanza and MUST be dropped. + assert(message.element(forName: "envelope") == nil, "Encountered unencrypted child element in outgoing message") + message.removeElements(forName: "envelope") + + return message + } } From fb33d44f7ee470d7faae71779031f224d404475d Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 11:35:50 +0200 Subject: [PATCH 08/62] Implement incoming encrypted message handling --- .../XMPPStanzaContentEncryption.swift | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index e83c700790..f8166444bd 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -80,4 +80,86 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { return message } + public func xmppStream(_ sender: XMPPStream, willReceive message: XMPPMessage) -> XMPPMessage? { + // The recipient of the message decrypts its encrypted payload. + profile.decryptEnvelopeXML(from: message) { envelopeXML in + self.performBlock { + if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { + // The result is the element containing the element and the affix elements as direct child elements. + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryption?(self, didReceiveEnvelope: decryptedEnvelope, in: message) + } + } else { + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryption?(self, didFailToReceiveEnvelopeIn: message) + } + } + } + } + + message.removeElementsIgnoredOutsideStanzaContentEncryptionEnvelope() + + return message + } + + private func receiveEncryptedMessage(_ encryptedMessage: XMPPMessage, withEnvelopeXML envelopeXML: String) -> XMLElement? { + // The recipient MUST verify that the decrypted element contains valid XML before processing it any further. Invalid XML must be rejected. + guard let decryptedEnvelope = try? XMLElement(xmlString: envelopeXML), decryptedEnvelope.isStanzaContentEncryptionEnvelope else { + return nil + } + + // Depending on the affix profiles specified by the used encryption protocol, the affix elements are verified to prevent certain attacks from taking place. + guard profile.verifyAffixElements(in: decryptedEnvelope, from: encryptedMessage) else { + return nil + } + + // Afterwards, the extension elements inside the element are checked against the permitted list and any disallowed elements are discarded. + decryptedEnvelope.withStanzaContentEncryptionEnvelopeContent { content in + content.removeElementsDisallowedinStanzaContentEncryptionEnvelope() + } + + // The following is not implemented as it contradicts section 11. Implementation Notes, which calls to handle encrypted elements explicitly: + // As a last step, the original unencrypted stanza is recreated by replacing the element of the stanza with the elements inside of the element. + + return decryptedEnvelope + } +} + +private extension XMLElement { + func removeElementsDisallowedinStanzaContentEncryptionEnvelope() { + // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it + removeAllElements(where: { $0.isServerProcessed }) + } + + func removeElementsIgnoredOutsideStanzaContentEncryptionEnvelope() { + // Furthermore the receiving client MUST ignore any extension elements considered as sensitive which are found outside of the element, especially as direct unencrypted child elements of the enclosing stanza. + removeAllElements(where: { $0.isSensitive }) + } + + private func removeAllElements(where shouldBeRemoved: (XMLElement) -> Bool) { + guard let childrenIndices = children?.indices else { return } + for childIndex in childrenIndices.reversed() { + guard let element = child(at: UInt(childIndex)) as? XMLElement, shouldBeRemoved(element) else { continue } + removeChild(at: UInt(childIndex)) + } + } +} + +private extension XMLElement { + // There are certain extension elements which are required to be available to the server in order to do message routing and processing + // Additionally there are some elements that MUST be filtered by the server. + // Allowing for those elements to be included in, and parsed from the encrypted payload would allow a malicious client to perform a number of attacks. + var isServerProcessed: Bool { + ["urn:xmpp:hints", // Message Processing Hints are addressed to the server and MUST therefore be accessible in plaintext. + XMPPStanzaIdXmlns, // Sending clients MUST NOT include Stanza-ID elements inside the element, as this would prevent the server from filtering it. + "http://jabber.org/protocol/address", // The server MUST be able to access the and
elements in order to do message routing, so they MUST NOT be encrypted. + ].contains(xmlns) + } + + // Contrary to this, other elements are considered sensitive and MUST NOT be available in plaintext outside the element. + var isSensitive: Bool { + // The specification does enforce any specific format for encrypted content elements which are not considered sensitive themselves + // This implementation allows any element named "encrypted" regardless of namespace + !isServerProcessed || name == "encrypted" + } } From 460e91f727dcca17be21946b60e915846f246524 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 12:43:50 +0200 Subject: [PATCH 09/62] Fix unused local variable warning --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index f8166444bd..75e162cc1e 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -42,7 +42,7 @@ public class XMPPStanzaContentEncryption: XMPPModule { let envelope = XMPPElement.makeStanzaContentEncryptionEnvelope() envelope.withStanzaContentEncryptionEnvelopeContent { content in for sensitiveElement in sensitiveContent { - guard let name = sensitiveElement.name, sensitiveElement.xmlns != nil else { + guard sensitiveElement.name != nil, sensitiveElement.xmlns != nil else { // Elements in the element MUST be identified using an element name and namespace. assertionFailure("Encountered element without name or namespace in element") continue From 68372f84c32d4aca38ce659b862890bf1b7d8ebc Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 12:45:06 +0200 Subject: [PATCH 10/62] Remove message parameter from encryption completion callback --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 75e162cc1e..872755e8cc 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -11,7 +11,7 @@ import XMPPFramework public protocol XMPPStanzaContentEncryptionProfile { func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement - func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMPPMessage, XMLElement?) -> Void) + func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool } @@ -55,17 +55,17 @@ public class XMPPStanzaContentEncryption: XMPPModule { let finalEnvelope = self.profile.addAffixElemenets(to: envelope, for: outgoingMessage) // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. - self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: outgoingMessage) { finalOutgoingMessage, encrypted in + self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: outgoingMessage) { encrypted in guard let encrypted else { return } // The result is appended to the message. - finalOutgoingMessage.addChild(encrypted) + outgoingMessage.addChild(encrypted) // Since the outer message element does not contain a element the sender appends an unencrypted hint as specified in Message Processing Hints (XEP-0334) [7]. - finalOutgoingMessage.addStorageHint(.store) + outgoingMessage.addStorageHint(.store) // The message can then be sent to the recipient. - self.performBlock { self.xmppStream?.send(finalOutgoingMessage) } + self.performBlock { self.xmppStream?.send(outgoingMessage) } } } } From 68db7094ee780b983a99561296554f69cb9472ad Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 23 Jul 2025 12:47:33 +0200 Subject: [PATCH 11/62] Add comments to clarify how encryption profile implementation may handle processed XMPP message objects --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 872755e8cc..02e453dc49 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -11,7 +11,11 @@ import XMPPFramework public protocol XMPPStanzaContentEncryptionProfile { func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement + /// - Note: The implementation may modify the provided message before invoking the completion handler, for example to assign some stanza identifier for later processing. func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) + /// - Note: + /// The implementation may modify the provided message, for example to assign some stanza identifier for later processing. + /// However, in order to maintain message processing pipeline consistency, any modifications have to be performed before returning from the method. func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool } From 7f15c3cd11d084f31e62af76f30c9ddbf866a68a Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 11:00:11 +0200 Subject: [PATCH 12/62] Fix type of elements returned from envelope creation helper --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index dd85642bb4..ba1fbd5912 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -10,8 +10,8 @@ import XMPPFramework #endif extension XMLElement { - public static func makeStanzaContentEncryptionEnvelope() -> XMPPElement { - XMPPElement(name: "envelope", xmlns: "urn:xmpp:sce:1") + public static func makeStanzaContentEncryptionEnvelope() -> XMLElement { + XMLElement(name: "envelope", xmlns: "urn:xmpp:sce:1") } public var isStanzaContentEncryptionEnvelope: Bool { From 811088afed35bf3bbb35759c94b5e5aab15cc855 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 12:18:36 +0200 Subject: [PATCH 13/62] Apply review feedback: fix sensitive element classification bug --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 02e453dc49..5153042304 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -164,6 +164,6 @@ private extension XMLElement { var isSensitive: Bool { // The specification does enforce any specific format for encrypted content elements which are not considered sensitive themselves // This implementation allows any element named "encrypted" regardless of namespace - !isServerProcessed || name == "encrypted" + !isServerProcessed && name != "encrypted" } } From ca4fdd7c7ba394189a9100a99aeee970484ef1a2 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 12:19:39 +0200 Subject: [PATCH 14/62] Apply review feedback: require exactly one element to be present when verifying standard affixes --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index ba1fbd5912..48c83a5428 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -63,38 +63,45 @@ extension XMLElement { // Receiving clients MUST check whether the difference between the timestamp and the sending time derived from the stanza itself lays within a reasonable margin. // The client SHOULD use the content of the timestamp element when displaying the send date of the message public func verifyStanzaContentEncryptionTimestamp(expecting expectedDate: Date, withMargin verificationMargin: TimeInterval = 10) -> Bool { - for affix in elements(forName: "time") { + verifyStanzaContentEncryptionAffix(named: "time") { affix in guard let stamp = affix.attributeStringValue(forName: "stamp"), - let actualDate = Date.from(xmppDateTimeString: stamp), - abs(actualDate.timeIntervalSince(expectedDate)) <= verificationMargin else { + let actualDate = Date.from(xmppDateTimeString: stamp) else { return false } + return abs(actualDate.timeIntervalSince(expectedDate)) <= verificationMargin } - return true } // Receiving clients MUST check if the JID matches the to attribute of the enclosing stanza and otherwise alert the user/reject the message public func verifyStanzaContentEncryptionRecipient(expecting expectedJID: XMPPJID) -> Bool { - for affix in elements(forName: "to") { + verifyStanzaContentEncryptionAffix(named: "to") { affix in guard let jid = affix.attributeStringValue(forName: "jid"), - let actualJID = XMPPJID(string: jid), - actualJID.isEqual(to: expectedJID, options: .bare) else { + let actualJID = XMPPJID(string: jid) else { return false } + return actualJID.isEqual(to: expectedJID, options: .bare) } - return true } // Receiving clients MUST check if the value matches the from attribute of the enclosing stanza and otherwise alert the user/reject the message public func verifyStanzaContentEncryptionSender(expecting expectedJID: XMPPJID) -> Bool { - for affix in elements(forName: "from") { + verifyStanzaContentEncryptionAffix(named: "from") { affix in guard let jid = affix.attributeStringValue(forName: "jid"), - let actualJID = XMPPJID(string: jid), - actualJID.isEqual(to: expectedJID, options: .bare) else { + let actualJID = XMPPJID(string: jid) else { return false } + return actualJID.isEqual(to: expectedJID, options: .bare) } - return true + } + + private func verifyStanzaContentEncryptionAffix(named affixName: String, _ isValid: (_ affix: XMLElement) -> Bool) -> Bool { + // XML schema for the extension is undefined as of specification version 0.4.1 + // This implementation requires each verified affix element to appear exactly once in an envelope + let matchingAffixes = elements(forName: affixName) + guard matchingAffixes.count == 1, let affix = matchingAffixes.first else { + return false + } + return isValid(affix) } } From e608d7f09daef2cfc078558812abf4aa5e663843 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 12:20:32 +0200 Subject: [PATCH 15/62] Apply review feedback: do not require synchronous module code invocation --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 5153042304..033aace800 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -37,7 +37,7 @@ public class XMPPStanzaContentEncryption: XMPPModule { } public func sendEncryptedMessage(withSensitiveContent sensitiveContent: [XMLElement], to: XMPPJID, messageType: XMPPMessage.MessageType? = nil, elementId: String? = nil) { - performBlock { + performBlock(async: true) { let outgoingMessage = XMPPMessage(messageType: messageType, to: to, elementID: elementId) // TODO: Allow modifying sensitiveContent via multidelegation @@ -69,7 +69,7 @@ public class XMPPStanzaContentEncryption: XMPPModule { outgoingMessage.addStorageHint(.store) // The message can then be sent to the recipient. - self.performBlock { self.xmppStream?.send(outgoingMessage) } + self.performBlock(async: true) { self.xmppStream?.send(outgoingMessage) } } } } From f564ec6e7e94073b77158259abc4fe421d9d3806 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 12:22:24 +0200 Subject: [PATCH 16/62] Apply review feedback: API improvements --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 17 ++++------- .../XMPPStanzaContentEncryption.swift | 30 +++++++++---------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index 48c83a5428..4ac10b3582 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -17,24 +17,13 @@ extension XMLElement { public var isStanzaContentEncryptionEnvelope: Bool { name == "envelope" && xmlns == "urn:xmpp:sce:1" } - - public func withStanzaContentEncryptionEnvelopeContent(_ body: (_ content: XMLElement) throws -> T) rethrows -> T { - let content: XMLElement - if let existingContent = element(forName: "content") { - content = existingContent - } else { - content = XMLElement(name: "content") - addChild(content) - } - return try body(content) - } } // In order to prevent certain attacks, different affix elements MAY be added as direct child elements of the element. extension XMLElement { // Prevent known ciphertext and message length correlation attacks. public func addStanzaContentEncryptionRandomPaddingAffix() { - addChild(XMLElement(name: "rpad", stringValue: String(StanzaContentEncryptionPaddingGenerator()))) + addChild(XMLElement(name: "rpad", stringValue: StanzaContentEncryptionPaddingGenerator.randomPadding())) } // Prevent replay attacks using old messages. @@ -106,6 +95,10 @@ extension XMLElement { } private struct StanzaContentEncryptionPaddingGenerator: Sequence, IteratorProtocol { + static func randomPadding() -> String { + String(StanzaContentEncryptionPaddingGenerator()) + } + private static let alphabet: [Character] = { let printableASCIICharacterCodes = 33...126 let xmlUnsafeCharacters = CharacterSet(charactersIn: #""'<>&"#) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 033aace800..8d920b2e6b 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -21,8 +21,8 @@ public protocol XMPPStanzaContentEncryptionProfile { } @objc public protocol XMPPStanzaContentEncryptionDelegate: NSObjectProtocol { - @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEnvelope: XMLElement, in message: XMPPMessage) - @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToReceiveEnvelopeIn message: XMPPMessage) + @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope: XMLElement, from message: XMPPMessage) + @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToDecryptEnvelopeFrom message: XMPPMessage) } extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} @@ -44,16 +44,16 @@ public class XMPPStanzaContentEncryption: XMPPModule { // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. let envelope = XMPPElement.makeStanzaContentEncryptionEnvelope() - envelope.withStanzaContentEncryptionEnvelopeContent { content in - for sensitiveElement in sensitiveContent { - guard sensitiveElement.name != nil, sensitiveElement.xmlns != nil else { - // Elements in the element MUST be identified using an element name and namespace. - assertionFailure("Encountered element without name or namespace in element") - continue - } - content.addChild(sensitiveElement) + let content = XMLElement(name: "content") + for sensitiveElement in sensitiveContent { + guard sensitiveElement.name != nil, sensitiveElement.xmlns != nil else { + // Elements in the element MUST be identified using an element name and namespace. + assertionFailure("Encountered element without name or namespace in element") + continue } + content.addChild(sensitiveElement) } + envelope.addChild(content) // Depending on the encryption-specific SCE-profile, some affix elements are added as child elements of the element. let finalEnvelope = self.profile.addAffixElemenets(to: envelope, for: outgoingMessage) @@ -91,11 +91,11 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { // The result is the element containing the element and the affix elements as direct child elements. self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption?(self, didReceiveEnvelope: decryptedEnvelope, in: message) + multicast.stanzaContentEncryption?(self, didDecryptEnvelope: decryptedEnvelope, from: message) } } else { self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption?(self, didFailToReceiveEnvelopeIn: message) + multicast.stanzaContentEncryption?(self, didFailToDecryptEnvelopeFrom: message) } } } @@ -118,9 +118,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } // Afterwards, the extension elements inside the element are checked against the permitted list and any disallowed elements are discarded. - decryptedEnvelope.withStanzaContentEncryptionEnvelopeContent { content in - content.removeElementsDisallowedinStanzaContentEncryptionEnvelope() - } + decryptedEnvelope.element(forName: "content")?.removeElementsDisallowedInStanzaContentEncryptionEnvelope() // The following is not implemented as it contradicts section 11. Implementation Notes, which calls to handle encrypted elements explicitly: // As a last step, the original unencrypted stanza is recreated by replacing the element of the stanza with the elements inside of the element. @@ -130,7 +128,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } private extension XMLElement { - func removeElementsDisallowedinStanzaContentEncryptionEnvelope() { + func removeElementsDisallowedInStanzaContentEncryptionEnvelope() { // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it removeAllElements(where: { $0.isServerProcessed }) } From 2d66918b11a8e999d01cb6a096a9f79996be885e Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 12:23:57 +0200 Subject: [PATCH 17/62] Apply review feedback: add comments with specification links --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 1 + Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index 4ac10b3582..8e2ba53aa9 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -10,6 +10,7 @@ import XMPPFramework #endif extension XMLElement { + // https://xmpp.org/extensions/xep-0420.html#example-5 public static func makeStanzaContentEncryptionEnvelope() -> XMLElement { XMLElement(name: "envelope", xmlns: "urn:xmpp:sce:1") } diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 8d920b2e6b..977dbc2aa4 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -36,6 +36,7 @@ public class XMPPStanzaContentEncryption: XMPPModule { super.init(dispatchQueue: dispatchQueue) } + // https://xmpp.org/extensions/xep-0420.html#sending public func sendEncryptedMessage(withSensitiveContent sensitiveContent: [XMLElement], to: XMPPJID, messageType: XMPPMessage.MessageType? = nil, elementId: String? = nil) { performBlock(async: true) { let outgoingMessage = XMPPMessage(messageType: messageType, to: to, elementID: elementId) @@ -106,6 +107,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { return message } + // https://xmpp.org/extensions/xep-0420.html#receiving private func receiveEncryptedMessage(_ encryptedMessage: XMPPMessage, withEnvelopeXML envelopeXML: String) -> XMLElement? { // The recipient MUST verify that the decrypted element contains valid XML before processing it any further. Invalid XML must be rejected. guard let decryptedEnvelope = try? XMLElement(xmlString: envelopeXML), decryptedEnvelope.isStanzaContentEncryptionEnvelope else { From a98f36464f2bbe09230b6c145b40253fb3cc7a8a Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 24 Jul 2025 14:20:58 +0200 Subject: [PATCH 18/62] Apply review feedback: refactor affix verification implementation --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 39 +++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index 8e2ba53aa9..71d860ce87 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -53,45 +53,42 @@ extension XMLElement { // Receiving clients MUST check whether the difference between the timestamp and the sending time derived from the stanza itself lays within a reasonable margin. // The client SHOULD use the content of the timestamp element when displaying the send date of the message public func verifyStanzaContentEncryptionTimestamp(expecting expectedDate: Date, withMargin verificationMargin: TimeInterval = 10) -> Bool { - verifyStanzaContentEncryptionAffix(named: "time") { affix in - guard let stamp = affix.attributeStringValue(forName: "stamp"), - let actualDate = Date.from(xmppDateTimeString: stamp) else { - return false - } - return abs(actualDate.timeIntervalSince(expectedDate)) <= verificationMargin + guard let affix = stanzaContentEncryptionAffix(named: "time"), + let stamp = affix.attributeStringValue(forName: "stamp"), + let actualDate = Date.from(xmppDateTimeString: stamp) else { + return false } + return abs(actualDate.timeIntervalSince(expectedDate)) <= verificationMargin } // Receiving clients MUST check if the JID matches the to attribute of the enclosing stanza and otherwise alert the user/reject the message public func verifyStanzaContentEncryptionRecipient(expecting expectedJID: XMPPJID) -> Bool { - verifyStanzaContentEncryptionAffix(named: "to") { affix in - guard let jid = affix.attributeStringValue(forName: "jid"), - let actualJID = XMPPJID(string: jid) else { - return false - } - return actualJID.isEqual(to: expectedJID, options: .bare) + guard let affix = stanzaContentEncryptionAffix(named: "to"), + let jid = affix.attributeStringValue(forName: "jid"), + let actualJID = XMPPJID(string: jid) else { + return false } + return actualJID.isEqual(to: expectedJID, options: .bare) } // Receiving clients MUST check if the value matches the from attribute of the enclosing stanza and otherwise alert the user/reject the message public func verifyStanzaContentEncryptionSender(expecting expectedJID: XMPPJID) -> Bool { - verifyStanzaContentEncryptionAffix(named: "from") { affix in - guard let jid = affix.attributeStringValue(forName: "jid"), - let actualJID = XMPPJID(string: jid) else { - return false - } - return actualJID.isEqual(to: expectedJID, options: .bare) + guard let affix = stanzaContentEncryptionAffix(named: "from"), + let jid = affix.attributeStringValue(forName: "jid"), + let actualJID = XMPPJID(string: jid) else { + return false } + return actualJID.isEqual(to: expectedJID, options: .bare) } - private func verifyStanzaContentEncryptionAffix(named affixName: String, _ isValid: (_ affix: XMLElement) -> Bool) -> Bool { + private func stanzaContentEncryptionAffix(named affixName: String) -> XMLElement? { // XML schema for the extension is undefined as of specification version 0.4.1 // This implementation requires each verified affix element to appear exactly once in an envelope let matchingAffixes = elements(forName: affixName) guard matchingAffixes.count == 1, let affix = matchingAffixes.first else { - return false + return nil } - return isValid(affix) + return affix } } From a7a1db0b49e984d85f1ea1ffedfd15e01429a874 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 25 Jul 2025 08:13:57 +0200 Subject: [PATCH 19/62] Adjust message sending API to be more flexible --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 977dbc2aa4..3daa1e78d5 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -37,10 +37,8 @@ public class XMPPStanzaContentEncryption: XMPPModule { } // https://xmpp.org/extensions/xep-0420.html#sending - public func sendEncryptedMessage(withSensitiveContent sensitiveContent: [XMLElement], to: XMPPJID, messageType: XMPPMessage.MessageType? = nil, elementId: String? = nil) { + public func sendEncryptedMessage(_ message: XMPPMessage, withSensitiveContent sensitiveContent: [XMLElement]) { performBlock(async: true) { - let outgoingMessage = XMPPMessage(messageType: messageType, to: to, elementID: elementId) - // TODO: Allow modifying sensitiveContent via multidelegation // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. @@ -57,20 +55,20 @@ public class XMPPStanzaContentEncryption: XMPPModule { envelope.addChild(content) // Depending on the encryption-specific SCE-profile, some affix elements are added as child elements of the element. - let finalEnvelope = self.profile.addAffixElemenets(to: envelope, for: outgoingMessage) + let finalEnvelope = self.profile.addAffixElemenets(to: envelope, for: message) // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. - self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: outgoingMessage) { encrypted in + self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: message) { encrypted in guard let encrypted else { return } // The result is appended to the message. - outgoingMessage.addChild(encrypted) + message.addChild(encrypted) // Since the outer message element does not contain a element the sender appends an unencrypted hint as specified in Message Processing Hints (XEP-0334) [7]. - outgoingMessage.addStorageHint(.store) + message.addStorageHint(.store) // The message can then be sent to the recipient. - self.performBlock(async: true) { self.xmppStream?.send(outgoingMessage) } + self.performBlock(async: true) { self.xmppStream?.send(message) } } } } From cded6375bdbc9ae3fa39cbcdfbfb8c2b85682f13 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Mon, 28 Jul 2025 13:30:35 +0200 Subject: [PATCH 20/62] Make server-processed element list customisable --- .../XMPPStanzaContentEncryption.swift | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 3daa1e78d5..6d856f9dd1 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -30,6 +30,12 @@ extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} /// A module implementing XMPP stanza content encryption specification as defined in [XEP-0420 version 0.4.1](https://xmpp.org/extensions/attic/xep-0420-0.4.1.html). public class XMPPStanzaContentEncryption: XMPPModule { private let profile: XMPPStanzaContentEncryptionProfile + private var serverProcessedElements = XMPPStanzaContentEncryptionServerProcessedElements() + + public var serverProcessedElementsList: [XMPPStanzaContentEncryptionServerProcessedElements.Entry] { + get { serverProcessedElements.list } + set { serverProcessedElements.list = newValue } + } public init(profile: XMPPStanzaContentEncryptionProfile, dispatchQueue: DispatchQueue? = nil) { self.profile = profile @@ -100,7 +106,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } } - message.removeElementsIgnoredOutsideStanzaContentEncryptionEnvelope() + serverProcessedElements.removeIgnoredElementsOutsideEnvelope(fromMessage: message) return message } @@ -118,7 +124,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } // Afterwards, the extension elements inside the element are checked against the permitted list and any disallowed elements are discarded. - decryptedEnvelope.element(forName: "content")?.removeElementsDisallowedInStanzaContentEncryptionEnvelope() + serverProcessedElements.removeDisallowedElements(fromEnvelope: decryptedEnvelope) // The following is not implemented as it contradicts section 11. Implementation Notes, which calls to handle encrypted elements explicitly: // As a last step, the original unencrypted stanza is recreated by replacing the element of the stanza with the elements inside of the element. @@ -127,41 +133,57 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } } -private extension XMLElement { - func removeElementsDisallowedInStanzaContentEncryptionEnvelope() { +public struct XMPPStanzaContentEncryptionServerProcessedElements { + public struct Entry { + public let xmlns: String + public let elementNames: [String] + + public init(xmlns: String, elementNames: [String] = []) { + self.xmlns = xmlns + self.elementNames = elementNames + } + } + + var list = [ + // Message Processing Hints are addressed to the server and MUST therefore be accessible in plaintext. + Entry(xmlns: "urn:xmpp:hints"), + // Sending clients MUST NOT include Stanza-ID elements inside the element, as this would prevent the server from filtering it. + Entry(xmlns: XMPPStanzaIdXmlns, elementNames: [XMPPStanzaIdElementName, XMPPOriginIdElementName]), + // The server MUST be able to access the and
elements in order to do message routing, so they MUST NOT be encrypted. + Entry(xmlns: "http://jabber.org/protocol/address"), + ] + + func removeDisallowedElements(fromEnvelope envelope: XMLElement) { // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it - removeAllElements(where: { $0.isServerProcessed }) + envelope.element(forName: "content")?.removeAllElements(where: { isServerProcessed($0) }) } - func removeElementsIgnoredOutsideStanzaContentEncryptionEnvelope() { + func removeIgnoredElementsOutsideEnvelope(fromMessage message: XMPPMessage) { // Furthermore the receiving client MUST ignore any extension elements considered as sensitive which are found outside of the element, especially as direct unencrypted child elements of the enclosing stanza. - removeAllElements(where: { $0.isSensitive }) + message.removeAllElements(where: { isSensitive($0) }) } - private func removeAllElements(where shouldBeRemoved: (XMLElement) -> Bool) { - guard let childrenIndices = children?.indices else { return } - for childIndex in childrenIndices.reversed() { - guard let element = child(at: UInt(childIndex)) as? XMLElement, shouldBeRemoved(element) else { continue } - removeChild(at: UInt(childIndex)) - } - } -} - -private extension XMLElement { // There are certain extension elements which are required to be available to the server in order to do message routing and processing // Additionally there are some elements that MUST be filtered by the server. // Allowing for those elements to be included in, and parsed from the encrypted payload would allow a malicious client to perform a number of attacks. - var isServerProcessed: Bool { - ["urn:xmpp:hints", // Message Processing Hints are addressed to the server and MUST therefore be accessible in plaintext. - XMPPStanzaIdXmlns, // Sending clients MUST NOT include Stanza-ID elements inside the element, as this would prevent the server from filtering it. - "http://jabber.org/protocol/address", // The server MUST be able to access the and
elements in order to do message routing, so they MUST NOT be encrypted. - ].contains(xmlns) + private func isServerProcessed(_ element: XMLElement) -> Bool { + list.contains(where: { $0.xmlns == element.xmlns && ($0.elementNames.contains(where: { $0 == element.name }) || $0.elementNames.isEmpty) }) } // Contrary to this, other elements are considered sensitive and MUST NOT be available in plaintext outside the element. - var isSensitive: Bool { + private func isSensitive(_ element: XMLElement) -> Bool { // The specification does enforce any specific format for encrypted content elements which are not considered sensitive themselves // This implementation allows any element named "encrypted" regardless of namespace - !isServerProcessed && name != "encrypted" + !isServerProcessed(element) && element.name != "encrypted" + } +} + +private extension XMLElement { + func removeAllElements(where shouldBeRemoved: (XMLElement) -> Bool) { + guard let childrenIndices = children?.indices else { return } + for childIndex in childrenIndices.reversed() { + guard let element = child(at: UInt(childIndex)) as? XMLElement, shouldBeRemoved(element) else { continue } + removeChild(at: UInt(childIndex)) + } } } From 29425ab0e61082fe5bfeb74764077a1759e175d7 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Tue, 19 Aug 2025 08:40:20 +0200 Subject: [PATCH 21/62] Fix module delegate invocations --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 6d856f9dd1..b47f914aad 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -21,7 +21,7 @@ public protocol XMPPStanzaContentEncryptionProfile { } @objc public protocol XMPPStanzaContentEncryptionDelegate: NSObjectProtocol { - @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope: XMLElement, from message: XMPPMessage) + @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope decryptedEnvelope: XMLElement, from message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToDecryptEnvelopeFrom message: XMPPMessage) } @@ -96,11 +96,11 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { // The result is the element containing the element and the affix elements as direct child elements. self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption?(self, didDecryptEnvelope: decryptedEnvelope, from: message) + multicast.stanzaContentEncryption!(self, didDecryptEnvelope: decryptedEnvelope, from: message) } } else { self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption?(self, didFailToDecryptEnvelopeFrom: message) + multicast.stanzaContentEncryption!(self, didFailToDecryptEnvelopeFrom: message) } } } From b4582b0e412ceddc72686fae9729587023054531 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 29 Aug 2025 11:42:01 +0200 Subject: [PATCH 22/62] Include stanza errors in default list of server processed elements --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index b47f914aad..b43a63626b 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -151,6 +151,8 @@ public struct XMPPStanzaContentEncryptionServerProcessedElements { Entry(xmlns: XMPPStanzaIdXmlns, elementNames: [XMPPStanzaIdElementName, XMPPOriginIdElementName]), // The server MUST be able to access the and
elements in order to do message routing, so they MUST NOT be encrypted. Entry(xmlns: "http://jabber.org/protocol/address"), + // The server needs to be able to provide stanza error information + Entry(xmlns: "jabber:client", elementNames: ["error"]), Entry(xmlns: "jabber:server", elementNames: ["error"]), ] func removeDisallowedElements(fromEnvelope envelope: XMLElement) { From e5fb01a358505ed07fbc7e64c5584af996e44c1b Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 10 Sep 2025 09:31:55 +0200 Subject: [PATCH 23/62] Modernize iq stanza for publishing bundle for device --- Extensions/OMEMO/NSXMLElement+OMEMO.h | 8 +- Extensions/OMEMO/NSXMLElement+OMEMO.m | 8 +- Extensions/OMEMO/OMEMOModule.h | 2 +- Extensions/OMEMO/OMEMOModule.m | 2 +- Extensions/OMEMO/XMPPIQ+OMEMO.m | 164 +++++++++++--------------- 5 files changed, 81 insertions(+), 103 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.h b/Extensions/OMEMO/NSXMLElement+OMEMO.h index 3bb2afac1d..2a693abff8 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.h +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.h @@ -17,11 +17,11 @@ NS_ASSUME_NONNULL_BEGIN @interface NSXMLElement (OMEMO) -/** If element contains */ +/** If element contains */ - (BOOL) omemo_hasEncryptedElement:(OMEMOModuleNamespace)ns; -/** If element IS */ +/** If element IS */ - (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns; -/** Child element */ +/** Child element */ - (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns; @@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN /** * The client may wish to transmit keying material to the contact. This first has to be generated. The client MUST generate a fresh, randomly generated key/IV pair. For each intended recipient device, i.e. both own devices as well as devices associated with the contact, this key is encrypted using the corresponding long-standing axolotl session. Each encrypted payload key is tagged with the recipient device's ID. This is all serialized into a KeyTransportElement, omitting the . - +
BASE64ENCODED... BASE64ENCODED... diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 84e4ea7f30..d6eea5c007 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -16,17 +16,17 @@ @implementation NSXMLElement (OMEMO) -/** If element contains */ +/** If element contains */ - (BOOL) omemo_hasEncryptedElement:(OMEMOModuleNamespace)ns { return [self omemo_encryptedElement:ns] != nil; } -/** If element IS */ +/** If element IS */ - (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns { return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO:ns]]; } -/** Child element */ +/** Child element */ - (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; } @@ -78,7 +78,7 @@ - (nullable NSData*) omemo_iv { /** * The client may wish to transmit keying material to the contact. This first has to be generated. The client MUST generate a fresh, randomly generated key/IV pair. For each intended recipient device, i.e. both own devices as well as devices associated with the contact, this key is encrypted using the corresponding long-standing axolotl session. Each encrypted payload key is tagged with the recipient device's ID. This is all serialized into a KeyTransportElement, omitting the as follows: - +
BASE64ENCODED... BASE64ENCODED... diff --git a/Extensions/OMEMO/OMEMOModule.h b/Extensions/OMEMO/OMEMOModule.h index 18376a90c5..4486150b47 100644 --- a/Extensions/OMEMO/OMEMOModule.h +++ b/Extensions/OMEMO/OMEMOModule.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { /** Uses "eu.siacs.conversations.axolotl" namespace and compatible with the latest Conversations and ChatSecure versions as of Feb 8, 2017. */ OMEMOModuleNamespaceConversationsLegacy, - /** Uses "urn:xmpp:omemo:0" namespace. XEP is still experimental. Do not use in production yet as it may change! See https://xmpp.org/extensions/xep-0384.html */ + /** Uses "urn:xmpp:omemo:2" namespace. XEP is still experimental. Do not use in production yet as it may change! See https://xmpp.org/extensions/xep-0384.html */ OMEMOModuleNamespaceOMEMO }; diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 6bab077676..6da4034954 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -303,7 +303,7 @@ - (void) receiveMessage:(XMPPMessage*)message forJID:(XMPPJID*)forJID isIncoming + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns { if (ns == OMEMOModuleNamespaceOMEMO) { - return @"urn:xmpp:omemo:0"; + return @"urn:xmpp:omemo:2"; } else { // OMEMOModuleNamespaceConversationsLegacy return @"eu.siacs.conversations.axolotl"; } diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index 2288bc3eb0..9c6c3a2fdc 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -117,127 +117,105 @@ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(n return iq; } -/** iq stanza for publishing bundle for device +/** iq stanza for publishing bundle for device - - - - - - - BASE64ENCODED... - - - BASE64ENCODED... - - - BASE64ENCODED... - - - - BASE64ENCODED... - - - BASE64ENCODED... - - - BASE64ENCODED... - - - - - - - - - - http://jabber.org/protocol/pubsub#publish-options - - - 1 - - - open - - - - - + https://xmpp.org/extensions/xep-0384.html#example-3 + + + + + + + b64/encoded/data + b64/encoded/data + b64/encoded/data + + b64/encoded/data + b64/encoded/data + + b64/encoded/data + + + + + + + + http://jabber.org/protocol/pubsub#publish-options + + + max + + + + + */ + (XMPPIQ*) omemo_iqPublishBundle:(OMEMOBundle*)bundle elementId:(nullable NSString*)elementId xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { - NSXMLElement *signedPreKeyElement = nil; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [iq addChild:pubsub]; + + NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; + NSString *nodeName = [OMEMOModule xmlnsOMEMOBundles:xmlNamespace]; + [publish addAttributeWithName:@"node" stringValue:nodeName]; + [pubsub addChild:publish]; + + NSXMLElement *itemElement = [NSXMLElement elementWithName:@"item"]; + NSString *deviceId = [NSString stringWithFormat:@"%u", bundle.deviceId]; + [itemElement addAttributeWithName:@"id" stringValue:deviceId]; + [publish addChild:itemElement]; + + NSXMLElement *bundleElement = [XMPPElement elementWithName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO:xmlNamespace]]; + [itemElement addChild:bundleElement]; + if (bundle.signedPreKey.publicKey) { - signedPreKeyElement = [NSXMLElement elementWithName:@"signedPreKeyPublic" stringValue:[bundle.signedPreKey.publicKey base64EncodedStringWithOptions:0]]; - [signedPreKeyElement addAttributeWithName:@"signedPreKeyId" unsignedIntegerValue:bundle.signedPreKey.preKeyId]; + NSXMLElement *signedPreKeyElement = [NSXMLElement elementWithName:@"spk" stringValue:[bundle.signedPreKey.publicKey base64EncodedStringWithOptions:0]]; + [signedPreKeyElement addAttributeWithName:@"id" unsignedIntegerValue:bundle.signedPreKey.preKeyId]; + [bundleElement addChild:signedPreKeyElement]; } - NSXMLElement *signedPreKeySignatureElement = nil; + if (bundle.signedPreKey.signature) { - signedPreKeySignatureElement = [NSXMLElement elementWithName:@"signedPreKeySignature" stringValue:[bundle.signedPreKey.signature base64EncodedStringWithOptions:0]]; + NSXMLElement *signedPreKeySignatureElement = [NSXMLElement elementWithName:@"spks" stringValue:[bundle.signedPreKey.signature base64EncodedStringWithOptions:0]]; + [bundleElement addChild:signedPreKeySignatureElement]; } - NSXMLElement *identityKeyElement = nil; + if (bundle.identityKey) { - identityKeyElement = [NSXMLElement elementWithName:@"identityKey" stringValue:[bundle.identityKey base64EncodedStringWithOptions:0]]; + NSXMLElement *identityKeyElement = [NSXMLElement elementWithName:@"ik" stringValue:[bundle.identityKey base64EncodedStringWithOptions:0]]; + [bundleElement addChild:identityKeyElement]; } + NSXMLElement *preKeysElement = [NSXMLElement elementWithName:@"prekeys"]; + [bundleElement addChild:preKeysElement]; + [bundle.preKeys enumerateObjectsUsingBlock:^(OMEMOPreKey * _Nonnull preKey, NSUInteger idx, BOOL * _Nonnull stop) { - NSXMLElement *preKeyElement = [NSXMLElement elementWithName:@"preKeyPublic" stringValue:[preKey.publicKey base64EncodedStringWithOptions:0]]; - [preKeyElement addAttributeWithName:@"preKeyId" unsignedIntegerValue:preKey.preKeyId]; + NSXMLElement *preKeyElement = [NSXMLElement elementWithName:@"pk" stringValue:[preKey.publicKey base64EncodedStringWithOptions:0]]; + [preKeyElement addAttributeWithName:@"id" unsignedIntegerValue:preKey.preKeyId]; [preKeysElement addChild:preKeyElement]; }]; - NSXMLElement *bundleElement = [XMPPElement elementWithName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO:xmlNamespace]]; - if (signedPreKeyElement) { - [bundleElement addChild:signedPreKeyElement]; - } - if (signedPreKeySignatureElement) { - [bundleElement addChild:signedPreKeySignatureElement]; - } - if (identityKeyElement) { - [bundleElement addChild:identityKeyElement]; - } - [bundleElement addChild:preKeysElement]; - NSXMLElement *itemElement = [NSXMLElement elementWithName:@"item"]; - [itemElement addChild:bundleElement]; - - NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; - NSString *nodeName = [OMEMOModule xmlnsOMEMOBundles:xmlNamespace deviceId:bundle.deviceId]; - [publish addAttributeWithName:@"node" stringValue:nodeName]; - [publish addChild:itemElement]; - NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; - [pubsub addChild:publish]; + NSXMLElement *publishOptions = [NSXMLElement elementWithName:@"publish-options"]; + [pubsub addChild:publishOptions]; NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; [x addAttributeWithName:@"type" stringValue:@"submit"]; + [publishOptions addChild:x]; NSXMLElement *formTypeField = [NSXMLElement elementWithName:@"field"]; [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"]; [formTypeField addAttributeWithName:@"type" stringValue:@"hidden"]; [formTypeField addChild:[NSXMLElement elementWithName:@"value" stringValue:XMLNS_PUBSUB_PUBLISH_OPTIONS]]; - [x addChild:formTypeField]; - NSXMLElement *persistanceField = [NSXMLElement elementWithName:@"field"]; - [persistanceField addAttributeWithName:@"var" stringValue:@"pubsub#persist_items"]; - [persistanceField addChild:[NSXMLElement elementWithName:@"value" objectValue:@"1"]]; - - [x addChild:persistanceField]; - - NSXMLElement *accessModelField = [NSXMLElement elementWithName:@"field"]; - [accessModelField addAttributeWithName:@"var" stringValue:@"pubsub#access_model"]; - [accessModelField addChild:[NSXMLElement elementWithName:@"value" objectValue:@"open"]]; - - [x addChild:accessModelField]; - - NSXMLElement *publishOptions = [NSXMLElement elementWithName:@"publish-options"]; - [publishOptions addChild:x]; + NSXMLElement *maxItemsField = [NSXMLElement elementWithName:@"field"]; + [maxItemsField addAttributeWithName:@"var" stringValue:@"pubsub#max_items"]; + [maxItemsField addChild:[NSXMLElement elementWithName:@"value" stringValue:@"max"]]; + [x addChild:maxItemsField]; - [pubsub addChild:publishOptions]; - - XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; - [iq addChild:pubsub]; return iq; } From 13ea0668b1ff171c200f6f19c7376d44331f9275 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 10 Sep 2025 09:52:56 +0200 Subject: [PATCH 24/62] Modernize iq stanza for manually fetching deviceIds list --- Extensions/OMEMO/OMEMOModule.m | 4 ++-- Extensions/OMEMO/XMPPIQ+OMEMO.m | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 6da4034954..226d10e974 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -311,9 +311,9 @@ + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns { + (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns { NSString *xmlns = [self xmlnsOMEMO:ns]; if (ns == OMEMOModuleNamespaceOMEMO) { - return [NSString stringWithFormat:@"%@:devicelist", xmlns]; + return [NSString stringWithFormat:@"%@:devices", xmlns]; } else { // OMEMOModuleNamespaceConversationsLegacy - return [NSString stringWithFormat:@"%@.devicelist", xmlns]; + return [NSString stringWithFormat:@"%@.devices", xmlns]; } } + (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns { diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index 9c6c3a2fdc..6bf071e413 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -18,11 +18,14 @@ @implementation XMPPIQ (OMEMO) /** - - - - - + https://xmpp.org/extensions/xep-0384.html#example-9 + + + + + + + */ + (XMPPIQ*) omemo_iqFetchDeviceIdsForJID:(XMPPJID*)jid elementId:(nullable NSString*)elementId From 858a4428477a297a291ba89cd94da4c95cc6c500 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 10 Sep 2025 11:04:07 +0200 Subject: [PATCH 25/62] Modernize iq stanza for publishing your device ids --- Extensions/OMEMO/XMPPIQ+OMEMO.m | 102 +++++++++++++++----------------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index 6bf071e413..6ef393bc59 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -41,82 +41,76 @@ + (XMPPIQ*) omemo_iqFetchDeviceIdsForJID:(XMPPJID*)jid } -/** - - - - - - - - - - - - - - - http://jabber.org/protocol/pubsub#publish-options - - - 1 - - - open - - - - - +/** + https://xmpp.org/extensions/xep-0384.html#example-2 + + + + + + + + + + + + + + + + http://jabber.org/protocol/pubsub#publish-options + + + open + + + + + + */ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(nullable NSString*)elementId xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { - NSXMLElement *listElement = [NSXMLElement elementWithName:@"list" xmlns:[OMEMOModule xmlnsOMEMO:xmlNamespace]]; - [deviceIds enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSXMLElement *device = [NSXMLElement elementWithName:@"device"]; - [device addAttributeWithName:@"id" numberValue:obj]; - [listElement addChild:device]; - }]; - NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; - [item addChild:listElement]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [iq addChild:pubsub]; NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; [publish addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMODeviceList:xmlNamespace]]; + [pubsub addChild:publish]; + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"id" stringValue:@"current"]; [publish addChild:item]; - NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; - [pubsub addChild:publish]; + NSXMLElement *devices = [NSXMLElement elementWithName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO:xmlNamespace]]; + [item addChild:devices]; + + [deviceIds enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + NSXMLElement *device = [NSXMLElement elementWithName:@"device"]; + [device addAttributeWithName:@"id" numberValue:obj]; + [devices addChild:device]; + }]; + + NSXMLElement *publishOptions = [NSXMLElement elementWithName:@"publish-options"]; + [pubsub addChild:publishOptions]; NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; [x addAttributeWithName:@"type" stringValue:@"submit"]; + [publishOptions addChild:x]; NSXMLElement *formTypeField = [NSXMLElement elementWithName:@"field"]; [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"]; [formTypeField addAttributeWithName:@"type" stringValue:@"hidden"]; [formTypeField addChild:[NSXMLElement elementWithName:@"value" stringValue:XMLNS_PUBSUB_PUBLISH_OPTIONS]]; - [x addChild:formTypeField]; - NSXMLElement *persistanceField = [NSXMLElement elementWithName:@"field"]; - [persistanceField addAttributeWithName:@"var" stringValue:@"pubsub#persist_items"]; - [persistanceField addChild:[NSXMLElement elementWithName:@"value" objectValue:@"1"]]; - - [x addChild:persistanceField]; - NSXMLElement *accessModelField = [NSXMLElement elementWithName:@"field"]; [accessModelField addAttributeWithName:@"var" stringValue:@"pubsub#access_model"]; - [accessModelField addChild:[NSXMLElement elementWithName:@"value" objectValue:@"open"]]; - + [accessModelField addChild:[NSXMLElement elementWithName:@"value" stringValue:@"open"]]; [x addChild:accessModelField]; - NSXMLElement *publishOptions = [NSXMLElement elementWithName:@"publish-options"]; - [publishOptions addChild:x]; - - [pubsub addChild:publishOptions]; - - XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; - [iq addChild:pubsub]; - return iq; } From cdb3f51684461174682ca6422b2e1e86b397e730 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 10 Sep 2025 14:23:06 +0200 Subject: [PATCH 26/62] Add access model to iq stanza for publishing bundle for device --- Extensions/OMEMO/XMPPIQ+OMEMO.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index 6ef393bc59..3763503008 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -117,6 +117,7 @@ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(n /** iq stanza for publishing bundle for device https://xmpp.org/extensions/xep-0384.html#example-3 + https://xmpp.org/extensions/xep-0384.html#example-4 - open access model node copied over to example-3 listing @@ -143,6 +144,9 @@ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(n max + + open + @@ -213,6 +217,11 @@ + (XMPPIQ*) omemo_iqPublishBundle:(OMEMOBundle*)bundle [maxItemsField addChild:[NSXMLElement elementWithName:@"value" stringValue:@"max"]]; [x addChild:maxItemsField]; + NSXMLElement *accessModelField = [NSXMLElement elementWithName:@"field"]; + [accessModelField addAttributeWithName:@"var" stringValue:@"pubsub#access_model"]; + [accessModelField addChild:[NSXMLElement elementWithName:@"value" stringValue:@"open"]]; + [x addChild:accessModelField]; + return iq; } From 605c5aa7529eed6445a97480965298e7e839da2c Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Thu, 11 Sep 2025 13:30:31 +0200 Subject: [PATCH 27/62] Modernize parsing code for iq stanza that lists devices --- Extensions/OMEMO/NSXMLElement+OMEMO.m | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index d6eea5c007..91daa1b785 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -110,16 +110,16 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) } /* - - - - - - - - - - + + + + + + + + + + */ - (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns { @@ -130,7 +130,7 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) - (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList:ns]]) { - NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"list" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; + NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; if (devicesList) { NSArray *children = [devicesList children]; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:children.count]; From 00eaa6564151df5011cf8686c7351611e6ff477a Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Fri, 12 Sep 2025 13:09:39 +0200 Subject: [PATCH 28/62] Modernize iq stanza for fetching remote bundle --- Extensions/OMEMO/XMPPIQ+OMEMO.m | 45 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index 3763503008..7c218d1b3f 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -225,19 +225,6 @@ + (XMPPIQ*) omemo_iqPublishBundle:(OMEMOBundle*)bundle return iq; } - -+ (XMPPIQ *) omemo_iqFetchNode:(NSString *)node to:(XMPPJID *)toJID elementId:(nullable NSString*)elementId { - XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:toJID elementID:elementId]; - NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; - NSXMLElement *itemsElement = [NSXMLElement elementWithName:@"items"]; - [itemsElement addAttributeWithName:@"node" stringValue:node]; - - [pubsub addChild:itemsElement]; - [iq addChild:pubsub]; - - return iq; -} - + (XMPPIQ *) omemo_iqDeleteNode:(NSString *)node elementId:(nullable NSString *)elementId { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; @@ -253,21 +240,35 @@ + (XMPPIQ *) omemo_iqDeleteNode:(NSString *)node elementId:(nullable NSString *) * iq stanza for fetching remote bundle - - - - + from='romeo@montague.lit' + to='juliet@capulet.lit' + id='fetch1'> + + + + + + */ + (XMPPIQ*) omemo_iqFetchBundleForDeviceId:(uint32_t)deviceId jid:(XMPPJID*)jid elementId:(nullable NSString*)elementId xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { - NSString *nodeName = [OMEMOModule xmlnsOMEMOBundles:xmlNamespace deviceId:deviceId]; - return [self omemo_iqFetchNode:nodeName to:jid elementId:elementId]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:elementId]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [iq addChild:pubsub]; + + NSXMLElement *itemsElement = [NSXMLElement elementWithName:@"items"]; + [itemsElement addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMOBundles:xmlNamespace]]; + [pubsub addChild:itemsElement]; + + NSXMLElement *itemElement = [NSXMLElement elementWithName:@"item"]; + [itemElement addAttributeWithName:@"id" stringValue:[NSString stringWithFormat:@"%u", deviceId]]; + [itemsElement addChild:itemElement]; + + return iq; } From 3591a3af735431d675761893c3457f5592650a1d Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Fri, 12 Sep 2025 13:55:43 +0200 Subject: [PATCH 29/62] Modernize parsing code for iq stanza for bundle --- Extensions/OMEMO/XMPPIQ+OMEMO.m | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index 7c218d1b3f..a5cb739c65 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -275,47 +275,53 @@ + (XMPPIQ*) omemo_iqFetchBundleForDeviceId:(uint32_t)deviceId - (nullable OMEMOBundle*) omemo_bundle:(OMEMOModuleNamespace)ns { NSXMLElement *pubsub = [self elementForName:@"pubsub" xmlns:XMLNS_PUBSUB]; if (!pubsub) { return nil; } + NSXMLElement *items = [pubsub elementForName:@"items"]; // If !items, this is a bundle and used for testing if (!items) { items = [pubsub elementForName:@"publish"]; } if (!items) { return nil; } + NSString *node = [items attributeForName:@"node"].stringValue; if (!node) { return nil; } - if (![node containsString:[OMEMOModule xmlnsOMEMOBundles:ns]]) { + if (![node isEqualToString:[OMEMOModule xmlnsOMEMOBundles:ns]]) { return nil; } - NSString *separator = [[OMEMOModule xmlnsOMEMOBundles:ns] stringByAppendingString:@":"]; - NSArray *components = [node componentsSeparatedByString:separator]; - NSString *deviceIdString = [components lastObject]; - uint32_t deviceId = (uint32_t)[deviceIdString integerValue]; NSXMLElement *itemElement = [items elementForName:@"item"]; if (!itemElement) { return nil; } + NSString *deviceIdString = [itemElement attributeStringValueForName:@"id"]; + uint32_t deviceId = (uint32_t)[deviceIdString integerValue]; + NSXMLElement *bundleElement = [itemElement elementForName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; if (!bundleElement) { return nil; } - NSXMLElement *signedPreKeyElement = [bundleElement elementForName:@"signedPreKeyPublic"]; + + NSXMLElement *signedPreKeyElement = [bundleElement elementForName:@"spk"]; if (!signedPreKeyElement) { return nil; } - uint32_t signedPreKeyId = [signedPreKeyElement attributeUInt32ValueForName:@"signedPreKeyId"]; + uint32_t signedPreKeyId = [signedPreKeyElement attributeUInt32ValueForName:@"id"]; NSString *signedPreKeyPublicBase64 = [signedPreKeyElement stringValue]; if (!signedPreKeyPublicBase64) { return nil; } NSData *signedPreKeyPublic = [[NSData alloc] initWithBase64EncodedString:signedPreKeyPublicBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]; if (!signedPreKeyPublic) { return nil; } - NSString *signedPreKeySignatureBase64 = [[bundleElement elementForName:@"signedPreKeySignature"] stringValue]; + + NSString *signedPreKeySignatureBase64 = [[bundleElement elementForName:@"spks"] stringValue]; if (!signedPreKeySignatureBase64) { return nil; } NSData *signedPreKeySignature = [[NSData alloc] initWithBase64EncodedString:signedPreKeySignatureBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]; if (!signedPreKeySignature) { return nil; } - NSString *identityKeyBase64 = [[bundleElement elementForName:@"identityKey"] stringValue]; + + NSString *identityKeyBase64 = [[bundleElement elementForName:@"ik"] stringValue]; if (!identityKeyBase64) { return nil; } NSData *identityKey = [[NSData alloc] initWithBase64EncodedString:identityKeyBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]; if (!identityKey) { return nil; } + NSXMLElement *preKeysElement = [bundleElement elementForName:@"prekeys"]; if (!preKeysElement) { return nil; } - NSArray *preKeyElements = [preKeysElement elementsForName:@"preKeyPublic"]; + + NSArray *preKeyElements = [preKeysElement elementsForName:@"pk"]; NSMutableArray *preKeys = [NSMutableArray arrayWithCapacity:preKeyElements.count]; [preKeyElements enumerateObjectsUsingBlock:^(NSXMLElement * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - uint32_t preKeyId = [obj attributeUInt32ValueForName:@"preKeyId"]; + uint32_t preKeyId = [obj attributeUInt32ValueForName:@"id"]; NSString *b64 = [obj stringValue]; NSData *data = nil; if (b64) { @@ -326,6 +332,7 @@ - (nullable OMEMOBundle*) omemo_bundle:(OMEMOModuleNamespace)ns { [preKeys addObject:preKey]; } }]; + OMEMOSignedPreKey *signedPreKey = [[OMEMOSignedPreKey alloc] initWithPreKeyId:signedPreKeyId publicKey:signedPreKeyPublic signature:signedPreKeySignature]; OMEMOBundle *bundle = [[OMEMOBundle alloc] initWithDeviceId:deviceId identityKey:identityKey signedPreKey:signedPreKey preKeys:preKeys]; return bundle; From e78dde65cc51efd8af1c724651fa2661bffa1065 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 23 Sep 2025 15:31:23 +0200 Subject: [PATCH 30/62] Apply feedback: remove namespace attribute from methods migrated to OMEMO 2 --- Extensions/OMEMO/NSXMLElement+OMEMO.h | 12 +++++------ Extensions/OMEMO/NSXMLElement+OMEMO.m | 24 ++++++++++----------- Extensions/OMEMO/OMEMOModule.h | 6 +++++- Extensions/OMEMO/OMEMOModule.m | 30 +++++++++++++++++++-------- Extensions/OMEMO/XMPPIQ+OMEMO.h | 14 +++++-------- Extensions/OMEMO/XMPPIQ+OMEMO.m | 29 ++++++++++++-------------- 6 files changed, 62 insertions(+), 53 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.h b/Extensions/OMEMO/NSXMLElement+OMEMO.h index 2a693abff8..59358c0cf2 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.h +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.h @@ -18,11 +18,11 @@ NS_ASSUME_NONNULL_BEGIN @interface NSXMLElement (OMEMO) /** If element contains */ -- (BOOL) omemo_hasEncryptedElement:(OMEMOModuleNamespace)ns; +- (BOOL) omemo_hasEncryptedElement; /** If element IS */ -- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns; +- (BOOL) omemo_isEncryptedElement; /** Child element */ -- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns; +- (nullable NSXMLElement*) omemo_encryptedElement; /** The Device ID is a randomly generated integer between 1 and 2^31 - 1. If zero it means the element was not found. Only works within element.
*/ @@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN /** * The client may wish to transmit keying material to the contact. This first has to be generated. The client MUST generate a fresh, randomly generated key/IV pair. For each intended recipient device, i.e. both own devices as well as devices associated with the contact, this key is encrypted using the corresponding long-standing axolotl session. Each encrypted payload key is tagged with the recipient device's ID. This is all serialized into a KeyTransportElement, omitting the . - +
BASE64ENCODED... BASE64ENCODED... @@ -55,9 +55,9 @@ NS_ASSUME_NONNULL_BEGIN /** Extracts device list from PEP element */ -- (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns; +- (nullable NSArray*)omemo_deviceListFromItems; /** Extracts device list from PEP iq respnse */ -- (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns; +- (nullable NSArray*)omemo_deviceListFromIqResponse; @end diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 91daa1b785..7b7f923e6b 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -17,18 +17,18 @@ @implementation NSXMLElement (OMEMO) /** If element contains */ -- (BOOL) omemo_hasEncryptedElement:(OMEMOModuleNamespace)ns { - return [self omemo_encryptedElement:ns] != nil; +- (BOOL) omemo_hasEncryptedElement { + return [self omemo_encryptedElement] != nil; } /** If element IS */ -- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns { - return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO:ns]]; +- (BOOL) omemo_isEncryptedElement { + return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO]]; } /** Child element */ -- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { - return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; +- (nullable NSXMLElement*) omemo_encryptedElement { + return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO]]; } - (NSXMLElement*) omemo_headerElement { @@ -78,7 +78,7 @@ - (nullable NSData*) omemo_iv { /** * The client may wish to transmit keying material to the contact. This first has to be generated. The client MUST generate a fresh, randomly generated key/IV pair. For each intended recipient device, i.e. both own devices as well as devices associated with the contact, this key is encrypted using the corresponding long-standing axolotl session. Each encrypted payload key is tagged with the recipient device's ID. This is all serialized into a KeyTransportElement, omitting the as follows: - +
BASE64ENCODED... BASE64ENCODED... @@ -122,15 +122,15 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) */ -- (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns { +- (nullable NSArray*)omemo_deviceListFromIqResponse { NSXMLElement *pubsub = [self elementForName:@"pubsub" xmlns:XMLNS_PUBSUB]; NSXMLElement *items = [pubsub elementForName:@"items"]; - return [items omemo_deviceListFromItems:ns]; + return [items omemo_deviceListFromItems]; } -- (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { - if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList:ns]]) { - NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; +- (nullable NSArray*)omemo_deviceListFromItems { + if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList]]) { + NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO]]; if (devicesList) { NSArray *children = [devicesList children]; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:children.count]; diff --git a/Extensions/OMEMO/OMEMOModule.h b/Extensions/OMEMO/OMEMOModule.h index 4486150b47..b88f9bb24d 100644 --- a/Extensions/OMEMO/OMEMOModule.h +++ b/Extensions/OMEMO/OMEMOModule.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { /** Uses "eu.siacs.conversations.axolotl" namespace and compatible with the latest Conversations and ChatSecure versions as of Feb 8, 2017. */ OMEMOModuleNamespaceConversationsLegacy, - /** Uses "urn:xmpp:omemo:2" namespace. XEP is still experimental. Do not use in production yet as it may change! See https://xmpp.org/extensions/xep-0384.html */ + /** Uses "urn:xmpp:omemo:0" namespace. XEP is still experimental. Do not use in production yet as it may change! See https://xmpp.org/extensions/xep-0384.html */ OMEMOModuleNamespaceOMEMO }; @@ -137,6 +137,10 @@ typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { #pragma mark Namespace methods ++ (NSString*) xmlnsOMEMO; ++ (NSString*) xmlnsOMEMODeviceList; ++ (NSString*) xmlnsOMEMOBundles; + + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns; + (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns; + (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns; diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 226d10e974..5b2e0e11b8 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -88,7 +88,7 @@ - (void) publishDeviceIds:(NSArray*)deviceIds elementId:(nullable NSS __weak id weakMulticast = multicastDelegate; [self performBlock:^{ NSString *eid = [self fixElementId:elementId]; - XMPPIQ *iq = [XMPPIQ omemo_iqPublishDeviceIds:deviceIds elementId:eid xmlNamespace:self.xmlNamespace]; + XMPPIQ *iq = [XMPPIQ omemo_iqPublishDeviceIds:deviceIds elementId:eid]; [self.tracker addElement:iq block:^(XMPPIQ *responseIq, id info) { __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { return; } @@ -119,7 +119,7 @@ - (void) fetchDeviceIdsForJID:(XMPPJID*)jid return; } - NSArray *devices = [responseIq omemo_deviceListFromIqResponse:self.xmlNamespace]; + NSArray *devices = [responseIq omemo_deviceListFromIqResponse]; if (!devices) { devices = @[]; XMPPLogWarn(@"Missing devices from element: %@ %@", info.element, responseIq); @@ -141,7 +141,7 @@ - (void) fetchDeviceIdsForJID:(nonnull XMPPJID*)jid completion:(void (^_Nonnull)(XMPPIQ *responseIq, id info))completion { [self performBlock:^{ NSString *eid = [self fixElementId:elementId]; - XMPPIQ *iq = [XMPPIQ omemo_iqFetchDeviceIdsForJID:jid elementId:eid xmlNamespace:self.xmlNamespace]; + XMPPIQ *iq = [XMPPIQ omemo_iqFetchDeviceIdsForJID:jid elementId:eid]; [self.tracker addElement:iq block:completion timeout:30]; [self->xmppStream sendElement:iq]; }]; @@ -155,7 +155,7 @@ - (void) publishBundle:(OMEMOBundle*)bundle __weak id weakMulticast = multicastDelegate; [self performBlock:^{ NSString *eid = [self fixElementId:elementId]; - XMPPIQ *iq = [XMPPIQ omemo_iqPublishBundle:bundle elementId:eid xmlNamespace:self.xmlNamespace]; + XMPPIQ *iq = [XMPPIQ omemo_iqPublishBundle:bundle elementId:eid]; [self.tracker addElement:iq block:^(XMPPIQ *responseIq, id info) { __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { return; } @@ -181,7 +181,7 @@ - (void) fetchBundleForDeviceId:(uint32_t)deviceId __weak id weakMulticast = multicastDelegate; [self performBlock:^{ NSString *eid = [self fixElementId:elementId]; - XMPPIQ *iq = [XMPPIQ omemo_iqFetchBundleForDeviceId:deviceId jid:jid.bareJID elementId:eid xmlNamespace:self.xmlNamespace]; + XMPPIQ *iq = [XMPPIQ omemo_iqFetchBundleForDeviceId:deviceId jid:jid.bareJID elementId:eid]; [self.tracker addElement:iq block:^(XMPPIQ *responseIq, id info) { __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { return; } @@ -191,7 +191,7 @@ - (void) fetchBundleForDeviceId:(uint32_t)deviceId [weakMulticast omemo:strongSelf failedToFetchBundleForDeviceId:deviceId fromJID:jid errorIq:responseIq outgoingIq:iq]; return; } - OMEMOBundle *bundle = [responseIq omemo_bundle:strongSelf.xmlNamespace]; + OMEMOBundle *bundle = [responseIq omemo_bundle]; if (bundle) { [weakMulticast omemo:strongSelf fetchedBundle:bundle fromJID:jid responseIq:responseIq outgoingIq:iq]; } else { @@ -301,9 +301,21 @@ - (void) receiveMessage:(XMPPMessage*)message forJID:(XMPPJID*)forJID isIncoming #pragma mark Namespace methods ++ (NSString*) xmlnsOMEMO { + return @"urn:xmpp:omemo:2"; +} + ++ (NSString*) xmlnsOMEMODeviceList { + return [NSString stringWithFormat:@"%@:devices", [self xmlnsOMEMO]]; +} + ++ (NSString*) xmlnsOMEMOBundles { + return [NSString stringWithFormat:@"%@:bundles", [self xmlnsOMEMO]]; +} + + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns { if (ns == OMEMOModuleNamespaceOMEMO) { - return @"urn:xmpp:omemo:2"; + return @"urn:xmpp:omemo:0"; } else { // OMEMOModuleNamespaceConversationsLegacy return @"eu.siacs.conversations.axolotl"; } @@ -311,9 +323,9 @@ + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns { + (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns { NSString *xmlns = [self xmlnsOMEMO:ns]; if (ns == OMEMOModuleNamespaceOMEMO) { - return [NSString stringWithFormat:@"%@:devices", xmlns]; + return [NSString stringWithFormat:@"%@:devicelist", xmlns]; } else { // OMEMOModuleNamespaceConversationsLegacy - return [NSString stringWithFormat:@"%@.devices", xmlns]; + return [NSString stringWithFormat:@"%@.devicelist", xmlns]; } } + (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns { diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.h b/Extensions/OMEMO/XMPPIQ+OMEMO.h index 87d41dd8bd..f9acbba612 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.h +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.h @@ -19,27 +19,23 @@ NS_ASSUME_NONNULL_BEGIN /** iq stanza for manually fetching deviceIds list. This should be handled automatically by PEP. */ + (XMPPIQ*) omemo_iqFetchDeviceIdsForJID:(XMPPJID*)jid - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace; + elementId:(nullable NSString*)elementId; /** iq stanza for publishing your device ids. The Device IDs are integers between 1 and 2^31 - 1 */ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace; + elementId:(nullable NSString*)elementId; /** iq stanza for publishing bundle for device */ + (XMPPIQ*) omemo_iqPublishBundle:(OMEMOBundle*)bundle - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace; + elementId:(nullable NSString*)elementId; /** iq stanza for fetching remote bundle */ + (XMPPIQ*) omemo_iqFetchBundleForDeviceId:(uint32_t)deviceId jid:(XMPPJID*)jid - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace; + elementId:(nullable NSString*)elementId; /** Serialize bundle from IQ */ -- (nullable OMEMOBundle*) omemo_bundle:(OMEMOModuleNamespace)ns; +- (nullable OMEMOBundle*) omemo_bundle; @end NS_ASSUME_NONNULL_END diff --git a/Extensions/OMEMO/XMPPIQ+OMEMO.m b/Extensions/OMEMO/XMPPIQ+OMEMO.m index a5cb739c65..e505bf80ef 100644 --- a/Extensions/OMEMO/XMPPIQ+OMEMO.m +++ b/Extensions/OMEMO/XMPPIQ+OMEMO.m @@ -28,10 +28,9 @@ @implementation XMPPIQ (OMEMO) */ + (XMPPIQ*) omemo_iqFetchDeviceIdsForJID:(XMPPJID*)jid - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { + elementId:(nullable NSString*)elementId { NSXMLElement *items = [NSXMLElement elementWithName:@"items"]; - [items addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMODeviceList:xmlNamespace]]; + [items addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMODeviceList]]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:items]; @@ -69,7 +68,7 @@ + (XMPPIQ*) omemo_iqFetchDeviceIdsForJID:(XMPPJID*)jid */ -+ (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(nullable NSString*)elementId xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { ++ (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(nullable NSString*)elementId { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; @@ -77,14 +76,14 @@ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(n [iq addChild:pubsub]; NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; - [publish addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMODeviceList:xmlNamespace]]; + [publish addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMODeviceList]]; [pubsub addChild:publish]; NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; [item addAttributeWithName:@"id" stringValue:@"current"]; [publish addChild:item]; - NSXMLElement *devices = [NSXMLElement elementWithName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO:xmlNamespace]]; + NSXMLElement *devices = [NSXMLElement elementWithName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO]]; [item addChild:devices]; [deviceIds enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { @@ -154,15 +153,14 @@ + (XMPPIQ*) omemo_iqPublishDeviceIds:(NSArray*)deviceIds elementId:(n */ + (XMPPIQ*) omemo_iqPublishBundle:(OMEMOBundle*)bundle - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { + elementId:(nullable NSString*)elementId { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementId]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [iq addChild:pubsub]; NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; - NSString *nodeName = [OMEMOModule xmlnsOMEMOBundles:xmlNamespace]; + NSString *nodeName = [OMEMOModule xmlnsOMEMOBundles]; [publish addAttributeWithName:@"node" stringValue:nodeName]; [pubsub addChild:publish]; @@ -171,7 +169,7 @@ + (XMPPIQ*) omemo_iqPublishBundle:(OMEMOBundle*)bundle [itemElement addAttributeWithName:@"id" stringValue:deviceId]; [publish addChild:itemElement]; - NSXMLElement *bundleElement = [XMPPElement elementWithName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO:xmlNamespace]]; + NSXMLElement *bundleElement = [XMPPElement elementWithName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO]]; [itemElement addChild:bundleElement]; if (bundle.signedPreKey.publicKey) { @@ -253,15 +251,14 @@ + (XMPPIQ *) omemo_iqDeleteNode:(NSString *)node elementId:(nullable NSString *) */ + (XMPPIQ*) omemo_iqFetchBundleForDeviceId:(uint32_t)deviceId jid:(XMPPJID*)jid - elementId:(nullable NSString*)elementId - xmlNamespace:(OMEMOModuleNamespace)xmlNamespace { + elementId:(nullable NSString*)elementId { XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:elementId]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [iq addChild:pubsub]; NSXMLElement *itemsElement = [NSXMLElement elementWithName:@"items"]; - [itemsElement addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMOBundles:xmlNamespace]]; + [itemsElement addAttributeWithName:@"node" stringValue:[OMEMOModule xmlnsOMEMOBundles]]; [pubsub addChild:itemsElement]; NSXMLElement *itemElement = [NSXMLElement elementWithName:@"item"]; @@ -272,7 +269,7 @@ + (XMPPIQ*) omemo_iqFetchBundleForDeviceId:(uint32_t)deviceId } -- (nullable OMEMOBundle*) omemo_bundle:(OMEMOModuleNamespace)ns { +- (nullable OMEMOBundle*) omemo_bundle { NSXMLElement *pubsub = [self elementForName:@"pubsub" xmlns:XMLNS_PUBSUB]; if (!pubsub) { return nil; } @@ -285,7 +282,7 @@ - (nullable OMEMOBundle*) omemo_bundle:(OMEMOModuleNamespace)ns { NSString *node = [items attributeForName:@"node"].stringValue; if (!node) { return nil; } - if (![node isEqualToString:[OMEMOModule xmlnsOMEMOBundles:ns]]) { + if (![node isEqualToString:[OMEMOModule xmlnsOMEMOBundles]]) { return nil; } @@ -294,7 +291,7 @@ - (nullable OMEMOBundle*) omemo_bundle:(OMEMOModuleNamespace)ns { NSString *deviceIdString = [itemElement attributeStringValueForName:@"id"]; uint32_t deviceId = (uint32_t)[deviceIdString integerValue]; - NSXMLElement *bundleElement = [itemElement elementForName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; + NSXMLElement *bundleElement = [itemElement elementForName:@"bundle" xmlns:[OMEMOModule xmlnsOMEMO]]; if (!bundleElement) { return nil; } NSXMLElement *signedPreKeyElement = [bundleElement elementForName:@"spk"]; From 19b0eae3d430d8cc4515728a0e93b18a2af5fbd5 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 23 Sep 2025 14:23:35 +0200 Subject: [PATCH 31/62] Apply feedback: add signatures for deprecated methods to document which parts of OMEMOModule was not migrated to OMEMO 2 --- Extensions/OMEMO/NSXMLElement+OMEMO.h | 3 +++ Extensions/OMEMO/NSXMLElement+OMEMO.m | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.h b/Extensions/OMEMO/NSXMLElement+OMEMO.h index 59358c0cf2..c260c1c8df 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.h +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.h @@ -59,6 +59,9 @@ NS_ASSUME_NONNULL_BEGIN /** Extracts device list from PEP iq respnse */ - (nullable NSArray*)omemo_deviceListFromIqResponse; +- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_encryptedElement instead"))); +- (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromItems instead"))); +- (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromIqResponse instead"))); @end NS_ASSUME_NONNULL_END diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 7b7f923e6b..461c51eb3c 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -149,4 +149,19 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) return nil; } +- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { + NSAssert(NO, @"Use omemo_encryptedElement instead"); + return nil; +} + +- (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { + NSAssert(NO, @"Use omemo_deviceListFromItems instead"); + return nil; +} + +- (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns { + NSAssert(NO, @"Use omemo_deviceListFromIqResponse instead"); + return nil; +} + @end From a317ed08ed1028de5720f4b585141798be535ab1 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 23 Sep 2025 15:09:28 +0200 Subject: [PATCH 32/62] Apply feedback: Fix OMEMOElementTests --- Extensions/OMEMO/NSXMLElement+OMEMO.h | 1 + Extensions/OMEMO/NSXMLElement+OMEMO.m | 5 + Xcode/Testing-Shared/OMEMOElementTests.m | 239 ++++++++++++----------- Xcode/Testing-Shared/OMEMOModuleTests.m | 2 +- Xcode/Testing-Shared/OMEMOTestStorage.m | 2 +- 5 files changed, 128 insertions(+), 121 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.h b/Extensions/OMEMO/NSXMLElement+OMEMO.h index c260c1c8df..b1ef181c94 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.h +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.h @@ -59,6 +59,7 @@ NS_ASSUME_NONNULL_BEGIN /** Extracts device list from PEP iq respnse */ - (nullable NSArray*)omemo_deviceListFromIqResponse; +- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_encryptedElement instead"))); - (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_encryptedElement instead"))); - (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromItems instead"))); - (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromIqResponse instead"))); diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 461c51eb3c..a654893ec0 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -149,6 +149,11 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) return nil; } +- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns { + // Use omemo_isEncryptedElement instead + return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO:ns]]; +} + - (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { NSAssert(NO, @"Use omemo_encryptedElement instead"); return nil; diff --git a/Xcode/Testing-Shared/OMEMOElementTests.m b/Xcode/Testing-Shared/OMEMOElementTests.m index ead7e9f23c..8377c072a8 100644 --- a/Xcode/Testing-Shared/OMEMOElementTests.m +++ b/Xcode/Testing-Shared/OMEMOElementTests.m @@ -35,36 +35,33 @@ - (void)tearDown { - (void)testDeviceIdSerialization { NSArray *deviceIds = @[@(12345), @(4223), @(31415)]; - XMPPIQ *iq = [XMPPIQ omemo_iqPublishDeviceIds:deviceIds elementId:@"announce1" xmlNamespace:self.ns]; + XMPPIQ *iq = [XMPPIQ omemo_iqPublishDeviceIds:deviceIds elementId:@"announce1"]; NSString *iqString = [iq XMLString]; - NSString *expectedString = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - http://jabber.org/protocol/pubsub#publish-options \ - \ - \ - 1 \ - \ - \ - open \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMODeviceList:self.ns], [OMEMOModule xmlnsOMEMO:self.ns]]; + NSString *expectedString = @"" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " http://jabber.org/protocol/pubsub#publish-options" + " " + " " + " open" + " " + " " + " " + " " + "" + ""; NSError *error = nil; NSXMLElement *outputIQ = [[NSXMLElement alloc] initWithXMLString:iqString error:&error]; XCTAssertNil(error); @@ -75,39 +72,39 @@ - (void)testDeviceIdSerialization { } - (void) testPublishDeviceBundle { - NSString *expectedString = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - c2lnbmVkUHJlS2V5UHVibGlj \ - c2lnbmVkUHJlS2V5U2lnbmF0dXJl \ - aWRlbnRpdHlLZXk= \ - \ - cHJlS2V5MQ== \ - cHJlS2V5Mg== \ - cHJlS2V5Mw== \ - \ - \ - \ - \ - \ - \ - \ - http://jabber.org/protocol/pubsub#publish-options \ - \ - \ - 1 \ - \ - \ - open \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMOBundles:self.ns], [OMEMOModule xmlnsOMEMO:self.ns]]; + NSString *expectedString = @"" + "" + " " + " " + " " + " " + " c2lnbmVkUHJlS2V5UHVibGlj" + " c2lnbmVkUHJlS2V5U2lnbmF0dXJl" + " aWRlbnRpdHlLZXk=" + " " + " cHJlS2V5MQ==" + " cHJlS2V5Mg==" + " cHJlS2V5Mw==" + " " + " " + " " + " " + " " + " " + " " + " http://jabber.org/protocol/pubsub#publish-options" + " " + " " + " max" + " " + " " + " open" + " " + " " + " " + " " + "" + ""; NSError *error = nil; NSXMLElement *expectedXML = [[NSXMLElement alloc] initWithXMLString:expectedString error:&error]; XCTAssertNotNil(expectedXML); @@ -130,7 +127,7 @@ - (void) testPublishDeviceBundle { ]; OMEMOSignedPreKey *signedPreKey = [[OMEMOSignedPreKey alloc] initWithPreKeyId:1 publicKey:signedPreKeyPublicData signature:signedPreKeySignatureData]; OMEMOBundle *bundle = [[OMEMOBundle alloc] initWithDeviceId:31415 identityKey:identityKeyData signedPreKey:signedPreKey preKeys:preKeys]; - XMPPIQ *iq = [XMPPIQ omemo_iqPublishBundle:bundle elementId:@"announce2" xmlNamespace:self.ns]; + XMPPIQ *iq = [XMPPIQ omemo_iqPublishBundle:bundle elementId:@"announce2"]; XCTAssertEqualObjects([iq XMLStringWithOptions:NSXMLNodePrettyPrint], [expectedXML XMLStringWithOptions:NSXMLNodePrettyPrint]); } @@ -147,20 +144,20 @@ - (void) testPublishDeviceBundle { */ - (void) testFetchBundleForDeviceId { - NSString *expected = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMOBundles:self.ns]]; + NSString *expected = @"" + "" + " " + " " + " " + " " + " " + "" + ""; NSError *error = nil; NSXMLElement *expectedElement = [[NSXMLElement alloc] initWithXMLString:expected error:&error]; XCTAssertNil(error); XCTAssertNotNil(expectedElement); - XMPPIQ *iq = [XMPPIQ omemo_iqFetchBundleForDeviceId:31415 jid:[XMPPJID jidWithString:@"juliet@capulet.lit"] elementId:@"fetch1" xmlNamespace:self.ns]; + XMPPIQ *iq = [XMPPIQ omemo_iqFetchBundleForDeviceId:31415 jid:[XMPPJID jidWithString:@"juliet@capulet.lit"] elementId:@"fetch1"]; XCTAssertEqualObjects([iq XMLStringWithOptions:NSXMLNodePrettyPrint], [expectedElement XMLStringWithOptions:NSXMLNodePrettyPrint]); } @@ -255,39 +252,42 @@ - (void) testKeyTransportElement { */ - (void) testBundleParsing { - NSString *expectedString = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - c2lnbmVkUHJlS2V5UHVibGlj \ - c2lnbmVkUHJlS2V5U2lnbmF0dXJl \ - aWRlbnRpdHlLZXk= \ - \ - cHJlS2V5MQ== \ - cHJlS2V5Mg== \ - cHJlS2V5Mw== \ - \ - \ - \ - \ - \ - \ - \ - http://jabber.org/protocol/pubsub#publish-options \ - \ - \ - 1 \ - \ - \ - open \ - \ - \ - \ - \ - \ - ",[OMEMOModule xmlnsOMEMOBundles:self.ns], [OMEMOModule xmlnsOMEMO:self.ns]]; + NSString *expectedString = @"" + "" + " " + " " + " " + " " + " c2lnbmVkUHJlS2V5UHVibGlj" + " c2lnbmVkUHJlS2V5U2lnbmF0dXJl" + " aWRlbnRpdHlLZXk=" + " " + " cHJlS2V5MQ==" + " cHJlS2V5Mg==" + " cHJlS2V5Mw==" + " " + " " + " " + " " + " " + " " + " " + " http://jabber.org/protocol/pubsub#publish-options" + " " + " " + " max" + " " + " " + " open" + " " + " " + " " + " " + "" + ""; NSError *error = nil; NSXMLElement *expectedXML = [[NSXMLElement alloc] initWithXMLString:expectedString error:&error]; XCTAssertNotNil(expectedXML); @@ -310,32 +310,33 @@ - (void) testBundleParsing { ]; OMEMOSignedPreKey *signedPreKey = [[OMEMOSignedPreKey alloc] initWithPreKeyId:1 publicKey:signedPreKeyPublicData signature:signedPreKeySignatureData]; OMEMOBundle *bundle = [[OMEMOBundle alloc] initWithDeviceId:31415 identityKey:identityKeyData signedPreKey:signedPreKey preKeys:preKeys]; - XMPPIQ *iq = [XMPPIQ omemo_iqPublishBundle:bundle elementId:@"announce2" xmlNamespace:self.ns]; + XMPPIQ *iq = [XMPPIQ omemo_iqPublishBundle:bundle elementId:@"announce2"]; XCTAssertEqualObjects([iq XMLStringWithOptions:NSXMLNodePrettyPrint], [expectedXML XMLStringWithOptions:NSXMLNodePrettyPrint]); - OMEMOBundle *expectedBundle = [[XMPPIQ iqFromElement:expectedXML] omemo_bundle:self.ns]; - OMEMOBundle *bundle2 = [iq omemo_bundle:self.ns]; + OMEMOBundle *expectedBundle = [[XMPPIQ iqFromElement:expectedXML] omemo_bundle]; + OMEMOBundle *bundle2 = [iq omemo_bundle]; - XMPPIQ *expectedIQ = [XMPPIQ omemo_iqPublishBundle:expectedBundle elementId:@"eid" xmlNamespace:self.ns]; - XMPPIQ *bundle2iq = [XMPPIQ omemo_iqPublishBundle:bundle2 elementId:@"eid" xmlNamespace:self.ns]; + XMPPIQ *expectedIQ = [XMPPIQ omemo_iqPublishBundle:expectedBundle elementId:@"eid"]; + XMPPIQ *bundle2iq = [XMPPIQ omemo_iqPublishBundle:bundle2 elementId:@"eid"]; XCTAssertEqualObjects([expectedIQ XMLStringWithOptions:NSXMLNodePrettyPrint], [bundle2iq XMLStringWithOptions:NSXMLNodePrettyPrint]); } - (void) testFetchDeviceList { - NSString *expected = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - ",[OMEMOModule xmlnsOMEMODeviceList:self.ns]]; + NSString *expected = @"" + "" + " " + " " + " " + "" + ""; NSError *error = nil; NSXMLElement *expXml = [[NSXMLElement alloc] initWithXMLString:expected error:&error]; XCTAssertNil(error); XCTAssertNotNil(expXml); XMPPJID *jid = [XMPPJID jidWithString:@"juliet@capulet.lit"]; - XMPPIQ *iq = [XMPPIQ omemo_iqFetchDeviceIdsForJID:jid elementId:@"fetch1" xmlNamespace:self.ns]; + XMPPIQ *iq = [XMPPIQ omemo_iqFetchDeviceIdsForJID:jid elementId:@"fetch1"]; XMPPIQ *expIq = [XMPPIQ iqFromElement:expXml]; XCTAssertEqualObjects([expIq type], [iq type]); XCTAssertEqualObjects([expIq to], [iq to]); diff --git a/Xcode/Testing-Shared/OMEMOModuleTests.m b/Xcode/Testing-Shared/OMEMOModuleTests.m index 45609866b3..531d67ba55 100644 --- a/Xcode/Testing-Shared/OMEMOModuleTests.m +++ b/Xcode/Testing-Shared/OMEMOModuleTests.m @@ -462,7 +462,7 @@ - (NSXMLElement*)innerBundleElement { - (OMEMOBundle*) bundle { OMEMOModuleNamespace ns = self.omemoModule.xmlNamespace; - OMEMOBundle *bundle = [[self iq_SetBundleWithEid:@"announce1"] omemo_bundle:ns]; + OMEMOBundle *bundle = [[self iq_SetBundleWithEid:@"announce1"] omemo_bundle]; XCTAssertNotNil(bundle); return bundle; } diff --git a/Xcode/Testing-Shared/OMEMOTestStorage.m b/Xcode/Testing-Shared/OMEMOTestStorage.m index e76f8a6640..5c3baf089c 100644 --- a/Xcode/Testing-Shared/OMEMOTestStorage.m +++ b/Xcode/Testing-Shared/OMEMOTestStorage.m @@ -124,7 +124,7 @@ + (NSXMLElement*)innerBundleElement:(OMEMOModuleNamespace)ns { + (OMEMOBundle*) testBundle:(OMEMOModuleNamespace)ns { [self innerBundleElement:ns]; - OMEMOBundle *bundle = [[self iq_SetBundleWithEid:@"announce1" xmlNamespace:ns] omemo_bundle:ns]; + OMEMOBundle *bundle = [[self iq_SetBundleWithEid:@"announce1" xmlNamespace:ns] omemo_bundle]; return bundle; } From 5d6ea5e751c73f7c39b880db2ce28b04f1b8b7f0 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 23 Sep 2025 15:48:26 +0200 Subject: [PATCH 33/62] Apply feedback: Fix OMEMOModuleTests --- Extensions/OMEMO/NSXMLElement+OMEMO.m | 23 +++++++++++++--- Xcode/Testing-Shared/OMEMOModuleTests.m | 36 ++++++++++++------------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index a654893ec0..19fa6cb730 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -155,12 +155,29 @@ - (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns { } - (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { - NSAssert(NO, @"Use omemo_encryptedElement instead"); - return nil; + // Use omemo_encryptedElement instead + return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; } - (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { - NSAssert(NO, @"Use omemo_deviceListFromItems instead"); + // Use omemo_deviceListFromItems instead + if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList:ns]]) { + NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; + if (devicesList) { + NSArray *children = [devicesList children]; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:children.count]; + [children enumerateObjectsUsingBlock:^(NSXMLElement * _Nonnull node, NSUInteger idx, BOOL * _Nonnull stop) { + if ([node.name isEqualToString:@"device"]) { + NSNumber *number = [node attributeNumberUInt32ValueForName:@"id"]; + if (number){ + [result addObject:number]; + } + } + }]; + return result; + } + return @[]; + } return nil; } diff --git a/Xcode/Testing-Shared/OMEMOModuleTests.m b/Xcode/Testing-Shared/OMEMOModuleTests.m index 531d67ba55..08a0fcebc0 100644 --- a/Xcode/Testing-Shared/OMEMOModuleTests.m +++ b/Xcode/Testing-Shared/OMEMOModuleTests.m @@ -437,24 +437,24 @@ - (XMPPIQ*) iq_testIQFromJID:(XMPPJID*)fromJID eid:(NSString*)eid type:(NSString - (NSXMLElement*)innerBundleElement { OMEMOModuleNamespace ns = self.omemoModule.xmlNamespace; - NSString *expectedString = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - c2lnbmVkUHJlS2V5UHVibGlj \ - c2lnbmVkUHJlS2V5U2lnbmF0dXJl \ - aWRlbnRpdHlLZXk= \ - \ - cHJlS2V5MQ== \ - cHJlS2V5Mg== \ - cHJlS2V5Mw== \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMOBundles:ns], [OMEMOModule xmlnsOMEMO:ns]]; + NSString *expectedString = @"" + "" + " " + " " + " " + " c2lnbmVkUHJlS2V5UHVibGlj" + " c2lnbmVkUHJlS2V5U2lnbmF0dXJl" + " aWRlbnRpdHlLZXk=" + " " + " cHJlS2V5MQ==" + " cHJlS2V5Mg==" + " cHJlS2V5Mw==" + " " + " " + " " + " " + "" + ""; NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:expectedString error:nil]; XCTAssertNotNil(element); return element; From a8c6365374f5ead9cabe571cca2ff24a1b87f35e Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 07:51:49 +0200 Subject: [PATCH 34/62] Apply feedback: revert unneeded changes in omemo_* methods --- Extensions/OMEMO/NSXMLElement+OMEMO.h | 14 ++++++-------- Extensions/OMEMO/NSXMLElement+OMEMO.m | 28 +++++++++------------------ 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.h b/Extensions/OMEMO/NSXMLElement+OMEMO.h index b1ef181c94..32b3a28727 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.h +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.h @@ -17,12 +17,12 @@ NS_ASSUME_NONNULL_BEGIN @interface NSXMLElement (OMEMO) -/** If element contains */ -- (BOOL) omemo_hasEncryptedElement; -/** If element IS */ -- (BOOL) omemo_isEncryptedElement; -/** Child element */ -- (nullable NSXMLElement*) omemo_encryptedElement; +/** If element contains */ +- (BOOL) omemo_hasEncryptedElement:(OMEMOModuleNamespace)ns; +/** If element IS */ +- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns; +/** Child element */ +- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns; /** The Device ID is a randomly generated integer between 1 and 2^31 - 1. If zero it means the element was not found. Only works within element.
*/ @@ -59,8 +59,6 @@ NS_ASSUME_NONNULL_BEGIN /** Extracts device list from PEP iq respnse */ - (nullable NSArray*)omemo_deviceListFromIqResponse; -- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_encryptedElement instead"))); -- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_encryptedElement instead"))); - (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromItems instead"))); - (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromIqResponse instead"))); diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 19fa6cb730..78ac294e60 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -16,19 +16,19 @@ @implementation NSXMLElement (OMEMO) -/** If element contains */ -- (BOOL) omemo_hasEncryptedElement { - return [self omemo_encryptedElement] != nil; +/** If element contains */ +- (BOOL) omemo_hasEncryptedElement:(OMEMOModuleNamespace)ns { + return [self omemo_encryptedElement:ns] != nil; } -/** If element IS */ -- (BOOL) omemo_isEncryptedElement { - return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO]]; +/** If element IS */ +- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns { + return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO:ns]]; } -/** Child element */ -- (nullable NSXMLElement*) omemo_encryptedElement { - return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO]]; +/** Child element */ +- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { + return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; } - (NSXMLElement*) omemo_headerElement { @@ -149,16 +149,6 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) return nil; } -- (BOOL) omemo_isEncryptedElement:(OMEMOModuleNamespace)ns { - // Use omemo_isEncryptedElement instead - return [[self name] isEqualToString:@"encrypted"] && [[self xmlns] isEqualToString:[OMEMOModule xmlnsOMEMO:ns]]; -} - -- (nullable NSXMLElement*) omemo_encryptedElement:(OMEMOModuleNamespace)ns { - // Use omemo_encryptedElement instead - return [self elementForName:@"encrypted" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; -} - - (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { // Use omemo_deviceListFromItems instead if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList:ns]]) { From 6c4e7026408621f0de1559a20bea8d1da15e745d Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 07:54:31 +0200 Subject: [PATCH 35/62] Apply feedback: remove unneeded change from -omemo_deviceListFromItems: method --- Extensions/OMEMO/NSXMLElement+OMEMO.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 78ac294e60..3e020ae943 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -152,7 +152,7 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) - (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { // Use omemo_deviceListFromItems instead if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList:ns]]) { - NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"devices" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; + NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"list" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; if (devicesList) { NSArray *children = [devicesList children]; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:children.count]; From 10514c7687f1b0de6409bde2ed0a4fdf3e5291d2 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 08:17:39 +0200 Subject: [PATCH 36/62] Fix testRequestSlot test --- Xcode/Testing-Shared/XMPPHTTPFileUploadTests.m | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Xcode/Testing-Shared/XMPPHTTPFileUploadTests.m b/Xcode/Testing-Shared/XMPPHTTPFileUploadTests.m index 4fe9ead148..4ae7eb4c3d 100644 --- a/Xcode/Testing-Shared/XMPPHTTPFileUploadTests.m +++ b/Xcode/Testing-Shared/XMPPHTTPFileUploadTests.m @@ -100,10 +100,7 @@ - (void) testRequestSlot { NSMutableString *s = [NSMutableString string]; [s appendString:@""]; - [s appendString:@" "]; - [s appendString:@" my_juliet.png"]; - [s appendString:@" 23456"]; - [s appendString:@" image/jpeg"]; + [s appendString:@" "]; [s appendString:@" "]; [s appendString:@""]; @@ -122,12 +119,12 @@ - (void) testRequestSlot { NSXMLElement *sentRequest = [sentIQ childElement]; NSXMLElement *request = [iq childElement]; - XCTAssertEqualObjects(sentRequest.xmlns, @"urn:xmpp:http:upload"); + XCTAssertEqualObjects(sentRequest.xmlns, @"urn:xmpp:http:upload:0"); XCTAssertEqualObjects(sentRequest.xmlns, request.xmlns); - NSString *filename = [sentRequest elementForName:@"filename"].stringValue; - NSString *size = [sentRequest elementForName:@"size"].stringValue; - NSString *contentType = [sentRequest elementForName:@"content-type"].stringValue; + NSString *filename = [sentRequest attributeStringValueForName:@"filename"]; + NSString *size = [sentRequest attributeStringValueForName:@"size"]; + NSString *contentType = [sentRequest attributeStringValueForName:@"content-type"]; XCTAssertEqualObjects(filename, @"my_juliet.png"); XCTAssertEqualObjects(size, @"23456"); From eba06ab4e5ae46aa10d70c4f86cc3fa2cd1ebb15 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 09:17:01 +0200 Subject: [PATCH 37/62] Apply feedback: bring back -omemo_deviceListFromIqResponse: implementation --- Extensions/OMEMO/NSXMLElement+OMEMO.m | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 3e020ae943..09daf1d6b4 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -171,9 +171,24 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) return nil; } +/* + + + + + + + + + + + + */ - (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns { - NSAssert(NO, @"Use omemo_deviceListFromIqResponse instead"); - return nil; + // Use omemo_deviceListFromIqResponse instead + NSXMLElement *pubsub = [self elementForName:@"pubsub" xmlns:XMLNS_PUBSUB]; + NSXMLElement *items = [pubsub elementForName:@"items"]; + return [items omemo_deviceListFromItems:ns]; } @end From 2e5b815ac04c5d5544329e372bfdadb655402a35 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 09:45:36 +0200 Subject: [PATCH 38/62] Apply feedback: mark xmlnsOMEMO:, xmlnsOMEMODeviceList:, xmlnsOMEMOBundles: as deprecated --- Extensions/OMEMO/OMEMOModule.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Extensions/OMEMO/OMEMOModule.h b/Extensions/OMEMO/OMEMOModule.h index b88f9bb24d..eeb9ca136b 100644 --- a/Extensions/OMEMO/OMEMOModule.h +++ b/Extensions/OMEMO/OMEMOModule.h @@ -141,10 +141,10 @@ typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { + (NSString*) xmlnsOMEMODeviceList; + (NSString*) xmlnsOMEMOBundles; -+ (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns; -+ (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns; ++ (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMO instead"))); ++ (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMODeviceList instead"))); + (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns; -+ (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns; ++ (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMOBundles instead"))); + (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns deviceId:(uint32_t)deviceId; @end From 579d889fc1569deb19c965b9a32bd7e6503fedf8 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 09:45:51 +0200 Subject: [PATCH 39/62] Apply feedback: remove dead code --- Extensions/OMEMO/OMEMOModule.h | 1 - Extensions/OMEMO/OMEMOModule.m | 4 ---- 2 files changed, 5 deletions(-) diff --git a/Extensions/OMEMO/OMEMOModule.h b/Extensions/OMEMO/OMEMOModule.h index eeb9ca136b..665b2cb4b9 100644 --- a/Extensions/OMEMO/OMEMOModule.h +++ b/Extensions/OMEMO/OMEMOModule.h @@ -145,7 +145,6 @@ typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { + (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMODeviceList instead"))); + (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns; + (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMOBundles instead"))); -+ (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns deviceId:(uint32_t)deviceId; @end diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 5b2e0e11b8..39cce4132d 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -342,10 +342,6 @@ + (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns { return xmlns; } -+ (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns deviceId:(uint32_t)deviceId { - return [NSString stringWithFormat:@"%@:%d", [self xmlnsOMEMOBundles:ns], (int)deviceId]; -} - #pragma mark XMPPStreamDelegate methods - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { From 54389eedeae11c18c856d939e6f7b0ac4b02880e Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 10:03:59 +0200 Subject: [PATCH 40/62] Apply feedback: fix XMPPCapabilitiesDelegate implementation --- Extensions/OMEMO/OMEMOModule.h | 2 +- Extensions/OMEMO/OMEMOModule.m | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Extensions/OMEMO/OMEMOModule.h b/Extensions/OMEMO/OMEMOModule.h index 665b2cb4b9..3a46eb94ef 100644 --- a/Extensions/OMEMO/OMEMOModule.h +++ b/Extensions/OMEMO/OMEMOModule.h @@ -139,11 +139,11 @@ typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { + (NSString*) xmlnsOMEMO; + (NSString*) xmlnsOMEMODeviceList; ++ (NSString*) xmlnsOMEMODeviceListNotify; + (NSString*) xmlnsOMEMOBundles; + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMO instead"))); + (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMODeviceList instead"))); -+ (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns; + (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMOBundles instead"))); @end diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 39cce4132d..58774c0e3b 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -309,6 +309,10 @@ + (NSString*) xmlnsOMEMODeviceList { return [NSString stringWithFormat:@"%@:devices", [self xmlnsOMEMO]]; } ++ (NSString*) xmlnsOMEMODeviceListNotify { + return [NSString stringWithFormat:@"%@+notify", [self xmlnsOMEMODeviceList]]; +} + + (NSString*) xmlnsOMEMOBundles { return [NSString stringWithFormat:@"%@:bundles", [self xmlnsOMEMO]]; } @@ -328,9 +332,6 @@ + (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns { return [NSString stringWithFormat:@"%@.devicelist", xmlns]; } } -+ (NSString*) xmlnsOMEMODeviceListNotify:(OMEMOModuleNamespace)ns { - return [NSString stringWithFormat:@"%@+notify", [self xmlnsOMEMODeviceList:ns]]; -} + (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns { NSString *xmlns = [self xmlnsOMEMO:ns]; if (ns == OMEMOModuleNamespaceOMEMO) { @@ -403,7 +404,7 @@ - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq { #pragma mark XMPPCapabilitiesDelegate methods - (NSArray*) myFeaturesForXMPPCapabilities:(XMPPCapabilities *)sender { - return @[[[self class] xmlnsOMEMODeviceList:self.xmlNamespace], [[self class] xmlnsOMEMODeviceListNotify:self.xmlNamespace]]; + return @[[[self class] xmlnsOMEMODeviceList], [[self class] xmlnsOMEMODeviceListNotify]]; } #pragma mark Utility From 2af8ada66905072dedf485c602b004fa6376b9d3 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 11:48:07 +0200 Subject: [PATCH 41/62] Apply feedback: remove deprecated functions and use new versions where possible --- Extensions/OMEMO/NSXMLElement+OMEMO.h | 3 -- Extensions/OMEMO/NSXMLElement+OMEMO.m | 42 ------------------- Extensions/OMEMO/OMEMOModule.h | 2 - Extensions/OMEMO/OMEMOModule.m | 20 +-------- Extensions/OMEMO/XMPPMessage+OMEMO.m | 2 +- Xcode/Testing-Shared/OMEMOModuleTests.m | 56 ++++++++++++------------- Xcode/Testing-Shared/OMEMOTestStorage.m | 36 ++++++++-------- 7 files changed, 46 insertions(+), 115 deletions(-) diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.h b/Extensions/OMEMO/NSXMLElement+OMEMO.h index 32b3a28727..6cd6f37563 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.h +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.h @@ -59,8 +59,5 @@ NS_ASSUME_NONNULL_BEGIN /** Extracts device list from PEP iq respnse */ - (nullable NSArray*)omemo_deviceListFromIqResponse; -- (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromItems instead"))); -- (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use omemo_deviceListFromIqResponse instead"))); - @end NS_ASSUME_NONNULL_END diff --git a/Extensions/OMEMO/NSXMLElement+OMEMO.m b/Extensions/OMEMO/NSXMLElement+OMEMO.m index 09daf1d6b4..e58ff48f57 100644 --- a/Extensions/OMEMO/NSXMLElement+OMEMO.m +++ b/Extensions/OMEMO/NSXMLElement+OMEMO.m @@ -149,46 +149,4 @@ + (NSXMLElement*) omemo_keyTransportElementWithKeyData:(NSArray*) return nil; } -- (nullable NSArray*)omemo_deviceListFromItems:(OMEMOModuleNamespace)ns { - // Use omemo_deviceListFromItems instead - if ([[self attributeStringValueForName:@"node"] isEqualToString:[OMEMOModule xmlnsOMEMODeviceList:ns]]) { - NSXMLElement * devicesList = [[self elementForName:@"item"] elementForName:@"list" xmlns:[OMEMOModule xmlnsOMEMO:ns]]; - if (devicesList) { - NSArray *children = [devicesList children]; - NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:children.count]; - [children enumerateObjectsUsingBlock:^(NSXMLElement * _Nonnull node, NSUInteger idx, BOOL * _Nonnull stop) { - if ([node.name isEqualToString:@"device"]) { - NSNumber *number = [node attributeNumberUInt32ValueForName:@"id"]; - if (number){ - [result addObject:number]; - } - } - }]; - return result; - } - return @[]; - } - return nil; -} - -/* - - - - - - - - - - - - */ -- (nullable NSArray*)omemo_deviceListFromIqResponse:(OMEMOModuleNamespace)ns { - // Use omemo_deviceListFromIqResponse instead - NSXMLElement *pubsub = [self elementForName:@"pubsub" xmlns:XMLNS_PUBSUB]; - NSXMLElement *items = [pubsub elementForName:@"items"]; - return [items omemo_deviceListFromItems:ns]; -} - @end diff --git a/Extensions/OMEMO/OMEMOModule.h b/Extensions/OMEMO/OMEMOModule.h index 3a46eb94ef..5464a9e6d8 100644 --- a/Extensions/OMEMO/OMEMOModule.h +++ b/Extensions/OMEMO/OMEMOModule.h @@ -143,8 +143,6 @@ typedef NS_ENUM(NSUInteger, OMEMOModuleNamespace) { + (NSString*) xmlnsOMEMOBundles; + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMO instead"))); -+ (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMODeviceList instead"))); -+ (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns __attribute__((deprecated("Use xmlnsOMEMOBundles instead"))); @end diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 58774c0e3b..69db61062e 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -218,7 +218,7 @@ - (void) removeDeviceIds:(NSArray*)deviceIds elementId:(NSString *)el return; } - NSArray *devices = [responseIq omemo_deviceListFromIqResponse:strongSelf.xmlNamespace]; + NSArray *devices = [responseIq omemo_deviceListFromIqResponse]; NSIndexSet *indexSet = [devices indexesOfObjectsPassingTest:^BOOL(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { return [deviceIds containsObject:obj]; }]; @@ -324,24 +324,6 @@ + (NSString*) xmlnsOMEMO:(OMEMOModuleNamespace)ns { return @"eu.siacs.conversations.axolotl"; } } -+ (NSString*) xmlnsOMEMODeviceList:(OMEMOModuleNamespace)ns { - NSString *xmlns = [self xmlnsOMEMO:ns]; - if (ns == OMEMOModuleNamespaceOMEMO) { - return [NSString stringWithFormat:@"%@:devicelist", xmlns]; - } else { // OMEMOModuleNamespaceConversationsLegacy - return [NSString stringWithFormat:@"%@.devicelist", xmlns]; - } -} -+ (NSString*) xmlnsOMEMOBundles:(OMEMOModuleNamespace)ns { - NSString *xmlns = [self xmlnsOMEMO:ns]; - if (ns == OMEMOModuleNamespaceOMEMO) { - xmlns = [NSString stringWithFormat:@"%@:bundles", xmlns]; - } else { // OMEMOModuleNamespaceConversationsLegacy - xmlns = [NSString stringWithFormat:@"%@.bundles", xmlns]; - } - NSParameterAssert(xmlns != nil); - return xmlns; -} #pragma mark XMPPStreamDelegate methods diff --git a/Extensions/OMEMO/XMPPMessage+OMEMO.m b/Extensions/OMEMO/XMPPMessage+OMEMO.m index 45c0c53457..0a70e4ee59 100644 --- a/Extensions/OMEMO/XMPPMessage+OMEMO.m +++ b/Extensions/OMEMO/XMPPMessage+OMEMO.m @@ -25,7 +25,7 @@ @implementation XMPPMessage (OMEMO) if (!event) { return nil; } NSXMLElement * itemsList = [event elementForName:@"items"]; if (!itemsList) { return nil; } - return [itemsList omemo_deviceListFromItems:ns]; + return [itemsList omemo_deviceListFromItems]; } /** diff --git a/Xcode/Testing-Shared/OMEMOModuleTests.m b/Xcode/Testing-Shared/OMEMOModuleTests.m index 08a0fcebc0..15c2b4fe04 100644 --- a/Xcode/Testing-Shared/OMEMOModuleTests.m +++ b/Xcode/Testing-Shared/OMEMOModuleTests.m @@ -60,19 +60,18 @@ - (void)testFetchDeviceIds { XMPPJID *testJID = [XMPPJID jidWithString:@"test@example.com"]; OMEMOModuleNamespace ns = self.omemoModule.xmlNamespace; - NSString *items = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMODeviceList:ns], [OMEMOModule xmlnsOMEMO:ns]]; - + NSString *items = @"" + "" + " " + " " + " " + " " + " " + " " + " " + " " + "" + ""; NSError *error = nil; NSXMLElement *pubsub = [[NSXMLElement alloc] initWithXMLString:items error:&error]; XCTAssertNil(error); @@ -270,23 +269,20 @@ - (void) testReceiveMessage { - (void) testDeviceListUpdate { OMEMOModuleNamespace ns = self.omemoModule.xmlNamespace; - NSString *incoming = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMODeviceList:ns], [OMEMOModule xmlnsOMEMO:ns]]; + NSString *incoming = @"" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "" + ""; NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:incoming error:nil]; self.expectation = [self expectationWithDescription:@"testDeviceListUpdate"]; XCTAssertNotNil(element); diff --git a/Xcode/Testing-Shared/OMEMOTestStorage.m b/Xcode/Testing-Shared/OMEMOTestStorage.m index 5c3baf089c..0f3ba6f865 100644 --- a/Xcode/Testing-Shared/OMEMOTestStorage.m +++ b/Xcode/Testing-Shared/OMEMOTestStorage.m @@ -99,24 +99,24 @@ + (XMPPIQ*) iq_testIQFromJID:(XMPPJID*)fromJID eid:(NSString*)eid type:(NSString + (NSXMLElement*)innerBundleElement:(OMEMOModuleNamespace)ns { - NSString *expectedString = [NSString stringWithFormat:@" \ - \ - \ - \ - \ - c2lnbmVkUHJlS2V5UHVibGlj \ - c2lnbmVkUHJlS2V5U2lnbmF0dXJl \ - aWRlbnRpdHlLZXk= \ - \ - cHJlS2V5MQ== \ - cHJlS2V5Mg== \ - cHJlS2V5Mw== \ - \ - \ - \ - \ - \ - ", [OMEMOModule xmlnsOMEMODeviceList:ns], [OMEMOModule xmlnsOMEMO:ns]]; + NSString *expectedString = @"" + "" + " " + " " + " " + " c2lnbmVkUHJlS2V5UHVibGlj=" + " c2lnbmVkUHJlS2V5U2lnbmF0dXJl==" + " aWRlbnRpdHlLZXk=" + " " + " cHJlS2V5MQ==" + " cHJlS2V5Mg==" + " cHJlS2V5Mw==" + " " + " " + " " + " " + "" + ""; NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:expectedString error:nil]; return element; From f51c5481f1d28d5cbdf06900d195a59d105835ea Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Wed, 24 Sep 2025 11:57:07 +0200 Subject: [PATCH 42/62] Apply feedback: remove ns argument from -omemo_deviceListFromPEPUpdate: --- Extensions/OMEMO/OMEMOModule.m | 2 +- Extensions/OMEMO/XMPPMessage+OMEMO.h | 2 +- Extensions/OMEMO/XMPPMessage+OMEMO.m | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Extensions/OMEMO/OMEMOModule.m b/Extensions/OMEMO/OMEMOModule.m index 69db61062e..64da1da056 100644 --- a/Extensions/OMEMO/OMEMOModule.m +++ b/Extensions/OMEMO/OMEMOModule.m @@ -277,7 +277,7 @@ - (void) receiveMessage:(XMPPMessage*)message forJID:(XMPPJID*)forJID isIncoming return; } // Check for incoming device list updates - NSArray *deviceIds = [message omemo_deviceListFromPEPUpdate:self.xmlNamespace]; + NSArray *deviceIds = [message omemo_deviceListFromPEPUpdate]; XMPPJID *bareJID = forJID.bareJID; if (deviceIds && message == originalMessage) { [multicastDelegate omemo:self deviceListUpdate:deviceIds fromJID:bareJID incomingElement:message]; diff --git a/Extensions/OMEMO/XMPPMessage+OMEMO.h b/Extensions/OMEMO/XMPPMessage+OMEMO.h index c652ebd348..f08165aba3 100644 --- a/Extensions/OMEMO/XMPPMessage+OMEMO.h +++ b/Extensions/OMEMO/XMPPMessage+OMEMO.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @interface XMPPMessage (OMEMO) /** Extracts device list from PEP update */ -- (nullable NSArray*)omemo_deviceListFromPEPUpdate:(OMEMOModuleNamespace)ns; +- (nullable NSArray*)omemo_deviceListFromPEPUpdate; /** In order to send a chat message, its first has to be encrypted. The client MUST use fresh, randomly generated key/IV pairs with AES-128 in Galois/Counter Mode (GCM). For each intended recipient device, i.e. both own devices as well as devices associated with the contact, this key is encrypted using the corresponding long-standing axolotl session. Each encrypted payload key is tagged with the recipient device's ID. This is all serialized into a MessageElement. diff --git a/Extensions/OMEMO/XMPPMessage+OMEMO.m b/Extensions/OMEMO/XMPPMessage+OMEMO.m index 0a70e4ee59..32cf9c98ab 100644 --- a/Extensions/OMEMO/XMPPMessage+OMEMO.m +++ b/Extensions/OMEMO/XMPPMessage+OMEMO.m @@ -19,7 +19,7 @@ @implementation XMPPMessage (OMEMO) -- (nullable NSArray*)omemo_deviceListFromPEPUpdate:(OMEMOModuleNamespace)ns +- (nullable NSArray*)omemo_deviceListFromPEPUpdate { NSXMLElement *event = [self elementForName:@"event" xmlns:XMLNS_PUBSUB_EVENT]; if (!event) { return nil; } From 30004c0d9164fb3769109675301ae82b2fc79327 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 1 Oct 2025 16:15:16 +0200 Subject: [PATCH 43/62] Add callback to inform delegate when outgoing message processing is aborted --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index b43a63626b..8f8006c216 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -21,6 +21,7 @@ public protocol XMPPStanzaContentEncryptionProfile { } @objc public protocol XMPPStanzaContentEncryptionDelegate: NSObjectProtocol { + @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, willNotSend message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope decryptedEnvelope: XMLElement, from message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToDecryptEnvelopeFrom message: XMPPMessage) } @@ -65,7 +66,12 @@ public class XMPPStanzaContentEncryption: XMPPModule { // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: message) { encrypted in - guard let encrypted else { return } + guard let encrypted else { + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryption!(self, willNotSend: message) + } + return + } // The result is appended to the message. message.addChild(encrypted) From 0cb5acf975bd0369471c8106935d2292803717d4 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 2 Oct 2025 13:41:02 +0200 Subject: [PATCH 44/62] Add encryption profile configuration hook --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 8f8006c216..8e44bec760 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -10,6 +10,7 @@ import XMPPFramework #endif public protocol XMPPStanzaContentEncryptionProfile { + func configure(withParent aParent: XMPPStanzaContentEncryption, queue: dispatch_queue_t) func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement /// - Note: The implementation may modify the provided message before invoking the completion handler, for example to assign some stanza identifier for later processing. func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) @@ -41,6 +42,7 @@ public class XMPPStanzaContentEncryption: XMPPModule { public init(profile: XMPPStanzaContentEncryptionProfile, dispatchQueue: DispatchQueue? = nil) { self.profile = profile super.init(dispatchQueue: dispatchQueue) + profile.configure(withParent: self, queue: moduleQueue) } // https://xmpp.org/extensions/xep-0420.html#sending From a5abde6ce23e1209bea326a4ee4efb37795ee775 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 9 Jan 2026 09:01:53 +0100 Subject: [PATCH 45/62] Abort crypto processing if module is deactivated --- .../XEP-0420/XMPPStanzaContentEncryption.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 8e44bec760..291e017459 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -48,6 +48,8 @@ public class XMPPStanzaContentEncryption: XMPPModule { // https://xmpp.org/extensions/xep-0420.html#sending public func sendEncryptedMessage(_ message: XMPPMessage, withSensitiveContent sensitiveContent: [XMLElement]) { performBlock(async: true) { + guard self.beginProcessingEnvelope() else { return } + // TODO: Allow modifying sensitiveContent via multidelegation // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. @@ -98,6 +100,11 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } public func xmppStream(_ sender: XMPPStream, willReceive message: XMPPMessage) -> XMPPMessage? { + guard beginProcessingEnvelope() else { + // Message will not be decrypted and needs to be filtered out + return nil + } + // The recipient of the message decrypts its encrypted payload. profile.decryptEnvelopeXML(from: message) { envelopeXML in self.performBlock { @@ -188,6 +195,16 @@ public struct XMPPStanzaContentEncryptionServerProcessedElements { } } +private extension XMPPStanzaContentEncryption { + func beginProcessingEnvelope() -> Bool { + // Envelope will only be processed if module is active + guard xmppStream != nil else { + return false + } + return true + } +} + private extension XMLElement { func removeAllElements(where shouldBeRemoved: (XMLElement) -> Bool) { guard let childrenIndices = children?.indices else { return } From 61634f4e2f9de0c41b4ed260b7e8fa783aa960fc Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 9 Jan 2026 09:03:07 +0100 Subject: [PATCH 46/62] Track number of processed message envelopes --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 291e017459..be24af017c 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -33,6 +33,7 @@ extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} public class XMPPStanzaContentEncryption: XMPPModule { private let profile: XMPPStanzaContentEncryptionProfile private var serverProcessedElements = XMPPStanzaContentEncryptionServerProcessedElements() + private var envelopesInProgressCount = 0 public var serverProcessedElementsList: [XMPPStanzaContentEncryptionServerProcessedElements.Entry] { get { serverProcessedElements.list } @@ -70,6 +71,10 @@ public class XMPPStanzaContentEncryption: XMPPModule { // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: message) { encrypted in + self.performBlock(async: true) { + self.endProcessingEnvelope() + } + guard let encrypted else { self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in multicast.stanzaContentEncryption!(self, willNotSend: message) @@ -108,6 +113,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { // The recipient of the message decrypts its encrypted payload. profile.decryptEnvelopeXML(from: message) { envelopeXML in self.performBlock { + self.endProcessingEnvelope() if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { // The result is the element containing the element and the affix elements as direct child elements. self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in @@ -201,8 +207,13 @@ private extension XMPPStanzaContentEncryption { guard xmppStream != nil else { return false } + envelopesInProgressCount += 1 return true } + + func endProcessingEnvelope() { + envelopesInProgressCount -= 1 + } } private extension XMLElement { From 7b0588bc2f706f1474b9c7f733f77d9015cdb765 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 9 Jan 2026 09:09:10 +0100 Subject: [PATCH 47/62] Notify delegate when crypto processing finishes upon deactivation --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index be24af017c..42572522a0 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -25,6 +25,7 @@ public protocol XMPPStanzaContentEncryptionProfile { @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, willNotSend message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope decryptedEnvelope: XMLElement, from message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToDecryptEnvelopeFrom message: XMPPMessage) + @objc optional func stanzaContentEncryptionDidFinishProcessingEnvelopes(_ encryption: XMPPStanzaContentEncryption) } extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} @@ -93,6 +94,15 @@ public class XMPPStanzaContentEncryption: XMPPModule { } } } + + // Deactivation override hook is not exposed in any header + @objc func willDeactivate() { + if envelopesInProgressCount == 0 { + multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryptionDidFinishProcessingEnvelopes!(self) + } + } + } } extension XMPPStanzaContentEncryption: XMPPStreamDelegate { @@ -213,6 +223,11 @@ private extension XMPPStanzaContentEncryption { func endProcessingEnvelope() { envelopesInProgressCount -= 1 + if envelopesInProgressCount == 0, xmppStream == nil { + multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryptionDidFinishProcessingEnvelopes!(self) + } + } } } From d7872d723f416a6e6665e589710bca7577cd153d Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 31 Mar 2026 14:03:48 +0200 Subject: [PATCH 48/62] Add missing XMPPMessageArchiving's XMPPStreamDelegate conformance declaration --- Extensions/XEP-0136/XMPPMessageArchiving.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extensions/XEP-0136/XMPPMessageArchiving.h b/Extensions/XEP-0136/XMPPMessageArchiving.h index 716bc58daa..de00f9ea45 100644 --- a/Extensions/XEP-0136/XMPPMessageArchiving.h +++ b/Extensions/XEP-0136/XMPPMessageArchiving.h @@ -9,7 +9,7 @@ * This class provides support for storing message history. * The functionality is formalized in XEP-0136. **/ -@interface XMPPMessageArchiving : XMPPModule +@interface XMPPMessageArchiving : XMPPModule { @protected From 8b7fc898eb953f1c48b5d654f32e93ccfa69a9e5 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Thu, 2 Apr 2026 15:22:15 +0200 Subject: [PATCH 49/62] Move message decryption processing from XMPPStreamDelegate's willReceive to didReceive callback --- .../XEP-0420/XMPPStanzaContentEncryption.swift | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 42572522a0..b40a6dcf4d 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -12,11 +12,7 @@ import XMPPFramework public protocol XMPPStanzaContentEncryptionProfile { func configure(withParent aParent: XMPPStanzaContentEncryption, queue: dispatch_queue_t) func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement - /// - Note: The implementation may modify the provided message before invoking the completion handler, for example to assign some stanza identifier for later processing. func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) - /// - Note: - /// The implementation may modify the provided message, for example to assign some stanza identifier for later processing. - /// However, in order to maintain message processing pipeline consistency, any modifications have to be performed before returning from the method. func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool } @@ -115,14 +111,19 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } public func xmppStream(_ sender: XMPPStream, willReceive message: XMPPMessage) -> XMPPMessage? { + serverProcessedElements.removeIgnoredElementsOutsideEnvelope(fromMessage: message) + return message + } + + public func xmppStream(_ sender: XMPPStream, didReceive message: XMPPMessage) { guard beginProcessingEnvelope() else { // Message will not be decrypted and needs to be filtered out - return nil + return } // The recipient of the message decrypts its encrypted payload. profile.decryptEnvelopeXML(from: message) { envelopeXML in - self.performBlock { + self.performBlock(async: true) { self.endProcessingEnvelope() if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { // The result is the element containing the element and the affix elements as direct child elements. @@ -136,10 +137,6 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } } } - - serverProcessedElements.removeIgnoredElementsOutsideEnvelope(fromMessage: message) - - return message } // https://xmpp.org/extensions/xep-0420.html#receiving From b4b85d5f14624246ce5b7bd5c746a578159edfe4 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 7 Apr 2026 11:37:38 +0200 Subject: [PATCH 50/62] Notify the delegate that the inactive XMPPStanzaContentEncryption module will not decrypt the message --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index b40a6dcf4d..8ec4d0dc18 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -117,7 +117,9 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { public func xmppStream(_ sender: XMPPStream, didReceive message: XMPPMessage) { guard beginProcessingEnvelope() else { - // Message will not be decrypted and needs to be filtered out + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryption!(self, didFailToDecryptEnvelopeFrom: message) + } return } From d41751bd0009a538c57ab26172156b75a2213802 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 7 Apr 2026 11:10:13 +0200 Subject: [PATCH 51/62] Notify the delegate that the inactive XMPPStanzaContentEncryption module will not send the message --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 8ec4d0dc18..2cf7a2592e 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -46,7 +46,12 @@ public class XMPPStanzaContentEncryption: XMPPModule { // https://xmpp.org/extensions/xep-0420.html#sending public func sendEncryptedMessage(_ message: XMPPMessage, withSensitiveContent sensitiveContent: [XMLElement]) { performBlock(async: true) { - guard self.beginProcessingEnvelope() else { return } + guard self.beginProcessingEnvelope() else { + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + multicast.stanzaContentEncryption!(self, willNotSend: message) + } + return + } // TODO: Allow modifying sensitiveContent via multidelegation From 95ac7fc0b0952b6b7e119b5f9b89209be0c88f35 Mon Sep 17 00:00:00 2001 From: Wojciech Nagrodzki Date: Tue, 7 Apr 2026 14:18:59 +0200 Subject: [PATCH 52/62] Add didInsertMessage hook to XMPPMessageArchivingCoreDataStorage --- .../CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m index d4b801e475..7f432e0401 100644 --- a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m @@ -128,6 +128,11 @@ - (void)willInsertMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message // Override hook } +- (void)didInsertMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message +{ + // Override hook +} + - (void)didUpdateMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message { // Override hook @@ -482,6 +487,7 @@ - (void)archiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing xmppStre [archivedMessage willInsertObject]; // Override hook [self willInsertMessage:archivedMessage]; // Override hook [moc insertObject:archivedMessage]; + [self didInsertMessage:archivedMessage]; // Override hook } else { From 36b651359aeeab027c50d8cc415f6a8ac8815c47 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 17 Apr 2026 11:56:34 +0200 Subject: [PATCH 53/62] Revert "Merge pull request #7 from trifork/feature/stanza-content-encryption" This reverts commit 0df6a3dfcaf5b5267d6d1617ab7c6d998c7d3bd3, reversing changes made to 288804346aed951160023be285810fadcd01f9d2. # Conflicts: # Swift/XEP-0420/XMPPStanzaContentEncryption.swift --- .../XMPPStanzaContentEncryption.swift | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 2cf7a2592e..ca4aa5f112 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -21,7 +21,6 @@ public protocol XMPPStanzaContentEncryptionProfile { @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, willNotSend message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope decryptedEnvelope: XMLElement, from message: XMPPMessage) @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToDecryptEnvelopeFrom message: XMPPMessage) - @objc optional func stanzaContentEncryptionDidFinishProcessingEnvelopes(_ encryption: XMPPStanzaContentEncryption) } extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} @@ -30,7 +29,6 @@ extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} public class XMPPStanzaContentEncryption: XMPPModule { private let profile: XMPPStanzaContentEncryptionProfile private var serverProcessedElements = XMPPStanzaContentEncryptionServerProcessedElements() - private var envelopesInProgressCount = 0 public var serverProcessedElementsList: [XMPPStanzaContentEncryptionServerProcessedElements.Entry] { get { serverProcessedElements.list } @@ -46,13 +44,6 @@ public class XMPPStanzaContentEncryption: XMPPModule { // https://xmpp.org/extensions/xep-0420.html#sending public func sendEncryptedMessage(_ message: XMPPMessage, withSensitiveContent sensitiveContent: [XMLElement]) { performBlock(async: true) { - guard self.beginProcessingEnvelope() else { - self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption!(self, willNotSend: message) - } - return - } - // TODO: Allow modifying sensitiveContent via multidelegation // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. @@ -73,10 +64,6 @@ public class XMPPStanzaContentEncryption: XMPPModule { // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: message) { encrypted in - self.performBlock(async: true) { - self.endProcessingEnvelope() - } - guard let encrypted else { self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in multicast.stanzaContentEncryption!(self, willNotSend: message) @@ -95,15 +82,6 @@ public class XMPPStanzaContentEncryption: XMPPModule { } } } - - // Deactivation override hook is not exposed in any header - @objc func willDeactivate() { - if envelopesInProgressCount == 0 { - multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryptionDidFinishProcessingEnvelopes!(self) - } - } - } } extension XMPPStanzaContentEncryption: XMPPStreamDelegate { @@ -121,17 +99,9 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } public func xmppStream(_ sender: XMPPStream, didReceive message: XMPPMessage) { - guard beginProcessingEnvelope() else { - self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption!(self, didFailToDecryptEnvelopeFrom: message) - } - return - } - // The recipient of the message decrypts its encrypted payload. profile.decryptEnvelopeXML(from: message) { envelopeXML in self.performBlock(async: true) { - self.endProcessingEnvelope() if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { // The result is the element containing the element and the affix elements as direct child elements. self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in @@ -215,26 +185,6 @@ public struct XMPPStanzaContentEncryptionServerProcessedElements { } } -private extension XMPPStanzaContentEncryption { - func beginProcessingEnvelope() -> Bool { - // Envelope will only be processed if module is active - guard xmppStream != nil else { - return false - } - envelopesInProgressCount += 1 - return true - } - - func endProcessingEnvelope() { - envelopesInProgressCount -= 1 - if envelopesInProgressCount == 0, xmppStream == nil { - multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryptionDidFinishProcessingEnvelopes!(self) - } - } - } -} - private extension XMLElement { func removeAllElements(where shouldBeRemoved: (XMLElement) -> Bool) { guard let childrenIndices = children?.indices else { return } From f9203eac37ab5dc9117262aa6784801539ff53c0 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 7 May 2026 08:54:53 +0200 Subject: [PATCH 54/62] Add MUC light room delegate callback for outgoing IQ stanzas --- Extensions/XMPPMUCLight/XMPPRoomLight.h | 1 + Extensions/XMPPMUCLight/XMPPRoomLight.m | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Extensions/XMPPMUCLight/XMPPRoomLight.h b/Extensions/XMPPMUCLight/XMPPRoomLight.h index bc763b9150..1dd4402ea3 100644 --- a/Extensions/XMPPMUCLight/XMPPRoomLight.h +++ b/Extensions/XMPPMUCLight/XMPPRoomLight.h @@ -56,6 +56,7 @@ @optional - (void)xmppRoomLight:(nonnull XMPPRoomLight *)sender didReceiveMessage:(nonnull XMPPMessage *)message; +- (void)xmppRoomLight:(nonnull XMPPRoomLight *)sender willSendIQElementWithID:(nonnull NSString *)iqID; - (void)xmppRoomLight:(nonnull XMPPRoomLight *)sender didCreateRoomLight:(nonnull XMPPIQ *)iq; - (void)xmppRoomLight:(nonnull XMPPRoomLight *)sender didFailToCreateRoomLight:(nonnull XMPPIQ *)iq; diff --git a/Extensions/XMPPMUCLight/XMPPRoomLight.m b/Extensions/XMPPMUCLight/XMPPRoomLight.m index 2bed21a9e8..c7af0796fb 100644 --- a/Extensions/XMPPMUCLight/XMPPRoomLight.m +++ b/Extensions/XMPPMUCLight/XMPPRoomLight.m @@ -290,6 +290,7 @@ - (void)createRoomLightWithMembersJID:(nullable NSArray *) members{ selector:@selector(handleCreateRoomLight:withInfo:) timeout:60.0]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; [self->xmppStream sendElement:iq]; }}; @@ -339,6 +340,7 @@ - (void)leaveRoomLight{ selector:@selector(handleLeaveRoomLight:withInfo:) timeout:60.0]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; [self->xmppStream sendElement:iq]; }}; @@ -389,6 +391,8 @@ - (void)addUsers:(nonnull NSArray *)users{ target:self selector:@selector(handleAddUsers:withInfo:) timeout:60.0]; + + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; [self->xmppStream sendElement:iq]; }}; @@ -431,6 +435,7 @@ - (void)fetchMembersList{ selector:@selector(handleFetchMembersListResponse:withInfo:) timeout:60.0]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; [self->xmppStream sendElement:iq]; }}; @@ -481,6 +486,7 @@ - (void)destroyRoom { selector:@selector(handleDestroyRoom:withInfo:) timeout:60.0]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; [self->xmppStream sendElement:iq]; }}; @@ -561,7 +567,8 @@ - (void)changeAffiliations:(nonnull NSArray *)members{ selector:@selector(handleChangeAffiliations:withInfo:) timeout:60.0]; - [self->xmppStream sendElement:iq]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; + [self->xmppStream sendElement:iq]; }}; if (dispatch_get_specific(moduleQueueTag)) @@ -602,7 +609,8 @@ - (void)getConfiguration { selector:@selector(handleGetConfiguration:withInfo:) timeout:60.0]; - [self->xmppStream sendElement:iq]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; + [self->xmppStream sendElement:iq]; }}; if (dispatch_get_specific(moduleQueueTag)) @@ -654,7 +662,8 @@ - (void)setConfiguration:(nonnull NSArray *)configs{ selector:@selector(handleSetConfiguration:withInfo:) timeout:60.0]; - [self->xmppStream sendElement:iq]; + [multicastDelegate xmppRoomLight:self willSendIQElementWithID:iqID]; + [self->xmppStream sendElement:iq]; }}; if (dispatch_get_specific(moduleQueueTag)) From 4c8ac1496a0c4c844d3a3ea8bd67287a3554985c Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 7 May 2026 14:40:12 +0200 Subject: [PATCH 55/62] Introduce override hook to control whether new contact should be created for archived message --- .../XMPPMessageArchivingCoreDataStorage.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m index 7f432e0401..de644e946c 100644 --- a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m @@ -153,6 +153,12 @@ - (void)didUpdateContact:(XMPPMessageArchiving_Contact_CoreDataObject *)contact // Override hook } +- (BOOL)shouldInsertContactForMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message +{ + // Override hook + return YES; +} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Private API //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -506,7 +512,7 @@ - (void)archiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing xmppStre XMPPMessageArchiving_Contact_CoreDataObject *contact = [self contactForMessage:archivedMessage]; XMPPLogVerbose(@"Previous contact: %@", contact); - if (contact == nil) + if (contact == nil && [self shouldInsertContactForMessage:archivedMessage]) { contact = (XMPPMessageArchiving_Contact_CoreDataObject *) [[NSManagedObject alloc] initWithEntity:[self contactEntity:moc] @@ -514,6 +520,11 @@ - (void)archiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing xmppStre didCreateNewContact = YES; } + + if (contact == nil) { + XMPPLogVerbose(@"No contact for archived message"); + return; + } contact.streamBareJidStr = archivedMessage.streamBareJidStr; contact.bareJid = archivedMessage.bareJid; From b84221f5757be72aae69074e937ebaf129b5227e Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 13 May 2026 09:39:21 +0200 Subject: [PATCH 56/62] Decouple encryption profile from XMPP module --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 60 ++++++- .../XMPPStanzaContentEncryption.swift | 170 ++++-------------- .../XMPPStanzaContentEncryptionProfile.swift | 94 ++++++++++ 3 files changed, 189 insertions(+), 135 deletions(-) create mode 100644 Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index 71d860ce87..67bbe87b5f 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -11,13 +11,36 @@ import XMPPFramework extension XMLElement { // https://xmpp.org/extensions/xep-0420.html#example-5 - public static func makeStanzaContentEncryptionEnvelope() -> XMLElement { - XMLElement(name: "envelope", xmlns: "urn:xmpp:sce:1") + public static func makeStanzaContentEncryptionEnvelope(sensitiveElements: [XMLElement]) -> XMLElement { + // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. + let envelope = XMLElement(name: "envelope", xmlns: "urn:xmpp:sce:1") + let content = XMLElement(name: "content") + for sensitiveElement in sensitiveElements { + guard sensitiveElement.name != nil, sensitiveElement.xmlns != nil else { + // Elements in the element MUST be identified using an element name and namespace. + assertionFailure("Encountered element without name or namespace in element") + continue + } + content.addChild(sensitiveElement) + } + envelope.addChild(content) + return envelope } public var isStanzaContentEncryptionEnvelope: Bool { name == "envelope" && xmlns == "urn:xmpp:sce:1" } + + /// - Note: Applications that rely on server processed elements not mentioned in the XEP need to apply their own element filtering on top of what unpacking does. + public func unpackStanzaContentEncryptionEnvelope() -> [XMLElement] { + guard isStanzaContentEncryptionEnvelope, let contentChildren = element(forName: "content")?.children else { return [] } + // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it + return contentChildren.compactMap { + $0 as? XMLElement + } .filter { + !$0.isServerProcessed + } + } } // In order to prevent certain attacks, different affix elements MAY be added as direct child elements of the element. @@ -92,6 +115,39 @@ extension XMLElement { } } +extension XMLElement { + static func makeStanzaContentEncryptionEnvelope(xmlString: String) -> XMLElement? { + guard let envelope = try? XMLElement(xmlString: xmlString), envelope.isStanzaContentEncryptionEnvelope else { + return nil + } + return envelope + } + + // There are certain extension elements which are required to be available to the server in order to do message routing and processing + // Additionally there are some elements that MUST be filtered by the server. + // Allowing for those elements to be included in, and parsed from the encrypted payload would allow a malicious client to perform a number of attacks. + // Contrary to this, other elements are considered sensitive and MUST NOT be available in plaintext outside the element. + var isServerProcessed: Bool { + // Message Processing Hints are addressed to the server and MUST therefore be accessible in plaintext. + if xmlns == "urn:xmpp:hints" { + return true + } + // Sending clients MUST NOT include Stanza-ID elements inside the element, as this would prevent the server from filtering it. + if xmlns == XMPPStanzaIdXmlns, [XMPPStanzaIdElementName, XMPPOriginIdElementName].contains(name) { + return true + } + // The server MUST be able to access the and
elements in order to do message routing, so they MUST NOT be encrypted. + if xmlns == "http://jabber.org/protocol/address" { + return true + } + // The server needs to be able to provide stanza error information + if name == "error", ["jabber:client", "jabber:server"].contains(xmlns) { + return true + } + return false + } +} + private struct StanzaContentEncryptionPaddingGenerator: Sequence, IteratorProtocol { static func randomPadding() -> String { String(StanzaContentEncryptionPaddingGenerator()) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index ca4aa5f112..a66af8e5eb 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -10,7 +10,6 @@ import XMPPFramework #endif public protocol XMPPStanzaContentEncryptionProfile { - func configure(withParent aParent: XMPPStanzaContentEncryption, queue: dispatch_queue_t) func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) @@ -18,69 +17,27 @@ public protocol XMPPStanzaContentEncryptionProfile { } @objc public protocol XMPPStanzaContentEncryptionDelegate: NSObjectProtocol { - @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, willNotSend message: XMPPMessage) - @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didDecryptEnvelope decryptedEnvelope: XMLElement, from message: XMPPMessage) - @objc optional func stanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didFailToDecryptEnvelopeFrom message: XMPPMessage) + @objc optional func xmppStanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEncryptedMessage encryptedMessage: XMPPMessage) } extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} /// A module implementing XMPP stanza content encryption specification as defined in [XEP-0420 version 0.4.1](https://xmpp.org/extensions/attic/xep-0420-0.4.1.html). public class XMPPStanzaContentEncryption: XMPPModule { - private let profile: XMPPStanzaContentEncryptionProfile - private var serverProcessedElements = XMPPStanzaContentEncryptionServerProcessedElements() + private let encryptedElementsNamespace: String - public var serverProcessedElementsList: [XMPPStanzaContentEncryptionServerProcessedElements.Entry] { - get { serverProcessedElements.list } - set { serverProcessedElements.list = newValue } - } - - public init(profile: XMPPStanzaContentEncryptionProfile, dispatchQueue: DispatchQueue? = nil) { - self.profile = profile + public init(encryptedElementsNamespace: String, dispatchQueue: DispatchQueue? = nil) { + self.encryptedElementsNamespace = encryptedElementsNamespace super.init(dispatchQueue: dispatchQueue) - profile.configure(withParent: self, queue: moduleQueue) } +} + +// Override hooks +public extension XMPPStanzaContentEncryption { - // https://xmpp.org/extensions/xep-0420.html#sending - public func sendEncryptedMessage(_ message: XMPPMessage, withSensitiveContent sensitiveContent: [XMLElement]) { - performBlock(async: true) { - // TODO: Allow modifying sensitiveContent via multidelegation - - // In order to send an encrypted message without leaking extension elements, the sender prepares the message by placing the sensitive extension elements inside a element and that inside an element. - let envelope = XMPPElement.makeStanzaContentEncryptionEnvelope() - let content = XMLElement(name: "content") - for sensitiveElement in sensitiveContent { - guard sensitiveElement.name != nil, sensitiveElement.xmlns != nil else { - // Elements in the element MUST be identified using an element name and namespace. - assertionFailure("Encountered element without name or namespace in element") - continue - } - content.addChild(sensitiveElement) - } - envelope.addChild(content) - - // Depending on the encryption-specific SCE-profile, some affix elements are added as child elements of the element. - let finalEnvelope = self.profile.addAffixElemenets(to: envelope, for: message) - - // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. - self.profile.encryptEnvelopeXML(finalEnvelope.xmlString, for: message) { encrypted in - guard let encrypted else { - self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption!(self, willNotSend: message) - } - return - } - - // The result is appended to the message. - message.addChild(encrypted) - - // Since the outer message element does not contain a element the sender appends an unencrypted hint as specified in Message Processing Hints (XEP-0334) [7]. - message.addStorageHint(.store) - - // The message can then be sent to the recipient. - self.performBlock(async: true) { self.xmppStream?.send(message) } - } - } + /// - Note: Applications that rely on server processed elements not mentioned in the XEP will need to override this logic. + @objc func shouldIgnoreElementOutsideEnvelope(_ elementOutsideEnvelope: XMLElement) -> Bool { + elementOutsideEnvelope.xmlns != encryptedElementsNamespace && !elementOutsideEnvelope.isServerProcessed } } @@ -94,102 +51,49 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } public func xmppStream(_ sender: XMPPStream, willReceive message: XMPPMessage) -> XMPPMessage? { - serverProcessedElements.removeIgnoredElementsOutsideEnvelope(fromMessage: message) + // Furthermore the receiving client MUST ignore any extension elements considered as sensitive which are found outside of the element, especially as direct unencrypted child elements of the enclosing stanza. + message.removeElementsRecursive(withPredicate: shouldIgnoreElementOutsideEnvelope(_:)) return message } public func xmppStream(_ sender: XMPPStream, didReceive message: XMPPMessage) { - // The recipient of the message decrypts its encrypted payload. - profile.decryptEnvelopeXML(from: message) { envelopeXML in - self.performBlock(async: true) { - if let envelopeXML, let decryptedEnvelope = self.receiveEncryptedMessage(message, withEnvelopeXML: envelopeXML) { - // The result is the element containing the element and the affix elements as direct child elements. - self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption!(self, didDecryptEnvelope: decryptedEnvelope, from: message) - } - } else { - self.multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in - multicast.stanzaContentEncryption!(self, didFailToDecryptEnvelopeFrom: message) - } - } + if !message.elements(forXmlns: encryptedElementsNamespace).isEmpty { + // The recipient of the message decrypts its encrypted payload. + multicast.invoke(ofType: XMPPStanzaContentEncryptionDelegate.self) { multicast in + // This should eventually lead to XMPPStanzaContentEncryptionProfileDelegate callback invocation + multicast.xmppStanzaContentEncryption!(self, didReceiveEncryptedMessage: message) } } } - - // https://xmpp.org/extensions/xep-0420.html#receiving - private func receiveEncryptedMessage(_ encryptedMessage: XMPPMessage, withEnvelopeXML envelopeXML: String) -> XMLElement? { - // The recipient MUST verify that the decrypted element contains valid XML before processing it any further. Invalid XML must be rejected. - guard let decryptedEnvelope = try? XMLElement(xmlString: envelopeXML), decryptedEnvelope.isStanzaContentEncryptionEnvelope else { - return nil - } - - // Depending on the affix profiles specified by the used encryption protocol, the affix elements are verified to prevent certain attacks from taking place. - guard profile.verifyAffixElements(in: decryptedEnvelope, from: encryptedMessage) else { - return nil +} + +extension XMPPStanzaContentEncryption: XMPPStanzaContentEncryptionProfileDelegate { + public func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, didPrepareEncryptedElement encryptedElement: XMLElement, for message: XMPPMessage) { + guard let xmppStream else { + assertionFailure("Stream not ready to send") + return } - // Afterwards, the extension elements inside the element are checked against the permitted list and any disallowed elements are discarded. - serverProcessedElements.removeDisallowedElements(fromEnvelope: decryptedEnvelope) + // The result is appended to the message. + message.addChild(encryptedElement) - // The following is not implemented as it contradicts section 11. Implementation Notes, which calls to handle encrypted elements explicitly: - // As a last step, the original unencrypted stanza is recreated by replacing the element of the stanza with the elements inside of the element. + // Since the outer message element does not contain a element the sender appends an unencrypted hint as specified in Message Processing Hints (XEP-0334) [7]. + message.addStorageHint(.store) - return decryptedEnvelope - } -} - -public struct XMPPStanzaContentEncryptionServerProcessedElements { - public struct Entry { - public let xmlns: String - public let elementNames: [String] - - public init(xmlns: String, elementNames: [String] = []) { - self.xmlns = xmlns - self.elementNames = elementNames - } - } - - var list = [ - // Message Processing Hints are addressed to the server and MUST therefore be accessible in plaintext. - Entry(xmlns: "urn:xmpp:hints"), - // Sending clients MUST NOT include Stanza-ID elements inside the element, as this would prevent the server from filtering it. - Entry(xmlns: XMPPStanzaIdXmlns, elementNames: [XMPPStanzaIdElementName, XMPPOriginIdElementName]), - // The server MUST be able to access the and
elements in order to do message routing, so they MUST NOT be encrypted. - Entry(xmlns: "http://jabber.org/protocol/address"), - // The server needs to be able to provide stanza error information - Entry(xmlns: "jabber:client", elementNames: ["error"]), Entry(xmlns: "jabber:server", elementNames: ["error"]), - ] - - func removeDisallowedElements(fromEnvelope envelope: XMLElement) { - // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it - envelope.element(forName: "content")?.removeAllElements(where: { isServerProcessed($0) }) - } - - func removeIgnoredElementsOutsideEnvelope(fromMessage message: XMPPMessage) { - // Furthermore the receiving client MUST ignore any extension elements considered as sensitive which are found outside of the element, especially as direct unencrypted child elements of the enclosing stanza. - message.removeAllElements(where: { isSensitive($0) }) - } - - // There are certain extension elements which are required to be available to the server in order to do message routing and processing - // Additionally there are some elements that MUST be filtered by the server. - // Allowing for those elements to be included in, and parsed from the encrypted payload would allow a malicious client to perform a number of attacks. - private func isServerProcessed(_ element: XMLElement) -> Bool { - list.contains(where: { $0.xmlns == element.xmlns && ($0.elementNames.contains(where: { $0 == element.name }) || $0.elementNames.isEmpty) }) - } - - // Contrary to this, other elements are considered sensitive and MUST NOT be available in plaintext outside the element. - private func isSensitive(_ element: XMLElement) -> Bool { - // The specification does enforce any specific format for encrypted content elements which are not considered sensitive themselves - // This implementation allows any element named "encrypted" regardless of namespace - !isServerProcessed(element) && element.name != "encrypted" + // The message can then be sent to the recipient. + xmppStream.send(message) } } private extension XMLElement { - func removeAllElements(where shouldBeRemoved: (XMLElement) -> Bool) { + func removeElementsRecursive(withPredicate shouldBeRemoved: (XMLElement) -> Bool) { guard let childrenIndices = children?.indices else { return } for childIndex in childrenIndices.reversed() { - guard let element = child(at: UInt(childIndex)) as? XMLElement, shouldBeRemoved(element) else { continue } + guard let element = child(at: UInt(childIndex)) as? XMLElement else { continue } + guard shouldBeRemoved(element) else { + element.removeElementsRecursive(withPredicate: shouldBeRemoved) + continue + } removeChild(at: UInt(childIndex)) } } diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift new file mode 100644 index 0000000000..0717debd63 --- /dev/null +++ b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift @@ -0,0 +1,94 @@ +// +// XMPPStanzaContentEncryptionProfile.swift +// XMPPFramework +// +// Created by Piotr Wegrzynek on 12/05/2026. +// + +#if canImport(XMPPFramework) +import XMPPFramework +#endif + +@objc public protocol XMPPStanzaContentEncryptionProfileDelegate: NSObjectProtocol { + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + didPrepareEncryptedElement encryptedElement: XMLElement, + for message: XMPPMessage) + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + didFailToPrepareEncryptedElementFor message: XMPPMessage) + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + didDecryptEnvelopeElement envelopeElement: XMLElement, + from message: XMPPMessage) + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + didFailToHandleEncryptedElementFrom message: XMPPMessage) +} + +public class XMPPStanzaContentEncryptionProfileAbs: NSObject { + private let multicast = GCDMulticastDelegate() + + public final func prepareEncryptedElement(withEmbeddedEnvelope envelopeElement: XMLElement, forOutgoingMessage outgoingMessage: XMPPMessage) { + // Depending on the encryption-specific SCE-profile, some affix elements are added as child elements of the element. + let finalEnvelope = addAffixElemenets(to: envelopeElement, for: outgoingMessage) + + // The element is then serialized into XML and encrypted using the SCE-specific profile of the encryption mechanism in place. + encryptEnvelopeXML(finalEnvelope.xmlString, for: outgoingMessage) { encrypted in + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionProfileDelegate.self) { multicast in + if let encrypted { + multicast.xmppStanzaContentEncryptionProfile!(self, didPrepareEncryptedElement: encrypted, for: outgoingMessage) + } else { + multicast.xmppStanzaContentEncryptionProfile!(self, didFailToPrepareEncryptedElementFor: outgoingMessage) + } + } + } + } + + public final func handleEncryptedElement(fromIncomingMessage incomingMessage: XMPPMessage) { + decryptEnvelopeXML(from: incomingMessage) { envelopeXML in + self.multicast.invoke(ofType: XMPPStanzaContentEncryptionProfileDelegate.self) { multicast in + guard let envelopeXML, + // The recipient MUST verify that the decrypted element contains valid XML before processing it any further. Invalid XML must be rejected. + let decryptedEnvelope = XMLElement.makeStanzaContentEncryptionEnvelope(xmlString: envelopeXML), + // Depending on the affix profiles specified by the used encryption protocol, the affix elements are verified to prevent certain attacks from taking place. + self.verifyAffixElements(in: decryptedEnvelope, from: incomingMessage) + else { + multicast.xmppStanzaContentEncryptionProfile!(self, didFailToHandleEncryptedElementFrom: incomingMessage) + return + } + + // The result is the element containing the element and the affix elements as direct child elements. + multicast.xmppStanzaContentEncryptionProfile!(self, didDecryptEnvelopeElement: decryptedEnvelope, from: incomingMessage) + + // The following is not implemented as it contradicts section 11. Implementation Notes, which calls to handle encrypted elements explicitly: + // As a last step, the original unencrypted stanza is recreated by replacing the element of the stanza with the elements inside of the element. + } + } + } +} + +// Override hooks +public extension XMPPStanzaContentEncryptionProfileAbs { + @objc func add(_ delegate: XMPPStanzaContentEncryptionProfileDelegate, delegateQueue: dispatch_queue_t) { + multicast.add(delegate, delegateQueue: delegateQueue) + } + + @objc func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement { + envelope + } + + @objc func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) { + completion(nil) + } + + @objc func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) { + completion(nil) + } + + @objc func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool { + false + } +} + +extension XMPPStanzaContentEncryptionProfileAbs: XMPPStanzaContentEncryptionDelegate { + public final func xmppStanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEncryptedMessage encryptedMessage: XMPPMessage) { + handleEncryptedElement(fromIncomingMessage: encryptedMessage) + } +} From b31e83b42561f33a4334c9e2eba18593ac837d63 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 13 May 2026 09:45:02 +0200 Subject: [PATCH 57/62] Remove no longer used protocol --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 9 +-------- .../XMPPStanzaContentEncryptionProfile.swift | 14 +++++++------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index a66af8e5eb..02be81e041 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -9,13 +9,6 @@ import XMPPFramework #endif -public protocol XMPPStanzaContentEncryptionProfile { - func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement - func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) - func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) - func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool -} - @objc public protocol XMPPStanzaContentEncryptionDelegate: NSObjectProtocol { @objc optional func xmppStanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEncryptedMessage encryptedMessage: XMPPMessage) } @@ -68,7 +61,7 @@ extension XMPPStanzaContentEncryption: XMPPStreamDelegate { } extension XMPPStanzaContentEncryption: XMPPStanzaContentEncryptionProfileDelegate { - public func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, didPrepareEncryptedElement encryptedElement: XMLElement, for message: XMPPMessage) { + public func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfile, didPrepareEncryptedElement encryptedElement: XMLElement, for message: XMPPMessage) { guard let xmppStream else { assertionFailure("Stream not ready to send") return diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift index 0717debd63..d6326bdd20 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift @@ -10,19 +10,19 @@ import XMPPFramework #endif @objc public protocol XMPPStanzaContentEncryptionProfileDelegate: NSObjectProtocol { - @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfile, didPrepareEncryptedElement encryptedElement: XMLElement, for message: XMPPMessage) - @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfile, didFailToPrepareEncryptedElementFor message: XMPPMessage) - @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfile, didDecryptEnvelopeElement envelopeElement: XMLElement, from message: XMPPMessage) - @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfileAbs, + @objc optional func xmppStanzaContentEncryptionProfile(_ profile: XMPPStanzaContentEncryptionProfile, didFailToHandleEncryptedElementFrom message: XMPPMessage) } -public class XMPPStanzaContentEncryptionProfileAbs: NSObject { +public class XMPPStanzaContentEncryptionProfile: NSObject { private let multicast = GCDMulticastDelegate() public final func prepareEncryptedElement(withEmbeddedEnvelope envelopeElement: XMLElement, forOutgoingMessage outgoingMessage: XMPPMessage) { @@ -65,7 +65,7 @@ public class XMPPStanzaContentEncryptionProfileAbs: NSObject { } // Override hooks -public extension XMPPStanzaContentEncryptionProfileAbs { +public extension XMPPStanzaContentEncryptionProfile { @objc func add(_ delegate: XMPPStanzaContentEncryptionProfileDelegate, delegateQueue: dispatch_queue_t) { multicast.add(delegate, delegateQueue: delegateQueue) } @@ -87,7 +87,7 @@ public extension XMPPStanzaContentEncryptionProfileAbs { } } -extension XMPPStanzaContentEncryptionProfileAbs: XMPPStanzaContentEncryptionDelegate { +extension XMPPStanzaContentEncryptionProfile: XMPPStanzaContentEncryptionDelegate { public final func xmppStanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEncryptedMessage encryptedMessage: XMPPMessage) { handleEncryptedElement(fromIncomingMessage: encryptedMessage) } From dfd8fb48fe4af9d069366918f4160d668a0efab9 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 13 May 2026 09:57:32 +0200 Subject: [PATCH 58/62] Apply review feedback: add missing multicast delegate conformance --- Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift index d6326bdd20..1cc96b479a 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift @@ -22,6 +22,8 @@ import XMPPFramework didFailToHandleEncryptedElementFrom message: XMPPMessage) } +extension GCDMulticastDelegate: XMPPStanzaContentEncryptionProfileDelegate {} + public class XMPPStanzaContentEncryptionProfile: NSObject { private let multicast = GCDMulticastDelegate() From b702670644a40137ff696eb6d8024d9724cc98d8 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Wed, 13 May 2026 11:28:25 +0200 Subject: [PATCH 59/62] Fix access modifiers to allow subclassing in client modules --- .../XMPPStanzaContentEncryption.swift | 6 +++--- .../XMPPStanzaContentEncryptionProfile.swift | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 02be81e041..2769e0bcf6 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -16,7 +16,7 @@ import XMPPFramework extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} /// A module implementing XMPP stanza content encryption specification as defined in [XEP-0420 version 0.4.1](https://xmpp.org/extensions/attic/xep-0420-0.4.1.html). -public class XMPPStanzaContentEncryption: XMPPModule { +open class XMPPStanzaContentEncryption: XMPPModule { private let encryptedElementsNamespace: String public init(encryptedElementsNamespace: String, dispatchQueue: DispatchQueue? = nil) { @@ -26,10 +26,10 @@ public class XMPPStanzaContentEncryption: XMPPModule { } // Override hooks -public extension XMPPStanzaContentEncryption { +extension XMPPStanzaContentEncryption { /// - Note: Applications that rely on server processed elements not mentioned in the XEP will need to override this logic. - @objc func shouldIgnoreElementOutsideEnvelope(_ elementOutsideEnvelope: XMLElement) -> Bool { + @objc open func shouldIgnoreElementOutsideEnvelope(_ elementOutsideEnvelope: XMLElement) -> Bool { elementOutsideEnvelope.xmlns != encryptedElementsNamespace && !elementOutsideEnvelope.isServerProcessed } } diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift index 1cc96b479a..64b6aac887 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift @@ -24,10 +24,10 @@ import XMPPFramework extension GCDMulticastDelegate: XMPPStanzaContentEncryptionProfileDelegate {} -public class XMPPStanzaContentEncryptionProfile: NSObject { +open class XMPPStanzaContentEncryptionProfile: NSObject { private let multicast = GCDMulticastDelegate() - public final func prepareEncryptedElement(withEmbeddedEnvelope envelopeElement: XMLElement, forOutgoingMessage outgoingMessage: XMPPMessage) { + public func prepareEncryptedElement(withEmbeddedEnvelope envelopeElement: XMLElement, forOutgoingMessage outgoingMessage: XMPPMessage) { // Depending on the encryption-specific SCE-profile, some affix elements are added as child elements of the element. let finalEnvelope = addAffixElemenets(to: envelopeElement, for: outgoingMessage) @@ -43,7 +43,7 @@ public class XMPPStanzaContentEncryptionProfile: NSObject { } } - public final func handleEncryptedElement(fromIncomingMessage incomingMessage: XMPPMessage) { + public func handleEncryptedElement(fromIncomingMessage incomingMessage: XMPPMessage) { decryptEnvelopeXML(from: incomingMessage) { envelopeXML in self.multicast.invoke(ofType: XMPPStanzaContentEncryptionProfileDelegate.self) { multicast in guard let envelopeXML, @@ -67,30 +67,30 @@ public class XMPPStanzaContentEncryptionProfile: NSObject { } // Override hooks -public extension XMPPStanzaContentEncryptionProfile { - @objc func add(_ delegate: XMPPStanzaContentEncryptionProfileDelegate, delegateQueue: dispatch_queue_t) { +extension XMPPStanzaContentEncryptionProfile { + @objc open func add(_ delegate: XMPPStanzaContentEncryptionProfileDelegate, delegateQueue: dispatch_queue_t) { multicast.add(delegate, delegateQueue: delegateQueue) } - @objc func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement { + @objc open func addAffixElemenets(to envelope: XMLElement, for message: XMPPMessage) -> XMLElement { envelope } - @objc func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) { + @objc open func encryptEnvelopeXML(_ envelopeXML: String, for message: XMPPMessage, completion: @escaping (XMLElement?) -> Void) { completion(nil) } - @objc func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) { + @objc open func decryptEnvelopeXML(from message: XMPPMessage, completion: @escaping (String?) -> Void) { completion(nil) } - @objc func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool { + @objc open func verifyAffixElements(in envelope: XMLElement, from message: XMPPMessage) -> Bool { false } } extension XMPPStanzaContentEncryptionProfile: XMPPStanzaContentEncryptionDelegate { - public final func xmppStanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEncryptedMessage encryptedMessage: XMPPMessage) { + public func xmppStanzaContentEncryption(_ encryption: XMPPStanzaContentEncryption, didReceiveEncryptedMessage encryptedMessage: XMPPMessage) { handleEncryptedElement(fromIncomingMessage: encryptedMessage) } } From ccf61e3fcd124b0e95b907e34b27ad45ac124ec4 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 28 May 2026 12:29:33 +0200 Subject: [PATCH 60/62] Apply review feedback: cosmetic API adjustments and additional clarifying comments --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 13 +++---------- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 1 + .../XMPPStanzaContentEncryptionProfile.swift | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index 67bbe87b5f..7843a67b18 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -27,12 +27,8 @@ extension XMLElement { return envelope } - public var isStanzaContentEncryptionEnvelope: Bool { - name == "envelope" && xmlns == "urn:xmpp:sce:1" - } - /// - Note: Applications that rely on server processed elements not mentioned in the XEP need to apply their own element filtering on top of what unpacking does. - public func unpackStanzaContentEncryptionEnvelope() -> [XMLElement] { + public func filteredStanzaContentEncryptionEnvelopeContent() -> [XMLElement]? { guard isStanzaContentEncryptionEnvelope, let contentChildren = element(forName: "content")?.children else { return [] } // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it return contentChildren.compactMap { @@ -116,11 +112,8 @@ extension XMLElement { } extension XMLElement { - static func makeStanzaContentEncryptionEnvelope(xmlString: String) -> XMLElement? { - guard let envelope = try? XMLElement(xmlString: xmlString), envelope.isStanzaContentEncryptionEnvelope else { - return nil - } - return envelope + var isStanzaContentEncryptionEnvelope: Bool { + name == "envelope" && xmlns == "urn:xmpp:sce:1" } // There are certain extension elements which are required to be available to the server in order to do message routing and processing diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 2769e0bcf6..22c0b48572 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -17,6 +17,7 @@ extension GCDMulticastDelegate: XMPPStanzaContentEncryptionDelegate {} /// A module implementing XMPP stanza content encryption specification as defined in [XEP-0420 version 0.4.1](https://xmpp.org/extensions/attic/xep-0420-0.4.1.html). open class XMPPStanzaContentEncryption: XMPPModule { + // Incoming messags are considered encrypted if they contain immediate children in this namespace private let encryptedElementsNamespace: String public init(encryptedElementsNamespace: String, dispatchQueue: DispatchQueue? = nil) { diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift index 64b6aac887..c653c8148c 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryptionProfile.swift @@ -48,7 +48,7 @@ open class XMPPStanzaContentEncryptionProfile: NSObject { self.multicast.invoke(ofType: XMPPStanzaContentEncryptionProfileDelegate.self) { multicast in guard let envelopeXML, // The recipient MUST verify that the decrypted element contains valid XML before processing it any further. Invalid XML must be rejected. - let decryptedEnvelope = XMLElement.makeStanzaContentEncryptionEnvelope(xmlString: envelopeXML), + let decryptedEnvelope = try? XMLElement(xmlString: envelopeXML), decryptedEnvelope.isStanzaContentEncryptionEnvelope, // Depending on the affix profiles specified by the used encryption protocol, the affix elements are verified to prevent certain attacks from taking place. self.verifyAffixElements(in: decryptedEnvelope, from: incomingMessage) else { From acebecc603a078c30471385f6e99db5a57fbd794 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 28 May 2026 12:37:14 +0200 Subject: [PATCH 61/62] Apply review feedback: further cosmetic adjustments --- Swift/XEP-0420/XMPPStanzaContentEncryption.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift index 22c0b48572..fe2416aef1 100644 --- a/Swift/XEP-0420/XMPPStanzaContentEncryption.swift +++ b/Swift/XEP-0420/XMPPStanzaContentEncryption.swift @@ -84,11 +84,11 @@ private extension XMLElement { guard let childrenIndices = children?.indices else { return } for childIndex in childrenIndices.reversed() { guard let element = child(at: UInt(childIndex)) as? XMLElement else { continue } - guard shouldBeRemoved(element) else { + if shouldBeRemoved(element) { + removeChild(at: UInt(childIndex)) + } else { element.removeElementsRecursive(withPredicate: shouldBeRemoved) - continue } - removeChild(at: UInt(childIndex)) } } } From 991dcb7c53666229fc738cb00840535426d27d57 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 28 May 2026 12:41:34 +0200 Subject: [PATCH 62/62] Revert unintended API change --- Swift/XEP-0420/XMLElement+XEP_0420.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swift/XEP-0420/XMLElement+XEP_0420.swift b/Swift/XEP-0420/XMLElement+XEP_0420.swift index 7843a67b18..c01d5a10f9 100644 --- a/Swift/XEP-0420/XMLElement+XEP_0420.swift +++ b/Swift/XEP-0420/XMLElement+XEP_0420.swift @@ -28,7 +28,7 @@ extension XMLElement { } /// - Note: Applications that rely on server processed elements not mentioned in the XEP need to apply their own element filtering on top of what unpacking does. - public func filteredStanzaContentEncryptionEnvelopeContent() -> [XMLElement]? { + public func filteredStanzaContentEncryptionEnvelopeContent() -> [XMLElement] { guard isStanzaContentEncryptionEnvelope, let contentChildren = element(forName: "content")?.children else { return [] } // After verifying the integrity of the element, the recipient needs to make sure that no server-processed elements are found inside of it return contentChildren.compactMap {