Skip to content

Commit bcdb1d7

Browse files
committed
refactor: implement key package upload endpoint in WireNetwork - WPB-23048
1 parent cfd977d commit bcdb1d7

18 files changed

+300
-0
lines changed

WireNetwork/Sources/WireNetwork/APIs/Rest/MLSAPI/MLSAPI.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ public protocol MLSAPI {
3535

3636
func postCommitBundle(_ bundle: CommitBundle) async throws -> [UpdateEvent]
3737

38+
/// Upload MLS key packages for a client.
39+
///
40+
/// - Parameters:
41+
/// - clientID: The client ID to upload key packages for.
42+
/// - keyPackages: The key packages to upload.
43+
///
44+
/// Available from ``APIVersion`` v5.
45+
///
46+
47+
func uploadKeyPackages(clientID: String, keyPackages: KeyPackageUpload) async throws
48+
3849
/// Reset an MLS Conversation to epoch 0
3950
/// - Parameters:
4051
/// - epoch: current epoch

WireNetwork/Sources/WireNetwork/APIs/Rest/MLSAPI/MLSAPIError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public enum MLSAPIError: Error, Equatable {
6262

6363
case mlsProtocolError(message: String)
6464

65+
/// Key package credential does not match qualified client ID
66+
67+
case mlsIdentityMismatch
68+
6569
/// The group ID version of the conversation is not supported by one of the federated backends
6670

6771
case mlsGroupIdNotSupported(message: String)

WireNetwork/Sources/WireNetwork/APIs/Rest/MLSAPI/MLSAPIV0.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ class MLSAPIV0: MLSAPI, VersionedAPI {
4242
throw MLSAPIError.unsupportedEndpointForAPIVersion
4343
}
4444

45+
func uploadKeyPackages(clientID: String, keyPackages: KeyPackageUpload) async throws {
46+
throw MLSAPIError.unsupportedEndpointForAPIVersion
47+
}
48+
4549
func resetMLSConversation(epoch: UInt64, groupID: String) async throws {
4650
throw MLSAPIError.unsupportedEndpointForAPIVersion
4751
}

WireNetwork/Sources/WireNetwork/APIs/Rest/MLSAPI/MLSAPIV5.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,45 @@ class MLSAPIV5: MLSAPIV4 {
8585

8686
}
8787

88+
override func uploadKeyPackages(clientID: String, keyPackages: KeyPackageUpload) async throws {
89+
let body = try JSONEncoder.defaultEncoder.encode(keyPackages.toNetworkModel())
90+
91+
let path = "\(pathPrefix)/mls/key-packages/self/\(clientID)"
92+
93+
let request = try URLRequestBuilder(path: path)
94+
.withMethod(.post)
95+
.withBody(body, contentType: .json)
96+
.build()
97+
98+
let (data, response) = try await apiService.executeRequest(
99+
request,
100+
requiringAccessToken: true
101+
)
102+
103+
return try ResponseParser()
104+
.success(
105+
code: .created
106+
)
107+
.failure(
108+
code: .badRequest,
109+
error: MLSAPIError.invalidRequestBody
110+
)
111+
.failure(
112+
code: .badRequest,
113+
label: "mls-protocol-error",
114+
error: MLSAPIError.mlsProtocolError(message: "")
115+
)
116+
.failure(
117+
code: .forbidden,
118+
label: "mls-identity-mismatch",
119+
error: MLSAPIError.mlsIdentityMismatch
120+
)
121+
.parse(
122+
code: response.statusCode,
123+
data: data
124+
)
125+
}
126+
88127
}
89128

90129
private struct BackendMLSPublicKeysResponseV5: Decodable, ToAPIModelConvertible {
@@ -107,3 +146,23 @@ struct CommitBundleResponseV5: Decodable, ToAPIModelConvertible {
107146
}
108147

109148
}
149+
150+
struct KeyPackageUploadV0: Equatable, Sendable, Encodable {
151+
152+
enum CodingKeys: String, CodingKey {
153+
case keyPackages = "key_packages"
154+
}
155+
156+
let keyPackages: [String]
157+
158+
}
159+
160+
extension KeyPackageUpload {
161+
162+
func toNetworkModel() -> KeyPackageUploadV0 {
163+
KeyPackageUploadV0(
164+
keyPackages: keyPackages.map(\.base64EncodedData)
165+
)
166+
}
167+
168+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
21+
/// A base64-encoded MLS key package.
22+
23+
public struct KeyPackage: Equatable, Sendable {
24+
25+
/// The base64-encoded key package data.
26+
27+
public let base64EncodedData: String
28+
29+
/// Create a new `KeyPackage`.
30+
///
31+
/// - Parameter base64EncodedData: The base64-encoded key package data.
32+
33+
public init(base64EncodedData: String) {
34+
self.base64EncodedData = base64EncodedData
35+
}
36+
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
21+
/// Payload to upload MLS key packages.
22+
23+
public struct KeyPackageUpload: Equatable, Sendable {
24+
25+
/// The list of base64-encoded key packages to upload.
26+
27+
public let keyPackages: [KeyPackage]
28+
29+
/// Create a new `KeyPackageUpload`.
30+
///
31+
/// - Parameter keyPackages: The list of key packages to upload.
32+
33+
public init(keyPackages: [KeyPackage]) {
34+
self.keyPackages = keyPackages
35+
}
36+
37+
}

WireNetwork/Tests/WireNetworkTests/APIs/Rest/MLSAPI/MLSAPITests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,86 @@ final class MLSAPITests: XCTestCase {
222222
try await api.resetMLSConversation(epoch: Scaffolding.epoch, groupID: Scaffolding.groupID)
223223
}
224224

225+
// MARK: - Upload key packages
226+
227+
func testUploadKeyPackagesRequest() async throws {
228+
// Given
229+
let apiVersions = APIVersion.v5.andNextVersions
230+
231+
// Then
232+
try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in
233+
// When
234+
_ = try await sut.uploadKeyPackages(
235+
clientID: Scaffolding.clientID,
236+
keyPackages: Scaffolding.keyPackageUpload
237+
)
238+
}
239+
}
240+
241+
func testUploadKeyPackages_SuccessResponse_201_V5_And_Next_Versions() async throws {
242+
// Given
243+
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
244+
let testedVersions = APIVersion.v5.andNextVersions
245+
246+
for version in testedVersions {
247+
let apiService = MockAPIServiceProtocol.withResponses([
248+
(.created, nil)
249+
])
250+
let sut = version.buildAPI(apiService: apiService)
251+
252+
taskGroup.addTask {
253+
// When
254+
try await sut.uploadKeyPackages(
255+
clientID: Scaffolding.clientID,
256+
keyPackages: Scaffolding.keyPackageUpload
257+
)
258+
}
259+
260+
for try await _ in taskGroup {
261+
// Then - no assertion needed, just checking it doesn't throw
262+
}
263+
}
264+
}
265+
}
266+
267+
func testUploadKeyPackages_givenV5AndProtocolErrorResponse() async throws {
268+
// Given
269+
let apiService = MockAPIServiceProtocol.withError(
270+
statusCode: .badRequest,
271+
label: "mls-protocol-error"
272+
)
273+
274+
let api = MLSAPIV5(apiService: apiService)
275+
276+
// Then
277+
await XCTAssertThrowsErrorAsync(MLSAPIError.mlsProtocolError(message: "")) {
278+
// When
279+
try await api.uploadKeyPackages(
280+
clientID: Scaffolding.clientID,
281+
keyPackages: Scaffolding.keyPackageUpload
282+
)
283+
}
284+
}
285+
286+
func testUploadKeyPackages_givenV5AndIdentityMismatchErrorResponse() async throws {
287+
// Given
288+
let apiService = MockAPIServiceProtocol.withError(
289+
statusCode: .forbidden,
290+
label: "mls-identity-mismatch"
291+
)
292+
293+
let api = MLSAPIV5(apiService: apiService)
294+
295+
// Then
296+
await XCTAssertThrowsErrorAsync(MLSAPIError.mlsIdentityMismatch) {
297+
// When
298+
try await api.uploadKeyPackages(
299+
clientID: Scaffolding.clientID,
300+
keyPackages: Scaffolding.keyPackageUpload
301+
)
302+
}
303+
}
304+
225305
}
226306

