import PromiseKit @objc(LKGroupMessage) public final class LokiPublicChatMessage : NSObject { public let serverID: UInt64? public let hexEncodedPublicKey: String public let displayName: String public let body: String /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. public let timestamp: UInt64 public let type: String public let quote: Quote? public var attachments: [Attachment] = [] public let signature: Signature? @objc(serverID) public var objc_serverID: UInt64 { return serverID ?? 0 } // MARK: Settings private let signatureVersion: UInt64 = 1 private let attachmentType = "net.app.core.oembed" // MARK: Types public struct Quote { public let quotedMessageTimestamp: UInt64 public let quoteeHexEncodedPublicKey: String public let quotedMessageBody: String public let quotedMessageServerID: UInt64? } public struct Attachment { public let serverID: UInt64 public let kind: Kind public let width: UInt public let height: UInt public let caption: String public let url: String public let server: String public let serverDisplayName: String public enum Kind : String { case photo, video } } public struct Signature { public let data: Data public let version: UInt64 } // MARK: Initialization public init(serverID: UInt64?, hexEncodedPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quote: Quote?, signature: Signature?) { self.serverID = serverID self.hexEncodedPublicKey = hexEncodedPublicKey self.displayName = displayName self.body = body self.type = type self.timestamp = timestamp self.quote = quote self.signature = signature super.init() } @objc public convenience init(hexEncodedPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quotedMessageTimestamp: UInt64, quoteeHexEncodedPublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, signatureData: Data?, signatureVersion: UInt64) { let quote: Quote? if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteeHexEncodedPublicKey, let quotedMessageBody = quotedMessageBody { let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil quote = Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID) } else { quote = nil } let signature: Signature? if let signatureData = signatureData, signatureVersion != 0 { signature = Signature(data: signatureData, version: signatureVersion) } else { signature = nil } self.init(serverID: nil, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, signature: signature) } // MARK: Crypto internal func sign(with privateKey: Data) -> LokiPublicChatMessage? { guard let data = getValidationData(for: signatureVersion) else { print("[Loki] Failed to sign public chat message.") return nil } let userKeyPair = OWSIdentityManager.shared().identityKeyPair()! guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else { print("[Loki] Failed to sign public chat message.") return nil } let signature = Signature(data: signatureData, version: signatureVersion) return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, signature: signature) } internal func hasValidSignature() -> Bool { guard let signature = signature else { return false } guard let data = getValidationData(for: signature.version) else { return false } let publicKey = Data(hex: hexEncodedPublicKey.removing05PrefixIfNeeded()) return (try? Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)) ?? false } // MARK: JSON internal func toJSON() -> JSON { var value: JSON = [ "timestamp" : timestamp ] if let quote = quote { value["quote"] = [ "id" : quote.quotedMessageTimestamp, "author" : quote.quoteeHexEncodedPublicKey, "text" : quote.quotedMessageBody ] } if let signature = signature { value["sig"] = signature.data.toHexString() value["sigver"] = signature.version } let annotation: JSON = [ "type" : type, "value" : value ] let attachmentAnnotations: [JSON] = self.attachments.map { attachment in let attachmentValue: JSON = [ "version" : 1, "type" : attachment.kind.rawValue, "width" : attachment.width, "height" : attachment.height, "title" : attachment.caption, "url" : attachment.url, "provider_name" : attachment.serverDisplayName, "provider_url" : attachment.server ] return [ "type" : attachmentType, "value" : attachmentValue ] } var result: JSON = [ "text" : body, "annotations": [ annotation ] + attachmentAnnotations ] if let quotedMessageServerID = quote?.quotedMessageServerID { result["reply_to"] = quotedMessageServerID } return result } // MARK: Convenience @objc public func addAttachment(serverID: UInt64, kind: String, width: UInt, height: UInt, caption: String, url: String, server: String, serverDisplayName: String) { guard let kind = Attachment.Kind(rawValue: kind) else { preconditionFailure() } let attachment = Attachment(serverID: serverID, kind: kind, width: width, height: height, caption: caption, url: url, server: server, serverDisplayName: serverDisplayName) attachments.append(attachment) } private func getValidationData(for signatureVersion: UInt64) -> Data? { var string = "\(body.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))\(timestamp)" if let quote = quote { string += "\(quote.quotedMessageTimestamp)\(quote.quoteeHexEncodedPublicKey)\(quote.quotedMessageBody.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))" if let quotedMessageServerID = quote.quotedMessageServerID { string += "\(quotedMessageServerID)" } } string += attachments.map { "\($0.serverID)" }.joined(separator: "") string += "\(signatureVersion)" return string.data(using: String.Encoding.utf8) } }