From 384ad9f03503a6de8c33bbcae1b91dcacbfe1556 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 5 Oct 2022 17:21:48 +0300 Subject: [PATCH 1/3] Add rendezvous service (MSC3886) and ECDH X25519 AES 256 based secure channel creation establishing implementation and simple tests. --- .../Rendezvous/MockRendezvousTransport.swift | 57 ++++++ .../Modules/Rendezvous/RendezvousModels.swift | 37 ++++ .../Rendezvous/RendezvousService.swift | 170 ++++++++++++++++++ .../Rendezvous/RendezvousTransport.swift | 149 +++++++++++++++ .../RendezvousTransportProtocol.swift | 33 ++++ RiotTests/RendezvousServiceTests.swift | 62 +++++++ 6 files changed, 508 insertions(+) create mode 100644 Riot/Modules/Rendezvous/MockRendezvousTransport.swift create mode 100644 Riot/Modules/Rendezvous/RendezvousModels.swift create mode 100644 Riot/Modules/Rendezvous/RendezvousService.swift create mode 100644 Riot/Modules/Rendezvous/RendezvousTransport.swift create mode 100644 Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift create mode 100644 RiotTests/RendezvousServiceTests.swift diff --git a/Riot/Modules/Rendezvous/MockRendezvousTransport.swift b/Riot/Modules/Rendezvous/MockRendezvousTransport.swift new file mode 100644 index 0000000000..2761ea989e --- /dev/null +++ b/Riot/Modules/Rendezvous/MockRendezvousTransport.swift @@ -0,0 +1,57 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MockRendezvousTransport: RendezvousTransportProtocol { + var rendezvousURL: URL? + + private var currentPayload: Data? + + func create(body: T) async -> Result<(), RendezvousTransportError> { + guard let url = URL(string: "rendezvous.mock/1234") else { + fatalError() + } + + rendezvousURL = url + + guard let encodedBody = try? JSONEncoder().encode(body) else { + fatalError() + } + + currentPayload = encodedBody + + return .success(()) + } + + func get() async -> Result { + guard let data = currentPayload else { + fatalError() + } + + return .success(data) + } + + func send(body: T) async -> Result<(), RendezvousTransportError> { + guard let encodedBody = try? JSONEncoder().encode(body) else { + fatalError() + } + + currentPayload = encodedBody + + return .success(()) + } +} diff --git a/Riot/Modules/Rendezvous/RendezvousModels.swift b/Riot/Modules/Rendezvous/RendezvousModels.swift new file mode 100644 index 0000000000..af4dbc50f8 --- /dev/null +++ b/Riot/Modules/Rendezvous/RendezvousModels.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct RendezvousPayload: Codable { + var rendezvous: RendezvousDetails + var user: String +} + +struct RendezvousDetails: Codable { + var transport: RendezvousTransportDetails? + var algorithm: String + var key: String +} + +struct RendezvousTransportDetails: Codable { + var type: String + var uri: String +} + +struct RendezvousMessage: Codable { + var combined: String +} diff --git a/Riot/Modules/Rendezvous/RendezvousService.swift b/Riot/Modules/Rendezvous/RendezvousService.swift new file mode 100644 index 0000000000..370703ea15 --- /dev/null +++ b/Riot/Modules/Rendezvous/RendezvousService.swift @@ -0,0 +1,170 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import CryptoKit +import Combine + +enum RendezvousServiceError: Error { + case invalidInterlocutorKey + case decodingError + case internalError + case channelNotReady + case transportError(RendezvousTransportError) +} + +enum RendezvousServiceCallback { + case error(RendezvousServiceError) +} + +enum RendezvousChannelAlgorithm: String { + case ECDH_V1 = "m.rendezvous.v1.x25519-aes-sha256" +} + +@MainActor +class RendezvousService { + private let transport: RendezvousTransportProtocol + private let privateKey: Curve25519.KeyAgreement.PrivateKey + + private var interlocutorPublicKey: Curve25519.KeyAgreement.PublicKey? + private var symmetricKey: SymmetricKey? + + init(transport: RendezvousTransportProtocol) { + self.transport = transport + self.privateKey = Curve25519.KeyAgreement.PrivateKey() + } + + func createRendezvous() async -> Result<(), RendezvousServiceError> { + let publicKeyString = self.privateKey.publicKey.rawRepresentation.base64EncodedString() + let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, + key: publicKeyString) + + switch await transport.create(body: payload) { + case .failure(let transportError): + return .failure(.transportError(transportError)) + case .success: + return .success(()) + } + } + + func waitForInterlocutor() async -> Result<(), RendezvousServiceError> { + switch await transport.get() { + case .failure(let error): + return .failure(.transportError(error)) + case .success(let data): + guard let response = try? JSONDecoder().decode(RendezvousDetails.self, from: data) else { + return .failure(.decodingError) + } + + guard let interlocutorPublicKeyData = Data(base64Encoded: response.key), + let interlocutorPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: interlocutorPublicKeyData) else { + return .failure(.invalidInterlocutorKey) + } + + self.interlocutorPublicKey = interlocutorPublicKey + + guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: interlocutorPublicKey) else { + return .failure(.internalError) + } + + self.symmetricKey = generateSymmetricKeyFrom(sharedSecret: sharedSecret) + + return .success(()) + } + } + + func joinRendezvous() async -> Result<(), RendezvousServiceError> { + guard case let .success(data) = await transport.get() else { + return .failure(.internalError) + } + + guard let response = try? JSONDecoder().decode(RendezvousDetails.self, from: data) else { + return .failure(.decodingError) + } + + guard let interlocutorPublicKeyData = Data(base64Encoded: response.key), + let interlocutorPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: interlocutorPublicKeyData) else { + return .failure(.invalidInterlocutorKey) + } + + let publicKeyString = self.privateKey.publicKey.rawRepresentation.base64EncodedString() + let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, + key: publicKeyString) + + guard case .success = await transport.send(body: payload) else { + return .failure(.internalError) + } + + // Channel established + guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: interlocutorPublicKey) else { + return .failure(.internalError) + } + + self.symmetricKey = generateSymmetricKeyFrom(sharedSecret: sharedSecret) + + return .success(()) + } + + func send(data: Data) async -> Result<(), RendezvousServiceError> { + guard let symmetricKey = symmetricKey else { + return .failure(.channelNotReady) + } + + guard let sealedBox = try? AES.GCM.seal(data, using: symmetricKey), + let combinedData = sealedBox.combined else { + return .failure(.internalError) + } + + let body = RendezvousMessage(combined: combinedData.base64EncodedString()) + + switch await transport.send(body: body) { + case .failure(let transportError): + return .failure(.transportError(transportError)) + case .success: + return .success(()) + } + } + + func receive() async -> Result { + guard let symmetricKey = symmetricKey else { + return .failure(.channelNotReady) + } + + switch await transport.get() { + case.failure(let transportError): + return .failure(.transportError(transportError)) + case .success(let data): + guard let response = try? JSONDecoder().decode(RendezvousMessage.self, from: data) else { + return .failure(.decodingError) + } + + guard let combinedData = Data(base64Encoded: response.combined), + let sealedBox = try? AES.GCM.SealedBox(combined: combinedData), + let messageData = try? AES.GCM.open(sealedBox, using: symmetricKey) else { + return .failure(.decodingError) + } + + return .success(messageData) + } + } + + // MARK: - Private + + private func generateSymmetricKeyFrom(sharedSecret: SharedSecret) -> SymmetricKey { + let salt = Data(repeating: 0, count: 8) + return sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(), outputByteCount: 32) + } +} diff --git a/Riot/Modules/Rendezvous/RendezvousTransport.swift b/Riot/Modules/Rendezvous/RendezvousTransport.swift new file mode 100644 index 0000000000..6ea923d635 --- /dev/null +++ b/Riot/Modules/Rendezvous/RendezvousTransport.swift @@ -0,0 +1,149 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class RendezvousTransport: RendezvousTransportProtocol { + private let baseURL: URL + + private var currentEtag: String? + + private(set) var rendezvousURL: URL? { + didSet { + self.currentEtag = nil + } + } + + init(baseURL: URL, rendezvousURL: URL? = nil) { + self.baseURL = baseURL + self.rendezvousURL = rendezvousURL + } + + func get() async -> Result { + guard let url = rendezvousURL else { + return .failure(.rendezvousURLInvalid) + } + + // Keep trying until resource changed + while true { + var request = URLRequest(url: url) + request.httpMethod = "GET" + + request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + if let etag = currentEtag { + request.addValue(etag, forHTTPHeaderField: "If-None-Match") + } + + // Newer swift concurrency api unavailable due to iOS 14 support + let result: Result = await withCheckedContinuation { continuation in + URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, + let response = response, + let httpURLResponse = response as? HTTPURLResponse else { + continuation.resume(returning: .failure(.networkError)) + return + } + + // Return empty data from here if unchanged so that the external while can continue + if httpURLResponse.statusCode == 404 { + continuation.resume(returning: .failure(.rendezvousCancelled)) + } else if httpURLResponse.statusCode == 304 { + continuation.resume(returning: .success(nil)) + } else if httpURLResponse.statusCode == 200 { + if httpURLResponse.allHeaderFields["Content-Type"] as? String != "application/json" { + continuation.resume(returning: .success(nil)) + } else { + if let etag = httpURLResponse.allHeaderFields["Etag"] as? String { + self.currentEtag = etag + } + + continuation.resume(returning: .success(data)) + } + } + }.resume() + } + + switch result { + case .failure(let error): + return .failure(error) + case .success(let data): + guard let data = data else { + continue + } + + return .success(data) + } + } + } + + func create(body: T) async -> Result<(), RendezvousTransportError> { + switch await send(body: body, url: baseURL, usingMethod: "POST") { + case .failure(let error): + return .failure(error) + case .success(let response): + guard let rendezvousIdentifier = response.allHeaderFields["Location"] as? String else { + return .failure(.networkError) + } + + rendezvousURL = baseURL.appendingPathComponent(rendezvousIdentifier) + + return .success(()) + } + } + + func send(body: T) async -> Result<(), RendezvousTransportError> { + guard let url = rendezvousURL else { + return .failure(.rendezvousURLInvalid) + } + + switch await send(body: body, url: url, usingMethod: "PUT") { + case .failure(let error): + return .failure(error) + case .success: + return .success(()) + } + } + + // MARK: - Private + + private func send(body: T, url: URL, usingMethod method: String) async -> Result { + guard let body = try? JSONEncoder().encode(body) else { + return .failure(.encodingError) + } + + var request = URLRequest(url: url) + request.httpMethod = method + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + request.httpBody = body + + return await withCheckedContinuation { continuation in + URLSession.shared.dataTask(with: request) { data, response, error in + guard let httpURLResponse = response as? HTTPURLResponse else { + continuation.resume(returning: .failure(.networkError)) + return + } + + if let etag = httpURLResponse.allHeaderFields["Etag"] as? String { + self.currentEtag = etag + } + + continuation.resume(returning: .success(httpURLResponse)) + }.resume() + } + } +} diff --git a/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift b/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift new file mode 100644 index 0000000000..6aea032b47 --- /dev/null +++ b/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift @@ -0,0 +1,33 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum RendezvousTransportError: Error { + case rendezvousURLInvalid + case encodingError + case networkError + case rendezvousCancelled +} + +@MainActor +protocol RendezvousTransportProtocol { + var rendezvousURL: URL? { get } + + func create(body: T) async -> Result<(), RendezvousTransportError> + func get() async -> Result + func send(body: T) async -> Result<(), RendezvousTransportError> +} diff --git a/RiotTests/RendezvousServiceTests.swift b/RiotTests/RendezvousServiceTests.swift new file mode 100644 index 0000000000..cd3a9b0ddd --- /dev/null +++ b/RiotTests/RendezvousServiceTests.swift @@ -0,0 +1,62 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Element + +@MainActor +class RendezvousServiceTests: XCTestCase { + func testEnd2End() async { + let mockTransport = MockRendezvousTransport() + + let aliceService = RendezvousService(transport: mockTransport) + + guard case .success = await aliceService.createRendezvous() else { + XCTFail("Rendezvous creation failed") + return + } + + XCTAssertNotNil(mockTransport.rendezvousURL) + + let bobService = RendezvousService(transport: mockTransport) + + guard case .success = await bobService.joinRendezvous() else { + XCTFail("Bob failed to join") + return + } + + guard case .success = await aliceService.waitForInterlocutor() else { + XCTFail("Alice failed to establish connection") + return + } + + guard let messageData = "Hello from alice".data(using: .utf8) else { + fatalError() + } + + guard case .success = await aliceService.send(data: messageData) else { + XCTFail("Alice failed to send message") + return + } + + guard case .success(let data) = await bobService.receive() else { + XCTFail("Bob failed to receive message") + return + } + + XCTAssertEqual(messageData, data) + } +} From c93f05be4b83c7ad11bc1d88462e17f50b31c821 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 6 Oct 2022 14:43:36 +0300 Subject: [PATCH 2/3] Implement cross platform AES encryption support; add documentation --- .../Modules/Rendezvous/RendezvousModels.swift | 3 +- .../Rendezvous/RendezvousService.swift | 64 +++++++++++++++---- .../Rendezvous/RendezvousTransport.swift | 13 ++-- .../RendezvousTransportProtocol.swift | 10 +++ 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/Riot/Modules/Rendezvous/RendezvousModels.swift b/Riot/Modules/Rendezvous/RendezvousModels.swift index af4dbc50f8..24edbf1cfd 100644 --- a/Riot/Modules/Rendezvous/RendezvousModels.swift +++ b/Riot/Modules/Rendezvous/RendezvousModels.swift @@ -33,5 +33,6 @@ struct RendezvousTransportDetails: Codable { } struct RendezvousMessage: Codable { - var combined: String + var iv: String + var ciphertext: String } diff --git a/Riot/Modules/Rendezvous/RendezvousService.swift b/Riot/Modules/Rendezvous/RendezvousService.swift index 370703ea15..84583a583a 100644 --- a/Riot/Modules/Rendezvous/RendezvousService.swift +++ b/Riot/Modules/Rendezvous/RendezvousService.swift @@ -26,14 +26,12 @@ enum RendezvousServiceError: Error { case transportError(RendezvousTransportError) } -enum RendezvousServiceCallback { - case error(RendezvousServiceError) -} - +/// Algorithm name as per MSC3903 enum RendezvousChannelAlgorithm: String { - case ECDH_V1 = "m.rendezvous.v1.x25519-aes-sha256" + case ECDH_V1 = "m.rendezvous.v1.curve25519-aes-sha256" } +/// Allows communication through a secure channel. Based on MSC3886 and MSC3903 @MainActor class RendezvousService { private let transport: RendezvousTransportProtocol @@ -47,6 +45,7 @@ class RendezvousService { self.privateKey = Curve25519.KeyAgreement.PrivateKey() } + /// Creates a new rendezvous endpoint and publishes the creator's public key func createRendezvous() async -> Result<(), RendezvousServiceError> { let publicKeyString = self.privateKey.publicKey.rawRepresentation.base64EncodedString() let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, @@ -60,6 +59,8 @@ class RendezvousService { } } + /// After creation we need to wait for the pair to publish its public key as well + /// At the end of this a symmetric key will be available for encryption func waitForInterlocutor() async -> Result<(), RendezvousServiceError> { switch await transport.get() { case .failure(let error): @@ -86,6 +87,8 @@ class RendezvousService { } } + /// Joins an existing rendezvous and publishes the joiner's public key + /// At the end of this a symmetric key will be available for encryption func joinRendezvous() async -> Result<(), RendezvousServiceError> { guard case let .success(data) = await transport.get() else { return .failure(.internalError) @@ -100,6 +103,8 @@ class RendezvousService { return .failure(.invalidInterlocutorKey) } + self.interlocutorPublicKey = interlocutorPublicKey + let publicKeyString = self.privateKey.publicKey.rawRepresentation.base64EncodedString() let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, key: publicKeyString) @@ -118,17 +123,28 @@ class RendezvousService { return .success(()) } + /// Send arbitrary data over the secure channel + /// This will use the previously generated symmetric key to AES encrypt the payload + /// - Parameter data: the data to be encrypted and sent + /// - Returns: nothing if succeeded or a RendezvousServiceError failure func send(data: Data) async -> Result<(), RendezvousServiceError> { guard let symmetricKey = symmetricKey else { return .failure(.channelNotReady) } - - guard let sealedBox = try? AES.GCM.seal(data, using: symmetricKey), - let combinedData = sealedBox.combined else { + + // Generate a custom random 256 bit nonce/iv as per MSC3903. The default one is 96 bit. + guard let nonce = try? AES.GCM.Nonce(data: generateRandomData(ofLength: 32)), + let sealedBox = try? AES.GCM.seal(data, using: symmetricKey, nonce: nonce) else { return .failure(.internalError) } - let body = RendezvousMessage(combined: combinedData.base64EncodedString()) + // The resulting cipher text needs to contain both the message and the authentication tag + // in order to play nicely with other platforms + var ciphertext = sealedBox.ciphertext + ciphertext.append(contentsOf: sealedBox.tag) + + let body = RendezvousMessage(iv: Data(nonce).base64EncodedString(), + ciphertext: ciphertext.base64EncodedString()) switch await transport.send(body: body) { case .failure(let transportError): @@ -138,6 +154,9 @@ class RendezvousService { } } + + /// Waits for and returns newly available rendezvous channel data + /// - Returns: The unencrypted data or a RendezvousServiceError func receive() async -> Result { guard let symmetricKey = symmetricKey else { return .failure(.channelNotReady) @@ -151,8 +170,17 @@ class RendezvousService { return .failure(.decodingError) } - guard let combinedData = Data(base64Encoded: response.combined), - let sealedBox = try? AES.GCM.SealedBox(combined: combinedData), + guard let ciphertextData = Data(base64Encoded: response.ciphertext), + let nonceData = Data(base64Encoded: response.iv), + let nonce = try? AES.GCM.Nonce(data: nonceData) else { + return .failure(.decodingError) + } + + // Split the ciphertext into the message and authentication tag data + let messageData = ciphertextData.dropLast(16) // The last 16 bytes are the tag + let tagData = ciphertextData.dropFirst(messageData.count) + + guard let sealedBox = try? AES.GCM.SealedBox(nonce: nonce, ciphertext: messageData, tag: tagData), let messageData = try? AES.GCM.open(sealedBox, using: symmetricKey) else { return .failure(.decodingError) } @@ -164,7 +192,21 @@ class RendezvousService { // MARK: - Private private func generateSymmetricKeyFrom(sharedSecret: SharedSecret) -> SymmetricKey { + // MSC3903 asks for a 8 zero byte salt when deriving the keys let salt = Data(repeating: 0, count: 8) return sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(), outputByteCount: 32) } + + private func generateRandomData(ofLength length: Int) -> Data { + var data = Data(count: length) + _ = data.withUnsafeMutableBytes { pointer -> Int32 in + if let baseAddress = pointer.baseAddress { + return SecRandomCopyBytes(kSecRandomDefault, length, baseAddress) + } + + return 0 + } + + return data + } } diff --git a/Riot/Modules/Rendezvous/RendezvousTransport.swift b/Riot/Modules/Rendezvous/RendezvousTransport.swift index 6ea923d635..40b7db2cb1 100644 --- a/Riot/Modules/Rendezvous/RendezvousTransport.swift +++ b/Riot/Modules/Rendezvous/RendezvousTransport.swift @@ -63,15 +63,12 @@ class RendezvousTransport: RendezvousTransportProtocol { } else if httpURLResponse.statusCode == 304 { continuation.resume(returning: .success(nil)) } else if httpURLResponse.statusCode == 200 { - if httpURLResponse.allHeaderFields["Content-Type"] as? String != "application/json" { - continuation.resume(returning: .success(nil)) - } else { - if let etag = httpURLResponse.allHeaderFields["Etag"] as? String { - self.currentEtag = etag - } - - continuation.resume(returning: .success(data)) + // The resouce changed, update the etag + if let etag = httpURLResponse.allHeaderFields["Etag"] as? String { + self.currentEtag = etag } + + continuation.resume(returning: .success(data)) } }.resume() } diff --git a/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift b/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift index 6aea032b47..4c608ace83 100644 --- a/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift +++ b/Riot/Modules/Rendezvous/RendezvousTransportProtocol.swift @@ -23,11 +23,21 @@ enum RendezvousTransportError: Error { case rendezvousCancelled } +/// HTTP based MSC3886 channel implementation @MainActor protocol RendezvousTransportProtocol { + /// The current rendezvous endpoint. + /// Automatically assigned after a successful creation var rendezvousURL: URL? { get } + /// Creates a new rendezvous point containing the body + /// - Parameter body: arbitrary data to publish on the rendevous + /// - Returns:a transport error in case of failure func create(body: T) async -> Result<(), RendezvousTransportError> + + /// Waits for and returns newly availalbe rendezvous data func get() async -> Result + + /// Publishes new rendezvous data func send(body: T) async -> Result<(), RendezvousTransportError> } From 5b2a0cc7ea71d3956163887535dfa7dba0abae40 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 6 Oct 2022 16:07:38 +0300 Subject: [PATCH 3/3] Add changelog --- changelog.d/pr-6806.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-6806.feature diff --git a/changelog.d/pr-6806.feature b/changelog.d/pr-6806.feature new file mode 100644 index 0000000000..a5308aeef3 --- /dev/null +++ b/changelog.d/pr-6806.feature @@ -0,0 +1 @@ +Added RendezvousService and secure channel establishment implementation \ No newline at end of file