Implement group chat message signing
This commit is contained in:
parent
62bb1f1db8
commit
bef7a2e3c8
|
@ -127,7 +127,7 @@
|
|||
<key>NSContactsUsageDescription</key>
|
||||
<string>Signal uses your contacts to find users you know. We do not store your contacts on the server.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Loki Messenger's Screen Lock feature uses Face ID.</string>
|
||||
<string>Loki Messenger's Screen Lock feature uses Face ID.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Signal needs access to your microphone to make and receive phone calls and record voice messages.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
|
|
|
@ -75,7 +75,8 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
|
|||
let isDeleted = (message["is_deleted"] as? Int == 1)
|
||||
guard !isDeleted else { return nil }
|
||||
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first, let value = annotation["value"] as? JSON,
|
||||
let serverID = message["id"] as? UInt64, let body = message["text"] as? String, let hexEncodedPublicKey = value["source"] as? String, let displayName = value["from"] as? String,
|
||||
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? Int,
|
||||
let body = message["text"] as? String, let hexEncodedPublicKey = value["source"] as? String, let displayName = value["from"] as? String,
|
||||
let timestamp = value["timestamp"] as? UInt64 else {
|
||||
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(message).")
|
||||
return nil
|
||||
|
@ -84,20 +85,28 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
|
|||
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: group, on: server, to: serverID) }
|
||||
let quote: LokiGroupMessage.Quote?
|
||||
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteeHexEncodedPublicKey = quoteAsJSON["author"] as? String, let quotedMessageBody = quoteAsJSON["text"] as? String {
|
||||
quote = LokiGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody)
|
||||
let quotedMessageServerID = message["reply_to"] as? UInt64
|
||||
quote = LokiGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
|
||||
} else {
|
||||
quote = nil
|
||||
}
|
||||
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote)
|
||||
let signature = LokiGroupMessage.Signature(hexEncodedData: hexEncodedSignatureData, version: UInt64(signatureVersion))
|
||||
let result = LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, signature: signature)
|
||||
guard result.hasValidSignature() else {
|
||||
print("[Loki] Ignoring group chat message with invalid signature.")
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}.sorted { $0.timestamp < $1.timestamp }
|
||||
}
|
||||
}
|
||||
|
||||
public static func sendMessage(_ message: LokiGroupMessage, to group: UInt64, on server: String) -> Promise<LokiGroupMessage> {
|
||||
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: Error.signingFailed) }
|
||||
return getAuthToken(for: server).then { token -> Promise<LokiGroupMessage> in
|
||||
print("[Loki] Sending message to group chat with ID: \(group) on server: \(server).")
|
||||
let url = URL(string: "\(server)/channels/\(group)/messages")!
|
||||
let parameters = message.toJSON()
|
||||
let parameters = signedMessage.toJSON()
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
let displayName = userDisplayName
|
||||
|
@ -111,7 +120,7 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
|
|||
throw Error.parsingFailed
|
||||
}
|
||||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: message.quote)
|
||||
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, signature: signedMessage.signature)
|
||||
}
|
||||
}.recover { error -> Promise<LokiGroupMessage> in
|
||||
if let error = error as? NetworkManagerError, error.statusCode == 401 {
|
||||
|
|
|
@ -10,17 +10,29 @@ public final class LokiGroupMessage : NSObject {
|
|||
public let timestamp: UInt64
|
||||
public let type: String
|
||||
public let quote: Quote?
|
||||
public let signature: Signature?
|
||||
|
||||
@objc(serverID)
|
||||
public var objc_serverID: UInt64 { return serverID ?? 0 }
|
||||
|
||||
// MARK: Settings
|
||||
private let signatureVersion: UInt64 = 1
|
||||
|
||||
// MARK: Types
|
||||
public struct Quote {
|
||||
public let quotedMessageTimestamp: UInt64
|
||||
public let quoteeHexEncodedPublicKey: String
|
||||
public let quotedMessageBody: String
|
||||
public let quotedMessageServerID: UInt64?
|
||||
}
|
||||
|
||||
public init(serverID: UInt64?, hexEncodedPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quote: Quote?) {
|
||||
public struct Signature {
|
||||
public let hexEncodedData: String
|
||||
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
|
||||
|
@ -28,24 +40,74 @@ public final class LokiGroupMessage : NSObject {
|
|||
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?) {
|
||||
@objc public convenience init(hexEncodedPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quotedMessageTimestamp: UInt64, quoteeHexEncodedPublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, hexEncodedSignatureData: String?, signatureVersion: UInt64) {
|
||||
let quote: Quote?
|
||||
if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteeHexEncodedPublicKey, let quotedMessageBody = quotedMessageBody {
|
||||
quote = Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody)
|
||||
let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil
|
||||
quote = Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
|
||||
} else {
|
||||
quote = nil
|
||||
}
|
||||
self.init(serverID: nil, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote)
|
||||
let signature: Signature?
|
||||
if let hexEncodedData = hexEncodedSignatureData, signatureVersion != 0 {
|
||||
signature = Signature(hexEncodedData: hexEncodedData, 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) -> LokiGroupMessage? {
|
||||
guard let data = getValidationData() else {
|
||||
print("[Loki] Failed to sign group chat message.")
|
||||
return nil
|
||||
}
|
||||
let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
|
||||
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
|
||||
print("[Loki] Failed to sign group chat message.")
|
||||
return nil
|
||||
}
|
||||
let hexEncodedSignatureData = signatureData.toHexString()
|
||||
let signature = Signature(hexEncodedData: hexEncodedSignatureData, version: signatureVersion)
|
||||
return LokiGroupMessage(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() else { return false }
|
||||
return (try? Ed25519.verifySignature(Data(hex: signature.hexEncodedData), publicKey: Data(hex: hexEncodedPublicKey), data: data)) ?? false
|
||||
}
|
||||
|
||||
// MARK: JSON
|
||||
internal func toJSON() -> JSON {
|
||||
var value: JSON = [ "timestamp" : timestamp, "from" : displayName, "source" : hexEncodedPublicKey ]
|
||||
var value: JSON = [ "timestamp" : timestamp ]
|
||||
if let quote = quote {
|
||||
value["quote"] = [ "id" : quote.quotedMessageTimestamp, "author" : quote.quoteeHexEncodedPublicKey, "text" : quote.quotedMessageBody ]
|
||||
}
|
||||
return [ "text" : body, "annotations": [ [ "type" : type, "value" : value ] ] ]
|
||||
if let signature = signature {
|
||||
value["sig"] = signature.hexEncodedData
|
||||
value["sigver"] = signature.version
|
||||
}
|
||||
let annotation: JSON = [ "type" : type, "value" : value ]
|
||||
var result: JSON = [ "text" : body, "annotations": [ annotation ] ]
|
||||
if let quotedMessageServerID = quote?.quotedMessageServerID {
|
||||
result["reply_to"] = quotedMessageServerID
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func getValidationData() -> Data? {
|
||||
var string = "\(body.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))\(timestamp)"
|
||||
if let quote = quote {
|
||||
string += "\(quote.quotedMessageTimestamp)\(quote.quoteeHexEncodedPublicKey)\(quote.quotedMessageBody.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))"
|
||||
}
|
||||
string += "\(signatureVersion)"
|
||||
return string.data(using: String.Encoding.utf8)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ import PromiseKit
|
|||
public class LokiDotNetAPI : NSObject {
|
||||
|
||||
// MARK: Convenience
|
||||
private static let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
|
||||
internal static let storage = OWSPrimaryStorage.shared()
|
||||
internal static let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
|
||||
internal static let userHexEncodedPublicKey = userKeyPair.hexEncodedPublicKey
|
||||
|
||||
// MARK: Error
|
||||
public enum Error : Swift.Error {
|
||||
case parsingFailed, decryptionFailed
|
||||
case parsingFailed, decryptionFailed, signingFailed
|
||||
}
|
||||
|
||||
// MARK: Database
|
||||
|
@ -58,7 +58,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
}
|
||||
// Discard the "05" prefix if needed
|
||||
if (serverPublicKey.count == 33) {
|
||||
let hexEncodedServerPublicKey = serverPublicKey.hexadecimalString
|
||||
let hexEncodedServerPublicKey = serverPublicKey.toHexString()
|
||||
serverPublicKey = Data.data(fromHex: hexEncodedServerPublicKey.substring(from: 2))!
|
||||
}
|
||||
// The challenge is prefixed by the 16 bit IV
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
@objc(LKDatabaseUtilities)
|
||||
public final class LokiDatabaseUtilities : NSObject {
|
||||
|
||||
private override init() { }
|
||||
|
||||
@objc(getServerIDForQuoteWithID:quoteeHexEncodedPublicKey:threadID:transaction:)
|
||||
public static func getServerID(quoteID: UInt64, quoteeHexEncodedPublicKey: String, threadID: String, transaction: YapDatabaseReadTransaction) -> UInt64 {
|
||||
guard let message = TSInteraction.interactions(withTimestamp: quoteID, filter: { interaction in
|
||||
let senderHexEncodedPublicKey: String
|
||||
if let message = interaction as? TSIncomingMessage {
|
||||
senderHexEncodedPublicKey = message.authorId
|
||||
} else if let message = interaction as? TSOutgoingMessage {
|
||||
senderHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return (senderHexEncodedPublicKey == quoteeHexEncodedPublicKey) && (interaction.uniqueThreadId == threadID)
|
||||
}, with: transaction).first as! TSMessage? else { return 0 }
|
||||
return message.groupChatMessageID
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
extension OWSPrimaryStorage {
|
||||
|
||||
public extension OWSPrimaryStorage {
|
||||
|
||||
private func getCollection(for primaryDevice: String) -> String {
|
||||
return "LokiDeviceLinkCollection-\(primaryDevice)"
|
||||
}
|
||||
|
|
|
@ -1148,8 +1148,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName;
|
||||
if (displayName == nil) { displayName = @"Anonymous"; }
|
||||
TSQuotedMessage *quote = message.quotedMessage;
|
||||
uint64_t quoteID = quote.timestamp;
|
||||
NSString *quoteeHexEncodedPublicKey = quote.authorId;
|
||||
__block uint64_t quotedMessageServerID;
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey threadID:messageSend.thread.uniqueId transaction:transaction];
|
||||
}];
|
||||
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:message.body type:LKGroupChatAPI.publicChatMessageType
|
||||
timestamp:message.timestamp quotedMessageTimestamp:quote.timestamp quoteeHexEncodedPublicKey:quote.authorId quotedMessageBody:quote.body];
|
||||
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID hexEncodedSignatureData:nil signatureVersion:0];
|
||||
[[LKGroupChatAPI sendMessage:groupMessage toGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer]
|
||||
.thenOn(OWSDispatch.sendingQueue, ^(LKGroupMessage *groupMessage) {
|
||||
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
|
|
Loading…
Reference in New Issue