From ad0fc7944e7a2aaf197cbb84a249d8caee639e3e Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 6 May 2019 12:38:36 +1000 Subject: [PATCH] Made ProofOfWork work in objective c. Added proof of work calculation when sending message. --- SignalServiceKit/src/Loki/ProofOfWork.swift | 46 ++--- .../src/Messages/OWSMessageSender.m | 163 ++++++++++++------ .../Network/API/Requests/OWSRequestFactory.h | 5 + .../Network/API/Requests/OWSRequestFactory.m | 52 ++++++ 4 files changed, 182 insertions(+), 84 deletions(-) diff --git a/SignalServiceKit/src/Loki/ProofOfWork.swift b/SignalServiceKit/src/Loki/ProofOfWork.swift index a1a89291b..245de1daf 100644 --- a/SignalServiceKit/src/Loki/ProofOfWork.swift +++ b/SignalServiceKit/src/Loki/ProofOfWork.swift @@ -43,7 +43,8 @@ private extension MutableCollection where Element == UInt8, Index == Int { * This was copied from the desktop messenger. * Ref: libloki/proof-of-work.js */ -public enum ProofOfWork { +@objc public class ProofOfWork: NSObject { + private override init() {} // If this changes then we also have to use something other than UInt64 to support the new length private static let nonceLength = 8 @@ -55,36 +56,19 @@ public enum ProofOfWork { } }() - public struct Configuration { - var pubKey: String - var data: String - var timestamp: Date - var ttl: Int - - var payload: [UInt8] { - let timestampString = String(Int(timestamp.timeIntervalSince1970)) - let ttlString = String(ttl) - let payloadString = timestampString + ttlString + pubKey + data - return payloadString.bytes - } - - public init(pubKey: String, data: String, timestamp: Date, ttl: Int) { - self.pubKey = pubKey - self.data = data - self.timestamp = timestamp - self.ttl = ttl - } - } - /// Calculate a proof of work with the given configuration /// /// Ref: https://bitmessage.org/wiki/Proof_of_work /// - /// - Parameter config: The configuration + /// - Parameters: + /// - data: The message data + /// - pubKey: The message recipient + /// - timestamp: The timestamp + /// - ttl: The message time to live /// - Returns: A nonce string or nil if it failed - public static func calculate(with config: Configuration) -> String? { - let payload = config.payload - let target = calcTarget(ttl: config.ttl, payloadLength: payload.count, nonceTrials: nonceTrialCount) + @objc public class func calculate(forData data: String, pubKey: String, timestamp: UInt64, ttl: Int) -> String? { + let payload = getPayload(pubKey: pubKey, data: data, timestamp: timestamp, ttl: ttl) + let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: nonceTrialCount) // Start with the max value var trialValue = UInt64.max @@ -105,8 +89,16 @@ public enum ProofOfWork { return nonce.toBase64() } + /// Get the proof of work payload + private class func getPayload(pubKey: String, data: String, timestamp: UInt64, ttl: Int) -> [UInt8] { + let timestampString = String(timestamp) + let ttlString = String(ttl) + let payloadString = timestampString + ttlString + pubKey + data + return payloadString.bytes + } + /// Calculate the target we need to reach - private static func calcTarget(ttl: Int, payloadLength: Int, nonceTrials: Int) -> UInt64 { + private class func calcTarget(ttl: Int, payloadLength: Int, nonceTrials: Int) -> UInt64 { let two16 = UInt64(pow(2, 16) - 1) let two64 = UInt64(pow(2, 64) - 1) diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index ad3703552..0697213f2 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -915,6 +915,38 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; return deviceMessages; } +- (AnyPromise *)calculateProofOfWorkForDeviceMessages:(NSArray *)deviceMessages + ttl:(NSNumber *)ttl +{ + // LOKI: Calculate the proof of work for each device message + NSMutableArray *promises = [[NSMutableArray alloc] init]; + for (NSDictionary *deviceMessage in deviceMessages) { + AnyPromise *promise = [AnyPromise promiseWithValue:deviceMessage] + .thenOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(NSDictionary *message) { + NSTimeInterval timestampInterval = [[NSDate date] timeIntervalSince1970]; + NSNumber *timestamp = [NSNumber numberWithDouble:timestampInterval]; + + NSString *destination = message[@"destination"]; + NSString *data = message[@"content"]; + + NSString *_Nullable nonce = [ProofOfWork calculateForData:data pubKey:destination timestamp:timestamp.unsignedIntegerValue ttl:ttl.integerValue]; + + // Return our timestamp along with the nonce + // These will help us identify which nonce belongs to which message + return @{ + @"destination": destination, + @"deviceId": message[@"destinationDeviceId"], + @"timestamp": timestamp, + @"nonce": nonce + }; + }); + [promises addObject:promise]; + } + + // Wait for all the PoW Calculations to finish + return PMKWhen(promises); +} + - (void)sendMessageToRecipient:(OWSMessageSend *)messageSend { OWSAssertDebug(messageSend); @@ -1087,65 +1119,82 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // linked devices that we don't know about. OWSLogWarn(@"Sending a message with no device messages."); } - - OWSRequestMaker *requestMaker = [[OWSRequestMaker alloc] initWithLabel:@"Message Send" - requestFactoryBlock:^(SMKUDAccessKey *_Nullable udAccessKey) { - return [OWSRequestFactory submitMessageRequestWithRecipient:recipient.recipientId - messages:deviceMessages - timeStamp:message.timestamp - udAccessKey:udAccessKey]; - } - udAuthFailureBlock:^{ - // Note the UD auth failure so subsequent retries - // to this recipient also use basic auth. - [messageSend setHasUDAuthFailed]; - } - websocketFailureBlock:^{ - // Note the websocket failure so subsequent retries - // to this recipient also use REST. - messageSend.hasWebsocketSendFailed = YES; - } - recipientId:recipient.recipientId - udAccess:messageSend.udAccess - canFailoverUDAuth:NO]; - [[requestMaker makeRequestObjc] - .then(^(OWSRequestMakerResult *result) { - dispatch_async([OWSDispatch sendingQueue], ^{ - [self messageSendDidSucceed:messageSend - deviceMessages:deviceMessages - wasSentByUD:result.wasSentByUD - wasSentByWebsocket:result.wasSentByWebsocket]; - }); - }) - .catch(^(NSError *error) { - dispatch_async([OWSDispatch sendingQueue], ^{ - NSUInteger statusCode = 0; - NSData *_Nullable responseData = nil; - if ([error.domain isEqualToString:@"SignalServiceKit.RequestMakerUDAuthError"]) { - // Try again. - OWSLogInfo(@"UD request auth failed; failing over to non-UD request."); - [error setIsRetryable:YES]; - } else if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { - statusCode = error.code; - - NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; - if (underlyingError) { - responseData - = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; - } else { - OWSFailDebug(@"Missing underlying error: %@", error); - } + + // TODO: Update message here to show the pow cog icon + + // LOKI: Calculate the proof of work for each device message + NSNumber *ttl = [NSNumber numberWithInteger:@(4 * 24 * 60 * 60)]; + AnyPromise *PoWPromise = [self calculateProofOfWorkForDeviceMessages:deviceMessages ttl:ttl]; + [PoWPromise + .thenOn([OWSDispatch sendingQueue], ^(NSArray *nonceArray) { + OWSRequestMaker *requestMaker = [[OWSRequestMaker alloc] initWithLabel:@"Message Send" + requestFactoryBlock:^(SMKUDAccessKey *_Nullable udAccessKey) { + // Loki Changes: + return [OWSRequestFactory submitLokiMessageRequestWithRecipient:recipient.recipientId + messages:deviceMessages + nonceArray:nonceArray + ttl:ttl]; + /* Original Code: + return [OWSRequestFactory submitMessageRequestWithRecipient:recipient.recipientId + messages:deviceMessages + timeStamp:message.timestamp + udAccessKey:udAccessKey]; + */ + } + udAuthFailureBlock:^{ + // Note the UD auth failure so subsequent retries + // to this recipient also use basic auth. + [messageSend setHasUDAuthFailed]; + } + websocketFailureBlock:^{ + // Note the websocket failure so subsequent retries + // to this recipient also use REST. + messageSend.hasWebsocketSendFailed = YES; + } + recipientId:recipient.recipientId + udAccess:messageSend.udAccess + canFailoverUDAuth:NO]; + return requestMaker; + }) + .thenOn([OWSDispatch sendingQueue], ^(OWSRequestMaker *requestMaker) { + return [requestMaker makeRequestObjc]; + }).then(^(OWSRequestMakerResult *result) { + dispatch_async([OWSDispatch sendingQueue], ^{ + [self messageSendDidSucceed:messageSend + deviceMessages:deviceMessages + wasSentByUD:result.wasSentByUD + wasSentByWebsocket:result.wasSentByWebsocket]; + }); + }) + .catch(^(NSError *error) { + dispatch_async([OWSDispatch sendingQueue], ^{ + NSUInteger statusCode = 0; + NSData *_Nullable responseData = nil; + if ([error.domain isEqualToString:@"SignalServiceKit.RequestMakerUDAuthError"]) { + // Try again. + OWSLogInfo(@"UD request auth failed; failing over to non-UD request."); + [error setIsRetryable:YES]; + } else if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { + statusCode = error.code; + + NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; + if (underlyingError) { + responseData + = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; } else { - OWSFailDebug(@"Unexpected error: %@", error); + OWSFailDebug(@"Missing underlying error: %@", error); } - - [self messageSendDidFail:messageSend - deviceMessages:deviceMessages - statusCode:statusCode - error:error - responseData:responseData]; - }); - }) retainUntilComplete]; + } else { + OWSFailDebug(@"Unexpected error: %@", error); + } + + [self messageSendDidFail:messageSend + deviceMessages:deviceMessages + statusCode:statusCode + error:error + responseData:responseData]; + }); + }) retainUntilComplete]; } - (void)messageSendDidSucceed:(OWSMessageSend *)messageSend diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h index a83c9f7ef..71eeb07f3 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h @@ -59,6 +59,11 @@ typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVo captchaToken:(nullable NSString *)captchaToken transport:(TSVerificationTransport)transport; ++ (TSRequest *)submitLokiMessageRequestWithRecipient:(NSString *)recipientId + messages:(NSArray *)messages + nonceArray:(NSArray *)nonceArray + ttl: (NSNumber *)ttl; + + (TSRequest *)submitMessageRequestWithRecipient:(NSString *)recipientId messages:(NSArray *)messages timeStamp:(uint64_t)timeStamp diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index 68214a7d7..fdb6ea50e 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -354,6 +354,58 @@ NS_ASSUME_NONNULL_BEGIN return [accountAttributes copy]; } +// LOKI: Convert Signal JSON messages to Loki messages ++ (NSDictionary *)lokiMessagesFromMessages:(NSArray *)messages + nonceArray:(NSArray *)nonceArray + ttl:(NSNumber *)ttl { + NSMutableArray *modifiedMessages = [[NSMutableArray alloc] init]; + for (NSDictionary *message in messages) { + NSMutableDictionary *lokiMessage = [[NSMutableDictionary alloc] init]; + + // Params for our message server + lokiMessage[@"pubKey"] = message[@"destination"]; + lokiMessage[@"data"] = message[@"content"]; + lokiMessage[@"ttl"] = ttl; + + NSDictionary *_Nullable nonce = [self getNonceFromArray:nonceArray forMessage:message]; + if (nonce) { + lokiMessage[@"timestamp"] = nonce[@"timestmap"]; + lokiMessage[@"nonce"] = nonce[@"nonce"]; + } + + [modifiedMessages addObject:lokiMessage]; + } + + return modifiedMessages; +} + + ++ (NSDictionary *_Nullable)getNonceFromArray:(NSArray *)nonceArray forMessage:(NSDictionary *)message { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"destination == %@ AND deviceId == %d", message[@"destination"], message[@"destinationDeviceId"]]; + NSArray *filtered = [nonceArray filteredArrayUsingPredicate:predicate]; + return filtered.count > 0 ? [filtered objectAtIndex:0] : nil; +} + +// LOKI: This is the function below with our changes ++ (TSRequest *)submitLokiMessageRequestWithRecipient:(NSString *)recipientId + messages:(NSArray *)messages + nonceArray:(NSArray *)nonceArray + ttl: (NSNumber *)ttl +{ + // NOTE: messages may be empty; See comments in OWSDeviceManager. + OWSAssertDebug(recipientId.length > 0); + + NSDictionary *lokiMessages = [self lokiMessagesFromMessages:messages nonceArray:nonceArray ttl:ttl]; + + NSString *path = [textSecureMessagesAPI stringByAppendingString:recipientId]; + NSDictionary *parameters = @{ + @"messages" : lokiMessages, + }; + + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:parameters]; + return request; +} + + (TSRequest *)submitMessageRequestWithRecipient:(NSString *)recipientId messages:(NSArray *)messages timeStamp:(uint64_t)timeStamp