From 7c359a1d56b1394d68287ef14b1f776c044579e9 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 17 Jan 2026 09:44:22 -0800 Subject: [PATCH 01/14] address warnings --- Sources/DistributedMLS/Group/DiGroup.swift | 4 +--- Sources/DistributedMLS/Group/SendChannel.swift | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 51d504f..33f270e 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -199,8 +199,6 @@ extension DiMLS.DiGroup { let input = try prepareCommit() return try commit(input: input, sender: sender) - - throw DiMLSError.notImplemented } private func commit( @@ -293,7 +291,7 @@ extension DiMLS.DiGroup { let senderReferenceId = try welcome.senderReferenceId assert(!totalGroup.members.keys.contains(senderReferenceId)) - guard totalGroup.canAdd(try welcome.senderReferenceId) == nil else { + guard totalGroup.canAdd(try welcome.senderReferenceId) else { throw DiMLSError.duplicateSendGroup } throw DiMLSError.notImplemented diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index a65164d..241fdae 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -115,9 +115,9 @@ extension LazySendChannel { case .ready(let r): try .ready(r.archive) case .queued: - try .queued + .queued case .creating: - try .queued + .queued } } } From fac20b9778a2eafe1fe88f5068b274b507a91632 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 17 Jan 2026 09:46:06 -0800 Subject: [PATCH 02/14] remove unused --- Sources/DistributedMLS/WireInterfaces.swift | 29 --------------------- 1 file changed, 29 deletions(-) delete mode 100644 Sources/DistributedMLS/WireInterfaces.swift diff --git a/Sources/DistributedMLS/WireInterfaces.swift b/Sources/DistributedMLS/WireInterfaces.swift deleted file mode 100644 index 4a99546..0000000 --- a/Sources/DistributedMLS/WireInterfaces.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// WireInterfaces.swift -// DiMLS -// -// Created by Mark @ Germ on 12/31/25. -// - -import Foundation - -///Interfaces for DiGroup wireformats - -public protocol DiControlMessage: Sendable, Archivable { - associatedtype Welcome: DiWelcome - associatedtype Commit: DiCommit - - var asCommit: Commit? { get } - var asWelcome: Welcome? { get } - var epoch: UInt64 { get } -} - -public protocol DiWelcome: Sendable, Archivable { - //should hold onto init key of the recipent for header encryption - var recipientInitKeyData: Data { get } - var epoch: UInt64 { get } -} - -public protocol DiCommit: Sendable, Archivable { - var epoch: UInt64 { get } -} From c011b8ed20ea74a845b72fe0c874343077e071a5 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 17 Jan 2026 09:49:56 -0800 Subject: [PATCH 03/14] organize and remove unused diInvitation --- Sources/DistributedMLS/Client.swift | 14 +++------- Sources/DistributedMLS/DiInvitation.swift | 27 ------------------- .../DistributedMLS/Helpers/Archivable.swift | 15 +++++++++++ .../{ => Helpers}/ConvenienceError.swift | 0 .../DistributedMLS/{ => Helpers}/Error.swift | 0 5 files changed, 19 insertions(+), 37 deletions(-) delete mode 100644 Sources/DistributedMLS/DiInvitation.swift create mode 100644 Sources/DistributedMLS/Helpers/Archivable.swift rename Sources/DistributedMLS/{ => Helpers}/ConvenienceError.swift (100%) rename Sources/DistributedMLS/{ => Helpers}/Error.swift (100%) diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index 360dd5a..bb8f8f0 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -10,12 +10,9 @@ import Foundation extension DiMLS { public protocol Client: Actor, Archivable { associatedtype Credential: DiMLSCredential - associatedtype Invitation: DiInvitation //where Invitation.Credential == Credential associatedtype Group: DiGroup where Group.WelcomeOutput == WelcomeOutput associatedtype WelcomeOutput: WelcomeOutputInterface - where - Group.Credential == Credential - // Invitation.WelcomeOutput == WelcomeOutput + where Group.Credential == Credential static func create(credential: Credential) throws -> Self @@ -46,10 +43,7 @@ extension DiMLS { } } -public protocol Archivable { - associatedtype Archive: Sendable, Codable - init(archive: Archive) throws - - //helps to define this in the protocol for Actor protocols - // var archive: Archive { get throws } +extension DiMLS { + public typealias KeyPackageId = Data } + diff --git a/Sources/DistributedMLS/DiInvitation.swift b/Sources/DistributedMLS/DiInvitation.swift deleted file mode 100644 index c9ec76c..0000000 --- a/Sources/DistributedMLS/DiInvitation.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// DiInvitation.swift -// DiMLS -// -// Created by Mark @ Germ on 1/4/26. -// - -import Foundation - -///An invitation is a logical entity that can be added to DiGroups -extension DiMLS { - public typealias KeyPackageId = Data - public protocol DiInvitation: Archivable { - // associatedtype Credential: DiMLSCredential - // associatedtype WelcomeOutput - - var currentKeyPackage: (keyPackageId: Data, keyPackageMessage: Data) { get throws } - - ///returns an atomic group (archive) - ///the application determines which DiGroup it should belong to based - ///on the resulting group's groupId - // func process(mlsWelcome: Data, keyPackageId: KeyPackageId?) throws -> ( - // KeyPackageId, - // WelcomeOutput - // ) - } -} diff --git a/Sources/DistributedMLS/Helpers/Archivable.swift b/Sources/DistributedMLS/Helpers/Archivable.swift new file mode 100644 index 0000000..cb67628 --- /dev/null +++ b/Sources/DistributedMLS/Helpers/Archivable.swift @@ -0,0 +1,15 @@ +// +// Archivable.swift +// DistributedMLS +// +// Created by Mark @ Germ on 1/17/26. +// + + +public protocol Archivable { + associatedtype Archive: Sendable, Codable + init(archive: Archive) throws + + //helps to define this in the protocol for Actor protocols + // var archive: Archive { get throws } +} \ No newline at end of file diff --git a/Sources/DistributedMLS/ConvenienceError.swift b/Sources/DistributedMLS/Helpers/ConvenienceError.swift similarity index 100% rename from Sources/DistributedMLS/ConvenienceError.swift rename to Sources/DistributedMLS/Helpers/ConvenienceError.swift diff --git a/Sources/DistributedMLS/Error.swift b/Sources/DistributedMLS/Helpers/Error.swift similarity index 100% rename from Sources/DistributedMLS/Error.swift rename to Sources/DistributedMLS/Helpers/Error.swift From fd67a0c06af1874467387e6a03c5307320487b4e Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 17 Jan 2026 09:52:01 -0800 Subject: [PATCH 04/14] fix test import --- .periphery.yml | 1 + .../{Intefaces => Interfaces}/DiMLSCredential.swift | 0 .../{Intefaces => Interfaces}/WelcomeOutputInterface.swift | 0 Tests/DistributedMLSTests/DiMLSTests.swift | 2 +- 4 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .periphery.yml rename Sources/DistributedMLS/{Intefaces => Interfaces}/DiMLSCredential.swift (100%) rename Sources/DistributedMLS/{Intefaces => Interfaces}/WelcomeOutputInterface.swift (100%) diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..85b884a --- /dev/null +++ b/.periphery.yml @@ -0,0 +1 @@ +retain_public: true diff --git a/Sources/DistributedMLS/Intefaces/DiMLSCredential.swift b/Sources/DistributedMLS/Interfaces/DiMLSCredential.swift similarity index 100% rename from Sources/DistributedMLS/Intefaces/DiMLSCredential.swift rename to Sources/DistributedMLS/Interfaces/DiMLSCredential.swift diff --git a/Sources/DistributedMLS/Intefaces/WelcomeOutputInterface.swift b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift similarity index 100% rename from Sources/DistributedMLS/Intefaces/WelcomeOutputInterface.swift rename to Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift diff --git a/Tests/DistributedMLSTests/DiMLSTests.swift b/Tests/DistributedMLSTests/DiMLSTests.swift index bd4cbc2..2208da8 100644 --- a/Tests/DistributedMLSTests/DiMLSTests.swift +++ b/Tests/DistributedMLSTests/DiMLSTests.swift @@ -1,6 +1,6 @@ import Testing -@testable import DiMLS +@testable import DistributedMLS @Test func example() async throws { // Write your test here and use APIs like `#expect(...)` to check expected conditions. From 34faf8a76bc8c33b7911c04f91417b8aae675f2f Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 17 Jan 2026 12:41:29 -0800 Subject: [PATCH 05/14] remove unused --- Sources/DistributedMLS/Group/DiGroup.swift | 33 +--------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 33f270e..a7d57a6 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -37,39 +37,8 @@ extension DiMLS { func prepareCommit() throws -> DiMLS.CommitInput func received(welcome: WelcomeOutput) throws - //Deprecate: - ///DiGgroup Operations - // associatedtype RemoteState: RemoteStateInterface - // var remoteStates: [ReferenceID: RemoteState] { get } - - //Local mutations only have meaning when I broadcast them into the world - // func stageAdd(member: DiMLS.CredentialedKeyPackage) throws - // func stageDelete(member: Credential) throws - // //application should drive if new key material is needed - // func stageNewLocalKeyMaterial() throws - - ///DiGroup 1:1 Control Plane - ///expect sender crendential implicit in assigned transport path - // func receive(ciphertext: Data, from: Credential) throws -> DiMLS.DecryptOutput - - //let the application get and resend unack'd commits - //TODO: attach metadata to the result like an epoch no - // func inFlightFor(remote: Credential) throws -> [Data] - - //Application messages - //this will commit any pending changes - //TODO: also report state changes - //for now, authenticated data is included in the output - //(because MLS does in the PrivateMessage format) - //returns private message and recipients - // func encrypt(plaintext: Data, authenticating: Data) throws - // -> DiMLS.PrivateMessage - // func stapledEncrypt(plaintext: Data) throws - // -> DiMLS.PrivateMessage - - //process incoming, which may be an application message, commit - //or a welcome to another user's send group + // func stageNewLocalKeyMaterial() throws } } From 17cbf1b2ef11e2f81d2c741d2115f65740587a3f Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sun, 18 Jan 2026 00:55:22 -0800 Subject: [PATCH 06/14] add dependency object --- Sources/DistributedMLS/Client.swift | 1 - .../DistributedMLS/Group/CommitInput.swift | 35 +++++++++++-------- Sources/DistributedMLS/Group/DiGroup.swift | 24 +++++++------ .../DistributedMLS/Group/SendChannel.swift | 21 +++++------ .../DistributedMLS/Helpers/Archivable.swift | 3 +- Sources/DistributedMLS/Helpers/Error.swift | 2 +- .../TotalGroup/DiGroupState.swift | 7 ---- 7 files changed, 47 insertions(+), 46 deletions(-) diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index bb8f8f0..8197c82 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -46,4 +46,3 @@ extension DiMLS { extension DiMLS { public typealias KeyPackageId = Data } - diff --git a/Sources/DistributedMLS/Group/CommitInput.swift b/Sources/DistributedMLS/Group/CommitInput.swift index c6d8b8f..465a4aa 100644 --- a/Sources/DistributedMLS/Group/CommitInput.swift +++ b/Sources/DistributedMLS/Group/CommitInput.swift @@ -5,33 +5,38 @@ // Created by Mark @ Germ on 1/11/26. // +import Foundation + extension DiMLS { public struct CommitInput { - public var proposals: Set> + public var localOps: Set> + public var followOps: Set> - //psk's - struct Dependency { - let pskSource: ReferenceID - let epoch: EpochID - } var dependencies: [Dependency] public var newSenderLeafNode: Bool public init() { - proposals = [] + localOps = [] + followOps = [] dependencies = [] newSenderLeafNode = false } + } - private init( - proposals: [DiMLSOperations], - dependencies: [Dependency], - newSenderLeafNode: Bool - ) { - self.proposals = .init(proposals) - self.dependencies = dependencies - self.newSenderLeafNode = newSenderLeafNode + //psk's + public struct Dependency: Codable, Sendable { + public let pskSource: ReferenceID + public let epoch: EpochID + + public init(pskSource: ReferenceID, epoch: EpochID) { + self.pskSource = pskSource + self.epoch = epoch } } + + public struct KeyedDependency: Codable, Sendable { + public let dependency: Dependency + public let keyData: Data + } } diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index a7d57a6..09b83d4 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -37,7 +37,6 @@ extension DiMLS { func prepareCommit() throws -> DiMLS.CommitInput func received(welcome: WelcomeOutput) throws - // func stageNewLocalKeyMaterial() throws } } @@ -60,8 +59,8 @@ extension DiMLS.DiGroup { credentialFetcher: @escaping CredentialKeyPackageFetcher, referenceIdFetcher: @escaping ReferenceIdKeyPackageFetcher ) throws -> Task { - guard case .queued = lazySender else { - if case .creating(let task) = lazySender { + guard case .queued(let dependency) = lazySender else { + if case .creating(let task, _) = lazySender { return task } throw DiMLSError.sendGroupNotReady @@ -70,6 +69,10 @@ extension DiMLS.DiGroup { do { let archive = try await createSendGroup( myCredential: myCredential, + members: + totalGroup + .membershipForCreating(sender: myCredential.referenceId), + dependency: dependency, credentialFetcher: credentialFetcher, referenceIdFetcher: referenceIdFetcher ) @@ -87,22 +90,22 @@ extension DiMLS.DiGroup { ) } catch { print("error creating: \(error)") - lazySender = .queued + lazySender = .queued(dependency) throw error } } - lazySender = .creating(task) + lazySender = .creating(task, dependency) return task } private func createSendGroup( myCredential: Credential, + members: [DiMLS.ReferenceID: DiMLS.Participant], + dependency: DiMLS.KeyedDependency?, credentialFetcher: CredentialKeyPackageFetcher, referenceIdFetcher: ReferenceIdKeyPackageFetcher ) async throws -> Sender.Archive { - if case .ready = lazySender { - throw DiMLSError.disallowed - } + //capture a snapshot of what the group needs var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote]() @@ -138,7 +141,8 @@ extension DiMLS.DiGroup { identityProvider: Sender.identityProvider( totalGroup: totalGroup, sender: myCredential - ) + ), + dependency: dependency ) } @@ -177,7 +181,7 @@ extension DiMLS.DiGroup { var newRemotes: [DiMLS.CredentialedKeyPackage] = [] //modify remotes and group - for action in input.proposals { + for action in input.localOps { switch action { case .add(let credentialKeyPackage): newRemotes.append(credentialKeyPackage) diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index 241fdae..a32e86f 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -23,7 +23,8 @@ public protocol SendChannel { static func create( input: SendChannelInputs, - identityProvider: IdentityProvider + identityProvider: IdentityProvider, + dependency: DiMLS.KeyedDependency? ) throws -> Archive init(archive: Archive, identityProvider: IdentityProvider) throws @@ -75,9 +76,9 @@ public struct SendChannelInputs { public enum LazySendChannel { case ready(R) - case queued + case queued(DiMLS.KeyedDependency?) //serves as a mutex on snapshot of Q state - case creating(Task) + case creating(Task, DiMLS.KeyedDependency?) public init( archive: Archive, @@ -88,8 +89,8 @@ public enum LazySendChannel { self = .ready( try .init(archive: archive, identityProvider: identityProvider) ) - case .queued: - self = .queued + case .queued(let dependency): + self = .queued(dependency) } } @@ -106,7 +107,7 @@ public enum LazySendChannel { extension LazySendChannel { public enum Archive: Sendable, Codable { case ready(R.Archive) - case queued + case queued(DiMLS.KeyedDependency?) } public var archive: Archive { @@ -114,10 +115,10 @@ extension LazySendChannel { switch self { case .ready(let r): try .ready(r.archive) - case .queued: - .queued - case .creating: - .queued + case .queued(let dependency): + .queued(dependency) + case .creating(_, let dependency): + .queued(dependency) } } } diff --git a/Sources/DistributedMLS/Helpers/Archivable.swift b/Sources/DistributedMLS/Helpers/Archivable.swift index cb67628..5d0f070 100644 --- a/Sources/DistributedMLS/Helpers/Archivable.swift +++ b/Sources/DistributedMLS/Helpers/Archivable.swift @@ -5,11 +5,10 @@ // Created by Mark @ Germ on 1/17/26. // - public protocol Archivable { associatedtype Archive: Sendable, Codable init(archive: Archive) throws //helps to define this in the protocol for Actor protocols // var archive: Archive { get throws } -} \ No newline at end of file +} diff --git a/Sources/DistributedMLS/Helpers/Error.swift b/Sources/DistributedMLS/Helpers/Error.swift index c94138d..5aa6fd9 100644 --- a/Sources/DistributedMLS/Helpers/Error.swift +++ b/Sources/DistributedMLS/Helpers/Error.swift @@ -7,7 +7,7 @@ import Foundation -enum DiMLSError: Error { +public enum DiMLSError: Error { case missingRemoteState case mismatchedGroupId case duplicateSendGroup diff --git a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift index 8514899..65a8ab3 100644 --- a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift +++ b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift @@ -67,13 +67,6 @@ public final class DiGroupState { } } -extension DiMLS { - enum Operation { - //in this context, the creator added themselves implicitly - case add(ReferenceID) - } -} - extension DiGroupState: Archivable { public struct Archive: Codable, Sendable { public let diGroupId: Data From 7566c07d94d72d62bda9c34ddfdf16f632dc7b83 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sun, 18 Jan 2026 17:56:40 -0800 Subject: [PATCH 07/14] need a keyed dependency --- Sources/DistributedMLS/Client.swift | 3 ++- Sources/DistributedMLS/Group/CommitInput.swift | 7 ++++++- Sources/DistributedMLS/Helpers/Error.swift | 4 +++- .../DistributedMLS/Interfaces/WelcomeOutputInterface.swift | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index 8197c82..b5c6da3 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -35,7 +35,8 @@ extension DiMLS { func process( wireWelcome: Data, - keyPackageId: KeyPackageId? + keyPackageId: KeyPackageId?, + knownDependencies: [DiMLS.KeyedDependency] ) throws -> ( KeyPackageId, WelcomeOutput diff --git a/Sources/DistributedMLS/Group/CommitInput.swift b/Sources/DistributedMLS/Group/CommitInput.swift index 465a4aa..7243bb6 100644 --- a/Sources/DistributedMLS/Group/CommitInput.swift +++ b/Sources/DistributedMLS/Group/CommitInput.swift @@ -25,7 +25,7 @@ extension DiMLS { } //psk's - public struct Dependency: Codable, Sendable { + public struct Dependency: Codable, Sendable, Hashable { public let pskSource: ReferenceID public let epoch: EpochID @@ -38,5 +38,10 @@ extension DiMLS { public struct KeyedDependency: Codable, Sendable { public let dependency: Dependency public let keyData: Data + + public init(dependency: Dependency, keyData: Data) { + self.dependency = dependency + self.keyData = keyData + } } } diff --git a/Sources/DistributedMLS/Helpers/Error.swift b/Sources/DistributedMLS/Helpers/Error.swift index 5aa6fd9..19c0b3a 100644 --- a/Sources/DistributedMLS/Helpers/Error.swift +++ b/Sources/DistributedMLS/Helpers/Error.swift @@ -15,6 +15,7 @@ public enum DiMLSError: Error { case disallowed case sendGroupNotReady case duplicateMember + case expecting(DiMLS.Dependency) } extension DiMLSError: LocalizedError { @@ -26,7 +27,8 @@ extension DiMLSError: LocalizedError { case .notImplemented: "Not implemented" case .disallowed: "Disallowed" case .sendGroupNotReady: "Send group not ready" - case .duplicateMember: "Duplicate memeber" + case .duplicateMember: "Duplicate member" + case .expecting: "Missing dependency" } } } diff --git a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift index d7e5bac..44f5751 100644 --- a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift +++ b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift @@ -10,4 +10,5 @@ import Foundation public protocol WelcomeOutputInterface: Sendable { var diGroupId: Data { get } var senderReferenceId: DiMLS.ReferenceID { get throws } + var keyedDependency: DiMLS.KeyedDependency? { get } } From 30e9942a87a7d9945490a33b889b81aeae6c7eb9 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sun, 18 Jan 2026 20:47:13 -0800 Subject: [PATCH 08/14] public dependency property --- Sources/DistributedMLS/Group/CommitInput.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DistributedMLS/Group/CommitInput.swift b/Sources/DistributedMLS/Group/CommitInput.swift index 7243bb6..1cecf38 100644 --- a/Sources/DistributedMLS/Group/CommitInput.swift +++ b/Sources/DistributedMLS/Group/CommitInput.swift @@ -12,7 +12,7 @@ extension DiMLS { public var localOps: Set> public var followOps: Set> - var dependencies: [Dependency] + public var dependencies: [KeyedDependency] public var newSenderLeafNode: Bool From 62c959b0069fd167ef7ac7d81d3dd4aedc288b25 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sun, 18 Jan 2026 22:47:54 -0800 Subject: [PATCH 09/14] move dhe diGroupId to the group from groupContext --- Sources/DistributedMLS/Group/DiGroup.swift | 4 +++- Sources/DistributedMLS/Group/SendChannel.swift | 13 +++++++++++-- .../TotalGroup/DiGroupState.swift | 17 ++--------------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 09b83d4..1e0f10a 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -24,6 +24,7 @@ extension DiMLS { //associated State //mutable object + nonisolated var diGroupId: Data { get } var totalGroup: DiGroupState { get } var receivers: [ReferenceID: Receiver] { get set } var pendingState: PendingState { get } @@ -85,6 +86,7 @@ extension DiMLS.DiGroup { lazySender = .ready( try .init( archive: archive, + diGroupId: diGroupId, identityProvider: identityProvider ) ) @@ -134,7 +136,7 @@ extension DiMLS.DiGroup { return try Sender.create( input: .init( - diGroupID: totalGroup.diGroupId, + diGroupID: diGroupId, myCredential: myCredential, remotes: remotes ), diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index a32e86f..faed0ef 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -27,7 +27,11 @@ public protocol SendChannel { dependency: DiMLS.KeyedDependency? ) throws -> Archive - init(archive: Archive, identityProvider: IdentityProvider) throws + init( + archive: Archive, + diGroupId: Data, + identityProvider: IdentityProvider + ) throws var archive: Archive { get throws } @@ -82,12 +86,17 @@ public enum LazySendChannel { public init( archive: Archive, + diGroupId: Data, identityProvider: R.IdentityProvider ) throws { switch archive { case .ready(let archive): self = .ready( - try .init(archive: archive, identityProvider: identityProvider) + try .init( + archive: archive, + diGroupId: diGroupId, + identityProvider: identityProvider + ) ) case .queued(let dependency): self = .queued(dependency) diff --git a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift index 65a8ab3..6977ff6 100644 --- a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift +++ b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift @@ -10,23 +10,16 @@ import Foundation //state of the observed group, including (permanently) removed members public final class DiGroupState { ///we can join at any time, and don't need to reconstruct adds from the group membership - public let diGroupId: Data - public private(set) var members: [DiMLS.ReferenceID: Membership] public var count: Int { members.count } - private init( - diGroupId: Data, - members: [DiMLS.ReferenceID: Membership], - ) { - self.diGroupId = diGroupId + private init(members: [DiMLS.ReferenceID: Membership]) { self.members = members } public convenience init(archive: Archive) throws { self.init( - diGroupId: archive.diGroupId, members: try archive.members.mapValues { try .init(archive: $0) }, ) } @@ -69,22 +62,16 @@ public final class DiGroupState { extension DiGroupState: Archivable { public struct Archive: Codable, Sendable { - public let diGroupId: Data public let members: [DiMLS.ReferenceID: Membership.Archive] //array makes it easier to encode stably over the wire - public init( - diGroupId: Data, - members: [DiMLS.ReferenceID: Membership.Archive] - ) { - self.diGroupId = diGroupId + public init(members: [DiMLS.ReferenceID: Membership.Archive]) { self.members = members } } public var archive: Archive { .init( - diGroupId: diGroupId, members: members.mapValues(\.archive), ) } From 9553b6afb6084f18264e1606a005d2c8bd62d9ab Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 19 Jan 2026 14:34:25 -0800 Subject: [PATCH 10/14] membership should be an enum --- Sources/DistributedMLS/Group/DiGroup.swift | 6 +- .../TotalGroup/DiGroupState.swift | 108 +++++++++--------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 1e0f10a..d92aa3b 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -111,10 +111,10 @@ extension DiMLS.DiGroup { //capture a snapshot of what the group needs var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote]() - for member - in totalGroup + let members = + try totalGroup .membershipForCreating(sender: myCredential.referenceId) - { + for member in members { switch member.value { case .credential(let credential, let epoch): let keyPackage = try await credentialFetcher(credential) diff --git a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift index 6977ff6..931ffdd 100644 --- a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift +++ b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift @@ -33,7 +33,7 @@ public final class DiGroupState { throw DiMLSError.duplicateMember } assert(members[member.referenceId] == nil) - members[member.referenceId] = .new(credential: member) + members[member.referenceId] = .new(dependency: nil) } // public func added(member: DiMLS.ReferenceID) throws { @@ -45,19 +45,31 @@ public final class DiGroupState { func membershipForCreating( sender: DiMLS.ReferenceID - ) -> [DiMLS.ReferenceID: DiMLS.Participant] { - members.compactMapValues { membership in - guard membership.referenceId != sender else { - return nil + ) throws -> [DiMLS.ReferenceID: DiMLS.Participant] { + try members.reduce(into: [:]) { + result, + pair in + guard pair.key != sender else { + return } - let epoch = membership.epochs.last - if let epoch { - return .credential(epoch.credential, epoch.epoch) - } else { - return .referenceId(membership.referenceId) + assert(result[pair.key] == nil) + switch pair.value { + case .invited(let dependencies): + result[pair.key] = .referenceId(pair.key) + case .claimed(let epochs): + let epoch = try epochs.last.tryUnwrap + result[pair.key] = .credential( + epoch.credential, + epoch.epoch + ) } } } + + public func readyToWelcome(credential: Credential) throws { + try members[credential.referenceId].tryUnwrap + .readyToWelcome(credential: credential) + } } extension DiGroupState: Archivable { @@ -78,22 +90,16 @@ extension DiGroupState: Archivable { } extension DiGroupState { - public struct Membership { - let referenceId: DiMLS.ReferenceID - //can prune, but should never prune to empty as - //empty indicates invited - public private(set) var epochs: [Epoch] //should be in increasing epoch order - - static func new(credential: Credential) -> Self { - .init( - referenceId: credential.referenceId, - epochs: [] - ) - } - - init(referenceId: DiMLS.ReferenceID, epochs: [Epoch]) { - self.referenceId = referenceId - self.epochs = epochs + public enum Membership { + //can be empty so that as an identity provider it allows me to add them, + //then lets me fill in the dependency + case invited([DiMLS.KeyedDependency]) + case claimed([Epoch]) + + static func new( + dependency: DiMLS.KeyedDependency? + ) -> Self { + .invited([dependency].compactMap(\.self)) } public struct Epoch: Archivable { @@ -126,49 +132,43 @@ extension DiGroupState { .init(epoch: epoch, credential: credential.encoded) } } + + func readyToWelcome(credential: Credential) throws { + guard case .invited = self else { + throw DiMLSError.duplicateMember + } + } } } extension DiGroupState.Membership: Archivable { - public struct Archive: Codable, Sendable { - let referenceId: DiMLS.ReferenceID - let epochs: [Epoch.Archive] - - public init(referenceId: DiMLS.ReferenceID, epochs: [Epoch.Archive]) { - self.referenceId = referenceId - self.epochs = epochs - } - - public static func create(referenceId: DiMLS.ReferenceID) -> Self { - .init(referenceId: referenceId, epochs: []) - } + public enum Archive: Codable, Sendable { + case invited([DiMLS.KeyedDependency]) + case claimed([Epoch.Archive]) public static func create( - referenceId: DiMLS.ReferenceID, credential: Data, epoch: UInt64 ) -> Self { - .init( - referenceId: referenceId, - epochs: [ - .init( - epoch: epoch, - credential: credential - ) - ] - ) + .claimed([.init(epoch: epoch, credential: credential)]) } } public init(archive: Archive) throws { - referenceId = archive.referenceId - epochs = try archive.epochs.map { try .init(archive: $0) } + switch archive { + case .claimed(let epochs): + self = .claimed(try epochs.map { try .init(archive: $0) }) + case .invited(let dependencies): + self = .invited(dependencies) + } } var archive: Archive { - .init( - referenceId: referenceId, - epochs: epochs.map(\.archive) - ) + switch self { + case .claimed(let epochs): + .claimed(epochs.map(\.archive)) + case .invited(let dependencies): + .invited(dependencies) + } } } From 984cdad02482ad47df94367d4eb8877af5aa3c3a Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 19 Jan 2026 15:25:49 -0800 Subject: [PATCH 11/14] finish receive welcome --- Sources/DistributedMLS/Group/DiGroup.swift | 81 +++---------------- .../DistributedMLS/Group/ReceiveChannel.swift | 3 +- .../TotalGroup/DiGroupState.swift | 8 +- 3 files changed, 19 insertions(+), 73 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index d92aa3b..5bd906d 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -13,7 +13,7 @@ extension DiMLS { associatedtype Credential: DiMLSCredential //State Types - associatedtype Receiver: ReceiveChannel + associatedtype Receiver: ReceiveChannel where Receiver.WelcomeOutput == WelcomeOutput associatedtype Sender: SendChannel where Sender.Credential == Credential associatedtype WelcomeOutput: WelcomeOutputInterface @@ -214,79 +214,24 @@ extension DiMLS.DiGroup { dependencies: [:] ) } -} -//(Deprecate) full-featured compound api's -extension DiMLS.DiGroup { - // public func encryptWithCommits( - // plaintext: Data, - // authenticating: Data, - // staplingCommit: Bool - // ) throws -> [(Credential, DiMLS.EncryptOutput)] { - // //use the ratchet tree - // let privateMessage = try encrypt( - // plaintext: plaintext, - // authenticating: authenticating - // ) - // - // return try privateMessage.addressees.map { credential in - // guard let remoteState = remoteStates[credential.referenceId] else { - // throw DiMLSError.missingRemoteState - // } - // - // return ( - // credential, - // try remoteState.package( - // privateMessage: privateMessage.body, - // epoch: privateMessage.epoch, - // staplingCommit: staplingCommit - // ) - // ) - // } - // } - - // public func received(welcome: WelcomeOutput) throws { - // guard welcome.diGroupId == totalGroup.diGroupId else { - // throw DiMLSError.mismatchedGroupId - // } - // - // let senderReferenceId = try welcome.senderReferenceId - // - // //is this a member of the group? - // if totalGroup.members.contains(senderReferenceId) { - // try expectedMember(welcome: welcome) - // } else { - // try newMember(welcome: welcome) - // } - // - // //is this a - // } - - private func newMember(welcome: WelcomeOutput) throws { + public func received(welcome: WelcomeOutput) throws { + guard welcome.diGroupId == diGroupId else { + throw DiMLSError.mismatchedGroupId + } + let senderReferenceId = try welcome.senderReferenceId - assert(!totalGroup.members.keys.contains(senderReferenceId)) - guard totalGroup.canAdd(try welcome.senderReferenceId) else { - throw DiMLSError.duplicateSendGroup + //is this a member of the group? + try totalGroup + .readyToWelcome(member: welcome.senderReferenceId) + + guard receivers[senderReferenceId] == nil else { + throw DiMLSError.duplicateMember } - throw DiMLSError.notImplemented + receivers[senderReferenceId] = try .create(welcome: welcome) } - // private func expectedMember(welcome: WelcomeOutput) throws { - // let senderReferenceId = try welcome.senderReferenceId - // assert(totalGroup.members.contains(senderReferenceId)) - // - // //where do I process this new welcome? - // //do I already have a sendgroup for this sender? - // if let remoteState = remoteStates[senderReferenceId] { - // guard !remoteState.receivedWelcome else { - // throw DiMLSError.duplicateSendGroup - // } - // //can setup the remoteSTate - // } else { - // - // } - // } } //we have a generic (Credential) and a non-generic and could simplify them diff --git a/Sources/DistributedMLS/Group/ReceiveChannel.swift b/Sources/DistributedMLS/Group/ReceiveChannel.swift index ce404f6..575d20a 100644 --- a/Sources/DistributedMLS/Group/ReceiveChannel.swift +++ b/Sources/DistributedMLS/Group/ReceiveChannel.swift @@ -8,5 +8,6 @@ import Foundation public protocol ReceiveChannel { - + associatedtype WelcomeOutput: WelcomeOutputInterface + static func create(welcome: WelcomeOutput) throws -> Self } diff --git a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift index 931ffdd..5bc8bb4 100644 --- a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift +++ b/Sources/DistributedMLS/TotalGroup/DiGroupState.swift @@ -66,9 +66,9 @@ public final class DiGroupState { } } - public func readyToWelcome(credential: Credential) throws { - try members[credential.referenceId].tryUnwrap - .readyToWelcome(credential: credential) + public func readyToWelcome(member: DiMLS.ReferenceID) throws { + try members[member].tryUnwrap + .readyToWelcome(member: member) } } @@ -133,7 +133,7 @@ extension DiGroupState { } } - func readyToWelcome(credential: Credential) throws { + func readyToWelcome(member: DiMLS.ReferenceID) throws { guard case .invited = self else { throw DiMLSError.duplicateMember } From 3fcb853ce09803ec8a225b871ac06fe56d686426 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 19 Jan 2026 15:46:02 -0800 Subject: [PATCH 12/14] clean up namespacing --- Sources/DistributedMLS/Client.swift | 7 +------ Sources/DistributedMLS/DiMLS.swift | 14 +++++++------- .../DistributedMLS/Group/PendingInterface.swift | 12 ------------ 3 files changed, 8 insertions(+), 25 deletions(-) delete mode 100644 Sources/DistributedMLS/Group/PendingInterface.swift diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index b5c6da3..d56e018 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -10,9 +10,8 @@ import Foundation extension DiMLS { public protocol Client: Actor, Archivable { associatedtype Credential: DiMLSCredential - associatedtype Group: DiGroup where Group.WelcomeOutput == WelcomeOutput + associatedtype Group: DiGroup where Group.WelcomeOutput == WelcomeOutput, Group.Credential == Credential associatedtype WelcomeOutput: WelcomeOutputInterface - where Group.Credential == Credential static func create(credential: Credential) throws -> Self @@ -43,7 +42,3 @@ extension DiMLS { ) } } - -extension DiMLS { - public typealias KeyPackageId = Data -} diff --git a/Sources/DistributedMLS/DiMLS.swift b/Sources/DistributedMLS/DiMLS.swift index ddd9e0e..f76f4c5 100644 --- a/Sources/DistributedMLS/DiMLS.swift +++ b/Sources/DistributedMLS/DiMLS.swift @@ -8,9 +8,9 @@ import Foundation public enum DiMLS { - //like send group id public typealias ReferenceID = Data public typealias EpochID = UInt64 + public typealias KeyPackageId = Data ///mirrors (is a) MLS private message in that it has an encrypted ///application message and plaintext metadata and auth data @@ -18,13 +18,13 @@ public enum DiMLS { ///of the collective group. public struct PrivateMessage: Sendable { public let body: Data //encoded MLS Private message - public let epoch: UInt64 //we may retry transmit after a later commit + public let epoch: EpochID //we may retry transmit after a later commit public let sender: C public let addressees: [C] public init( body: Data, - epoch: UInt64, + epoch: EpochID, sender: C, addressees: [C] ) { @@ -45,12 +45,12 @@ public enum DiMLS { //encrypt headers public let privateMessage: Data //if not stapled let the caller judge if it wants resend the commit - public let epoch: UInt64 + public let epoch: EpochID public let additionalCommits: [EpochCommit] public init( privateMessage: Data, - epoch: UInt64, + epoch: EpochID, additionalCommits: [EpochCommit] ) { self.privateMessage = privateMessage @@ -60,10 +60,10 @@ public enum DiMLS { } public struct EpochCommit: Sendable, Codable { - public let epoch: UInt64 + public let epoch: EpochID public let commit: Data - public init(epoch: UInt64, commit: Data) { + public init(epoch: EpochID, commit: Data) { self.epoch = epoch self.commit = commit } diff --git a/Sources/DistributedMLS/Group/PendingInterface.swift b/Sources/DistributedMLS/Group/PendingInterface.swift deleted file mode 100644 index 27fa47c..0000000 --- a/Sources/DistributedMLS/Group/PendingInterface.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// PendingInterface.swift -// DiMLS -// -// Created by Mark @ Germ on 1/7/26. -// - -import Foundation - -protocol PendingInterface: Archivable { - -} From 97289c79665880a2459abf25dd6b4014fb94d6f0 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 19 Jan 2026 15:51:57 -0800 Subject: [PATCH 13/14] DiGroupState -> DiMLS.TotalGroup --- Sources/DistributedMLS/Client.swift | 3 +- Sources/DistributedMLS/Group/DiGroup.swift | 2 +- .../DistributedMLS/Group/SendChannel.swift | 2 +- .../{DiGroupState.swift => TotalGroup.swift} | 110 +++++++++--------- 4 files changed, 60 insertions(+), 57 deletions(-) rename Sources/DistributedMLS/TotalGroup/{DiGroupState.swift => TotalGroup.swift} (56%) diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index d56e018..4ce8cca 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -10,7 +10,8 @@ import Foundation extension DiMLS { public protocol Client: Actor, Archivable { associatedtype Credential: DiMLSCredential - associatedtype Group: DiGroup where Group.WelcomeOutput == WelcomeOutput, Group.Credential == Credential + associatedtype Group: DiGroup + where Group.WelcomeOutput == WelcomeOutput, Group.Credential == Credential associatedtype WelcomeOutput: WelcomeOutputInterface static func create(credential: Credential) throws -> Self diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 5bd906d..d4b7cb2 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -25,7 +25,7 @@ extension DiMLS { //mutable object nonisolated var diGroupId: Data { get } - var totalGroup: DiGroupState { get } + var totalGroup: DiMLS.TotalGroup { get } var receivers: [ReferenceID: Receiver] { get set } var pendingState: PendingState { get } var lazySender: LazySendChannel { get set } diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index faed0ef..3b7975c 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -17,7 +17,7 @@ public protocol SendChannel { associatedtype Welcome static func identityProvider( - totalGroup: DiGroupState, + totalGroup: DiMLS.TotalGroup, sender: Credential ) -> IdentityProvider diff --git a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift similarity index 56% rename from Sources/DistributedMLS/TotalGroup/DiGroupState.swift rename to Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 5bc8bb4..93cbb52 100644 --- a/Sources/DistributedMLS/TotalGroup/DiGroupState.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -8,71 +8,73 @@ import Foundation //state of the observed group, including (permanently) removed members -public final class DiGroupState { - ///we can join at any time, and don't need to reconstruct adds from the group membership - public private(set) var members: [DiMLS.ReferenceID: Membership] +extension DiMLS { + public final class TotalGroup { + ///we can join at any time, and don't need to reconstruct adds from the group membership + public private(set) var members: [ReferenceID: Membership] - public var count: Int { members.count } + public var count: Int { members.count } - private init(members: [DiMLS.ReferenceID: Membership]) { - self.members = members - } - - public convenience init(archive: Archive) throws { - self.init( - members: try archive.members.mapValues { try .init(archive: $0) }, - ) - } + private init(members: [ReferenceID: Membership]) { + self.members = members + } - public func canAdd(_ member: DiMLS.ReferenceID) -> Bool { - !members.keys.contains(member) - } + public convenience init(archive: Archive) throws { + self.init( + members: try archive.members.mapValues { try .init(archive: $0) }, + ) + } - public func add(member: Credential) throws { - guard canAdd(member.referenceId) else { - throw DiMLSError.duplicateMember + public func canAdd(_ member: ReferenceID) -> Bool { + !members.keys.contains(member) } - assert(members[member.referenceId] == nil) - members[member.referenceId] = .new(dependency: nil) - } - // public func added(member: DiMLS.ReferenceID) throws { - // guard canAdd(member) else { - // throw DiMLSError.disallowed - // } - // members.insert(member) - // } - - func membershipForCreating( - sender: DiMLS.ReferenceID - ) throws -> [DiMLS.ReferenceID: DiMLS.Participant] { - try members.reduce(into: [:]) { - result, - pair in - guard pair.key != sender else { - return + public func add(member: Credential) throws { + guard canAdd(member.referenceId) else { + throw DiMLSError.duplicateMember } - assert(result[pair.key] == nil) - switch pair.value { - case .invited(let dependencies): - result[pair.key] = .referenceId(pair.key) - case .claimed(let epochs): - let epoch = try epochs.last.tryUnwrap - result[pair.key] = .credential( - epoch.credential, - epoch.epoch - ) + assert(members[member.referenceId] == nil) + members[member.referenceId] = .new(dependency: nil) + } + + // public func added(member: DiMLS.ReferenceID) throws { + // guard canAdd(member) else { + // throw DiMLSError.disallowed + // } + // members.insert(member) + // } + + func membershipForCreating( + sender: ReferenceID + ) throws -> [ReferenceID: Participant] { + try members.reduce(into: [:]) { + result, + pair in + guard pair.key != sender else { + return + } + assert(result[pair.key] == nil) + switch pair.value { + case .invited(let dependencies): + result[pair.key] = .referenceId(pair.key) + case .claimed(let epochs): + let epoch = try epochs.last.tryUnwrap + result[pair.key] = .credential( + epoch.credential, + epoch.epoch + ) + } } } - } - public func readyToWelcome(member: DiMLS.ReferenceID) throws { - try members[member].tryUnwrap - .readyToWelcome(member: member) + public func readyToWelcome(member: ReferenceID) throws { + try members[member].tryUnwrap + .readyToWelcome(member: member) + } } } -extension DiGroupState: Archivable { +extension DiMLS.TotalGroup: Archivable { public struct Archive: Codable, Sendable { public let members: [DiMLS.ReferenceID: Membership.Archive] //array makes it easier to encode stably over the wire @@ -89,7 +91,7 @@ extension DiGroupState: Archivable { } } -extension DiGroupState { +extension DiMLS.TotalGroup { public enum Membership { //can be empty so that as an identity provider it allows me to add them, //then lets me fill in the dependency @@ -141,7 +143,7 @@ extension DiGroupState { } } -extension DiGroupState.Membership: Archivable { +extension DiMLS.TotalGroup.Membership: Archivable { public enum Archive: Codable, Sendable { case invited([DiMLS.KeyedDependency]) case claimed([Epoch.Archive]) From 1a357d234ac661c142ef53945c37e61b9944d648 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 19 Jan 2026 22:07:34 -0800 Subject: [PATCH 14/14] decrypt output and decrypt first app message --- Sources/DistributedMLS/DiMLS.swift | 5 ---- .../Group/Decrypt/DecryptOutput.swift | 29 +++++++++++++++++++ Sources/DistributedMLS/Group/DiGroup.swift | 6 +++- .../Helpers/ConvenienceError.swift | 17 +++++++++++ .../Interfaces/WelcomeOutputInterface.swift | 1 + 5 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift diff --git a/Sources/DistributedMLS/DiMLS.swift b/Sources/DistributedMLS/DiMLS.swift index f76f4c5..3d8710c 100644 --- a/Sources/DistributedMLS/DiMLS.swift +++ b/Sources/DistributedMLS/DiMLS.swift @@ -35,11 +35,6 @@ public enum DiMLS { } } - public enum DecryptOutput { - case control //todo: type out the control plane message - case application(plaintext: Data) - } - public struct EncryptOutput: Sendable { //Implementation may choose to staple the commit and/or //encrypt headers diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift new file mode 100644 index 0000000..01fe5b1 --- /dev/null +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -0,0 +1,29 @@ +// +// DecryptOutput.swift +// DistributedMLS +// +// Created by Mark @ Germ on 1/19/26. +// + +import Foundation + +extension DiMLS { + public struct DecryptOutput: Sendable { + let appPlaintext: AppPlaintext + let controlMessage: ControlMessage? + } + + public struct ControlMessage: Sendable { + + } + + public struct AppPlaintext: Sendable { + public let application: Data + public let authenticating: Data + + public init(application: Data, authenticating: Data) { + self.application = application + self.authenticating = authenticating + } + } +} diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index d4b7cb2..08aabd8 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -36,8 +36,12 @@ extension DiMLS { //(add + dependency), so we let the implementation modify the pending //state to pop off the actions it can make progress on func prepareCommit() throws -> DiMLS.CommitInput + //different interface as it is initially handled by the init key + //corresponding to a keyPackage func received(welcome: WelcomeOutput) throws - + //if we know we can process directly with the symmetric ratchet + func received(privateMessage: Data) throws -> AppPlaintext + func received(ciphertext: Data) throws -> DecryptOutput // func stageNewLocalKeyMaterial() throws } } diff --git a/Sources/DistributedMLS/Helpers/ConvenienceError.swift b/Sources/DistributedMLS/Helpers/ConvenienceError.swift index 8553ba5..e7c26b8 100644 --- a/Sources/DistributedMLS/Helpers/ConvenienceError.swift +++ b/Sources/DistributedMLS/Helpers/ConvenienceError.swift @@ -23,14 +23,31 @@ extension Optional { } } +extension Array { + public var expectOne: Element { + get throws { + switch count { + case 0: throw ConvenienceError.emptyArray + case 1: try first.tryUnwrap + default: throw ConvenienceError.tooManyElements + } + } + } +} + enum ConvenienceError: Error { case missingOptional(String) + case emptyArray + case tooManyElements } extension ConvenienceError: LocalizedError { var errorDescription: String? { switch self { case .missingOptional(let type): "Expected to find an optional \(type), but didn't." + case .emptyArray: "Expected to find a value in an array, but the array was empty." + case .tooManyElements: + "Expected to find a single value in an array, but the array had multiple values." } } } diff --git a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift index 44f5751..3a67f64 100644 --- a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift +++ b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift @@ -11,4 +11,5 @@ public protocol WelcomeOutputInterface: Sendable { var diGroupId: Data { get } var senderReferenceId: DiMLS.ReferenceID { get throws } var keyedDependency: DiMLS.KeyedDependency? { get } + var appPrivateMessage: Data? { get } }