Merge pull request #166 from loki-project/friend-requests

Friend Request Bug Fixes
pull/174/head
Niels Andriesse 5 years ago committed by GitHub
commit 3c9dacde60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,10 +23,11 @@ def shared_pods
# pod 'Curve25519Kit', path: '../Curve25519Kit', testspecs: ["Tests"]
# Don't update SignalMetadataKit. There's some Loki specific stuff in there that gets overwritten otherwise.
# FIXME: We should fork this, make it work with Cocoapods, and keep it up to date with Signal's repo.
# pod 'SignalMetadataKit', git: 'https://github.com/signalapp/SignalMetadataKit', testspecs: ["Tests"]
# pod 'SignalMetadataKit', path: '../SignalMetadataKit', testspecs: ["Tests"]
pod 'SignalServiceKit', path: '.', testspecs: ["Tests"]
pod 'SignalServiceKit', path: '.', testspecs: ["Tests"] # TODO: Signal moved this into the main repo. We should probably do the same eventually.
# Project does not compile with PromiseKit 6.7.1
# see: https://github.com/mxcl/PromiseKit/issues/990
@ -57,7 +58,7 @@ def shared_pods
###
# third party pods
####
###
pod 'AFNetworking', inhibit_warnings: true
pod 'PureLayout', :inhibit_warnings => true
@ -70,8 +71,11 @@ target 'Signal' do
shared_pods
pod 'SSZipArchive', :inhibit_warnings => true
# Loki
pod 'GCDWebServer', '~> 3.0', :inhibit_warnings => true
###
# Loki third party pods
###
pod 'GCDWebServer', '~> 3.0', :inhibit_warnings => true # TODO: We can probably ditch this as we're not doing P2P anymore
pod 'FeedKit', '~> 8.1', :inhibit_warnings => true
pod 'CryptoSwift', '~> 1.0', :inhibit_warnings => true
pod 'FirebaseCore', '~> 6.0', :inhibit_warnings => true # Used for internal testing
@ -91,8 +95,13 @@ end
target 'LokiPushNotificationService' do
project 'Signal'
pod 'CryptoSwift', '~> 1.0', :inhibit_warnings => true
shared_pods
###
# Loki third party pods
###
pod 'CryptoSwift', '~> 1.0', :inhibit_warnings => true
end
target 'SignalMessaging' do

@ -1 +1 @@
Subproject commit cfe3608532c42224ce3f5c24295616862f884fc7
Subproject commit ad77d840df0857c6bdecce4aad305137d17a8ef6

@ -1654,38 +1654,12 @@ typedef enum : NSUInteger {
#pragma mark - Updating
- (void)updateInputToolbar {
BOOL isEnabled;
BOOL isAttachmentButtonHidden;
if ([self.thread isKindOfClass:TSContactThread.class]) {
NSString *senderID = ((TSContactThread *)self.thread).contactIdentifier;
__block NSSet<TSContactThread *> *linkedDeviceThreads;
__block BOOL isNoteToSelf;
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
linkedDeviceThreads = [LKDatabaseUtilities getLinkedDeviceThreadsFor:senderID in:transaction];
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:senderID in:transaction];
}];
if ([linkedDeviceThreads contains:^BOOL(TSContactThread *thread) {
return thread.isContactFriend;
}] || isNoteToSelf) {
isEnabled = true;
isAttachmentButtonHidden = false;
} else if (![linkedDeviceThreads contains:^BOOL(TSContactThread *thread) {
return thread.hasPendingFriendRequest;
}]) {
isEnabled = true;
isAttachmentButtonHidden = true;
} else {
isEnabled = false;
isAttachmentButtonHidden = true;
}
} else {
isEnabled = true;
isAttachmentButtonHidden = false;
}
[self.inputToolbar setUserInteractionEnabled:isEnabled];
NSString *placeholderText = isEnabled ? NSLocalizedString(@"Message", "") : NSLocalizedString(@"Pending session request", "");
BOOL shouldInputBarBeEnabled = [LKFriendRequestProtocol shouldInputBarBeEnabledForThread:self.thread];
[self.inputToolbar setUserInteractionEnabled:shouldInputBarBeEnabled];
NSString *placeholderText = shouldInputBarBeEnabled ? NSLocalizedString(@"Message", "") : NSLocalizedString(@"Pending session request", "");
[self.inputToolbar setPlaceholderText:placeholderText];
[self.inputToolbar setAttachmentButtonHidden:isAttachmentButtonHidden];
BOOL shouldAttachmentButtonBeEnabled = [LKFriendRequestProtocol shouldAttachmentButtonBeEnabledForThread:self.thread];
[self.inputToolbar setAttachmentButtonHidden:!shouldAttachmentButtonBeEnabled];
}
#pragma mark - Identity
@ -4492,7 +4466,7 @@ typedef enum : NSUInteger {
- (void)acceptFriendRequest:(TSIncomingMessage *)friendRequest
{
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKFriendRequestProtocol acceptFriendRequest:friendRequest in:self.thread using:transaction];
[LKFriendRequestProtocol acceptFriendRequestFrom:friendRequest.authorId in:self.thread using:transaction];
}];
}

@ -32,7 +32,7 @@ public class RefreshPreKeysOperation: OWSOperation {
Logger.debug("")
guard tsAccountManager.isRegistered() else {
Logger.debug("skipping - not registered")
Logger.debug("Skipping pre key refresh; user isn't registered.")
return
}
@ -88,18 +88,18 @@ public class RefreshPreKeysOperation: OWSOperation {
switch error {
case let networkManagerError as NetworkManagerError:
guard !networkManagerError.isNetworkError else {
Logger.debug("don't report SPK rotation failure w/ network error")
Logger.debug("Don't report SPK rotation failure w/ network error")
return
}
guard networkManagerError.statusCode >= 400 && networkManagerError.statusCode <= 599 else {
Logger.debug("don't report SPK rotation failure w/ non application error")
Logger.debug("Don't report SPK rotation failure w/ non application error")
return
}
TSPreKeyManager.incrementPreKeyUpdateFailureCount()
default:
Logger.debug("don't report SPK rotation failure w/ non NetworkManager error: \(error)")
Logger.debug("Don't report SPK rotation failure w/ non NetworkManager error: \(error)")
}
}
}

@ -198,13 +198,8 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
+ (void)checkPreKeys
{
if (!CurrentAppContext().isMainApp) {
return;
}
if (!self.tsAccountManager.isRegisteredAndReady) {
return;
}
if (!CurrentAppContext().isMainApp) { return; }
if (!self.tsAccountManager.isRegisteredAndReady) { return; }
SSKRefreshPreKeysOperation *operation = [SSKRefreshPreKeysOperation new];
[self.operationQueue addOperation:operation];
}

@ -0,0 +1,12 @@
public extension Data {
/// Returns `size` bytes of random data generated using the default secure random number generator. See
/// [SecRandomCopyBytes](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) for more information.
public static func getSecureRandomData(ofSize size: UInt) -> Data? {
var data = Data(count: Int(size))
let result = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, Int(size), $0.baseAddress!) }
guard result == errSecSuccess else { return nil }
return data
}
}

@ -7,19 +7,10 @@ extension OnionRequestAPI {
internal typealias EncryptionResult = (ciphertext: Data, symmetricKey: Data, ephemeralPublicKey: Data)
/// Returns `size` bytes of random data generated using the default secure random number generator. See
/// [SecRandomCopyBytes](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) for more information.
private static func getSecureRandomData(ofSize size: UInt) throws -> Data {
var data = Data(count: Int(size))
let result = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, Int(size), $0.baseAddress!) }
guard result == errSecSuccess else { throw Error.randomDataGenerationFailed }
return data
}
/// - Note: Sync. Don't call from the main thread.
private static func encrypt(_ plaintext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data {
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.") }
let iv = try getSecureRandomData(ofSize: ivSize)
guard let iv = Data.getSecureRandomData(ofSize: ivSize) else { throw Error.randomDataGenerationFailed }
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)

@ -19,6 +19,11 @@ This document outlines the main abstractions (layers) in the Session app. These
* Customized sync messages protocol on top of Signal's implementation
* Customized transcripts, receipts & typing indicators protocol on top of Signal's implementation
# Signal Protocol Layer
Don't touch this. Ever.
# Push Notifications Layer
Only applicable to mobile. Speaks for itself.

@ -15,15 +15,15 @@ NS_ASSUME_NONNULL_BEGIN
- (PreKeyRecord *_Nullable)getPreKeyForContact:(NSString *)pubKey transaction:(YapDatabaseReadTransaction *)transaction;
- (PreKeyRecord *)getOrCreatePreKeyForContact:(NSString *)pubKey;
# pragma mark - Pre Key Bundle
# pragma mark - Pre Key Management
/**
* Generates a pre key bundle but doesn't store it as pre key bundles are supposed to be sent to other users without ever being stored.
* Generates a pre key bundle for the given contact. Doesn't store the pre key bundle (pre key bundles are supposed to be sent without ever being stored).
*/
- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)pubKey;
- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)pubKey;
- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)hexEncodedPublicKey;
- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)hexEncodedPublicKey;
- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)removePreKeyBundleForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction;
# pragma mark - Last Message Hash

@ -78,36 +78,35 @@
return record;
}
# pragma mark - Pre Key Bundle
# pragma mark - Pre Key Management
#define LKPreKeyBundleCollection @"LKPreKeyBundleCollection"
- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)pubKey forceClean:(BOOL)forceClean {
// Check pre keys to make sure we have them
- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)hexEncodedPublicKey forceClean:(BOOL)forceClean {
// Refresh signed pre key if needed
[TSPreKeyManager checkPreKeys];
ECKeyPair *_Nullable keyPair = self.identityManager.identityKeyPair;
OWSAssertDebug(keyPair);
// Refresh the signed pre key if needed
if (self.currentSignedPreKey == nil || forceClean) {
// Refresh signed pre key if needed
if (self.currentSignedPreKey == nil || forceClean) { // TODO: Is the self.currentSignedPreKey == nil check needed?
SignedPreKeyRecord *signedPreKeyRecord = [self generateRandomSignedRecord];
[signedPreKeyRecord markAsAcceptedByService];
[self storeSignedPreKey:signedPreKeyRecord.Id signedPreKeyRecord:signedPreKeyRecord];
[self setCurrentSignedPrekeyId:signedPreKeyRecord.Id];
[LKLogger print:@"[Loki] Pre keys refreshed successfully."];
[LKLogger print:@"[Loki] Signed pre key refreshed successfully."];
}
SignedPreKeyRecord *_Nullable signedPreKey = self.currentSignedPreKey;
if (!signedPreKey) {
OWSFailDebug(@"Signed pre key is null.");
if (signedPreKey == nil) {
OWSFailDebug(@"Signed pre key is nil.");
}
PreKeyRecord *preKey = [self getOrCreatePreKeyForContact:pubKey];
uint32_t registrationId = [self.accountManager getOrGenerateRegistrationId];
PreKeyRecord *preKey = [self getOrCreatePreKeyForContact:hexEncodedPublicKey];
uint32_t registrationID = [self.accountManager getOrGenerateRegistrationId];
PreKeyBundle *bundle = [[PreKeyBundle alloc] initWithRegistrationId:registrationId
PreKeyBundle *bundle = [[PreKeyBundle alloc] initWithRegistrationId:registrationID
deviceId:OWSDevicePrimaryDeviceId
preKeyId:preKey.Id
preKeyPublic:preKey.keyPair.publicKey.prependKeyType
@ -118,38 +117,42 @@
return bundle;
}
- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)pubKey {
- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)hexEncodedPublicKey {
NSInteger failureCount = 0;
BOOL forceClean = NO;
while (failureCount < 3) {
@try {
PreKeyBundle *preKeyBundle = [self generatePreKeyBundleForContact:pubKey forceClean:forceClean];
PreKeyBundle *preKeyBundle = [self generatePreKeyBundleForContact:hexEncodedPublicKey forceClean:forceClean];
if (![Ed25519 throws_verifySignature:preKeyBundle.signedPreKeySignature
publicKey:preKeyBundle.identityKey.throws_removeKeyType
data:preKeyBundle.signedPreKeyPublic]) {
@throw [NSException exceptionWithName:InvalidKeyException reason:@"KeyIsNotValidlySigned" userInfo:nil];
}
[LKLogger print:[NSString stringWithFormat:@"[Loki] Generated a new pre key bundle for: %@.", hexEncodedPublicKey]];
return preKeyBundle;
} @catch (NSException *exception) {
failureCount++;
failureCount += 1;
forceClean = YES;
}
}
[LKLogger print:[NSString stringWithFormat:@"[Loki] Failed to generate a valid pre key bundle for: %@.", pubKey]];
[LKLogger print:[NSString stringWithFormat:@"[Loki] Failed to generate a valid pre key bundle for: %@.", hexEncodedPublicKey]];
return nil;
}
- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)pubKey {
return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LKPreKeyBundleCollection];
- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)hexEncodedPublicKey {
return [self.dbReadConnection preKeyBundleForKey:hexEncodedPublicKey inCollection:LKPreKeyBundleCollection];
}
- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction setObject:bundle forKey:pubKey inCollection:LKPreKeyBundleCollection];
- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction setObject:bundle forKey:hexEncodedPublicKey inCollection:LKPreKeyBundleCollection];
[LKLogger print:[NSString stringWithFormat:@"[Loki] Stored pre key bundle for: %@.", hexEncodedPublicKey]];
// FIXME: I don't think the line below is good for anything
[transaction.connection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ }];
}
- (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction removeObjectForKey:pubKey inCollection:LKPreKeyBundleCollection];
- (void)removePreKeyBundleForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction {
[transaction removeObjectForKey:hexEncodedPublicKey inCollection:LKPreKeyBundleCollection];
[LKLogger print:[NSString stringWithFormat:@"[Loki] Removed pre key bundle for: %@.", hexEncodedPublicKey]];
}
# pragma mark - Last Message Hash

@ -9,31 +9,69 @@ import PromiseKit
// Document the expected cases for everything.
// 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.
@objc(LKFriendRequestProtocol)
public final class FriendRequestProtocol : NSObject {
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: - General
@objc(shouldInputBarBeEnabledForThread:)
public static func shouldInputBarBeEnabled(for thread: TSThread) -> Bool {
// Friend requests have nothing to do with groups, so if this isn't a contact thread the input bar should be enabled
guard let thread = thread as? TSContactThread else { return true }
// If this is a note to self, the input bar should be enabled
if SessionProtocol.isMessageNoteToSelf(thread) { return true }
let contactID = thread.contactIdentifier()
var linkedDeviceThreads: Set<TSContactThread> = []
storage.dbReadConnection.read { transaction in
linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction)
}
// If the current user is friends with any of the other user's devices, the input bar should be enabled
if linkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true }
// If no friend request has been sent, the input bar should be enabled
if !linkedDeviceThreads.contains(where: { $0.hasPendingFriendRequest }) { return true }
// There must be a pending friend request
return false
}
@objc(shouldAttachmentButtonBeEnabledForThread:)
public static func shouldAttachmentButtonBeEnabled(for thread: TSThread) -> Bool {
// Friend requests have nothing to do with groups, so if this isn't a contact thread the attachment button should be enabled
guard let thread = thread as? TSContactThread else { return true }
// If this is a note to self, the attachment button should be enabled
if SessionProtocol.isMessageNoteToSelf(thread) { return true }
let contactID = thread.contactIdentifier()
var linkedDeviceThreads: Set<TSContactThread> = []
storage.dbReadConnection.read { transaction in
linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction)
}
// If the current user is friends with any of the other user's devices, the attachment button should be enabled
if linkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true }
// If no friend request has been sent, the attachment button should be disabled
if !linkedDeviceThreads.contains(where: { $0.hasPendingFriendRequest }) { return false }
// There must be a pending friend request
return false
}
// MARK: - Sending
@objc(acceptFriendRequest:in:using:)
public static func acceptFriendRequest(_ friendRequest: TSIncomingMessage, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
@objc(acceptFriendRequestFrom:in:using:)
public static func acceptFriendRequest(from hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
// 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 senderID = friendRequest.authorId
let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: senderID, in: transaction)
let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction) // This doesn't create new threads if they don't exist yet
for thread in linkedDeviceThreads {
if thread.hasPendingFriendRequest {
// TODO: The Obj-C implementation was actually sending this to self.thread. I'm assuming that's not what we meant.
sendFriendRequestAcceptanceMessage(to: senderID, in: thread, using: transaction)
sendFriendRequestAcceptanceMessage(to: thread.contactIdentifier(), in: thread, using: transaction) // NOT hexEncodedPublicKey
thread.saveFriendRequestStatus(.friends, with: transaction)
} else {
let autoGeneratedFRMessageSend = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: senderID, in: transaction)
let autoGeneratedFRMessageSend = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: thread.contactIdentifier(), in: transaction) // NOT hexEncodedPublicKey
OWSDispatch.sendingQueue().async {
let messageSender = SSKEnvironment.shared.messageSender
messageSender.sendMessage(autoGeneratedFRMessageSend)
}
}
}
thread.saveFriendRequestStatus(.friends, with: transaction)
}
@objc(sendFriendRequestAcceptanceMessageToHexEncodedPublicKey:in:using:)
@ -46,7 +84,8 @@ public final class FriendRequestProtocol : NSObject {
@objc(declineFriendRequest:in:using:)
public static func declineFriendRequest(_ friendRequest: TSIncomingMessage, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
thread.saveFriendRequestStatus(.none, with: transaction)
// Delete pre keys
// Delete the pre key bundle for the given contact. This ensures that if we send a
// new message after this, it restarts the friend request process from scratch.
let senderID = friendRequest.authorId
storage.removePreKeyBundle(forContact: senderID, transaction: transaction)
}
@ -65,7 +104,7 @@ public final class FriendRequestProtocol : NSObject {
// 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
// friend request accepted message succeeded. Even if it doesn't, the thread's current friend
// friend request accepted message succeeds. Even if it doesn't, the thread's current friend
// request status will be set to LKThreadFriendRequestStatusFriends for Alice making it possible
// for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
// will then be set to LKThreadFriendRequestStatusFriends. If we do check for a successful send
@ -101,11 +140,13 @@ public final class FriendRequestProtocol : NSObject {
existingFriendRequestMessage.isFriendRequest {
existingFriendRequestMessage.saveFriendRequestStatus(.accepted, with: transaction)
}
/*
// Send our P2P details
if let addressMessage = LokiP2PAPI.onlineBroadcastMessage(forThread: thread) {
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
messageSenderJobQueue.add(message: addressMessage, transaction: transaction)
}
*/
}
@objc(handleFriendRequestMessageIfNeeded:associatedWith:wrappedIn:in:using:)
@ -146,8 +187,7 @@ public final class FriendRequestProtocol : NSObject {
let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction)
for thread in linkedDeviceThreads {
thread.enumerateInteractions(with: transaction) { interaction, _ in
guard let incomingMessage = interaction as? TSIncomingMessage,
incomingMessage.friendRequestStatus != .none else { return }
guard let incomingMessage = interaction as? TSIncomingMessage, incomingMessage.friendRequestStatus != .none else { return }
incomingMessage.saveFriendRequestStatus(.none, with: transaction)
}
}

@ -1,9 +1,68 @@
import CryptoSwift
import PromiseKit
@testable import SignalServiceKit
import XCTest
class FriendRequestProtocolTests : XCTestCase {
// TODO: Add tests
private var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
override func setUp() {
super.setUp()
// Activate the mock environment
ClearCurrentAppContextForTests()
SetCurrentAppContext(TestAppContext())
MockSSKEnvironment.activate()
// Register a mock user
let identityManager = OWSIdentityManager.shared()
let seed = Randomness.generateRandomBytes(16)!
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
TSAccountManager.sharedInstance().didRegister()
}
func testMultiDeviceFriendRequestAcceptance() {
// When Alice accepts Bob's friend request, she should accept all outstanding friend requests with Bob's
// linked devices and try to establish sessions with the subset of Bob's devices that haven't sent a friend request.
func getDevice() -> DeviceLink.Device? {
guard let publicKey = Data.getSecureRandomData(ofSize: 64) else { return nil }
let hexEncodedPublicKey = "05" + publicKey.toHexString()
guard let signature = Data.getSecureRandomData(ofSize: 64) else { return nil }
return DeviceLink.Device(hexEncodedPublicKey: hexEncodedPublicKey, signature: signature)
}
func createThread(for hexEncodedPublicKey: String) -> TSContactThread {
var result: TSContactThread!
storage.dbReadWriteConnection.readWrite { transaction in
result = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
}
return result
}
// Get devices
guard let bobMasterDevice = getDevice() else { return XCTFail() }
guard let bobSlaveDevice = getDevice() else { return XCTFail() }
// Create device link
let bobDeviceLink = DeviceLink(between: bobMasterDevice, and: bobSlaveDevice)
storage.dbReadWriteConnection.readWrite { transaction in
self.storage.addDeviceLink(bobDeviceLink, in: transaction)
}
// Create threads
let bobMasterThread = createThread(for: bobMasterDevice.hexEncodedPublicKey)
let bobSlaveThread = createThread(for: bobSlaveDevice.hexEncodedPublicKey)
// Scenario 1: Alice has a pending friend request from Bob's master device, and nothing
// from his slave device. After accepting the pending friend request we'd expect the
// friend request status for Bob's master thread to be `friends`, and that of Bob's
// slave thread to be `requestSent`.
storage.dbReadWriteConnection.readWrite { transaction in
bobMasterThread.saveFriendRequestStatus(.requestReceived, with: transaction)
bobSlaveThread.saveFriendRequestStatus(.none, with: transaction)
}
storage.dbReadWriteConnection.readWrite { transaction in
FriendRequestProtocol.acceptFriendRequest(from: bobMasterDevice.hexEncodedPublicKey, in: bobMasterThread, using: transaction)
}
XCTAssert(bobMasterThread.friendRequestStatus == .friends)
XCTAssert(bobSlaveThread.friendRequestStatus == .requestSent)
// TODO: Add other scenarios
}
}

@ -0,0 +1,7 @@
#import "TSOutgoingMessage.h"
/// See [The Session Friend Request Protocol](https://github.com/loki-project/session-protocol-docs/wiki/Friend-Requests) for more information.
NS_SWIFT_NAME(FriendRequestMessage)
@interface LKFriendRequestMessage : TSOutgoingMessage
@end

@ -1,7 +1,9 @@
#import "LKFriendRequestMessage.h"
#import "OWSPrimaryStorage+Loki.h"
#import "NSDate+OWS.h"
#import "ProfileManagerProtocol.h"
#import "SignalRecipient.h"
#import "SSKEnvironment.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
@implementation LKFriendRequestMessage
@ -9,6 +11,7 @@
#pragma mark Building
- (SSKProtoContentBuilder *)prepareCustomContentBuilder:(SignalRecipient *)recipient {
SSKProtoContentBuilder *contentBuilder = SSKProtoContent.builder;
// Attach the pre key bundle for the contact in question
PreKeyBundle *preKeyBundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId];
SSKProtoPrekeyBundleMessageBuilder *preKeyBundleMessageBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:preKeyBundle];
NSError *error;

@ -1,6 +0,0 @@
#import "TSOutgoingMessage.h"
NS_SWIFT_NAME(FriendRequestMessage)
@interface LKFriendRequestMessage : TSOutgoingMessage
@end

@ -83,6 +83,7 @@ public final class MultiDeviceProtocol : NSObject {
messageSender.sendMessage(messageSendCopy)
} else {
var frMessageSend: OWSMessageSend!
// FIXME: This crashes sometimes due to transaction nesting
storage.dbReadWriteConnection.readWrite { transaction in // TODO: Yet another transaction
frMessageSend = getAutoGeneratedMultiDeviceFRMessageSend(for: slaveDestination.hexEncodedPublicKey, in: transaction)
}
@ -107,7 +108,7 @@ public final class MultiDeviceProtocol : NSObject {
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction)
let isSlaveDeviceThread = masterHexEncodedPublicKey != hexEncodedPublicKey
thread.isForceHidden = isSlaveDeviceThread
thread.isForceHidden = isSlaveDeviceThread // TODO: Could we make this computed?
if thread.friendRequestStatus == .none || thread.friendRequestStatus == .requestExpired {
thread.saveFriendRequestStatus(.requestSent, with: transaction) // TODO: Should we always immediately mark the slave device as a friend?
}

@ -41,7 +41,7 @@ public final class SessionManagementProtocol : NSObject {
// 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 pre key refresh; using existing signed pre key.")
print("[Loki] Skipping signed pre key refresh; using existing signed pre key.")
return
}
let signedPreKeyRecord = storage.generateRandomSignedRecord()
@ -50,7 +50,7 @@ public final class SessionManagementProtocol : NSObject {
storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id)
TSPreKeyManager.clearPreKeyUpdateFailureCount()
TSPreKeyManager.clearSignedPreKeyRecords()
print("[Loki] Pre keys refreshed successfully.")
print("[Loki] Signed pre key refreshed successfully.")
}
@objc(rotateSignedPreKey)

@ -1087,27 +1087,30 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
{
OWSAssertDebug(self.thread);
SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilder];
if (!builder) {
OWSFailDebug(@"could not build protobuf.");
if (builder == nil) {
OWSFailDebug(@"Couldn't build protobuf.");
return nil;
}
[ProtoUtils addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId dataMessageBuilder:builder];
// Loki: Set display name & profile picture
// Loki: Set display name & profile picture (exclude the profile picture if this is a friend request
// to prevent unsolicited content from being sent)
id<ProfileManagerProtocol> profileManager = SSKEnvironment.shared.profileManager;
NSString *displayName = profileManager.localProfileName;
NSString *profilePictureURL = profileManager.profilePictureURL;
SSKProtoDataMessageLokiProfileBuilder *profileBuilder = [SSKProtoDataMessageLokiProfile builder];
[profileBuilder setDisplayName:displayName];
[profileBuilder setProfilePicture:profilePictureURL ?: @""];
if (![self isKindOfClass:LKFriendRequestMessage.class]) {
[profileBuilder setProfilePicture:profilePictureURL ?: @""];
}
SSKProtoDataMessageLokiProfile *profile = [profileBuilder buildAndReturnError:nil];
[builder setProfile:profile];
NSError *error;
SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error];
if (error || !dataProto) {
OWSFailDebug(@"could not build protobuf: %@", error);
if (error || dataProto == nil) {
OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error);
return nil;
}
return dataProto;

@ -1705,8 +1705,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if (!bundle) {
NSString *missingPrekeyBundleException = @"missingPrekeyBundleException";
OWSRaiseException(
missingPrekeyBundleException, @"Can't get a prekey bundle from the server with required information");
OWSRaiseException(missingPrekeyBundleException, @"Missing pre key bundle for: %@.", recipientID);
} else {
SessionBuilder *builder = [[SessionBuilder alloc] initWithSessionStore:storage
preKeyStore:storage
@ -1718,7 +1717,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@try {
[builder throws_processPrekeyBundle:bundle protocolContext:transaction];
// Loki: Discard the pre key bundle here since the session has been established
// Loki: Discard the pre key bundle as the session has now been established
[storage removePreKeyBundleForContact:recipientID transaction:transaction];
} @catch (NSException *caughtException) {
exception = caughtException;

@ -40,6 +40,7 @@ class OWSUDManagerTest: SSKBaseTestSwift {
let aliceRecipientId = "+13213214321"
override func setUp() {
/*
super.setUp()
tsAccountManager.registerForTests(withLocalNumber: aliceRecipientId)
@ -60,6 +61,7 @@ class OWSUDManagerTest: SSKBaseTestSwift {
signatureData: Randomness.generateRandomBytes(ECCSignatureLength))
udManager.setSenderCertificate(try! senderCertificate.serialized())
*/
}
override func tearDown() {

Loading…
Cancel
Save