diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index b921b298..c4042e25 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -356,6 +356,15 @@ public final class Client { throw ConversationImportError.invalidData } + var consentProof: ConsentProofPayload? = nil + if let exportConsentProof = export.consentProof { + var proof = ConsentProofPayload() + proof.signature = exportConsentProof.signature + proof.timestamp = exportConsentProof.timestamp + proof.payloadVersion = ConsentProofPayloadVersion.consentProofPayloadVersion1 + consentProof = proof + } + return .v2(ConversationV2( topic: export.topic, keyMaterial: keyMaterial, @@ -365,7 +374,8 @@ public final class Client { ), peerAddress: export.peerAddress, client: self, - header: SealedInvitationHeaderV1() + header: SealedInvitationHeaderV1(), + consentProof: consentProof )) } diff --git a/Sources/XMTPiOS/Contacts.swift b/Sources/XMTPiOS/Contacts.swift index 924d2840..5aad116d 100644 --- a/Sources/XMTPiOS/Contacts.swift +++ b/Sources/XMTPiOS/Contacts.swift @@ -145,7 +145,6 @@ public class ConsentList { } - let message = try LibXMTP.userPreferencesEncrypt( publicKey: publicKey, privateKey: privateKey, diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index c4631f5b..4fab7c6c 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -324,6 +324,17 @@ public enum Conversation: Sendable { } } + public var consentProof: ConsentProofPayload? { + switch self { + case .v1(_): + return nil + case let .v2(conversationV2): + return conversationV2.consentProof + case .group(_): + return nil + } + } + var client: Client { switch self { case let .v1(conversationV1): diff --git a/Sources/XMTPiOS/ConversationExport.swift b/Sources/XMTPiOS/ConversationExport.swift index f09450e0..2e8fba13 100644 --- a/Sources/XMTPiOS/ConversationExport.swift +++ b/Sources/XMTPiOS/ConversationExport.swift @@ -23,9 +23,15 @@ struct ConversationV2Export: Codable { var peerAddress: String var createdAt: String var context: ConversationV2ContextExport? + var consentProof: ConsentProofPayloadExport? } struct ConversationV2ContextExport: Codable { var conversationId: String var metadata: [String: String] } + +struct ConsentProofPayloadExport: Codable { + var signature: String + var timestamp: UInt64 +} diff --git a/Sources/XMTPiOS/ConversationV2.swift b/Sources/XMTPiOS/ConversationV2.swift index 91b83dc5..d0b9c437 100644 --- a/Sources/XMTPiOS/ConversationV2.swift +++ b/Sources/XMTPiOS/ConversationV2.swift @@ -17,10 +17,11 @@ public struct ConversationV2Container: Codable { var peerAddress: String var createdAtNs: UInt64? var header: SealedInvitationHeaderV1 + var consentProof: ConsentProofPayload? public func decode(with client: Client) -> ConversationV2 { let context = InvitationV1.Context(conversationID: conversationID ?? "", metadata: metadata) - return ConversationV2(topic: topic, keyMaterial: keyMaterial, context: context, peerAddress: peerAddress, client: client, createdAtNs: createdAtNs, header: header) + return ConversationV2(topic: topic, keyMaterial: keyMaterial, context: context, peerAddress: peerAddress, client: client, createdAtNs: createdAtNs, header: header, consentProof: consentProof) } } @@ -31,6 +32,7 @@ public struct ConversationV2 { public var context: InvitationV1.Context public var peerAddress: String public var client: Client + public var consentProof: ConsentProofPayload? var createdAtNs: UInt64? private var header: SealedInvitationHeaderV1 @@ -49,21 +51,23 @@ public struct ConversationV2 { peerAddress: peerAddress, client: client, createdAtNs: header.createdNs, - header: header + header: header, + consentProof: invitation.hasConsentProof ? invitation.consentProof : nil ) } - public init(topic: String, keyMaterial: Data, context: InvitationV1.Context, peerAddress: String, client: Client, createdAtNs: UInt64? = nil) { + public init(topic: String, keyMaterial: Data, context: InvitationV1.Context, peerAddress: String, client: Client, createdAtNs: UInt64? = nil, consentProof: ConsentProofPayload? = nil) { self.topic = topic self.keyMaterial = keyMaterial self.context = context self.peerAddress = peerAddress self.client = client self.createdAtNs = createdAtNs + self.consentProof = consentProof header = SealedInvitationHeaderV1() } - public init(topic: String, keyMaterial: Data, context: InvitationV1.Context, peerAddress: String, client: Client, createdAtNs: UInt64? = nil, header: SealedInvitationHeaderV1) { + public init(topic: String, keyMaterial: Data, context: InvitationV1.Context, peerAddress: String, client: Client, createdAtNs: UInt64? = nil, header: SealedInvitationHeaderV1, consentProof: ConsentProofPayload? = nil) { self.topic = topic self.keyMaterial = keyMaterial self.context = context @@ -71,10 +75,11 @@ public struct ConversationV2 { self.client = client self.createdAtNs = createdAtNs self.header = header + self.consentProof = consentProof } public var encodedContainer: ConversationV2Container { - ConversationV2Container(topic: topic, keyMaterial: keyMaterial, conversationID: context.conversationID, metadata: context.metadata, peerAddress: peerAddress, createdAtNs: createdAtNs, header: header) + ConversationV2Container(topic: topic, keyMaterial: keyMaterial, conversationID: context.conversationID, metadata: context.metadata, peerAddress: peerAddress, createdAtNs: createdAtNs, header: header, consentProof: consentProof) } func prepareMessage(encodedContent: EncodedContent, options: SendOptions?) async throws -> PreparedMessage { diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 6ec263ca..f95a1ec0 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, consentProofPayload: ConsentProofPayload? = 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, + consentProofPayload: consentProofPayload ) let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) @@ -539,10 +540,56 @@ public actor Conversations { private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 { let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header) + + let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header) return conversation } + + private func validateConsentSignature(signature: String, clientAddress: String, peerAddress: String, timestamp: UInt64) -> Bool { + let message = Signature.consentProofText(peerAddress: peerAddress, timestamp: timestamp) + + guard let signatureData = Data(hex: signature) else { + print("Invalid signature format") + return false + } + var sig = Signature() + do { + sig = try Signature(serializedData: signatureData) + } catch { + print("Invalid signature format: \(error)") + return false + } + // Convert the message to Data + guard let messageData = message.data(using: .utf8) else { + print("Invalid message format") + return false + } + do { + let recoveredKey = try KeyUtilx.recoverPublicKeyKeccak256(from: sig.rawData, message: messageData) + let address = KeyUtilx.generateAddress(from: recoveredKey).toChecksumAddress() + + return clientAddress == address + } catch { + return false + } + } + + private func handleConsentProof(consentProof: ConsentProofPayload, peerAddress: String) async throws { + let signature = consentProof.signature + if (signature == "") { + return + } + + if (!validateConsentSignature(signature: signature, clientAddress: client.address, peerAddress: peerAddress, timestamp: consentProof.timestamp)) { + return + } + let contacts = client.contacts + _ = try await contacts.refreshConsentList() + if await (contacts.consentList.state(address: peerAddress) == .unknown) { + try await contacts.allow(addresses: [peerAddress]) + } + } public func list(includeGroups: Bool = false) async throws -> [Conversation] { if (includeGroups) { @@ -577,9 +624,15 @@ public actor Conversations { for sealedInvitation in try await listInvitations(pagination: pagination) { do { - try newConversations.append( - Conversation.v2(makeConversation(from: sealedInvitation)) + let newConversation = Conversation.v2(try makeConversation(from: sealedInvitation)) + newConversations.append( + newConversation ) + if let consentProof = newConversation.consentProof { + if consentProof.signature != "" { + try await self.handleConsentProof(consentProof: consentProof, peerAddress: newConversation.peerAddress) + } + } } catch { print("Error loading invitations: \(error)") } diff --git a/Sources/XMTPiOS/Frames/ProxyClient.swift b/Sources/XMTPiOS/Frames/ProxyClient.swift index f260c457..6a7330b1 100644 --- a/Sources/XMTPiOS/Frames/ProxyClient.swift +++ b/Sources/XMTPiOS/Frames/ProxyClient.swift @@ -13,7 +13,6 @@ struct Metadata: Codable { let imageUrl: String } - class ProxyClient { var baseUrl: String @@ -42,8 +41,7 @@ class ProxyClient { return metadataResponse } - func post(url: String, payload: Codable) async throws -> GetMetadataResponse { - + func post(url: String, payload: Codable) async throws -> GetMetadataResponse { let encodedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let fullUrl = "\(self.baseUrl)?url=\(encodedUrl)" guard let url = URL(string: fullUrl) else { @@ -94,8 +92,6 @@ class ProxyClient { func mediaUrl(url: String) -> String { let encodedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let result = "\(self.baseUrl)media?url=\(encodedUrl)" - return result; + return result } } - - diff --git a/Sources/XMTPiOS/Messages/Invitation.swift b/Sources/XMTPiOS/Messages/Invitation.swift index 2c156fd1..15d2ee8b 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, + consentProofPayload: ConsentProofPayload? = nil ) throws -> InvitationV1 { let context = context ?? InvitationV1.Context() let myAddress = try sender.toV1().walletAddress @@ -33,14 +38,14 @@ extension InvitationV1 { var aes256GcmHkdfSha256 = InvitationV1.Aes256gcmHkdfsha256() aes256GcmHkdfSha256.keyMaterial = Data(keyMaterial) - 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 +53,9 @@ extension InvitationV1 { if let context { self.context = context } + if let consentProof { + self.consentProof = consentProof + } self.aes256GcmHkdfSha256 = aes256GcmHkdfSha256 } @@ -61,3 +69,38 @@ public extension InvitationV1.Context { self.metadata = metadata } } + +extension ConsentProofPayload: Codable { + enum CodingKeys: CodingKey { + case signature, timestamp, payloadVersion + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(signature, forKey: .signature) + try container.encode(timestamp, forKey: .timestamp) + try container.encode(payloadVersion, forKey: .payloadVersion) + } + + public init(from decoder: Decoder) throws { + self.init() + + let container = try decoder.container(keyedBy: CodingKeys.self) + signature = try container.decode(String.self, forKey: .signature) + timestamp = try container.decode(UInt64.self, forKey: .timestamp) + payloadVersion = try container.decode(Xmtp_MessageContents_ConsentProofPayloadVersion.self, forKey: .payloadVersion) + } +} + +extension ConsentProofPayloadVersion: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(Int.self) + self = ConsentProofPayloadVersion(rawValue: rawValue) ?? ConsentProofPayloadVersion.UNRECOGNIZED(0) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } +} diff --git a/Sources/XMTPiOS/Messages/Signature.swift b/Sources/XMTPiOS/Messages/Signature.swift index e2964e36..4893ac9d 100644 --- a/Sources/XMTPiOS/Messages/Signature.swift +++ b/Sources/XMTPiOS/Messages/Signature.swift @@ -62,6 +62,17 @@ extension Signature { "For more info: https://xmtp.org/signatures/" ) } + + static func consentProofText(peerAddress: String, timestamp: UInt64) -> String { + return ( + "XMTP : Grant inbox consent to sender\n" + + "\n" + + "Current Time: \(timestamp)\n" + + "From Address: \(peerAddress)\n" + + "\n" + + "For more info: https://xmtp.org/signatures/" + ) + } public init(bytes: Data, recovery: Int) { self.init() 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 + } +} diff --git a/Tests/XMTPTests/ConversationsTest.swift b/Tests/XMTPTests/ConversationsTest.swift index 5d9ccb91..18664058 100644 --- a/Tests/XMTPTests/ConversationsTest.swift +++ b/Tests/XMTPTests/ConversationsTest.swift @@ -193,4 +193,86 @@ class ConversationsTests: XCTestCase { } } } + + func testSendConversationWithConsentSignature() async throws { + let fixtures = await fixtures() + let bo = try PrivateKey.generate() + let alix = try PrivateKey.generate() + + let boClient = try await Client.create(account: bo, apiClient: fixtures.fakeApiClient) + let alixClient = try await Client.create(account: alix, apiClient: fixtures.fakeApiClient) + + let timestamp = UInt64(Date().timeIntervalSince1970 * 1000) + let signatureText = Signature.consentProofText(peerAddress: boClient.address, timestamp: timestamp) + let digest = Data(signatureText.utf8) + let signature = try await alix.sign(Util.keccak256(digest)) + let hex = Data(try signature.serializedData()).toHex + var consentProofPayload = ConsentProofPayload() + consentProofPayload.signature = hex + consentProofPayload.timestamp = timestamp + consentProofPayload.payloadVersion = .consentProofPayloadVersion1 + let boConversation = + try await boClient.conversations.newConversation(with: alixClient.address, context: nil, consentProofPayload: consentProofPayload) + let alixConversations = try await + alixClient.conversations.list() + let alixConversation = alixConversations.first(where: { $0.topic == boConversation.topic }) + XCTAssertNotNil(alixConversation) + let consentStatus = await alixClient.contacts.isAllowed(boClient.address) + XCTAssertTrue(consentStatus) + } + + func testNetworkConsentOverConsentProof() async throws { + let fixtures = await fixtures() + let bo = try PrivateKey.generate() + let alix = try PrivateKey.generate() + + let boClient = try await Client.create(account: bo, apiClient: fixtures.fakeApiClient) + let alixClient = try await Client.create(account: alix, apiClient: fixtures.fakeApiClient) + + let timestamp = UInt64(Date().timeIntervalSince1970 * 1000) + let signatureText = Signature.consentProofText(peerAddress: boClient.address, timestamp: timestamp) + let digest = Data(signatureText.utf8) + let signature = try await alix.sign(Util.keccak256(digest)) + let hex = Data(try signature.serializedData()).toHex + var consentProofPayload = ConsentProofPayload() + consentProofPayload.signature = hex + consentProofPayload.timestamp = timestamp + consentProofPayload.payloadVersion = .consentProofPayloadVersion1 + let boConversation = + try await boClient.conversations.newConversation(with: alixClient.address, context: nil, consentProofPayload: consentProofPayload) + try await alixClient.contacts.deny(addresses: [boClient.address]) + let alixConversations = try await + alixClient.conversations.list() + let alixConversation = alixConversations.first(where: { $0.topic == boConversation.topic }) + XCTAssertNotNil(alixConversation) + let isDenied = await alixClient.contacts.isDenied(boClient.address) + XCTAssertTrue(isDenied) + } + + func testConsentProofInvalidSignature() async throws { + let fixtures = await fixtures() + let bo = try PrivateKey.generate() + let alix = try PrivateKey.generate() + + let boClient = try await Client.create(account: bo, apiClient: fixtures.fakeApiClient) + let alixClient = try await Client.create(account: alix, apiClient: fixtures.fakeApiClient) + + let timestamp = UInt64(Date().timeIntervalSince1970 * 1000) + let signatureText = Signature.consentProofText(peerAddress: boClient.address, timestamp: timestamp + 1) + let digest = Data(signatureText.utf8) + let signature = try await alix.sign(Util.keccak256(digest)) + let hex = Data(try signature.serializedData()).toHex + var consentProofPayload = ConsentProofPayload() + consentProofPayload.signature = hex + consentProofPayload.timestamp = timestamp + consentProofPayload.payloadVersion = .consentProofPayloadVersion1 + let boConversation = + try await boClient.conversations.newConversation(with: alixClient.address, context: nil, consentProofPayload: consentProofPayload) + let alixConversations = try await + alixClient.conversations.list() + let alixConversation = alixConversations.first(where: { $0.topic == boConversation.topic }) + XCTAssertNotNil(alixConversation) + let isAllowed = await alixClient.contacts.isAllowed(boClient.address) + XCTAssertFalse(isAllowed) + } }