Implement group chat message signing

This commit is contained in:
Niels Andriesse 2019-10-02 13:34:34 +10:00
parent 62bb1f1db8
commit bef7a2e3c8
7 changed files with 117 additions and 18 deletions

View File

@ -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&apos;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>

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -1,6 +1,6 @@
extension OWSPrimaryStorage {
public extension OWSPrimaryStorage {
private func getCollection(for primaryDevice: String) -> String {
return "LokiDeviceLinkCollection-\(primaryDevice)"
}

View File

@ -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) {