diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme
index c5583e4d9..572fd60ab 100644
--- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme
+++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme
@@ -15,7 +15,7 @@
@@ -33,7 +33,7 @@
@@ -56,7 +56,7 @@
@@ -87,7 +87,7 @@
diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme
index 8c3c2011a..e523f2c19 100644
--- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme
+++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme
@@ -15,7 +15,7 @@
@@ -117,7 +117,7 @@
@@ -152,7 +152,7 @@
@@ -193,7 +193,7 @@
diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme
index 77ad0a66b..cc64d8a0e 100644
--- a/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme
+++ b/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme
@@ -30,7 +30,7 @@
@@ -72,7 +72,7 @@
@@ -92,7 +92,7 @@
diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist
index 7396df8e5..258a90a89 100644
--- a/Signal/Signal-Info.plist
+++ b/Signal/Signal-Info.plist
@@ -7,7 +7,7 @@
CarthageVersion
0.33.0
OSXVersion
- 10.14.4
+ 10.14.5
WebRTCCommit
1445d719bf05280270e9f77576f80f973fd847f8 M73
diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift b/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift
index 2a47b47e2..255a5652e 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift
@@ -35,22 +35,6 @@ extension LokiAPI {
}
}
- /// Unwrap data sent by the storage server.
- ///
- /// - Parameter data: The data from the storage server (not base 64 encoded).
- /// - Returns: An `SSKProtoEnvelope` object.
- /// - Throws: A `WrappingError` if something went wrong.
- static func unwrap(data: Data) throws -> SSKProtoEnvelope {
- do {
- let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data)
- let envelope = webSocketMessage.request!.body!
- return try SSKProtoEnvelope.parseData(envelope)
- } catch let error {
- owsFailDebug("[Loki] Failed to unwrap data: \(error).")
- throw WrappingError.failedToUnwrapData
- }
- }
-
/// Wrap an `SSKProtoEnvelope` in a `WebSocketProtoWebSocketMessage`.
private static func createWebSocketMessage(around envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage {
do {
@@ -91,4 +75,20 @@ extension LokiAPI {
throw WrappingError.failedToWrapMessageInEnvelope
}
}
+
+ /// Unwrap data sent by the storage server.
+ ///
+ /// - Parameter data: The data from the storage server (not base 64 encoded).
+ /// - Returns: An `SSKProtoEnvelope` object.
+ /// - Throws: A `WrappingError` if something went wrong.
+ static func unwrap(data: Data) throws -> SSKProtoEnvelope {
+ do {
+ let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data)
+ let envelope = webSocketMessage.request!.body!
+ return try SSKProtoEnvelope.parseData(envelope)
+ } catch let error {
+ owsFailDebug("[Loki] Failed to unwrap data: \(error).")
+ throw WrappingError.failedToUnwrapData
+ }
+ }
}
diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift
index 604e7dce9..a8e5463cf 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift
@@ -3,15 +3,15 @@ import PromiseKit
@objc public final class LokiAPI : NSObject {
private static let storage = OWSPrimaryStorage.shared()
- // MARK: Caching
- private static var swarmCache: [String:Set] = [:]
-
// MARK: Settings
private static let version = "v1"
private static let defaultSnodePort: UInt16 = 8080
private static let targetSnodeCount = 2
public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60
+ // MARK: Caching
+ private static var swarmCache: [String:[Target]] = [:]
+
// MARK: Types
private struct Target : Hashable {
let address: String
@@ -50,45 +50,43 @@ import PromiseKit
return Promise { _ in notImplemented() } // TODO: Implement
}
- private static func getSwarm(for hexEncodedPublicKey: String) -> Promise> {
+ private static func getSwarm(for hexEncodedPublicKey: String) -> Promise<[Target]> {
if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= targetSnodeCount {
- return Promise> { $0.fulfill(cachedSwarm) }
+ return Promise<[Target]> { $0.fulfill(cachedSwarm) }
} else {
- return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }
- .map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 }
+ let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ]
+ return getRandomSnode().then { invoke(.getSwarm, on: $0, with: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 }
}
}
- private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise> {
- return getSwarm(for: hexEncodedPublicKey).map { Set(Array($0).shuffled().prefix(targetSnodeCount)) }
+ private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise<[Target]> {
+ // shuffled() uses the system's default random generator, which is cryptographically secure
+ return getSwarm(for: hexEncodedPublicKey).map { Array($0.shuffled().prefix(targetSnodeCount)) }
}
// MARK: Public API
- public static func getMessages() -> Promise>>> {
+ public static func getMessages() -> Promise>> {
let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in
- let lastHash = getLastHash(for: targetSnode) ?? ""
+ let lastHash = getLastMessageHashValue(for: targetSnode) ?? ""
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ]
return invoke(.getMessages, on: targetSnode, with: parameters).map { rawResponse in
- if let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON], let lastMessage = messages.last,
- let hash = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int {
- setLastHash(for: targetSnode, hash: hash, expiresAt: UInt64(expiresAt))
- }
- return parseProtoEnvelopes(from: rawResponse)
+ guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
+ updateLastMessageHashValueIfPossible(for: targetSnode, from: rawMessages)
+ let newRawMessages = removeDuplicates(from: rawMessages)
+ return parseProtoEnvelopes(from: newRawMessages)
}
}.map { Set($0) }
}
- public static func sendMessage(_ lokiMessage: Message) -> Promise<[Promise]> {
+ public static func sendMessage(_ lokiMessage: Message) -> Promise>> {
let parameters = lokiMessage.toJSON()
- return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters)
- .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }
+ return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) }
}
- public static func ping(_ hexEncodedPublicKey: String) -> Promise<[Promise]> {
+ public static func ping(_ hexEncodedPublicKey: String) -> Promise>> {
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] // TODO: Figure out correct parameters
- return getTargetSnodes(for: hexEncodedPublicKey).mapValues { invoke(.sendMessage, on: $0, with: parameters)
- .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }
+ return getTargetSnodes(for: hexEncodedPublicKey).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) }
}
// MARK: Public API (Obj-C)
@@ -99,43 +97,86 @@ import PromiseKit
return anyPromise
}
- // MARK: Last Hash
- private static func setLastHash(for target: Target, hash: String, expiresAt: UInt64) {
- storage.dbReadWriteConnection.readWrite { transaction in
- storage.setLastMessageHash(forServiceNode: target.address, hash: hash, expiresAt: expiresAt, transaction: transaction)
+ // MARK: Parsing
+
+ // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
+
+ private static func parseTargets(from rawResponse: Any) -> [Target] {
+ guard let json = rawResponse as? JSON, let addresses = json["snodes"] as? [String] else {
+ Logger.warn("[Loki] Failed to parse targets from: \(rawResponse).")
+ return []
}
+ return addresses.map { Target(address: $0, port: defaultSnodePort) }
}
- private static func getLastHash(for target: Target) -> String? {
- var lastHash: String?
- storage.dbReadWriteConnection.readWrite { transaction in
- lastHash = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction)
+ private static func updateLastMessageHashValueIfPossible(for target: Target, from rawMessages: [JSON]) {
+ guard let lastMessage = rawMessages.last, let hashValue = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int else {
+ Logger.warn("[Loki] Failed to update last message hash value from: \(rawMessages).")
+ return
}
- return lastHash
+ setLastMessageHashValue(for: target, hashValue: hashValue, expiresAt: UInt64(expiresAt))
}
- // MARK: Parsing
- private static func parseTargets(from rawResponse: Any) -> Set {
- guard let json = rawResponse as? JSON, let addresses = json["snodes"] as? [String] else { return [] }
- return Set(addresses.map { Target(address: $0, port: defaultSnodePort) })
+ private static func removeDuplicates(from rawMessages: [JSON]) -> [JSON] {
+ var receivedMessageHashValues = getReceivedMessageHashValues()
+ return rawMessages.filter { rawMessage in
+ guard let hashValue = rawMessage["hash"] as? String else {
+ Logger.warn("[Loki] Missing hash value for message: \(rawMessage).")
+ return false
+ }
+ let isDuplicate = receivedMessageHashValues.contains(hashValue)
+ receivedMessageHashValues.insert(hashValue)
+ setReceivedMessageHashValues(to: receivedMessageHashValues)
+ return !isDuplicate
+ }
}
- private static func parseProtoEnvelopes(from rawResponse: Any) -> Set {
- guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] }
- return Set(messages.compactMap { message in
- guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
- Logger.warn("[Loki] Failed to decode data for message: \(message).")
+ private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
+ return rawMessages.compactMap { rawMessage in
+ guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
+ Logger.warn("[Loki] Failed to decode data for message: \(rawMessage).")
return nil
}
guard let envelope = try? unwrap(data: data) else {
- Logger.warn("[Loki] Failed to unwrap data for message: \(message).")
+ Logger.warn("[Loki] Failed to unwrap data for message: \(rawMessage).")
return nil
}
return envelope
- })
+ }
+ }
+
+ // MARK: Convenience
+ private static func getLastMessageHashValue(for target: Target) -> String? {
+ var result: String? = nil
+ // Uses a read/write connection because getting the last message hash value also removes expired messages as needed
+ storage.dbReadWriteConnection.readWrite { transaction in
+ result = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction)
+ }
+ return result
+ }
+
+ private static func setLastMessageHashValue(for target: Target, hashValue: String, expiresAt: UInt64) {
+ storage.dbReadWriteConnection.readWrite { transaction in
+ storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expiresAt, transaction: transaction)
+ }
+ }
+
+ private static func getReceivedMessageHashValues() -> Set {
+ var result: Set = []
+ storage.dbReadConnection.read { transaction in
+ result = storage.getReceivedMessageHashes(with: transaction)
+ }
+ return result
+ }
+
+ private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set) {
+ storage.dbReadWriteConnection.readWrite { transaction in
+ storage.setReceivedMessageHashes(receivedMessageHashValues, with: transaction)
+ }
}
}
+// MARK: Error Handling
private extension Promise {
func recoverNetworkErrorIfNeeded(on queue: DispatchQueue) -> Promise {
diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h
index 59a552744..b99592924 100644
--- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h
+++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h
@@ -72,7 +72,7 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction;
-# pragma mark - Last Hash
+# pragma mark - Last Hash Handling
/**
Get the last message hash for the given service node.
@@ -95,6 +95,9 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setLastMessageHash(forServiceNode:hash:expiresAt:transaction:));
+- (NSSet *)getReceivedMessageHashesWithTransaction:(YapDatabaseReadTransaction *)transaction;
+- (void)setReceivedMessageHashes:(NSSet *)receivedMessageHashes withTransaction:(YapDatabaseReadWriteTransaction *)transaction;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m
index ff57f5b35..7ccefb5ab 100644
--- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m
+++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m
@@ -10,11 +10,14 @@
#import "YapDatabaseConnection+OWS.h"
#import "YapDatabaseTransaction+OWS.h"
#import
+#import "NSObject+Casting.h"
#define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection"
-#define LokiPreKeyContactCollection @"LokiPreKeyContactCollection"
-#define LokiPreKeyBundleCollection @"LokiPreKeyBundleCollection"
-#define LokiLastHashCollection @"LokiLastHashCollection"
+#define LKPreKeyContactCollection @"LKPreKeyContactCollection"
+#define LKPreKeyBundleCollection @"LKPreKeyBundleCollection"
+#define LKLastMessageHashCollection @"LKLastMessageHashCollection"
+#define LKReceivedMessageHashesKey @"LKReceivedMessageHashesKey"
+#define LKReceivedMessageHashesCollection @"LKReceivedMessageHashesCollection"
@implementation OWSPrimaryStorage (Loki)
@@ -31,13 +34,13 @@
# pragma mark - Prekey for Contact
- (BOOL)hasPreKeyForContact:(NSString *)pubKey {
- int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LokiPreKeyContactCollection];
+ int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LKPreKeyContactCollection];
return preKeyId > 0;
}
- (PreKeyRecord *_Nullable)getPreKeyForContact:(NSString *)pubKey transaction:(YapDatabaseReadTransaction *)transaction {
OWSAssertDebug(pubKey.length > 0);
- int preKeyId = [transaction intForKey:pubKey inCollection:LokiPreKeyContactCollection];
+ int preKeyId = [transaction intForKey:pubKey inCollection:LKPreKeyContactCollection];
// If we don't have an id then return nil
if (preKeyId <= 0) { return nil; }
@@ -48,7 +51,7 @@
- (PreKeyRecord *)getOrCreatePreKeyForContact:(NSString *)pubKey {
OWSAssertDebug(pubKey.length > 0);
- int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LokiPreKeyContactCollection];
+ int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LKPreKeyContactCollection];
// If we don't have an id then generate and store a new one
if (preKeyId <= 0) {
@@ -73,7 +76,7 @@
OWSAssertDebug(records.count > 0);
PreKeyRecord *record = records.firstObject;
- [self.dbReadWriteConnection setInt:record.Id forKey:pubKey inCollection:LokiPreKeyContactCollection];
+ [self.dbReadWriteConnection setInt:record.Id forKey:pubKey inCollection:LKPreKeyContactCollection];
return record;
}
@@ -107,23 +110,23 @@
}
- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)pubKey {
- return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LokiPreKeyBundleCollection];
+ return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LKPreKeyBundleCollection];
}
- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction setObject:bundle
forKey:pubKey
- inCollection:LokiPreKeyBundleCollection];
+ inCollection:LKPreKeyBundleCollection];
}
- (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
- [transaction removeObjectForKey:pubKey inCollection:LokiPreKeyBundleCollection];
+ [transaction removeObjectForKey:pubKey inCollection:LKPreKeyBundleCollection];
}
# pragma mark - Last Hash
- (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction {
- NSDictionary *_Nullable dict = [transaction objectForKey:serviceNode inCollection:LokiLastHashCollection];
+ NSDictionary *_Nullable dict = [transaction objectForKey:serviceNode inCollection:LKLastMessageHashCollection];
if (!dict) { return nil; }
NSString *_Nullable hash = dict[@"hash"];
@@ -142,15 +145,20 @@
}
- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction {
- NSDictionary *dict = @{
- @"hash": hash,
- @"expiresAt": @(expiresAt)
- };
- [transaction setObject:dict forKey:serviceNode inCollection:LokiLastHashCollection];
+ NSDictionary *dict = @{ @"hash" : hash, @"expiresAt": @(expiresAt) };
+ [transaction setObject:dict forKey:serviceNode inCollection:LKLastMessageHashCollection];
}
- (void)removeLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction {
- [transaction removeObjectForKey:serviceNode inCollection:LokiLastHashCollection];
+ [transaction removeObjectForKey:serviceNode inCollection:LKLastMessageHashCollection];
+}
+
+- (NSSet *)getReceivedMessageHashesWithTransaction:(YapDatabaseReadTransaction *)transaction {
+ return (NSSet *)[[transaction objectForKey:LKReceivedMessageHashesKey inCollection:LKReceivedMessageHashesCollection] as:NSSet.class];
+}
+
+- (void)setReceivedMessageHashes:(NSSet *)receivedMessageHashes withTransaction:(YapDatabaseReadWriteTransaction *)transaction {
+ [transaction setObject:receivedMessageHashes forKey:LKReceivedMessageHashesKey inCollection:LKReceivedMessageHashesCollection];
}
@end