227307
private extension APIVersion {
@@ -250,4 +330,17 @@ private enum Scaffolding {
250330
UpdateEvent.unknown(eventType: "some event")
251331
]
252332

333+
static let clientID = "60f85e4b15ad3786"
334+
335+
static let keyPackageUpload = KeyPackageUpload(
336+
keyPackages: [
337+
KeyPackage(
338+
base64EncodedData: "pQABARn//wKhAFggwO2Any+CjiGP8XFYrY67zHPvLgp+ysY5k7vci57aaLwDoQChAFggQU/vrXc9MrQxPNubQz4NI0uNtF6qdJ0J0mF9XB2f/GEEY="
339+
),
340+
KeyPackage(
341+
base64EncodedData: "pQABARn//wKhAFgg0C2BN+Mxl7dLoDHNx7ZgUE7MR6hEqTmhoQrLmR5MQqYDoQChAFggJQvUqsCdqZ8o4s+OkSlRDPAf8DPQW25uG0+MvxWZxF4E="
342+
)
343+
]
344+
)
345+
253346
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
curl \
2+
--request POST \
3+
--header "Content-Type: application/json" \
4+
--data "{\"key_packages\":[\"pQABARn\/\/wKhAFggwO2Any+CjiGP8XFYrY67zHPvLgp+ysY5k7vci57aaLwDoQChAFggQU\/vrXc9MrQxPNubQz4NI0uNtF6qdJ0J0mF9XB2f\/GEEY=\",\"pQABARn\/\/wKhAFgg0C2BN+Mxl7dLoDHNx7ZgUE7MR6hEqTmhoQrLmR5MQqYDoQChAFggJQvUqsCdqZ8o4s+OkSlRDPAf8DPQW25uG0+MvxWZxF4E=\"]}" \
5+
"/v10/mls/key-packages/self/60f85e4b15ad3786"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
curl \
2+
--request POST \
3+
--header "Content-Type: application/json" \
4+
--data "{\"key_packages\":[\"pQABARn\/\/wKhAFggwO2Any+CjiGP8XFYrY67zHPvLgp+ysY5k7vci57aaLwDoQChAFggQU\/vrXc9MrQxPNubQz4NI0uNtF6qdJ0J0mF9XB2f\/GEEY=\",\"pQABARn\/\/wKhAFgg0C2BN+Mxl7dLoDHNx7ZgUE7MR6hEqTmhoQrLmR5MQqYDoQChAFggJQvUqsCdqZ8o4s+OkSlRDPAf8DPQW25uG0+MvxWZxF4E=\"]}" \
5+
"/v11/mls/key-packages/self/60f85e4b15ad3786"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
curl \
2+
--request POST \
3+
--header "Content-Type: application/json" \
4+
--data "{\"key_packages\":[\"pQABARn\/\/wKhAFggwO2Any+CjiGP8XFYrY67zHPvLgp+ysY5k7vci57aaLwDoQChAFggQU\/vrXc9MrQxPNubQz4NI0uNtF6qdJ0J0mF9XB2f\/GEEY=\",\"pQABARn\/\/wKhAFgg0C2BN+Mxl7dLoDHNx7ZgUE7MR6hEqTmhoQrLmR5MQqYDoQChAFggJQvUqsCdqZ8o4s+OkSlRDPAf8DPQW25uG0+MvxWZxF4E=\"]}" \
5+
"/v12/mls/key-packages/self/60f85e4b15ad3786"

0 commit comments

Comments
 (0)