From 3d5103e9372970ab5d2c58c0a7b467b2ec278b1b Mon Sep 17 00:00:00 2001 From: Pat Nakajima Date: Wed, 15 Nov 2023 10:11:59 -0800 Subject: [PATCH] Allow envelopes to be decrypted but not decoded (#189) * Allow envelopes to be decrypted but not decoded * Bump podspec --- Package.resolved | 2 +- Sources/XMTP/Conversation.swift | 9 +++ Sources/XMTP/ConversationV1.swift | 25 ++++++-- Sources/XMTP/ConversationV2.swift | 25 ++++---- Sources/XMTP/DecodedMessage.swift | 16 +++++ Sources/XMTP/Messages/DecryptedMessage.swift | 16 +++++ Sources/XMTP/Messages/MessageV2.swift | 67 ++++++++++++-------- Tests/XMTPTests/MessageTests.swift | 2 +- Tests/XMTPTests/RemoteAttachmentTest.swift | 2 +- XMTP.podspec | 2 +- dev/local/docker-compose.yml | 1 - 11 files changed, 120 insertions(+), 47 deletions(-) create mode 100644 Sources/XMTP/Messages/DecryptedMessage.swift diff --git a/Package.resolved b/Package.resolved index ffdbb7f1..39264615 100644 --- a/Package.resolved +++ b/Package.resolved @@ -150,7 +150,7 @@ "location" : "https://github.com/xmtp/xmtp-rust-swift", "state" : { "branch" : "main", - "revision" : "e857176b7e368c51e1dadcbbcce648bb20432f26" + "revision" : "eb931c2f467c2a71a621f54d7ae22887b234c13a" } } ], diff --git a/Sources/XMTP/Conversation.swift b/Sources/XMTP/Conversation.swift index c033ade6..a408169f 100644 --- a/Sources/XMTP/Conversation.swift +++ b/Sources/XMTP/Conversation.swift @@ -221,6 +221,15 @@ public enum Conversation: Sendable { } } + public func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] { + switch self { + case let .v1(conversationV1): + return try await conversationV1.decryptedMessages(limit: limit, before: before, after: after, direction: direction) + case let .v2(conversationV2): + return try await conversationV2.decryptedMessages(limit: limit, before: before, after: after, direction: direction) + } + } + var client: Client { switch self { case let .v1(conversationV1): diff --git a/Sources/XMTP/ConversationV1.swift b/Sources/XMTP/ConversationV1.swift index 3b0b6c82..d01510c3 100644 --- a/Sources/XMTP/ConversationV1.swift +++ b/Sources/XMTP/ConversationV1.swift @@ -180,6 +180,17 @@ public struct ConversationV1 { } } + func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] { + let pagination = Pagination(limit: limit, before: before, after: after, direction: direction) + + let envelopes = try await client.apiClient.envelopes( + topic: Topic.directMessageV1(client.address, peerAddress).description, + pagination: pagination + ) + + return try envelopes.map { try decrypt(envelope: $0) } + } + func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] { let pagination = Pagination(limit: limit, before: before, after: after, direction: direction) @@ -198,19 +209,25 @@ public struct ConversationV1 { } } - public func decode(envelope: Envelope) throws -> DecodedMessage { + func decrypt(envelope: Envelope) throws -> DecryptedMessage { let message = try Message(serializedData: envelope.message) let decrypted = try message.v1.decrypt(with: client.privateKeyBundleV1) let encodedMessage = try EncodedContent(serializedData: decrypted) let header = try message.v1.header + return DecryptedMessage(id: generateID(from: envelope), encodedContent: encodedMessage, senderAddress: header.sender.walletAddress, sentAt: message.v1.sentAt) + } + + public func decode(envelope: Envelope) throws -> DecodedMessage { + let decryptedMessage = try decrypt(envelope: envelope) + var decoded = DecodedMessage( client: client, topic: envelope.contentTopic, - encodedContent: encodedMessage, - senderAddress: header.sender.walletAddress, - sent: message.v1.sentAt + encodedContent: decryptedMessage.encodedContent, + senderAddress: decryptedMessage.senderAddress, + sent: decryptedMessage.sentAt ) decoded.id = generateID(from: envelope) diff --git a/Sources/XMTP/ConversationV2.swift b/Sources/XMTP/ConversationV2.swift index f6e09c71..c637f61c 100644 --- a/Sources/XMTP/ConversationV2.swift +++ b/Sources/XMTP/ConversationV2.swift @@ -118,13 +118,13 @@ public struct ConversationV2 { return try await prepareMessage(encodedContent: encoded, options: options) } - func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] { - let pagination = Pagination(limit: limit, before: before, after: after, direction: direction) + func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] { + let pagination = Pagination(limit: limit, before: before, after: after, direction: direction) let envelopes = try await client.apiClient.envelopes(topic: topic.description, pagination: pagination) return envelopes.compactMap { envelope in do { - return try decode(envelope: envelope) + return try decode(envelope: envelope) } catch { print("Error decoding envelope \(error)") return nil @@ -132,6 +132,16 @@ public struct ConversationV2 { } } + func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] { + let pagination = Pagination(limit: limit, before: before, after: after, direction: direction) + let envelopes = try await client.apiClient.envelopes(topic: topic.description, pagination: pagination) + + return try envelopes.map { envelope in + let message = try Message(serializedData: envelope.message) + return try MessageV2.decrypt(generateID(from: envelope), topic, message.v2, keyMaterial: keyMaterial, client: client) + } + } + var ephemeralTopic: String { topic.replacingOccurrences(of: "/xmtp/0/m", with: "/xmtp/0/mE") } @@ -168,15 +178,8 @@ public struct ConversationV2 { public func decode(envelope: Envelope) throws -> DecodedMessage { let message = try Message(serializedData: envelope.message) - var decoded = try decode(message.v2) - - decoded.id = generateID(from: envelope) - - return decoded - } - private func decode(_ message: MessageV2) throws -> DecodedMessage { - try MessageV2.decode(message, keyMaterial: keyMaterial, client: client) + return try MessageV2.decode(generateID(from: envelope), topic, message.v2, keyMaterial: keyMaterial, client: client) } @discardableResult func send(content: T, options: SendOptions? = nil) async throws -> String { diff --git a/Sources/XMTP/DecodedMessage.swift b/Sources/XMTP/DecodedMessage.swift index 715b6d2d..70f7bf3c 100644 --- a/Sources/XMTP/DecodedMessage.swift +++ b/Sources/XMTP/DecodedMessage.swift @@ -23,6 +23,22 @@ public struct DecodedMessage: Sendable { public var client: Client + init( + id: String, + client: Client, + topic: String, + encodedContent: EncodedContent, + senderAddress: String, + sent: Date + ) { + self.id = id + self.client = client + self.topic = topic + self.encodedContent = encodedContent + self.senderAddress = senderAddress + self.sent = sent +} + public init( client: Client, topic: String, diff --git a/Sources/XMTP/Messages/DecryptedMessage.swift b/Sources/XMTP/Messages/DecryptedMessage.swift new file mode 100644 index 00000000..9bf39d78 --- /dev/null +++ b/Sources/XMTP/Messages/DecryptedMessage.swift @@ -0,0 +1,16 @@ +// +// DecryptedMessage.swift +// +// +// Created by Pat Nakajima on 11/14/23. +// + +import Foundation + +public struct DecryptedMessage { + var id: String + var encodedContent: EncodedContent + var senderAddress: String + var sentAt: Date + var topic: String = "" +} diff --git a/Sources/XMTP/Messages/MessageV2.swift b/Sources/XMTP/Messages/MessageV2.swift index 0b65ffbf..07ef2061 100644 --- a/Sources/XMTP/Messages/MessageV2.swift +++ b/Sources/XMTP/Messages/MessageV2.swift @@ -22,42 +22,55 @@ extension MessageV2 { self.ciphertext = ciphertext } - static func decode(_ message: MessageV2, keyMaterial: Data, client: Client) throws -> DecodedMessage { - do { - let decrypted = try Crypto.decrypt(keyMaterial, message.ciphertext, additionalData: message.headerBytes) - let signed = try SignedContent(serializedData: decrypted) + static func decrypt(_ id: String, _ topic: String, _ message: MessageV2, keyMaterial: Data, client: Client) throws -> DecryptedMessage { + let decrypted = try Crypto.decrypt(keyMaterial, message.ciphertext, additionalData: message.headerBytes) + let signed = try SignedContent(serializedData: decrypted) + + guard signed.sender.hasPreKey, signed.sender.hasIdentityKey else { + throw MessageV2Error.decodeError("missing sender pre-key or identity key") + } - guard signed.sender.hasPreKey, signed.sender.hasIdentityKey else { - throw MessageV2Error.decodeError("missing sender pre-key or identity key") - } + let senderPreKey = try PublicKey(signed.sender.preKey) + let senderIdentityKey = try PublicKey(signed.sender.identityKey) + + // This is a bit confusing since we're passing keyBytes as the digest instead of a SHA256 hash. + // That's because our underlying crypto library always SHA256's whatever data is sent to it for this. + if !(try senderPreKey.signature.verify(signedBy: senderIdentityKey, digest: signed.sender.preKey.keyBytes)) { + throw MessageV2Error.decodeError("pre-key not signed by identity key") + } - let senderPreKey = try PublicKey(signed.sender.preKey) - let senderIdentityKey = try PublicKey(signed.sender.identityKey) + // Verify content signature + let key = try PublicKey.with { key in + key.secp256K1Uncompressed.bytes = try KeyUtilx.recoverPublicKeySHA256(from: signed.signature.rawData, message: Data(message.headerBytes + signed.payload)) + } - // This is a bit confusing since we're passing keyBytes as the digest instead of a SHA256 hash. - // That's because our underlying crypto library always SHA256's whatever data is sent to it for this. - if !(try senderPreKey.signature.verify(signedBy: senderIdentityKey, digest: signed.sender.preKey.keyBytes)) { - throw MessageV2Error.decodeError("pre-key not signed by identity key") - } + if key.walletAddress != (try PublicKey(signed.sender.preKey).walletAddress) { + throw MessageV2Error.invalidSignature + } - // Verify content signature - let key = try PublicKey.with { key in - key.secp256K1Uncompressed.bytes = try KeyUtilx.recoverPublicKeySHA256(from: signed.signature.rawData, message: Data(message.headerBytes + signed.payload)) - } + let encodedMessage = try EncodedContent(serializedData: signed.payload) + let header = try MessageHeaderV2(serializedData: message.headerBytes) - if key.walletAddress != (try PublicKey(signed.sender.preKey).walletAddress) { - throw MessageV2Error.invalidSignature - } + return DecryptedMessage( + id: id, + encodedContent: encodedMessage, + senderAddress: try signed.sender.walletAddress, + sentAt: Date(timeIntervalSince1970: Double(header.createdNs / 1_000_000) / 1000), + topic: topic + ) + } - let encodedMessage = try EncodedContent(serializedData: signed.payload) - let header = try MessageHeaderV2(serializedData: message.headerBytes) + static func decode(_ id: String, _ topic: String, _ message: MessageV2, keyMaterial: Data, client: Client) throws -> DecodedMessage { + do { + let decryptedMessage = try decrypt(id, topic, message, keyMaterial: keyMaterial, client: client) return DecodedMessage( + id: id, client: client, - topic: header.topic, - encodedContent: encodedMessage, - senderAddress: try signed.sender.walletAddress, - sent: Date(timeIntervalSince1970: Double(header.createdNs / 1_000_000) / 1000) + topic: decryptedMessage.topic, + encodedContent: decryptedMessage.encodedContent, + senderAddress: decryptedMessage.senderAddress, + sent: decryptedMessage.sentAt ) } catch { print("ERROR DECODING: \(error)") diff --git a/Tests/XMTPTests/MessageTests.swift b/Tests/XMTPTests/MessageTests.swift index fda95262..0f801327 100644 --- a/Tests/XMTPTests/MessageTests.swift +++ b/Tests/XMTPTests/MessageTests.swift @@ -65,7 +65,7 @@ class MessageTests: XCTestCase { let encodedContent = try encoder.encode(content: "Yo!", client: client) let message1 = try await MessageV2.encode(client: client, content: encodedContent, topic: invitationv1.topic, keyMaterial: invitationv1.aes256GcmHkdfSha256.keyMaterial) - let decoded = try MessageV2.decode(message1, keyMaterial: invitationv1.aes256GcmHkdfSha256.keyMaterial, client: client) + let decoded = try MessageV2.decode("", "", message1, keyMaterial: invitationv1.aes256GcmHkdfSha256.keyMaterial, client: client) let result: String = try decoded.content() XCTAssertEqual(result, "Yo!") } diff --git a/Tests/XMTPTests/RemoteAttachmentTest.swift b/Tests/XMTPTests/RemoteAttachmentTest.swift index 1af23159..712f55d4 100644 --- a/Tests/XMTPTests/RemoteAttachmentTest.swift +++ b/Tests/XMTPTests/RemoteAttachmentTest.swift @@ -116,7 +116,7 @@ class RemoteAttachmentTests: XCTestCase { XCTAssertThrowsError(try RemoteAttachment(url: tempFileURL.absoluteString, encryptedEncodedContent: encryptedEncodedContent)) { error in switch error as! RemoteAttachmentError { case let .invalidScheme(message): - XCTAssertEqual(message, "scheme must be https://") + XCTAssertEqual(message, "scheme must be https") default: XCTFail("did not raise correct error") } diff --git a/XMTP.podspec b/XMTP.podspec index 2fe5c3ad..4cb1aa34 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.6.7-alpha0" + spec.version = "0.6.8-alpha0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. diff --git a/dev/local/docker-compose.yml b/dev/local/docker-compose.yml index 227d5b80..90f2ed4f 100644 --- a/dev/local/docker-compose.yml +++ b/dev/local/docker-compose.yml @@ -2,7 +2,6 @@ version: "3.8" services: wakunode: image: xmtp/node-go - platform: linux/arm64 environment: - GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67 command: