Skip to content

Commit

Permalink
feat: Consent Proof Updates (#329)
Browse files Browse the repository at this point in the history
* Revert "Revert "feat: Consent Proof Payload (#317)" (#328)"

This reverts commit 7fcd96d.

* feat: Consent Proofs

Fixed handling for empty consent proofs

---------

Co-authored-by: Alex Risch <[email protected]>
  • Loading branch information
alexrisch and Alex Risch authored Apr 29, 2024
1 parent 895fb78 commit 28b3f33
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 22 deletions.
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

0 comments on commit 28b3f33

Please sign in to comment.