Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Consent Proof Updates #329

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -365,7 +374,8 @@ public final class Client {
),
peerAddress: export.peerAddress,
client: self,
header: SealedInvitationHeaderV1()
header: SealedInvitationHeaderV1(),
consentProof: consentProof
))
}

Expand Down
1 change: 0 additions & 1 deletion Sources/XMTPiOS/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ public class ConsentList {

}


let message = try LibXMTP.userPreferencesEncrypt(
publicKey: publicKey,
privateKey: privateKey,
Expand Down
11 changes: 11 additions & 0 deletions Sources/XMTPiOS/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions Sources/XMTPiOS/ConversationExport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
15 changes: 10 additions & 5 deletions Sources/XMTPiOS/ConversationV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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

Expand All @@ -49,32 +51,35 @@ 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
self.peerAddress = peerAddress
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 {
Expand Down
63 changes: 58 additions & 5 deletions Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)")
}
Expand Down
8 changes: 2 additions & 6 deletions Sources/XMTPiOS/Frames/ProxyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ struct Metadata: Codable {
let imageUrl: String
}


class ProxyClient {
var baseUrl: String

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}


51 changes: 47 additions & 4 deletions Sources/XMTPiOS/Messages/Invitation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,21 +38,24 @@ 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

if let context {
self.context = context
}
if let consentProof {
self.consentProof = consentProof
}

self.aes256GcmHkdfSha256 = aes256GcmHkdfSha256
}
Expand All @@ -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)
}
}
11 changes: 11 additions & 0 deletions Sources/XMTPiOS/Messages/Signature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading