diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index 4ce8cca..9c2b2a1 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -8,10 +8,10 @@ import Foundation extension DiMLS { - public protocol Client: Actor, Archivable { + public protocol Client: Archivable { associatedtype Credential: DiMLSCredential associatedtype Group: DiGroup - where Group.WelcomeOutput == WelcomeOutput, Group.Credential == Credential + where Group.Receiver.WelcomeOutput == WelcomeOutput, Group.Credential == Credential associatedtype WelcomeOutput: WelcomeOutputInterface static func create(credential: Credential) throws -> Self diff --git a/Sources/DistributedMLS/Group/CommitInput.swift b/Sources/DistributedMLS/Group/CommitInput.swift index 1cecf38..56489ea 100644 --- a/Sources/DistributedMLS/Group/CommitInput.swift +++ b/Sources/DistributedMLS/Group/CommitInput.swift @@ -10,7 +10,7 @@ import Foundation extension DiMLS { public struct CommitInput { public var localOps: Set> - public var followOps: Set> + public var followOps: [PendingState.FollowOp] public var dependencies: [KeyedDependency] @@ -45,3 +45,22 @@ extension DiMLS { } } } + +//extension DiMLS { +// //lookup table that we pass from total group to a client to process +// public typealias AvailableDependendencies = [ReferenceID: [EpochID: KeyedDependency]] +//} +//extension DiMLS.TotalGroup { +// public var keyedDependencies: DiMLS.AvailableDependendencies { +// members.compactMapValues { +// guard case .claimed(let epochs) = $0 else { +// return nil +// } +// return epochs.reduce(into: [:]) { result, epoch in +// if let keyedDependency = epoch.keyedDependency { +// result[epoch.epoch] = keyedDependency +// } +// } +// } +// } +//} diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift index 01fe5b1..f3fcecb 100644 --- a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -8,22 +8,37 @@ import Foundation extension DiMLS { - public struct DecryptOutput: Sendable { - let appPlaintext: AppPlaintext - let controlMessage: ControlMessage? - } + public struct DecryptOutput: Sendable { + public let appPlaintext: AppPlaintext + public let commitResult: CommitResult? - public struct ControlMessage: Sendable { + public init( + appPlaintext: AppPlaintext, + commitResult: CommitResult? + ) { + self.appPlaintext = appPlaintext + self.commitResult = commitResult + } + } + public struct CommitResult: Sendable { + public let added: [C] + public let keyedDependency: KeyedDependency + public init(added: [C], keyedDependency: KeyedDependency) { + self.added = added + self.keyedDependency = keyedDependency + } } - public struct AppPlaintext: Sendable { + public struct AppPlaintext: Sendable, Equatable { public let application: Data public let authenticating: Data + public let sender: Data - public init(application: Data, authenticating: Data) { + public init(application: Data, authenticating: Data, sender: Data) { self.application = application self.authenticating = authenticating + self.sender = sender } } } diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 08aabd8..289462a 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -9,15 +9,14 @@ import Foundation //higher level abstraction for the Local / extension DiMLS { - public protocol DiGroup: Actor, Archivable { + public protocol DiGroup: AnyObject, Archivable { associatedtype Credential: DiMLSCredential //State Types - associatedtype Receiver: ReceiveChannel where Receiver.WelcomeOutput == WelcomeOutput + associatedtype Receiver: ReceiveChannel + where Receiver.WelcomeOutput.Credential == Credential associatedtype Sender: SendChannel where Sender.Credential == Credential - associatedtype WelcomeOutput: WelcomeOutputInterface - //Archivable var archive: Archive { get throws } @@ -25,6 +24,7 @@ extension DiMLS { //mutable object nonisolated var diGroupId: Data { get } + nonisolated var myReferenceId: Data { get } var totalGroup: DiMLS.TotalGroup { get } var receivers: [ReferenceID: Receiver] { get set } var pendingState: PendingState { get } @@ -36,12 +36,7 @@ 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 } } @@ -59,96 +54,125 @@ extension DiMLS.DiGroup { return true } - public func setupChannel( - myCredential: Credential, - credentialFetcher: @escaping CredentialKeyPackageFetcher, - referenceIdFetcher: @escaping ReferenceIdKeyPackageFetcher - ) throws -> Task { - guard case .queued(let dependency) = lazySender else { - if case .creating(let task, _) = lazySender { - return task - } - throw DiMLSError.sendGroupNotReady + public func prepareSendChannelSetup() + throws -> SendChannelInputs.Snapshot + { + switch lazySender { + //can call this repeatedly to get a newer membership list + case .ready: + throw DiMLSError.sendGroupCreated + default: + break } - let task = Task { - do { - let archive = try await createSendGroup( - myCredential: myCredential, - members: - totalGroup - .membershipForCreating(sender: myCredential.referenceId), - dependency: dependency, - credentialFetcher: credentialFetcher, - referenceIdFetcher: referenceIdFetcher - ) - let identityProvider = Sender.identityProvider( - totalGroup: totalGroup, - sender: myCredential - ) + //can't construct group de novo by unioning when joining, + //need to consult the existing groups + //to find the widest one to references + let referenceEpoch = try chooseReceiver() + let referenceSender = referenceEpoch.senderCredential + + var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote.Snapshot]() + remotes[referenceSender.referenceId] = + .joined( + epoch: referenceEpoch.epoch, + credential: referenceSender + ) + for recipient in referenceEpoch.recipients { + guard recipient.credential.referenceId != myReferenceId else { + continue + } - lazySender = .ready( - try .init( - archive: archive, - diGroupId: diGroupId, - identityProvider: identityProvider + switch totalGroup.members[recipient.credential.referenceId] { + case .claimed(let epochs): + let epoch = try epochs.last.tryUnwrap + remotes[recipient.credential.referenceId] = + .joined( + epoch: epoch.epoch, + credential: epoch.senderCredential ) - ) - } catch { - print("error creating: \(error)") - lazySender = .queued(dependency) - throw error + case .invited: + remotes[recipient.credential.referenceId] = + .invited + case .known: + remotes[recipient.credential.referenceId] = + .known(recipient.credential) + case .none: + throw DiMLSError.disallowed } } - lazySender = .creating(task, dependency) - return task + + let snapshot = SendChannelInputs.Snapshot( + diGroupID: diGroupId, + remotes: remotes, + dependency: try referenceEpoch.keyedDependency.tryUnwrap + ) + lazySender = .preparing(snapshot) + return snapshot } - private func createSendGroup( - myCredential: Credential, - members: [DiMLS.ReferenceID: DiMLS.Participant], - dependency: DiMLS.KeyedDependency?, - credentialFetcher: CredentialKeyPackageFetcher, - referenceIdFetcher: ReferenceIdKeyPackageFetcher - ) async throws -> Sender.Archive { - - //capture a snapshot of what the group needs - var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote]() - - 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) - - remotes[member.key] = .init( - keyPackage: .init( - credential: credential, - keyPackage: keyPackage - ), - theirEpoch: epoch - ) - case .referenceId(let referenceId): - remotes[member.key] = .init( - keyPackage: try await referenceIdFetcher(referenceId), - theirEpoch: nil - ) + private func chooseReceiver() throws -> DiMLS.TotalGroup.Membership.Epoch { + var choice: DiMLS.TotalGroup.Membership.Epoch? = nil + for (id, receiver) in totalGroup.members { + guard id != myReferenceId else { + continue + } + guard case .claimed(let epochs) = receiver else { + continue } + let epoch = try epochs.last.tryUnwrap + if let _choice = choice { + if _choice.recipients.count < epoch.recipients.count { + choice = epoch + //TODO, if set equality, compare individual epochs + } + } else { + choice = epoch + } + } + return try choice.tryUnwrap + } + + public func setupChannel(input: SendChannelInputs) throws { + guard case .preparing(let stagedInput) = lazySender else { + throw DiMLSError.sendGroupNotReady } + guard input.snapshotId == stagedInput.id else { + throw DiMLSError.reentrantCreateCall + } + + do { + let (archive, epoch) = try createSendGroup(input: input) + try totalGroup.welcomed(member: epoch) + let identityProvider = Sender.identityProvider( + totalGroup: totalGroup, + sender: input.myCredential + ) + + lazySender = .ready( + try .init( + archive: archive, + diGroupId: diGroupId, + identityProvider: identityProvider + ) + ) + } catch { + print("error creating: \(error)") + lazySender = .queued + throw error + } + } + + private func createSendGroup( + input: SendChannelInputs + ) throws -> (Sender.Archive, DiMLS.TotalGroup.Membership.Epoch) { return try Sender.create( - input: .init( - diGroupID: diGroupId, - myCredential: myCredential, - remotes: remotes - ), + input: input, identityProvider: Sender.identityProvider( totalGroup: totalGroup, - sender: myCredential + sender: input.myCredential ), - dependency: dependency + dependency: input.dependency ) } @@ -156,7 +180,7 @@ extension DiMLS.DiGroup { plaintext: Data, authenticating: Data, staplingCommit: Bool - ) throws -> [(Credential, DiMLS.EncryptOutput)] { + ) throws -> [Credential: DiMLS.EncryptOutput] { let sender = try lazySender.readyChannel //commit if necessary @@ -170,7 +194,7 @@ extension DiMLS.DiGroup { private func commitIfNecessary( sender: Sender - ) throws -> DiMLS.CommitEffect? { + ) throws -> DiMLS.LocalCommitEffect? { guard pendingState.commitNeeded else { return nil } @@ -183,7 +207,7 @@ extension DiMLS.DiGroup { private func commit( input: DiMLS.CommitInput, sender: Sender, - ) throws -> DiMLS.CommitEffect { + ) throws -> DiMLS.LocalCommitEffect { var newRemotes: [DiMLS.CredentialedKeyPackage] = [] //modify remotes and group @@ -198,10 +222,11 @@ extension DiMLS.DiGroup { let sendChannel = try lazySender.readyChannel - let (commitMessage, welcome) = try sendChannel.commit(input: input) + //TODO: these are unused + let (commitMessage, welcome, historyEpoch) = try sendChannel.commit(input: input) + pendingState.committed(input: input) if !newRemotes.isEmpty { - for newRemote in newRemotes { let referenceId = newRemote.credential.referenceId @@ -211,6 +236,11 @@ extension DiMLS.DiGroup { } } + try totalGroup.committed( + referenceId: myReferenceId, + epoch: historyEpoch + ) + return .init( didIntroduceNewPubKey: input.newSenderLeafNode, localOps: [], @@ -219,7 +249,10 @@ extension DiMLS.DiGroup { ) } - public func received(welcome: WelcomeOutput) throws { + public func received( + welcome: Receiver.WelcomeOutput, + myCredential: Credential, + ) throws -> DiMLS.AppPlaintext? { guard welcome.diGroupId == diGroupId else { throw DiMLSError.mismatchedGroupId } @@ -234,8 +267,59 @@ extension DiMLS.DiGroup { throw DiMLSError.duplicateMember } receivers[senderReferenceId] = try .create(welcome: welcome) + try totalGroup.welcomed( + member: try welcome.membershipEpoch(myCredential: myCredential) + ) + + guard let appMessage = welcome.appPrivateMessage else { + return nil + } + return try receivers[senderReferenceId].tryUnwrap + .decrypt(messageData: appMessage) + } + public func received(privateMessage: Data, sender: DiMLS.ReferenceID) throws + -> DiMLS.AppPlaintext + { + try receivers[sender].tryUnwrap + .decrypt(messageData: privateMessage) + } + + public func received( + ciphertext: Data, + //helpful to have a hint here or you have to cycle through each send group + //as we use 1:1 channels this can be imferred from the channel + senderHint: DiMLS.ReferenceID? + ) throws -> DiMLS.DecryptOutput { + let matching: [Receiver] = try { + return if let senderHint { + [try receivers[senderHint].tryUnwrap] + } else { + .init(receivers.values) + } + }() + + for receiver in matching { + if let result = try receiver.received( + ciphertext: ciphertext, + diGroupId: diGroupId + ) { + if let commitResult = result.commitResult { + for added in commitResult.added { + try totalGroup.invited( + member: added.referenceId, + keyedDependency: commitResult.keyedDependency + ) + } + } + + return result + } + } + + throw DiMLSError.decryptFallthrough + } } //we have a generic (Credential) and a non-generic and could simplify them diff --git a/Sources/DistributedMLS/Group/CommitEffect.swift b/Sources/DistributedMLS/Group/LocalCommitEffect.swift similarity index 83% rename from Sources/DistributedMLS/Group/CommitEffect.swift rename to Sources/DistributedMLS/Group/LocalCommitEffect.swift index 209e45f..8e8b957 100644 --- a/Sources/DistributedMLS/Group/CommitEffect.swift +++ b/Sources/DistributedMLS/Group/LocalCommitEffect.swift @@ -9,10 +9,10 @@ extension DiMLS { public struct EncryptResult { let privateMessage: PrivateMessage //application messages always follow a commit. let the application - let commitEffect: CommitEffect? + let commitEffect: LocalCommitEffect? } - public struct CommitEffect { + public struct LocalCommitEffect { let didIntroduceNewPubKey: Bool let localOps: [DiMLSOperations] //need to annotate the causal dependencies as well diff --git a/Sources/DistributedMLS/Group/Participant.swift b/Sources/DistributedMLS/Group/Participant.swift index dae50d7..deceeb2 100644 --- a/Sources/DistributedMLS/Group/Participant.swift +++ b/Sources/DistributedMLS/Group/Participant.swift @@ -9,8 +9,17 @@ import Foundation //exists for clients to reason about which credential to point to extension DiMLS { - public enum Participant { + public enum Participant: Equatable { case credential(Credential, DiMLS.EpochID) case referenceId(DiMLS.ReferenceID) + + public var referenceId: DiMLS.ReferenceID { + switch self { + case .credential(let credential, _): + credential.referenceId + case .referenceId(let referenceID): + referenceID + } + } } } diff --git a/Sources/DistributedMLS/Group/PendingState.swift b/Sources/DistributedMLS/Group/PendingState.swift index ee876aa..95c6c59 100644 --- a/Sources/DistributedMLS/Group/PendingState.swift +++ b/Sources/DistributedMLS/Group/PendingState.swift @@ -10,68 +10,128 @@ import os extension DiMLS { public final class PendingState { var applicationRequestsNewKeyDistribution: Bool - public var pendingLocalOps: [DiMLS.ReferenceID: DiMLSOperations] + public var localOps: Set> //this includes incorporating PCS updates - var pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations] + var followOps: [FollowOp] public static func create() -> Self { .init( applicationRequestsNewKeyDistribution: false, - pendingLocalOps: [:], - pendingFollowOps: [:] + localOps: [], + followOps: [] ) } init( applicationRequestsNewKeyDistribution: Bool, - pendingLocalOps: [DiMLS.ReferenceID: DiMLSOperations], - pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations] + localOps: Set>, + followOps: [FollowOp] ) { self.applicationRequestsNewKeyDistribution = applicationRequestsNewKeyDistribution - self.pendingLocalOps = pendingLocalOps - self.pendingFollowOps = pendingFollowOps + self.localOps = localOps + self.followOps = followOps } //MARK: Archive public struct Archive: Sendable, Codable { let applicationRequestsNewKeyDistribution: Bool - let pendingLocalOps: [DiMLS.ReferenceID: DiMLSOperations.Archive] + let localOps: [DiMLSOperations.Archive] //this includes incorporating PCS updates - let pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations.Archive] + let followOps: [FollowOp.Archive] } public init(archive: Archive) throws { self.applicationRequestsNewKeyDistribution = archive.applicationRequestsNewKeyDistribution - self.pendingLocalOps = try archive.pendingLocalOps - .mapValues { try .init(archive: $0) } - self.pendingFollowOps = try archive.pendingFollowOps - .mapValues { try .init(archive: $0) } + self.localOps = try .init( + archive.localOps.map { + try .init(archive: $0) + }) + self.followOps = try archive.followOps + .map { try .init(archive: $0) } } public var archive: Archive { get throws { .init( applicationRequestsNewKeyDistribution: applicationRequestsNewKeyDistribution, - pendingLocalOps: try pendingLocalOps.mapValues { try $0.archive }, - pendingFollowOps: try pendingFollowOps.mapValues { try $0.archive }, + localOps: try localOps.map { try $0.archive }, + followOps: + try followOps + .map { try $0.archive }, ) } } public var commitNeeded: Bool { - applicationRequestsNewKeyDistribution || !pendingLocalOps.isEmpty - || !pendingFollowOps.isEmpty + applicationRequestsNewKeyDistribution || !localOps.isEmpty + || !followOps.isEmpty } public func stageAdd(member: DiMLS.CredentialedKeyPackage) throws { - //already staged conflicting op? we can just replace for now - if let pendingOp = pendingLocalOps[member.credential.referenceId] { - Logger(subsystem: "SenderGroupDiMLS", category: "validAdd") - .notice("substituting a staged \(pendingOp.description)") + localOps.insert(.add(member)) + } + + public func prepareCommit() throws -> DiMLS.CommitInput { + assert(commitNeeded) + var result = DiMLS.CommitInput() + //first follow remote ops + //TODO + + //then perform own actions if not redundant + for action in localOps { + result.localOps.insert(action) + } + + //inject causal dependency if needed + //TODO + + //broadcast new keys if necessary + //TODO + + return result + } + + func committed(input: CommitInput) { + for localOp in input.localOps { + localOps.remove(localOp) + } + } + + public func accumulateAdds() throws -> DiMLS.CommitInput? { + //currently the same + if commitNeeded { + try prepareCommit() + } else { + nil } + } + } +} + +extension DiMLS.PendingState { + public struct FollowOp { + let operation: DiMLSOperations + let dependency: DiMLS.KeyedDependency? + } +} + +extension DiMLS.PendingState.FollowOp: Archivable { + public struct Archive: Codable, Sendable { + let operation: DiMLSOperations.Archive + let dependency: DiMLS.KeyedDependency? + } + + public init(archive: Archive) throws { + self.init( + operation: try .init(archive: archive.operation), + dependency: archive.dependency + ) + } - pendingLocalOps[member.credential.referenceId] = .add(member) + var archive: Archive { + get throws { + try .init(operation: try operation.archive, dependency: dependency) } } } diff --git a/Sources/DistributedMLS/Group/ReceiveChannel.swift b/Sources/DistributedMLS/Group/ReceiveChannel.swift index 575d20a..3cd2ba2 100644 --- a/Sources/DistributedMLS/Group/ReceiveChannel.swift +++ b/Sources/DistributedMLS/Group/ReceiveChannel.swift @@ -10,4 +10,8 @@ import Foundation public protocol ReceiveChannel { associatedtype WelcomeOutput: WelcomeOutputInterface static func create(welcome: WelcomeOutput) throws -> Self + func decrypt(messageData: Data) throws -> DiMLS.AppPlaintext + func received(ciphertext: Data, diGroupId: Data, ) throws -> DistributedMLS.DiMLS.DecryptOutput< + WelcomeOutput.Credential + >? } diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index 3b7975c..86820d4 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -25,7 +25,7 @@ public protocol SendChannel { input: SendChannelInputs, identityProvider: IdentityProvider, dependency: DiMLS.KeyedDependency? - ) throws -> Archive + ) throws -> (Archive, DiMLS.TotalGroup.Membership.Epoch) init( archive: Archive, @@ -39,20 +39,39 @@ public protocol SendChannel { func commit(input: DiMLS.CommitInput) throws -> ( commitMessage: Commit, - welcomes: [Welcome] + welcomes: [Welcome], + historyEpoch: DiMLS.TotalGroup.Membership.Epoch ) //packaging the encrypted app message with metadata can all be done //in the send channel - func encrypt(plaintext: Data, authenticating: Data) throws -> [( - Credential, DiMLS.EncryptOutput - )] + func encrypt(plaintext: Data, authenticating: Data) throws -> [Credential: DiMLS.EncryptOutput] + var recipients: [Credential] { get throws } + + func exportDependencyKey(diGroupContext: Data) throws -> Data } public struct SendChannelInputs { + public let snapshotId: UUID public let diGroupID: Data public let myCredential: Credential public let remotes: [DiMLS.ReferenceID: Remote] + //for now, can only refer to a single group when distributing welcomes + public let dependency: DiMLS.KeyedDependency? + + public init( + snapshotId: UUID, + diGroupID: Data, + myCredential: Credential, + remotes: [DiMLS.ReferenceID: Remote], + dependency: DiMLS.KeyedDependency? + ) { + self.snapshotId = snapshotId + self.diGroupID = diGroupID + self.myCredential = myCredential + self.remotes = remotes + self.dependency = dependency + } public struct Remote { public let keyPackage: DiMLS.CredentialedKeyPackage @@ -66,6 +85,30 @@ public struct SendChannelInputs { self.keyPackage = keyPackage self.theirEpoch = theirEpoch } + + public enum Snapshot { + case invited + case known(Credential) + case joined(epoch: UInt64, credential: Credential) + + public var epoch: UInt64? { + switch self { + case .invited, .known: + nil + case .joined(let epoch): + epoch.epoch + } + } + } + } + + //get keypackages for these remotes and give me back a SendChannelInputs when done + public struct Snapshot { + public let id = UUID() + public let diGroupID: Data + public let remotes: [DiMLS.ReferenceID: Remote.Snapshot] + //for now, can only refer to a single group when distributing welcomes + public let dependency: DiMLS.KeyedDependency? } public var correspondingRecipientEpoch: [DiMLS.ReferenceID: UInt64] { @@ -80,9 +123,13 @@ public struct SendChannelInputs { public enum LazySendChannel { case ready(R) - case queued(DiMLS.KeyedDependency?) - //serves as a mutex on snapshot of Q state - case creating(Task, DiMLS.KeyedDependency?) + case queued + + //we want to externalize the async fetching of keyPackages + //so we allow for the app to ask for a membership set, freezing it + //then come back async to init that version + //it is ok for us to miss some updates in the interim + case preparing(SendChannelInputs.Snapshot) public init( archive: Archive, @@ -98,12 +145,12 @@ public enum LazySendChannel { identityProvider: identityProvider ) ) - case .queued(let dependency): - self = .queued(dependency) + case .queued: + self = .queued } } - var readyChannel: R { + public var readyChannel: R { get throws { guard case .ready(let r) = self else { throw DiMLSError.sendGroupNotReady @@ -116,7 +163,7 @@ public enum LazySendChannel { extension LazySendChannel { public enum Archive: Sendable, Codable { case ready(R.Archive) - case queued(DiMLS.KeyedDependency?) + case queued } public var archive: Archive { @@ -124,10 +171,8 @@ extension LazySendChannel { switch self { case .ready(let r): try .ready(r.archive) - case .queued(let dependency): - .queued(dependency) - case .creating(_, let dependency): - .queued(dependency) + case .queued, .preparing: + .queued } } } diff --git a/Sources/DistributedMLS/Helpers/Error.swift b/Sources/DistributedMLS/Helpers/Error.swift index 19c0b3a..ca2564e 100644 --- a/Sources/DistributedMLS/Helpers/Error.swift +++ b/Sources/DistributedMLS/Helpers/Error.swift @@ -14,7 +14,10 @@ public enum DiMLSError: Error { case notImplemented case disallowed case sendGroupNotReady + case sendGroupCreated + case reentrantCreateCall case duplicateMember + case decryptFallthrough case expecting(DiMLS.Dependency) } @@ -27,7 +30,10 @@ extension DiMLSError: LocalizedError { case .notImplemented: "Not implemented" case .disallowed: "Disallowed" case .sendGroupNotReady: "Send group not ready" + case .sendGroupCreated: "Send group already created" + case .reentrantCreateCall: "Reentrant create call" case .duplicateMember: "Duplicate member" + case .decryptFallthrough: "Decryption fallthrough" case .expecting: "Missing dependency" } } diff --git a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift index 3a67f64..dfd0c5c 100644 --- a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift +++ b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift @@ -8,8 +8,11 @@ import Foundation public protocol WelcomeOutputInterface: Sendable { + associatedtype Credential: DiMLSCredential var diGroupId: Data { get } var senderReferenceId: DiMLS.ReferenceID { get throws } var keyedDependency: DiMLS.KeyedDependency? { get } var appPrivateMessage: Data? { get } + func membershipEpoch(myCredential: Credential) throws + -> DiMLS.TotalGroup.Membership.Epoch } diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 93cbb52..4cdb938 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -37,39 +37,41 @@ extension DiMLS { 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 { + public func readyToWelcome(member: ReferenceID) throws { + try members[member].tryUnwrap + .readyToWelcome(member: member) + } + + func welcomed(member: Membership.Epoch) throws { + let referenceId = member.senderCredential.referenceId + guard case .invited = members[referenceId] else { + throw DiMLSError.disallowed + } + + let existing = try members[referenceId].tryUnwrap + members[referenceId] = try existing.welcomed(epoch: member) + } + + func invited( + member: ReferenceID, + keyedDependency: DiMLS.KeyedDependency + ) throws { + if let existing = members[member] { + guard case .invited(let array) = existing 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 - ) - } + members[member] = .invited(array + [keyedDependency]) + } else { + members[member] = .invited([keyedDependency]) } } - public func readyToWelcome(member: ReferenceID) throws { - try members[member].tryUnwrap - .readyToWelcome(member: member) + func committed( + referenceId: DiMLS.ReferenceID, + epoch: Membership.Epoch + ) throws { + let existing = try members[referenceId].tryUnwrap + members[referenceId] = try existing.committed(epoch: epoch) } } } @@ -96,6 +98,7 @@ extension DiMLS.TotalGroup { //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 known //I did not see the initial invite case claimed([Epoch]) static func new( @@ -105,33 +108,80 @@ extension DiMLS.TotalGroup { } public struct Epoch: Archivable { - let epoch: UInt64 - public let credential: Credential + let epoch: DiMLS.EpochID + public let senderCredential: Credential + public var recipients: [Recipient] + //can efface this when fully ack'd + var keyedDependency: DiMLS.KeyedDependency? + + //acknowledge other groups + let newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + + //accumulate these and don't accept rollbacks + let baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] - init(epoch: UInt64, credential: Credential) { + public init( + epoch: DiMLS.EpochID, + senderCredential: Credential, + recipients: [Recipient], + keyedDependency: DiMLS.KeyedDependency?, + newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID], + baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + ) { self.epoch = epoch - self.credential = credential + self.senderCredential = senderCredential + self.recipients = recipients + self.keyedDependency = keyedDependency + self.newDependencies = newDependencies + self.baseDependencies = baseDependencies } public init(archive: Archive) throws { self.init( epoch: archive.epoch, - credential: try .init(encoded: archive.credential) + senderCredential: try .init(encoded: archive.credential), + recipients: try archive.recipients + .map { try .init(archive: $0) }, + keyedDependency: archive.keyedDependency, + newDependencies: archive.newDependencies, + baseDependencies: archive.baseDependencies ) } public struct Archive: Codable, Sendable { - let epoch: UInt64 + let epoch: DiMLS.EpochID let credential: Data + let recipients: [Recipient.Archive] + let keyedDependency: DiMLS.KeyedDependency? + let newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + let baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] - public init(epoch: UInt64, credential: Data) { + public init( + epoch: DiMLS.EpochID, + credential: Data, + recipients: [Recipient.Archive], + keyedDependency: DiMLS.KeyedDependency?, + newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID], + baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + ) { self.epoch = epoch self.credential = credential + self.recipients = recipients + self.keyedDependency = keyedDependency + self.newDependencies = newDependencies + self.baseDependencies = baseDependencies } } var archive: Archive { - .init(epoch: epoch, credential: credential.encoded) + .init( + epoch: epoch, + credential: senderCredential.encoded, + recipients: recipients.map(\.archive), + keyedDependency: keyedDependency, + newDependencies: newDependencies, + baseDependencies: baseDependencies + ) } } @@ -140,20 +190,54 @@ extension DiMLS.TotalGroup { throw DiMLSError.duplicateMember } } + + func welcomed(epoch: Epoch) throws -> Membership { + switch self { + case .known, .invited: + break + case .claimed(let array): + throw DiMLSError.disallowed + } + + return .claimed([epoch]) + + } + + func committed(epoch: Epoch) throws -> Membership { + guard case .claimed(let epochs) = self else { + throw DiMLSError.disallowed + } + let newest = try epochs.last.tryUnwrap + assert(newest.epoch + 1 == epoch.epoch) + + var newBase = newest.baseDependencies + for (key, value) in epoch.newDependencies { + if let existing = newBase[key] { + assert(value > existing) + newBase[key] = value + } + } + + let newEpoch = Epoch( + epoch: epoch.epoch, + senderCredential: epoch.senderCredential, + recipients: epoch.recipients, + keyedDependency: epoch.keyedDependency, + newDependencies: epoch.newDependencies, + baseDependencies: epoch.baseDependencies + ) + + return .claimed(epochs + [epoch]) + + } } } extension DiMLS.TotalGroup.Membership: Archivable { public enum Archive: Codable, Sendable { case invited([DiMLS.KeyedDependency]) + case known case claimed([Epoch.Archive]) - - public static func create( - credential: Data, - epoch: UInt64 - ) -> Self { - .claimed([.init(epoch: epoch, credential: credential)]) - } } public init(archive: Archive) throws { @@ -162,6 +246,8 @@ extension DiMLS.TotalGroup.Membership: Archivable { self = .claimed(try epochs.map { try .init(archive: $0) }) case .invited(let dependencies): self = .invited(dependencies) + case .known: + self = .known } } @@ -171,6 +257,55 @@ extension DiMLS.TotalGroup.Membership: Archivable { .claimed(epochs.map(\.archive)) case .invited(let dependencies): .invited(dependencies) + case .known: + .known + } + } +} + +extension DiMLS.TotalGroup.Membership.Epoch { + public struct Recipient: Archivable { + let credential: Credential + var acknowledged: Bool + + public init(credential: Credential, acknowledged: Bool) { + self.credential = credential + self.acknowledged = acknowledged + } + + public init(archive: Archive) throws { + self.init( + credential: try .init(encoded: archive.credential), + acknowledged: archive.acknowledged + ) + } + + public struct Archive: Codable, Sendable { + let credential: Data + let acknowledged: Bool + + public init(credential: Data, acknowledged: Bool) { + self.credential = credential + self.acknowledged = acknowledged + } + } + + var archive: Archive { + .init(credential: credential.encoded, acknowledged: acknowledged) + } + } +} + +extension DiMLS.TotalGroup { + public var knownDependencies: [DiMLS.KeyedDependency] { + members.values.reduce(into: []) { result, member in + switch member { + case .invited(let dependencies): + result += dependencies + case .known: break + case .claimed(let epochs): + result += epochs.compactMap(\.keyedDependency) + } } } }