Partially implement protocol changes & refactor

pull/216/head
nielsandriesse 5 years ago
parent 51092dede1
commit 89846019cf

@ -75,16 +75,20 @@ public final class LokiAPI : NSObject {
}
public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise<Set<RawResponsePromise>> {
// Convert the message to a Loki message
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: LokiAPIError.messageConversionFailed) }
let notificationCenter = NotificationCenter.default
let destination = lokiMessage.destination
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
// Calculate proof of work
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
// Get the target snodes
return getTargetSnodes(for: destination).map2 { snodes in
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
let parameters = lokiMessageWithPoW.toJSON()
return Set(snodes.map { snode in
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
let parameters = lokiMessageWithPoW.toJSON()
// Send the message to the target snode
return attempt(maxRetryCount: maxRetryCount) {
invoke(.sendMessage, on: snode, associatedWith: destination, parameters: parameters)
}.map2 { rawResponse in

@ -3,7 +3,7 @@
public extension OWSPrimaryStorage {
// MARK: - Snode Pool
// MARK: Snode Pool
public func setSnodePool(_ snodePool: Set<LokiAPITarget>, in transaction: YapDatabaseReadWriteTransaction) {
clearSnodePool(in: transaction)
snodePool.forEach { snode in
@ -28,9 +28,7 @@ public extension OWSPrimaryStorage {
transaction.removeObject(forKey: snode.description, inCollection: Storage.snodePoolCollection)
}
// MARK: - Swarm
// MARK: Swarm
public func setSwarm(_ swarm: [Snode], for publicKey: String, in transaction: YapDatabaseReadWriteTransaction) {
print("[Loki] Caching swarm for: \(publicKey).")
clearSwarm(for: publicKey, in: transaction)
@ -55,9 +53,7 @@ public extension OWSPrimaryStorage {
return result
}
// MARK: - Onion Request Paths
// MARK: Onion Request Paths
public func setOnionRequestPaths(_ paths: [OnionRequestAPI.Path], in transaction: YapDatabaseReadWriteTransaction) {
// FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this.
guard paths.count == 2 else { return }
@ -89,9 +85,7 @@ public extension OWSPrimaryStorage {
transaction.removeAllObjects(inCollection: Storage.onionRequestPathCollection)
}
// MARK: - Session Requests
// MARK: Session Requests
public func setSessionRequestTimestamp(for publicKey: String, to timestamp: Date, in transaction: YapDatabaseReadWriteTransaction) {
transaction.setDate(timestamp, forKey: publicKey, inCollection: Storage.sessionRequestTimestampCollection)
}
@ -100,9 +94,7 @@ public extension OWSPrimaryStorage {
transaction.date(forKey: publicKey, inCollection: Storage.sessionRequestTimestampCollection)
}
// MARK: - Multi Device
// MARK: Multi Device
private static var deviceLinkCache: Set<DeviceLink> = []
public func setDeviceLinks(_ deviceLinks: Set<DeviceLink>) {
@ -129,9 +121,7 @@ public extension OWSPrimaryStorage {
return getDeviceLink(for: slaveHexEncodedPublicKey, in: transaction)?.master.hexEncodedPublicKey
}
// MARK: - Open Groups
// MARK: Open Groups
public func getUserCount(for publicChat: LokiPublicChat, in transaction: YapDatabaseReadTransaction) -> Int? {
return transaction.object(forKey: publicChat.id, inCollection: Storage.openGroupUserCountCollection) as? Int
}

@ -4,9 +4,9 @@ import PromiseKit
//
// Don't use a database transaction if you can avoid it.
// If you do need to use a database transaction, use a read transaction if possible.
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Document the expected cases in which a function will be used.
// Express those cases in tests.
@objc(LKClosedGroupsProtocol)
@ -14,38 +14,33 @@ public final class ClosedGroupsProtocol : NSObject {
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: - Receiving
@objc(shouldIgnoreClosedGroupMessage:inThread:wrappedIn:using:)
public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSThread, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadTransaction) -> Bool {
guard let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup,
dataMessage.group?.type == .deliver else { return false }
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
return !thread.isUser(inGroup: hexEncodedPublicKey, transaction: transaction)
let sender = envelope.source! // Set during UD decryption
return !thread.isUser(inGroup: sender, transaction: transaction)
}
@objc(shouldIgnoreClosedGroupUpdateMessage:in:using:)
public static func shouldIgnoreClosedGroupUpdateMessage(_ envelope: SSKProtoEnvelope, in thread: TSGroupThread?, using transaction: YapDatabaseReadTransaction) -> Bool {
guard let thread = thread else { return false }
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
return !thread.isUserAdmin(inGroup: hexEncodedPublicKey, transaction: transaction)
let sender = envelope.source! // Set during UD decryption
return !thread.isUserAdmin(inGroup: sender, transaction: transaction)
}
@objc(establishSessionsIfNeededWithClosedGroupMembers:in:using:)
public static func establishSessionsIfNeeded(with closedGroupMembers: [String], in thread: TSGroupThread, using transaction: YapDatabaseReadWriteTransaction) {
closedGroupMembers.forEach { hexEncodedPublicKey in
guard hexEncodedPublicKey != getUserHexEncodedPublicKey() else { return }
let hasSession = storage.containsSession(hexEncodedPublicKey, deviceId: Int32(OWSDevicePrimaryDeviceId), protocolContext: transaction)
closedGroupMembers.forEach { member in
guard member != getUserHexEncodedPublicKey() else { return }
let hasSession = storage.containsSession(member, deviceId: Int32(OWSDevicePrimaryDeviceId), protocolContext: transaction)
guard !hasSession else { return }
print("[Loki] Establishing session with: \(hexEncodedPublicKey).")
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
print("[Loki] Establishing session with: \(member).")
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let sessionRequestMessage = SessionRequestMessage(thread: thread)
storage.setSessionRequestTimestamp(for: hexEncodedPublicKey, to: Date(), in: transaction)
storage.setSessionRequestTimestamp(for: member, to: Date(), in: transaction)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
// This has to happen sync to ensure that session requests get sent before AFRs do (it's
// asssumed that the master device syncs closed groups first and contacts after that).
messageSenderJobQueue.add(message: sessionRequestMessage, transaction: transaction)
}
}

@ -4,9 +4,9 @@ import PromiseKit
//
// Don't use a database transaction if you can avoid it.
// If you do need to use a database transaction, use a read transaction if possible.
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Document the expected cases in which a function will be used
// Express those cases in tests.
/// See [The Session Friend Request Protocol](https://github.com/loki-project/session-protocol-docs/wiki/Friend-Requests) for more information.
@ -23,10 +23,10 @@ public final class FriendRequestProtocol : NSObject {
// MARK: - General
@objc(isFriendsWithAnyLinkedDeviceOfHexEncodedPublicKey:)
public static func isFriendsWithAnyLinkedDevice(of hexEncodedPublicKey: String) -> Bool {
public static func isFriendsWithAnyLinkedDevice(of publicKey: String) -> Bool {
var result = false
storage.dbReadConnection.read { transaction in
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: publicKey, in: transaction)
let friendRequestStatuses = linkedDevices.map {
storage.getFriendRequestStatus(for: $0, transaction: transaction)
}
@ -103,30 +103,29 @@ public final class FriendRequestProtocol : NSObject {
// MARK: - Sending
@objc(acceptFriendRequestFromHexEncodedPublicKey:using:)
public static func acceptFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
print("[Loki] Invalid Session ID: \(hexEncodedPublicKey).")
public static func acceptFriendRequest(from publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: publicKey) else {
print("[Loki] Invalid Session ID: \(publicKey).")
return
}
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction)
let userPublicKey = getUserHexEncodedPublicKey()
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
// Accept all outstanding friend requests associated with this user and try to establish sessions with the
// subset of their devices that haven't sent a friend request.
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: publicKey, in: transaction)
for device in linkedDevices {
let friendRequestStatus = storage.getFriendRequestStatus(for: device, transaction: transaction)
if friendRequestStatus == .requestReceived {
storage.setFriendRequestStatus(.friends, for: device, transaction: transaction)
sendFriendRequestAcceptanceMessage(to: device, using: transaction)
sendFriendRequestAcceptedMessage(to: device, using: transaction)
// Send a contact sync message if needed
guard !userLinkedDevices.contains(hexEncodedPublicKey) else { return }
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
guard !userLinkedDevices.contains(publicKey) else { return }
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
let syncManager = SSKEnvironment.shared.syncManager
syncManager.syncContact(masterHexEncodedPublicKey, transaction: transaction)
} else if friendRequestStatus == .requestSent {
// We sent a friend request to this device before, how can we be sure that it hasn't expired?
} else if !userLinkedDevices.contains(device) && (friendRequestStatus == .none || friendRequestStatus == .requestExpired) {
// TODO: We should track these so that we can expire them and resend if needed
MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: device, in: transaction)
.done(on: OWSDispatch.sendingQueue()) { autoGeneratedFRMessageSend in
let messageSender = SSKEnvironment.shared.messageSender
@ -136,25 +135,25 @@ public final class FriendRequestProtocol : NSObject {
}
}
@objc(sendFriendRequestAcceptanceMessageToHexEncodedPublicKey:using:)
public static func sendFriendRequestAcceptanceMessage(to hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
print("[Loki] Invalid Session ID: \(hexEncodedPublicKey).")
@objc(sendFriendRequestAcceptedMessageToHexEncodedPublicKey:using:)
public static func sendFriendRequestAcceptedMessage(to publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: publicKey) else {
print("[Loki] Invalid Session ID: \(publicKey).")
return
}
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
let ephemeralMessage = EphemeralMessage(thread: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
@objc(declineFriendRequestFromHexEncodedPublicKey:using:)
public static func declineFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
print("[Loki] Invalid Session ID: \(hexEncodedPublicKey).")
public static func declineFriendRequest(from publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: publicKey) else {
print("[Loki] Invalid Session ID: \(publicKey).")
return
}
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: publicKey, in: transaction)
for device in linkedDevices {
let friendRequestStatus = storage.getFriendRequestStatus(for: device, transaction: transaction)
// We only want to decline incoming requests
@ -201,14 +200,13 @@ public final class FriendRequestProtocol : NSObject {
// MARK: - Receiving
@objc(isFriendRequestFromBeforeRestoration:)
public static func isFriendRequestFromBeforeRestoration(_ envelope: SSKProtoEnvelope) -> Bool {
// The envelope type is set during UD decryption
let restorationTimeInMs = UInt64(storage.getRestorationTime() * 1000)
return (envelope.type == .friendRequest && envelope.timestamp < restorationTimeInMs)
return (envelope.type == .friendRequest && envelope.timestamp < restorationTimeInMs) // The envelope type is set during UD decryption
}
@objc(canFriendRequestBeAutoAcceptedForHexEncodedPublicKey:using:)
public static func canFriendRequestBeAutoAccepted(for hexEncodedPublicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool {
if storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction) == .requestSent {
public static func canFriendRequestBeAutoAccepted(for publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool {
if storage.getFriendRequestStatus(for: publicKey, transaction: transaction) == .requestSent {
// This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
// mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
// and send a friend request accepted message back to Bob. We don't check that sending the
@ -222,10 +220,10 @@ public final class FriendRequestProtocol : NSObject {
return true
}
// Auto-accept any friend requests from the user's own linked devices
let userLinkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
if userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey) { return true }
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
if userLinkedDevices.contains(publicKey) { return true }
// Auto-accept if the user is friends with any of the sender's linked devices.
let senderLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let senderLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: publicKey, in: transaction)
if senderLinkedDevices.contains(where: {
storage.getFriendRequestStatus(for: $0, transaction: transaction) == .friends
}) {
@ -237,23 +235,21 @@ public final class FriendRequestProtocol : NSObject {
@objc(handleFriendRequestAcceptanceIfNeeded:in:)
public static func handleFriendRequestAcceptanceIfNeeded(_ envelope: SSKProtoEnvelope, in transaction: YapDatabaseReadWriteTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
// The envelope type is set during UD decryption.
guard !envelope.isGroupChatMessage && envelope.type != .friendRequest else { return }
let sender = envelope.source! // Set during UD decryption
guard !envelope.isGroupChatMessage && envelope.type != .friendRequest else { return } // The envelope type is set during UD decryption
// If we get an envelope that isn't a friend request, then we can infer that we had to use
// Signal cipher decryption and thus that we have a session with the other person.
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction);
let friendRequestStatus = storage.getFriendRequestStatus(for: sender, transaction: transaction);
// We shouldn't be able to skip from none to friends
guard friendRequestStatus == .requestSending || friendRequestStatus == .requestSent
|| friendRequestStatus == .requestReceived else { return }
// Become friends
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: transaction)
storage.setFriendRequestStatus(.friends, for: sender, transaction: transaction)
// Send a contact sync message if needed
guard !LokiDatabaseUtilities.isUserLinkedDevice(hexEncodedPublicKey, transaction: transaction) else { return }
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
guard !LokiDatabaseUtilities.isUserLinkedDevice(sender, transaction: transaction) else { return }
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: sender, in: transaction) ?? sender
let syncManager = SSKEnvironment.shared.syncManager
syncManager.syncContact(masterHexEncodedPublicKey, transaction: transaction)
syncManager.syncContact(masterPublicKey, transaction: transaction)
}
@objc(handleFriendRequestMessageIfNeededFromEnvelope:using:)
@ -262,16 +258,14 @@ public final class FriendRequestProtocol : NSObject {
print("[Loki] Ignoring friend request in group chat.")
return
}
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
// The envelope type is set during UD decryption.
guard envelope.type == .friendRequest else {
let hexEncodedPublicKey = envelope.source! // Set during UD decryption
guard envelope.type == .friendRequest else { // Set during UD decryption
print("[Loki] Ignoring friend request logic for non friend request type envelope.")
return
}
if canFriendRequestBeAutoAccepted(for: hexEncodedPublicKey, using: transaction) {
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: transaction)
sendFriendRequestAcceptanceMessage(to: hexEncodedPublicKey, using: transaction)
sendFriendRequestAcceptedMessage(to: hexEncodedPublicKey, using: transaction)
} else if storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction) != .friends {
// Checking that the sender of the message isn't already a friend is necessary because otherwise
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his

@ -4,9 +4,9 @@ import PromiseKit
//
// Don't use a database transaction if you can avoid it.
// If you do need to use a database transaction, use a read transaction if possible.
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Document the expected cases in which a function will be used
// Express those cases in tests.
/// See [Receipts, Transcripts & Typing Indicators](https://github.com/loki-project/session-protocol-docs/wiki/Receipts,-Transcripts-&-Typing-Indicators) for more information.
@ -106,9 +106,8 @@ public final class SessionMetaProtocol : NSObject {
// MARK: - Receiving
@objc(shouldSkipMessageDecryptResult:)
public static func shouldSkipMessageDecryptResult(_ result: OWSMessageDecryptResult) -> Bool {
public static func shouldSkipMessageDecryptResult(_ result: OWSMessageDecryptResult) -> Bool { // TODO: Why is this function needed at all?
// Called from OWSMessageReceiver to prevent messages from even being added to the processing queue
// TODO: Why is this function needed at all?
return result.source == getUserHexEncodedPublicKey() // This intentionally doesn't take into account multi device
}

@ -4,9 +4,9 @@ import PromiseKit
//
// Don't use a database transaction if you can avoid it.
// If you do need to use a database transaction, use a read transaction if possible.
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Document the expected cases in which a function will be used
// Express those cases in tests.
@objc(LKMultiDeviceProtocol)
@ -166,20 +166,17 @@ public final class MultiDeviceProtocol : NSObject {
let udManager = SSKEnvironment.shared.udManager
let senderCertificate = udManager.getSenderCertificate()
let (promise, seal) = Promise<OWSMessageSend>.pending()
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
var recipientUDAccess: OWSUDAccess?
if let senderCertificate = senderCertificate {
recipientUDAccess = udManager.udAccess(forRecipientId: hexEncodedPublicKey, requireSyncAccess: true) // Starts a new write transaction internally
}
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
externalSeal?.fulfill(())
}, failure: { error in
externalSeal?.reject(error)
})
seal.fulfill(messageSend)
var recipientUDAccess: OWSUDAccess?
if let senderCertificate = senderCertificate {
recipientUDAccess = udManager.udAccess(forRecipientId: hexEncodedPublicKey, requireSyncAccess: true) // Starts a new write transaction internally
}
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
externalSeal?.fulfill(())
}, failure: { error in
externalSeal?.reject(error)
})
seal.fulfill(messageSend)
return promise
}
@ -224,16 +221,15 @@ public final class MultiDeviceProtocol : NSObject {
@objc(handleUnlinkDeviceMessage:wrappedIn:using:)
public static func handleUnlinkDeviceMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
let sender = envelope.source! // Set during UD decryption
// Check that the request was sent by our master device
guard let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
let wasSentByMasterDevice = (masterHexEncodedPublicKey == hexEncodedPublicKey)
guard let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
let wasSentByMasterDevice = (masterPublicKey == sender)
guard wasSentByMasterDevice else { return }
// Ignore the request if we don't know about the device link in question
let masterDeviceLinks = storage.getDeviceLinks(for: masterHexEncodedPublicKey, in: transaction)
let masterDeviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
if !masterDeviceLinks.contains(where: {
$0.master.hexEncodedPublicKey == masterHexEncodedPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
$0.master.hexEncodedPublicKey == masterPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
}) {
return
}
@ -242,7 +238,7 @@ public final class MultiDeviceProtocol : NSObject {
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
// device link as seen from the slave perspective hasn't.
if slaveDeviceLinks.contains(where: {
$0.master.hexEncodedPublicKey == masterHexEncodedPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
$0.master.hexEncodedPublicKey == masterPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
}) {
for deviceLink in slaveDeviceLinks { // In theory there should only be one
LokiFileServerAPI.removeDeviceLink(deviceLink) // Attempt to clean up on the file server
@ -278,35 +274,35 @@ public final class MultiDeviceProtocol : NSObject {
// Here (in a non-@objc extension) because it doesn't interoperate well with Obj-C
public extension MultiDeviceProtocol {
fileprivate static func getMultiDeviceDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Promise<Set<MultiDeviceDestination>> {
fileprivate static func getMultiDeviceDestinations(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> Promise<Set<MultiDeviceDestination>> {
let (promise, seal) = Promise<Set<MultiDeviceDestination>>.pending()
func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) {
storage.dbReadConnection.read { transaction in
var destinations: Set<MultiDeviceDestination> = []
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
let masterDestination = MultiDeviceDestination(hexEncodedPublicKey: masterHexEncodedPublicKey, isMaster: true)
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
let masterDestination = MultiDeviceDestination(hexEncodedPublicKey: masterPublicKey, isMaster: true)
destinations.insert(masterDestination)
let deviceLinks = storage.getDeviceLinks(for: masterHexEncodedPublicKey, in: transaction)
let deviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
let slaveDestinations = deviceLinks.map { MultiDeviceDestination(hexEncodedPublicKey: $0.slave.hexEncodedPublicKey, isMaster: false) }
destinations.formUnion(slaveDestinations)
seal.fulfill(destinations)
}
}
let timeSinceLastUpdate: TimeInterval
if let lastDeviceLinkUpdate = lastDeviceLinkUpdate[hexEncodedPublicKey] {
if let lastDeviceLinkUpdate = lastDeviceLinkUpdate[publicKey] {
timeSinceLastUpdate = Date().timeIntervalSince(lastDeviceLinkUpdate)
} else {
timeSinceLastUpdate = .infinity
}
if timeSinceLastUpdate > deviceLinkUpdateInterval {
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
LokiFileServerAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey).done2 { _ in
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
LokiFileServerAPI.getDeviceLinks(associatedWith: masterPublicKey).done2 { _ in
getDestinations()
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
lastDeviceLinkUpdate[publicKey] = Date()
}.catch2 { error in
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
lastDeviceLinkUpdate[publicKey] = Date()
getDestinations()
} else {
print("[Loki] Failed to get device links due to error: \(error).")

@ -4,9 +4,9 @@ import PromiseKit
//
// Don't use a database transaction if you can avoid it.
// If you do need to use a database transaction, use a read transaction if possible.
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Document the expected cases in which a function will be used
// Express those cases in tests.
@objc(LKSessionManagementProtocol)
@ -105,7 +105,7 @@ public final class SessionManagementProtocol : NSObject {
public static func getSessionResetMessage(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> SessionRestoreMessage {
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let result = SessionRestoreMessage(thread: thread)
result.skipSave = true // TODO: Why is this necessary again?
result.skipSave = true // TODO: Why is this necessary?
return result
}
@ -121,20 +121,17 @@ public final class SessionManagementProtocol : NSObject {
let udManager = SSKEnvironment.shared.udManager
let senderCertificate = udManager.getSenderCertificate()
let (promise, seal) = Promise<OWSMessageSend>.pending()
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
var recipientUDAccess: OWSUDAccess?
if let senderCertificate = senderCertificate {
recipientUDAccess = udManager.udAccess(forRecipientId: hexEncodedPublicKey, requireSyncAccess: true) // Starts a new write transaction internally
}
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
var recipientUDAccess: OWSUDAccess?
if let senderCertificate = senderCertificate {
recipientUDAccess = udManager.udAccess(forRecipientId: hexEncodedPublicKey, requireSyncAccess: true) // Starts a new write transaction internally
}
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
}, failure: { error in
}, failure: { error in
})
seal.fulfill(messageSend)
}
})
seal.fulfill(messageSend)
return promise
}
@ -195,70 +192,40 @@ public final class SessionManagementProtocol : NSObject {
@objc(handleSessionRequestMessage:wrappedIn:using:)
public static func handleSessionRequestMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
if let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: hexEncodedPublicKey, in: transaction),
let sender = envelope.source! // Set during UD decryption
if let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: sender, in: transaction),
envelope.timestamp < NSDate.ows_millisecondsSince1970(for: sentSessionRequestTimestamp) {
// We sent a session request after this one was sent
print("[Loki] Ignoring session request from: \(hexEncodedPublicKey).")
print("[Loki] Ignoring session request from: \(sender).")
return
}
var closedGroupMembers: Set<String> = []
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let group = object as? TSGroupThread, group.groupModel.groupType == .closedGroup,
group.shouldThreadBeVisible else { return }
closedGroupMembers.formUnion(group.groupModel.groupMemberIds)
}
LokiFileServerAPI.getDeviceLinks(associatedWith: closedGroupMembers).ensure {
Storage.write { transaction in
let validHEPKs = closedGroupMembers.flatMap {
LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: $0, in: transaction)
}
guard validHEPKs.contains(hexEncodedPublicKey) else { return }
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let ephemeralMessage = EphemeralMessage(thread: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
}
let thread = TSContactThread.getOrCreateThread(withContactId: sender, transaction: transaction)
let ephemeralMessage = EphemeralMessage(thread: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
// TODO: This needs an explanation of when we expect pre key bundles to be attached
@objc(handlePreKeyBundleMessageIfNeeded:wrappedIn:using:)
public static func handlePreKeyBundleMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
let sender = envelope.source! // Set during UD decryption
guard let preKeyBundleMessage = protoContent.prekeyBundleMessage else { return }
print("[Loki] Received a pre key bundle message from: \(hexEncodedPublicKey).")
print("[Loki] Received a pre key bundle message from: \(sender).")
guard let preKeyBundle = preKeyBundleMessage.getPreKeyBundle(with: transaction) else {
print("[Loki] Couldn't parse pre key bundle received from: \(hexEncodedPublicKey).")
print("[Loki] Couldn't parse pre key bundle received from: \(sender).")
return
}
storage.setPreKeyBundle(preKeyBundle, forContact: hexEncodedPublicKey, transaction: transaction)
// If we received a friend request (i.e. also a new pre key bundle), but we were already friends with the other user, reset the session.
// The envelope type is set during UD decryption.
if envelope.type == .friendRequest,
storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction) == .friends {
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
// Archive all sessions
storage.archiveAllSessions(forContact: hexEncodedPublicKey, protocolContext: transaction)
// Send an ephemeral message
let ephemeralMessage = EphemeralMessage(thread: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
storage.setPreKeyBundle(preKeyBundle, forContact: sender, transaction: transaction)
}
// TODO: Confusing that we have this but also a sending version
@objc(handleEndSessionMessageReceivedInThread:using:)
public static func handleEndSessionMessageReceived(in thread: TSContactThread, using transaction: YapDatabaseReadWriteTransaction) {
let hexEncodedPublicKey = thread.contactIdentifier()
print("[Loki] End session message received from: \(hexEncodedPublicKey).")
let publicKey = thread.contactIdentifier()
print("[Loki] End session message received from: \(publicKey).")
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetInProgress)
infoMessage.save(with: transaction)
// Archive all sessions
storage.archiveAllSessions(forContact: hexEncodedPublicKey, protocolContext: transaction)
storage.archiveAllSessions(forContact: publicKey, protocolContext: transaction)
// Update the session reset status
thread.sessionResetStatus = .requestReceived
thread.save(with: transaction)

@ -4,9 +4,9 @@ import PromiseKit
//
// Don't use a database transaction if you can avoid it.
// If you do need to use a database transaction, use a read transaction if possible.
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid nested or unnecessary transactions).
// Consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Document the expected cases in which a function will be used
// Express those cases in tests.
@objc(LKSyncMessagesProtocol)
@ -119,7 +119,6 @@ public final class SyncMessagesProtocol : NSObject {
syncMessageTimestamps[hexEncodedPublicKey] = timestamps
}
// TODO: We should probably look at why sync messages are being duplicated rather than doing this
@objc(isDuplicateSyncMessage:fromHexEncodedPublicKey:)
public static func isDuplicateSyncMessage(_ protoContent: SSKProtoContent, from hexEncodedPublicKey: String) -> Bool {
guard let syncMessage = protoContent.syncMessage?.sent else { return false }
@ -134,10 +133,9 @@ public final class SyncMessagesProtocol : NSObject {
@objc(updateProfileFromSyncMessageIfNeeded:wrappedIn:using:)
public static func updateProfileFromSyncMessageIfNeeded(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
let sender = envelope.source! // Set during UD decryption
guard let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
let wasSentByMasterDevice = (masterHexEncodedPublicKey == hexEncodedPublicKey)
let wasSentByMasterDevice = (masterHexEncodedPublicKey == sender)
guard wasSentByMasterDevice else { return }
SessionMetaProtocol.updateDisplayNameIfNeeded(for: masterHexEncodedPublicKey, using: dataMessage, appendingShortID: false, in: transaction)
SessionMetaProtocol.updateProfileKeyIfNeeded(for: masterHexEncodedPublicKey, using: dataMessage)
@ -186,18 +184,18 @@ public final class SyncMessagesProtocol : NSObject {
public static func handleContactSyncMessageData(_ data: Data, using transaction: YapDatabaseReadWriteTransaction) {
let parser = ContactParser(data: data)
let hexEncodedPublicKeys = parser.parseHexEncodedPublicKeys()
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction)
let publicKeys = parser.parseHexEncodedPublicKeys()
let userPublicKey = getUserHexEncodedPublicKey()
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
// Try to establish sessions
for hexEncodedPublicKey in hexEncodedPublicKeys {
guard !linkedDevices.contains(hexEncodedPublicKey) else { continue } // Skip self and linked devices
for publicKey in publicKeys {
guard !linkedDevices.contains(publicKey) else { continue } // Skip self and linked devices
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
let friendRequestStatus = storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
let friendRequestStatus = storage.getFriendRequestStatus(for: publicKey, transaction: transaction)
switch friendRequestStatus {
case .none, .requestExpired:
// We need to send the FR message to all of the user's devices as the contact sync message excludes slave devices
let autoGeneratedFRMessage = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessage(for: hexEncodedPublicKey, in: transaction)
let autoGeneratedFRMessage = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessage(for: publicKey, in: transaction)
autoGeneratedFRMessage.save(with: transaction)
// Use the message sender job queue for this to ensure that these messages get sent
// AFTER session requests (it's asssumed that the master device first syncs closed
@ -206,13 +204,13 @@ public final class SyncMessagesProtocol : NSObject {
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: autoGeneratedFRMessage, transaction: transaction)
case .requestReceived:
// Not sendFriendRequestAcceptanceMessage(to:using:) to take into account multi device
FriendRequestProtocol.acceptFriendRequest(from: hexEncodedPublicKey, using: transaction)
// Not sendFriendRequestAcceptedMessage(to:using:) to take into account multi device
FriendRequestProtocol.acceptFriendRequest(from: publicKey, using: transaction)
// It's important that the line below happens after the one above
storage.setFriendRequestStatus(.friends, for: hexEncodedPublicKey, transaction: transaction)
storage.setFriendRequestStatus(.friends, for: publicKey, transaction: transaction)
default: break
}
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
thread.shouldThreadBeVisible = true
thread.save(with: transaction)
}
@ -220,10 +218,9 @@ public final class SyncMessagesProtocol : NSObject {
@objc(handleClosedGroupSyncMessageIfNeeded:wrappedIn:using:)
public static func handleClosedGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let wasSentByLinkedDevice = linkedDevices.contains(hexEncodedPublicKey)
let publicKey = envelope.source! // Set during UD decryption
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: publicKey, in: transaction)
let wasSentByLinkedDevice = linkedDevices.contains(publicKey)
guard wasSentByLinkedDevice, let groups = syncMessage.groups, let groupsAsData = groups.data, groupsAsData.count > 0 else { return }
print("[Loki] Closed group sync message received.")
let parser = ClosedGroupParser(data: groupsAsData)
@ -241,8 +238,7 @@ public final class SyncMessagesProtocol : NSObject {
@objc(handleOpenGroupSyncMessageIfNeeded:wrappedIn:using:)
public static func handleOpenGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadTransaction) {
// The envelope source is set during UD decryption
let hexEncodedPublicKey = envelope.source!
let hexEncodedPublicKey = envelope.source! // Set during UD decryption
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction)
let wasSentByLinkedDevice = linkedDevices.contains(hexEncodedPublicKey)
guard wasSentByLinkedDevice else { return }

@ -430,9 +430,6 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
// Loki: Handle friend request acceptance if needed
[LKFriendRequestProtocol handleFriendRequestAcceptanceIfNeeded:envelope in:transaction];
if (envelope.content != nil) {
NSError *error;
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error];
@ -442,12 +439,24 @@ NS_ASSUME_NONNULL_BEGIN
}
OWSLogInfo(@"Handling content: <Content: %@>.", [self descriptionForContent:contentProto]);
// Loki: Workaround for duplicate sync transcript issue
// Loki: Ignore any duplicate sync transcripts
if ([LKSyncMessagesProtocol isDuplicateSyncMessage:contentProto fromHexEncodedPublicKey:envelope.source]) { return; }
// Loki: Handle pre key bundle message if needed
[LKSessionManagementProtocol handlePreKeyBundleMessageIfNeeded:contentProto wrappedIn:envelope using:transaction];
// Loki: Handle session request if needed
if ([LKSessionManagementProtocol isSessionRequestMessage:contentProto.dataMessage]) {
[LKSessionManagementProtocol handleSessionRequestMessage:contentProto.dataMessage wrappedIn:envelope using:transaction];
return; // Don't process the message any further
}
// Loki: Handle session restoration request if needed
if ([LKSessionManagementProtocol isSessionRestoreMessage:contentProto.dataMessage]) { return; } // Don't process the message any further
// Loki: Handle friend request acceptance if needed
[LKFriendRequestProtocol handleFriendRequestAcceptanceIfNeeded:envelope in:transaction];
// Loki: Handle device linking message if needed
if (contentProto.lokiDeviceLinkMessage != nil) {
[LKMultiDeviceProtocol handleDeviceLinkMessageIfNeeded:contentProto wrappedIn:envelope using:transaction];
@ -459,15 +468,6 @@ NS_ASSUME_NONNULL_BEGIN
[[OWSDeviceManager sharedManager] setHasReceivedSyncMessage];
} else if (contentProto.dataMessage) {
// Loki: Don't process session request messages any further
if ([LKSessionManagementProtocol isSessionRequestMessage:contentProto.dataMessage]) {
[LKSessionManagementProtocol handleSessionRequestMessage:contentProto.dataMessage wrappedIn:envelope using:transaction];
return;
}
// Loki: Don't process session restore messages any further
if ([LKSessionManagementProtocol isSessionRestoreMessage:contentProto.dataMessage]) { return; }
[self handleIncomingEnvelope:envelope
withDataMessage:contentProto.dataMessage
wasReceivedByUD:wasReceivedByUD

Loading…
Cancel
Save