From 87c1df32ee46cee812890adcaac3a88012050fd5 Mon Sep 17 00:00:00 2001 From: Pat Nakajima Date: Fri, 13 Oct 2023 10:14:18 -0700 Subject: [PATCH] encrypt --- Package.resolved | 4 +- Package.swift | 2 +- Sources/XMTP/Contacts.swift | 105 +++++++++++++++++------- Sources/XMTP/Conversations.swift | 2 +- Sources/XMTP/Messages/Topic.swift | 5 +- Tests/XMTPTests/ContactsTests.swift | 4 +- Tests/XMTPTests/ConversationTests.swift | 5 +- 7 files changed, 89 insertions(+), 38 deletions(-) diff --git a/Package.resolved b/Package.resolved index b1574ca2..875d144a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/xmtp-rust-swift", "state" : { - "revision" : "d1aaac47fc7c57645a6fe9e06972b957b3efa33c", - "version" : "0.3.5-beta0" + "branch" : "main", + "revision" : "d1aaac47fc7c57645a6fe9e06972b957b3efa33c" } } ], diff --git a/Package.swift b/Package.swift index 8ee196d6..82d8b97a 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", from: "0.3.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/xmtp-rust-swift", from: "0.3.5-beta0"), + .package(url: "https://github.com/xmtp/xmtp-rust-swift", branch: "main"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/XMTP/Contacts.swift b/Sources/XMTP/Contacts.swift index 1887e44b..8761281f 100644 --- a/Sources/XMTP/Contacts.swift +++ b/Sources/XMTP/Contacts.swift @@ -6,6 +6,7 @@ // import Foundation +import XMTPRust public enum AllowState: String, Codable { case allowed, blocked, unknown @@ -16,23 +17,81 @@ struct AllowListEntry: Codable, Hashable { case address } - static func address(_ address: String, type: AllowState) -> AllowListEntry { + static func address(_ address: String, type: AllowState = .unknown) -> AllowListEntry { AllowListEntry(value: address, entryType: .address, permissionType: type) } var value: String var entryType: EntryType var permissionType: AllowState + + var key: String { + "\(entryType)-\(value)" + } } -struct AllowList { - var allowedAddresses: Set = [] - var blockedAddresses: Set = [] +class AllowList { + var entries: [String: AllowState] = [:] + + static func load(from client: Client) async throws -> AllowList { + let envelopes = try await client.query(topic: .allowList(client.address)) + let allowList = AllowList() + + for envelope in envelopes.envelopes { + let publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes + let privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes + + let payload = try XMTPRust.ecies_decrypt_k256_sha3_256( + RustVec(publicKey), + RustVec(privateKey), + RustVec(envelope.message) + ) + + let entry = try JSONDecoder().decode(AllowListEntry.self, from: Data(payload)) + + allowList.entries[entry.key] = entry.permissionType + } + + return allowList + } + + static func publish(entry: AllowListEntry, to client: Client) async throws { + let payload = try JSONEncoder().encode(entry) + + let publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes + let privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes + + let message = try XMTPRust.ecies_encrypt_k256_sha3_256( + RustVec(publicKey), + RustVec(privateKey), + RustVec(payload) + ) + + let envelope = Envelope( + topic: Topic.allowList(client.address), + timestamp: Date(), + message: Data(message) + ) + + try await client.publish(envelopes: [envelope]) + } + + func allow(address: String) -> AllowListEntry { + entries[AllowListEntry.address(address).key] = .allowed + + return .address(address, type: .allowed) + } - var entries: Set = [] + func block(address: String) -> AllowListEntry { + entries[AllowListEntry.address(address).key] = .blocked + + return .address(address, type: .blocked) + } func state(address: String) -> AllowState { - entries.first(where: { $0.entryType == .address && $0.value == address })?.permissionType ?? AllowState.unknown + let state = entries[AllowListEntry.address(address).key] + + return state ?? .unknown } } @@ -52,41 +111,27 @@ public actor Contacts { self.client = client } - public func isAllowed(_ address: String) -> Bool { - for entry in allowList.entries { - switch entry.entryType { - case .address: - if address == entry.value { - return entry.permissionType == .allowed - } - } - } + public func refreshAllowList() async throws { + self.allowList = try await AllowList.load(from: client) + } - return false + public func isAllowed(_ address: String) -> Bool { + return allowList.state(address: address) == .allowed } public func isBlocked(_ address: String) -> Bool { - for entry in allowList.entries { - switch entry.entryType { - case .address: - if address == entry.value { - return entry.permissionType == .blocked - } - } - } - - return false + return allowList.state(address: address) == .blocked } - public func allow(addresses: [String]) { + public func allow(addresses: [String]) async throws { for address in addresses { - allowList.entries.insert(.address(address, type: .allowed)) + try await AllowList.publish(entry: allowList.allow(address: address), to: client) } } - public func block(addresses: [String]) { + public func block(addresses: [String]) async throws { for address in addresses { - allowList.entries.insert(.address(address, type: .blocked)) + try await AllowList.publish(entry: allowList.block(address: address), to: client) } } diff --git a/Sources/XMTP/Conversations.swift b/Sources/XMTP/Conversations.swift index b5db19eb..9b9bd3ea 100644 --- a/Sources/XMTP/Conversations.swift +++ b/Sources/XMTP/Conversations.swift @@ -155,7 +155,7 @@ public actor Conversations { let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) - await client.contacts.allow(addresses: [peerAddress]) + try await client.contacts.allow(addresses: [peerAddress]) let conversation: Conversation = .v2(conversationV2) conversationsByTopic[conversation.topic] = conversation diff --git a/Sources/XMTP/Messages/Topic.swift b/Sources/XMTP/Messages/Topic.swift index e0b9c0b6..638434b3 100644 --- a/Sources/XMTP/Messages/Topic.swift +++ b/Sources/XMTP/Messages/Topic.swift @@ -13,7 +13,8 @@ public enum Topic { userIntro(String), userInvite(String), directMessageV1(String, String), - directMessageV2(String) + directMessageV2(String), + allowList(String) var description: String { switch self { @@ -30,6 +31,8 @@ public enum Topic { return wrap("dm-\(addresses)") case let .directMessageV2(randomString): return wrap("m-\(randomString)") + case let .allowList(identifier): + return wrap("privatestore-\(identifier)/allowlist") } } diff --git a/Tests/XMTPTests/ContactsTests.swift b/Tests/XMTPTests/ContactsTests.swift index d4006e87..d17423c8 100644 --- a/Tests/XMTPTests/ContactsTests.swift +++ b/Tests/XMTPTests/ContactsTests.swift @@ -62,7 +62,7 @@ class ContactsTests: XCTestCase { XCTAssertFalse(result) - await contacts.allow(addresses: [fixtures.alice.address]) + try await contacts.allow(addresses: [fixtures.alice.address]) result = await contacts.isAllowed(fixtures.alice.address) XCTAssertTrue(result) @@ -76,7 +76,7 @@ class ContactsTests: XCTestCase { XCTAssertFalse(result) - await contacts.block(addresses: [fixtures.alice.address]) + try await contacts.block(addresses: [fixtures.alice.address]) result = await contacts.isBlocked(fixtures.alice.address) XCTAssertTrue(result) diff --git a/Tests/XMTPTests/ConversationTests.swift b/Tests/XMTPTests/ConversationTests.swift index 4f213547..6e56e966 100644 --- a/Tests/XMTPTests/ConversationTests.swift +++ b/Tests/XMTPTests/ConversationTests.swift @@ -619,7 +619,7 @@ class ConversationTests: XCTestCase { // Conversations started with you should start as unknown XCTAssertTrue(isUnknown) - await aliceClient.contacts.allow(addresses: [bob.address]) + try await aliceClient.contacts.allow(addresses: [bob.address]) let isBobAllowed = (await aliceConversation.allowState()) == .allowed XCTAssertTrue(isBobAllowed) @@ -627,8 +627,11 @@ class ConversationTests: XCTestCase { let aliceClient2 = try await Client.create(account: alice, apiClient: fakeApiClient) let aliceConversation2 = (try await aliceClient2.conversations.list())[0] + try await aliceClient2.contacts.refreshAllowList() + // Allow state should sync across clients let isBobAllowed2 = (await aliceConversation2.allowState()) == .allowed + XCTAssertTrue(isBobAllowed2) } }