session-ios/SignalServiceKit/src/Loki/Protocol/Session Management/SessionManagementProtocol.s...

266 lines
15 KiB
Swift

import PromiseKit
// A few notes about making changes in this file:
//
// 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).
// Think carefully about adding a function; there might already be one for what you need.
// Document the expected cases for everything.
// Express those cases in tests.
@objc(LKSessionManagementProtocol)
public final class SessionManagementProtocol : NSObject {
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: - General
// BEHAVIOR NOTE: OWSMessageSender.throws_encryptedMessageForMessageSend:recipientId:plaintext:transaction: sets
// isFriendRequest to true if the message in question is a friend request or a device linking request, but NOT if
// it's a session request.
// TODO: Does the above make sense?
@objc(createPreKeys)
public static func createPreKeys() {
// We don't generate new pre keys here like Signal does.
// This is because we need the records to be linked to a contact since we don't have a central server.
// It's done automatically when we generate a pre key bundle to send to a contact (generatePreKeyBundleForContact:).
// You can use getOrCreatePreKeyForContact: to generate one if needed.
let signedPreKeyRecord = storage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
print("[Loki] Pre keys created successfully.")
}
@objc(refreshSignedPreKey)
public static func refreshSignedPreKey() {
// We don't generate new pre keys here like Signal does.
// This is because we need the records to be linked to a contact since we don't have a central server.
// It's done automatically when we generate a pre key bundle to send to a contact (generatePreKeyBundleForContact:).
// You can use getOrCreatePreKeyForContact: to generate one if needed.
guard storage.currentSignedPrekeyId() == nil else {
print("[Loki] Skipping signed pre key refresh; using existing signed pre key.")
return
}
let signedPreKeyRecord = storage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Signed pre key refreshed successfully.")
}
@objc(rotateSignedPreKey)
public static func rotateSignedPreKey() {
// This is identical to what Signal does, except that it doesn't upload the signed pre key
// to a server.
let signedPreKeyRecord = storage.generateRandomSignedRecord()
signedPreKeyRecord.markAsAcceptedByService()
storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord)
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Signed pre key rotated successfully.")
}
@objc(shouldUseFallbackEncryptionForMessage:)
public static func shouldUseFallbackEncryption(_ message: TSOutgoingMessage) -> Bool {
return !isSessionRequired(for: message)
}
@objc(isSessionRequiredForMessage:)
public static func isSessionRequired(for message: TSOutgoingMessage) -> Bool {
if message is FriendRequestMessage { return false }
else if message is SessionRequestMessage { return false }
else if let message = message as? DeviceLinkMessage, message.kind == .request { return false }
return true
}
// MARK: - Sending
@objc(startSessionResetInThread:using:)
public static func startSessionReset(in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
guard let thread = thread as? TSContactThread else {
print("[Loki] Can't restore session for non contact thread.")
return
}
let messageSender = SSKEnvironment.shared.messageSender
let devices = thread.sessionRestoreDevices // TODO: Rename this
for device in devices {
guard device.count != 0 else { continue }
getSessionResetMessageSend(for: device, in: transaction).done(on: OWSDispatch.sendingQueue()) { sessionResetMessageSend in
messageSender.sendMessage(sessionResetMessageSend)
}
}
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetInProgress)
infoMessage.save(with: transaction)
thread.sessionResetStatus = .initiated
thread.save(with: transaction)
thread.removeAllSessionRestoreDevices(with: transaction)
}
@objc(getSessionResetMessageForHexEncodedPublicKey:in:)
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?
return result
}
@objc(getSessionResetMessageSendForHexEncodedPublicKey:in:)
public static func objc_getSessionResetMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
return AnyPromise.from(getSessionResetMessageSend(for: hexEncodedPublicKey, in: transaction))
}
public static func getSessionResetMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<OWSMessageSend> {
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let message = getSessionResetMessage(for: hexEncodedPublicKey, in: transaction)
let recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: hexEncodedPublicKey, transaction: transaction)
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: {
}, failure: { error in
})
seal.fulfill(messageSend)
}
return promise
}
@objc(repairSessionIfNeededForMessage:to:)
public static func repairSessionIfNeeded(for message: TSOutgoingMessage, to hexEncodedPublicKey: String) {
guard (message.thread as? TSGroupThread)?.groupModel.groupType == .closedGroup else { return }
DispatchQueue.main.async {
storage.dbReadWriteConnection.readWrite { transaction in
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let sessionRequestMessage = SessionRequestMessage(thread: thread)
storage.setSessionRequestTimestamp(for: hexEncodedPublicKey, to: Date(), in: transaction)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: sessionRequestMessage, transaction: transaction)
}
}
}
@objc(shouldIgnoreMissingPreKeyBundleExceptionForMessage:to:)
public static func shouldIgnoreMissingPreKeyBundleException(for message: TSOutgoingMessage, to hexEncodedPublicKey: String) -> Bool {
// When a closed group is created, members try to establish sessions with eachother in the background through
// session requests. Until ALL users those session requests were sent to have come online, stored the pre key
// bundles contained in the session requests and replied with background messages to finalize the session
// creation, a given user won't be able to successfully send a message to all members of a group. This check
// is so that until we can do better on this front the user at least won't see this as an error in the UI.
return (message.thread as? TSGroupThread)?.groupModel.groupType == .closedGroup
}
// MARK: - Receiving
@objc(handleDecryptionError:forHexEncodedPublicKey:using:)
public static func handleDecryptionError(_ rawValue: Int32, for hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
let type = TSErrorMessageType(rawValue: rawValue)
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
let thread = TSContactThread.getOrCreateThread(withContactId: masterHexEncodedPublicKey, transaction: transaction)
// Show the session reset prompt upon certain errors
switch type {
case .noSession, .invalidMessage, .invalidKeyException:
// Store the source device's public key in case it was a secondary device
thread.addSessionRestoreDevice(hexEncodedPublicKey, transaction: transaction)
default: break
}
}
@objc(isSessionRestoreMessage:)
public static func isSessionRestoreMessage(_ dataMessage: SSKProtoDataMessage) -> Bool {
let sessionRestoreFlag = SSKProtoDataMessage.SSKProtoDataMessageFlags.sessionRestore
return dataMessage.flags & UInt32(sessionRestoreFlag.rawValue) != 0
}
@objc(isSessionRequestMessage:)
public static func isSessionRequestMessage(_ dataMessage: SSKProtoDataMessage) -> Bool {
let sessionRequestFlag = SSKProtoDataMessage.SSKProtoDataMessageFlags.sessionRequest
return dataMessage.flags & UInt32(sessionRequestFlag.rawValue) != 0
}
@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),
envelope.timestamp < NSDate.ows_millisecondsSince1970(for: sentSessionRequestTimestamp) {
// We sent a session request after this one was sent
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.dbReadWriteConnection.readWrite { 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(in: 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!
guard let preKeyBundleMessage = protoContent.prekeyBundleMessage else { return }
print("[Loki] Received a pre key bundle message from: \(hexEncodedPublicKey).")
guard let preKeyBundle = preKeyBundleMessage.getPreKeyBundle(with: transaction) else {
print("[Loki] Couldn't parse pre key bundle received from: \(hexEncodedPublicKey).")
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(in: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, 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).")
// 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)
// Update the session reset status
thread.sessionResetStatus = .requestReceived
thread.save(with: transaction)
// Send an ephemeral message
let ephemeralMessage = EphemeralMessage(in: thread)
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
}
}