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

Allow envelopes to be decrypted but not decoded #189

Merged
merged 2 commits into from
Nov 15, 2023
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
2 changes: 1 addition & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
"location" : "https://github.com/xmtp/xmtp-rust-swift",
"state" : {
"branch" : "main",
"revision" : "e857176b7e368c51e1dadcbbcce648bb20432f26"
"revision" : "eb931c2f467c2a71a621f54d7ae22887b234c13a"
}
}
],
Expand Down
9 changes: 9 additions & 0 deletions Sources/XMTP/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
25 changes: 21 additions & 4 deletions Sources/XMTP/ConversationV1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down
25 changes: 14 additions & 11 deletions Sources/XMTP/ConversationV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,30 @@ 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
}
}
}

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")
}
Expand Down Expand Up @@ -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<T>(content: T, options: SendOptions? = nil) async throws -> String {
Expand Down
16 changes: 16 additions & 0 deletions Sources/XMTP/DecodedMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions Sources/XMTP/Messages/DecryptedMessage.swift
Original file line number Diff line number Diff line change
@@ -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 = ""
}
67 changes: 40 additions & 27 deletions Sources/XMTP/Messages/MessageV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
2 changes: 1 addition & 1 deletion Tests/XMTPTests/MessageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/XMTPTests/RemoteAttachmentTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
2 changes: 1 addition & 1 deletion XMTP.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion dev/local/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ version: "3.8"
services:
wakunode:
image: xmtp/node-go
platform: linux/arm64
environment:
- GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67
command:
Expand Down