Handle clock out of sync issue

Also generally improve error handling
This commit is contained in:
Niels Andriesse 2020-02-14 10:16:53 +11:00
parent afed01e4c0
commit a586c9db2d
9 changed files with 52 additions and 50 deletions

View File

@ -1,7 +1,5 @@
import PromiseKit
extension String : Error { }
public extension LokiAPI {
fileprivate static var failureCount: [LokiAPITarget:UInt] = [:]
@ -67,7 +65,7 @@ public extension LokiAPI {
print("[Loki] Invoking get_n_service_nodes on \(target).")
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { intermediate in
let rawResponse = intermediate.responseObject
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw "Failed to update random snode pool from: \(rawResponse)." }
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let idKey = rawTarget["pubkey_ed25519"] as? String, let encryptionKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
@ -138,7 +136,8 @@ internal extension Promise {
LokiAPI.failureCount[target] = 0
}
case 406:
break // TODO: Handle clock out of sync
print("[Loki] The user's clock is out of sync with the service node network.")
throw LokiAPI.LokiAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")

View File

@ -26,17 +26,12 @@ public final class LokiAPI : NSObject {
// MARK: Types
public typealias RawResponse = Any
public enum Error : LocalizedError {
/// Only applicable to snode targets as proof of work isn't required for P2P messaging.
case proofOfWorkCalculationFailed
case messageConversionFailed
@objc public class LokiAPIError : NSError { // Not called `Error` for Obj-C interoperablity
public var errorDescription: String? {
switch self {
case .proofOfWorkCalculationFailed: return NSLocalizedString("Failed to calculate proof of work.", comment: "")
case .messageConversionFailed: return "Failed to convert Signal message to Loki message."
}
}
@objc public static let proofOfWorkCalculationFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
@objc public static let messageConversionFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
@objc public static let clockOutOfSync = LokiAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
@objc public static let randomSnodePoolUpdatingFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
}
@objc(LKDestination)
@ -136,7 +131,7 @@ public final class LokiAPI : NSObject {
getDestinations()
lastDeviceLinkUpdate[hexEncodedPublicKey] = Date()
}.catch(on: DispatchQueue.global()) { error in
if (error as? LokiDotNetAPI.Error) == LokiDotNetAPI.Error.parsingFailed {
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()
getDestinations()
@ -152,7 +147,7 @@ public final class LokiAPI : NSObject {
}
public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise<Set<RawResponsePromise>> {
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: Error.messageConversionFailed) }
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: LokiAPIError.messageConversionFailed) }
let notificationCenter = NotificationCenter.default
let destination = lokiMessage.destination
func sendLokiMessage(_ lokiMessage: LokiMessage, to target: LokiAPITarget) -> RawResponsePromise {

View File

@ -12,14 +12,14 @@ public class LokiDotNetAPI : NSObject {
private static let attachmentType = "network.loki"
// MARK: Error
@objc public class Error : NSError {
@objc public class LokiDotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let generic = Error(domain: "com.loki-project.loki-messenger", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
@objc public static let parsingFailed = Error(domain: "com.loki-project.loki-messenger", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
@objc public static let signingFailed = Error(domain: "com.loki-project.loki-messenger", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
@objc public static let encryptionFailed = Error(domain: "com.loki-project.loki-messenger", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
@objc public static let decryptionFailed = Error(domain: "com.loki-project.loki-messenger", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
@objc public static let maxFileSizeExceeded = Error(domain: "com.loki-project.loki-messenger", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
@objc public static let generic = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
@objc public static let parsingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
@objc public static let signingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
@objc public static let encryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
@objc public static let decryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
@objc public static let maxFileSizeExceeded = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
}
// MARK: Database
@ -52,7 +52,7 @@ public class LokiDotNetAPI : NSObject {
let data: Data
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
print("[Loki] Couldn't read attachment from disk.")
return seal.reject(Error.generic)
return seal.reject(LokiDotNetAPIError.generic)
}
// Encrypt the attachment if needed
if isEncryptionRequired {
@ -60,7 +60,7 @@ public class LokiDotNetAPI : NSObject {
var digest = NSData()
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else {
print("[Loki] Couldn't encrypt attachment.")
return seal.reject(Error.encryptionFailed)
return seal.reject(LokiDotNetAPIError.encryptionFailed)
}
attachment.encryptionKey = encryptionKey as Data
attachment.digest = digest as Data
@ -71,7 +71,7 @@ public class LokiDotNetAPI : NSObject {
// Check the file size if needed
let isLokiFileServer = (server == LokiFileServerAPI.server)
if isLokiFileServer && data.count > LokiFileServerAPI.maxFileSize {
return seal.reject(Error.maxFileSizeExceeded)
return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded)
}
// Create the request
let url = "\(server)/files"
@ -90,7 +90,7 @@ public class LokiDotNetAPI : NSObject {
// Parse the server ID & download URL
guard let json = response as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse attachment from: \(response).")
return seal.reject(Error.parsingFailed)
return seal.reject(LokiDotNetAPIError.parsingFailed)
}
// Update the attachment
attachment.serverId = serverID
@ -125,7 +125,7 @@ public class LokiDotNetAPI : NSObject {
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload attachment.")
return seal.reject(Error.generic)
return seal.reject(LokiDotNetAPIError.generic)
}
parseResponse(responseObject)
})
@ -166,7 +166,7 @@ public class LokiDotNetAPI : NSObject {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
// Discard the "05" prefix if needed
if serverPublicKey.count == 33 {
@ -176,7 +176,7 @@ public class LokiDotNetAPI : NSObject {
// The challenge is prefixed by the 16 bit IV
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
let token = String(bytes: tokenAsData, encoding: .utf8) else {
throw Error.decryptionFailed
throw LokiDotNetAPIError.decryptionFailed
}
return token
}

View File

@ -35,7 +35,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject }.map { rawResponse -> Set<DeviceLink> in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return Set(data.flatMap { data -> [DeviceLink] in
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
@ -159,11 +159,11 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload profile picture.")
return seal.reject(Error.generic)
return seal.reject(LokiDotNetAPIError.generic)
}
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let profilePicture = data["avatar_image"] as? JSON, let downloadURL = profilePicture["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
return seal.reject(Error.parsingFailed)
return seal.reject(LokiDotNetAPIError.parsingFailed)
}
return seal.fulfill(downloadURL)
})

View File

@ -58,7 +58,7 @@ public struct LokiMessage {
result.nonce = nonce
seal.fulfill(result)
} else {
seal.reject(LokiAPI.Error.proofOfWorkCalculationFailed)
seal.reject(LokiAPI.LokiAPIError.proofOfWorkCalculationFailed)
}
}
}

View File

@ -85,7 +85,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
@ -155,7 +155,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
}
public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise<LokiPublicChatMessage> {
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: Error.signingFailed) }
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: LokiDotNetAPIError.signingFailed) }
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<LokiPublicChatMessage> in
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
let url = URL(string: "\(server)/channels/\(channel)/messages")!
@ -170,7 +170,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
@ -197,7 +197,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
@ -231,7 +231,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
let moderatorAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
@ -274,7 +274,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let users = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse user count for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
let userCount = users.count
let storage = OWSPrimaryStorage.shared()
@ -298,7 +298,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
storage.dbReadWriteConnection.readWrite { transaction in
data.forEach { data in
@ -361,7 +361,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
throw LokiDotNetAPIError.parsingFailed
}
return LokiPublicChatInfo(displayName: displayName)
}

View File

@ -196,7 +196,7 @@ public final class LokiPublicChatPoller : NSObject {
LokiAPI.lastDeviceLinkUpdate[$0] = Date()
}
}.catch(on: DispatchQueue.global()) { error in
if (error as? LokiDotNetAPI.Error) == LokiDotNetAPI.Error.parsingFailed {
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
// Don't immediately re-fetch in case of failure due to a parsing error
hexEncodedPublicKeysToUpdate.forEach {
LokiAPI.lastDeviceLinkUpdate[$0] = Date()

View File

@ -1,10 +1,17 @@
import CryptoSwift
import Curve25519Kit
public enum DiffieHellman {
@objc public final class DiffieHellman : NSObject {
@objc public class DiffieHellmanError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let decryptionFailed = DiffieHellmanError(domain: "DiffieHellmanErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt data." ])
}
public static let ivLength: Int32 = 16;
private override init() { }
public static func encrypt(_ plainTextData: Data, using symmetricKey: Data) throws -> Data {
let iv = Randomness.generateRandomBytes(ivLength)!
let ivBytes = [UInt8](iv)
@ -24,7 +31,7 @@ public enum DiffieHellman {
public static func decrypt(_ encryptedData: Data, using symmetricKey: Data) throws -> Data {
let symmetricKeyBytes = [UInt8](symmetricKey)
guard encryptedData.count >= ivLength else { throw "Couldn't decrypt data." }
guard encryptedData.count >= ivLength else { throw DiffieHellmanError.decryptionFailed }
let ivBytes = [UInt8](encryptedData[..<ivLength])
let cipherBytes = [UInt8](encryptedData[ivLength...])
let blockMode = CBC(iv: ivBytes)

View File

@ -1229,7 +1229,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}
void (^failedMessageSend)(NSError *error) = ^(NSError *error) {
// Handle the error
NSUInteger statusCode = 0;
NSData *_Nullable responseData = nil;
if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) {
@ -1240,9 +1239,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} else {
OWSFailDebug(@"Missing underlying error: %@.", error);
}
} else {
// TODO: Re-enable?
// OWSFailDebug(@"Unexpected error: %@.", error);
}
[self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData];
};
@ -1486,7 +1482,12 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
switch (statusCode) {
case 0: { // Loki
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
NSError *error;
if ([responseError isKindOfClass:LokiAPIError.class] || [responseError isKindOfClass:LokiDotNetAPIError.class] || [responseError isKindOfClass:DiffieHellmanError.class]) {
error = responseError;
} else {
error = OWSErrorMakeFailedToSendOutgoingMessageError();
}
[error setIsRetryable:NO];
return messageSend.failure(error);
}