From e05ead98edae0169e7b94fbd348a352b5824bac1 Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Wed, 17 Apr 2024 19:17:55 -0600 Subject: [PATCH] feat: Consent Proof Payload Updated invitiation protos Added handling for consent proof signature on conversation creation --- Sources/XMTPiOS/Conversations.swift | 5 +- Sources/XMTPiOS/Messages/Invitation.swift | 26 +++- .../message_contents/invitation.pb.swift | 132 ++++++++++++++++++ 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 6ec263ca..74ec9a2a 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -447,7 +447,7 @@ public actor Conversations { return Group(ffiGroup: group, client: client) } - public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil) async throws -> Conversation { + public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil, consentProofSignature: String? = nil) async throws -> Conversation { if peerAddress.lowercased() == client.address.lowercased() { throw ConversationError.recipientIsSender } @@ -470,7 +470,8 @@ public actor Conversations { let invitation = try InvitationV1.createDeterministic( sender: client.keys, recipient: recipient, - context: context + context: context, + consentProofSignature ) let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) diff --git a/Sources/XMTPiOS/Messages/Invitation.swift b/Sources/XMTPiOS/Messages/Invitation.swift index 2c156fd1..0bf4c318 100644 --- a/Sources/XMTPiOS/Messages/Invitation.swift +++ b/Sources/XMTPiOS/Messages/Invitation.swift @@ -6,12 +6,17 @@ import Foundation /// Handles topic generation for conversations. public typealias InvitationV1 = Xmtp_MessageContents_InvitationV1 +public typealias ConsentProofPayload = Xmtp_MessageContents_ConsentProofPayload +public typealias ConsentProofPayloadVersion = Xmtp_MessageContents_ConsentProofPayloadVersion + + extension InvitationV1 { static func createDeterministic( sender: PrivateKeyBundleV2, recipient: SignedPublicKeyBundle, - context: InvitationV1.Context? = nil + context: InvitationV1.Context? = nil, + consentProofSignature: String? = nil ) throws -> InvitationV1 { let context = context ?? InvitationV1.Context() let myAddress = try sender.toV1().walletAddress @@ -33,14 +38,26 @@ extension InvitationV1 { var aes256GcmHkdfSha256 = InvitationV1.Aes256gcmHkdfsha256() aes256GcmHkdfSha256.keyMaterial = Data(keyMaterial) + // If consentProofSignature is not nil, create a ConsentProofPayload + // with the signature and add it to the InvitationV1 + var consentProofPayload: ConsentProofPayload? = nil + + if let signature = consentProofSignature { + var consentProof = ConsentProofPayload() + consentProof.signature = signature + consentProof.timestamp = UInt64(Date().timeIntervalSince1970 * 1000) + consentProof.payloadVersion = .consentProofPayloadVersion1 + consentProofPayload = consentProof + } return try InvitationV1( topic: topic, context: context, - aes256GcmHkdfSha256: aes256GcmHkdfSha256) + aes256GcmHkdfSha256: aes256GcmHkdfSha256, + consentProof: consentProofPayload) } - init(topic: Topic, context: InvitationV1.Context? = nil, aes256GcmHkdfSha256: InvitationV1.Aes256gcmHkdfsha256) throws { + init(topic: Topic, context: InvitationV1.Context? = nil, aes256GcmHkdfSha256: InvitationV1.Aes256gcmHkdfsha256, consentProof: ConsentProofPayload? = nil) throws { self.init() self.topic = topic.description @@ -48,6 +65,9 @@ extension InvitationV1 { if let context { self.context = context } + if let consentProof { + self.consentProof = consentProof + } self.aes256GcmHkdfSha256 = aes256GcmHkdfSha256 } diff --git a/Sources/XMTPiOS/Proto/message_contents/invitation.pb.swift b/Sources/XMTPiOS/Proto/message_contents/invitation.pb.swift index 50b29960..f7194d60 100644 --- a/Sources/XMTPiOS/Proto/message_contents/invitation.pb.swift +++ b/Sources/XMTPiOS/Proto/message_contents/invitation.pb.swift @@ -24,6 +24,47 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +/// Version of consent proof payload +public enum Xmtp_MessageContents_ConsentProofPayloadVersion: SwiftProtobuf.Enum { + public typealias RawValue = Int + case unspecified // = 0 + case consentProofPayloadVersion1 // = 1 + case UNRECOGNIZED(Int) + + public init() { + self = .unspecified + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .consentProofPayloadVersion1 + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .unspecified: return 0 + case .consentProofPayloadVersion1: return 1 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Xmtp_MessageContents_ConsentProofPayloadVersion: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Xmtp_MessageContents_ConsentProofPayloadVersion] = [ + .unspecified, + .consentProofPayloadVersion1, + ] +} + +#endif // swift(>=4.2) + /// Unsealed invitation V1 public struct Xmtp_MessageContents_InvitationV1 { // SwiftProtobuf.Message conformance is added in an extension below. See the @@ -57,6 +98,16 @@ public struct Xmtp_MessageContents_InvitationV1 { set {encryption = .aes256GcmHkdfSha256(newValue)} } + /// The user's consent proof + public var consentProof: Xmtp_MessageContents_ConsentProofPayload { + get {return _consentProof ?? Xmtp_MessageContents_ConsentProofPayload()} + set {_consentProof = newValue} + } + /// Returns true if `consentProof` has been explicitly set. + public var hasConsentProof: Bool {return self._consentProof != nil} + /// Clears the value of `consentProof`. Subsequent reads from it will return its default value. + public mutating func clearConsentProof() {self._consentProof = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() /// message encryption scheme and keys for this conversation. @@ -115,6 +166,7 @@ public struct Xmtp_MessageContents_InvitationV1 { public init() {} fileprivate var _context: Xmtp_MessageContents_InvitationV1.Context? = nil + fileprivate var _consentProof: Xmtp_MessageContents_ConsentProofPayload? = nil } /// Sealed Invitation V1 Header @@ -222,7 +274,29 @@ public struct Xmtp_MessageContents_SealedInvitation { public init() {} } +/// Payload for user's consent proof to be set in the invitation +/// Signifying the conversation should be preapproved for the user on receipt +public struct Xmtp_MessageContents_ConsentProofPayload { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// the user's signature in hex format + public var signature: String = String() + + /// approximate time when the user signed + public var timestamp: UInt64 = 0 + + /// version of the payload + public var payloadVersion: Xmtp_MessageContents_ConsentProofPayloadVersion = .unspecified + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) +extension Xmtp_MessageContents_ConsentProofPayloadVersion: @unchecked Sendable {} extension Xmtp_MessageContents_InvitationV1: @unchecked Sendable {} extension Xmtp_MessageContents_InvitationV1.OneOf_Encryption: @unchecked Sendable {} extension Xmtp_MessageContents_InvitationV1.Aes256gcmHkdfsha256: @unchecked Sendable {} @@ -231,18 +305,27 @@ extension Xmtp_MessageContents_SealedInvitationHeaderV1: @unchecked Sendable {} extension Xmtp_MessageContents_SealedInvitationV1: @unchecked Sendable {} extension Xmtp_MessageContents_SealedInvitation: @unchecked Sendable {} extension Xmtp_MessageContents_SealedInvitation.OneOf_Version: @unchecked Sendable {} +extension Xmtp_MessageContents_ConsentProofPayload: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "xmtp.message_contents" +extension Xmtp_MessageContents_ConsentProofPayloadVersion: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "CONSENT_PROOF_PAYLOAD_VERSION_UNSPECIFIED"), + 1: .same(proto: "CONSENT_PROOF_PAYLOAD_VERSION_1"), + ] +} + extension Xmtp_MessageContents_InvitationV1: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".InvitationV1" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "topic"), 2: .same(proto: "context"), 3: .standard(proto: "aes256_gcm_hkdf_sha256"), + 4: .standard(proto: "consent_proof"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -266,6 +349,7 @@ extension Xmtp_MessageContents_InvitationV1: SwiftProtobuf.Message, SwiftProtobu self.encryption = .aes256GcmHkdfSha256(v) } }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._consentProof) }() default: break } } @@ -285,6 +369,9 @@ extension Xmtp_MessageContents_InvitationV1: SwiftProtobuf.Message, SwiftProtobu try { if case .aes256GcmHkdfSha256(let v)? = self.encryption { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() + try { if let v = self._consentProof { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -292,6 +379,7 @@ extension Xmtp_MessageContents_InvitationV1: SwiftProtobuf.Message, SwiftProtobu if lhs.topic != rhs.topic {return false} if lhs._context != rhs._context {return false} if lhs.encryption != rhs.encryption {return false} + if lhs._consentProof != rhs._consentProof {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -504,3 +592,47 @@ extension Xmtp_MessageContents_SealedInvitation: SwiftProtobuf.Message, SwiftPro return true } } + +extension Xmtp_MessageContents_ConsentProofPayload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ConsentProofPayload" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "signature"), + 2: .same(proto: "timestamp"), + 3: .standard(proto: "payload_version"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.signature) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.timestamp) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.payloadVersion) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.signature.isEmpty { + try visitor.visitSingularStringField(value: self.signature, fieldNumber: 1) + } + if self.timestamp != 0 { + try visitor.visitSingularUInt64Field(value: self.timestamp, fieldNumber: 2) + } + if self.payloadVersion != .unspecified { + try visitor.visitSingularEnumField(value: self.payloadVersion, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Xmtp_MessageContents_ConsentProofPayload, rhs: Xmtp_MessageContents_ConsentProofPayload) -> Bool { + if lhs.signature != rhs.signature {return false} + if lhs.timestamp != rhs.timestamp {return false} + if lhs.payloadVersion != rhs.payloadVersion {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +}