mirror of https://github.com/oxen-io/session-ios
Loki session reset (#14)
* Added session reset. * Hooked up session reset internals to UI. * Send empty message when we have received an end session message. * Verify incoming PreKeyWhisperMessage. * Fix indentations in SessionReset.mdpull/16/head
parent
fb1e27d633
commit
c43295eb7c
@ -1 +1 @@
|
||||
Subproject commit 7f42f93c7df8127331d26d0109170a0524f67f7b
|
||||
Subproject commit 870d1b5be23fd8fb5d68af6c20e36b3ed5dcde0f
|
@ -0,0 +1,56 @@
|
||||
# Loki Session Reset
|
||||
|
||||
## Signal
|
||||
Since Signal uses a centralised server, creating sessions is easy as the prekeys can be easily fetched.
|
||||
|
||||
The process is as follows:
|
||||
|
||||
1. `A` deletes all their sessions and sends `End Session` to `B`
|
||||
- `A` contacts the server and creates a new session
|
||||
2. `B` Gets this message and deletes all sessions.
|
||||
3. `B` Sends a message with a newly created session
|
||||
- `B` contacted server and established this
|
||||
4. `A` and `B` now have the same sessions so they can delete any archived ones.
|
||||
|
||||
## Loki
|
||||
Loki doesn't have a centralised server and thus we need to change the process above with something similar.
|
||||
|
||||
We have to introduce a session reset state `sessionState` which can take the following states:
|
||||
- `none`: No session reset is in progress
|
||||
- `initiated`: We have initiated the session reset
|
||||
- `received`: We have received a session reset from the other user
|
||||
|
||||
The new process is as follows:
|
||||
|
||||
1. `A` Sends `End Session` with a `PreKeyBundle` and archives its own session.
|
||||
- `sessionState = initiated`
|
||||
- The session is archived as we could get a message from `B` using the archived session, so we still want to be able to decrypt that.
|
||||
- We can show `Session reset in progress`
|
||||
2. `B` Gets this message and saves the `PreKeyBundle` and archives its own sessions.
|
||||
- `sessionState = received`
|
||||
- `B` sends an empty message, which will trigger a new session to be created.
|
||||
- `B` deletes the `PreKeyBundle` once session is created.
|
||||
- We can show `Session reset in progress`
|
||||
3. `A` and `B` both do the routine below when receiving messages.
|
||||
|
||||
### Upon receiving message (Only applies to PreKey and Cipher messages)
|
||||
|
||||
- Store the current active session `PS`
|
||||
- Decrypt the message
|
||||
- Decrypting a message can cause the active session to change
|
||||
- If `sessionState == none` then it means that we haven't started session reset and we can abort.
|
||||
- Get the current session `CS`
|
||||
- If `PS` is `nil` then abort as we didn't have a session before.
|
||||
- If `CS != PS` then sessions were changed.
|
||||
- If `sessionState == received` then it means that the sender used an old session to contact us. We need to wait for them to use the new one.
|
||||
- Archive `CS` and set the session to `PS`
|
||||
- If `sessionState == initiated` then it means that the sender acknowledged our session reset and sent a message with a new session
|
||||
- Delete all session except `CS`
|
||||
- `sessionState = none`
|
||||
- Send an empty message to confirm session adoption
|
||||
- We can show `Session reset done`
|
||||
- If `CS == PS` then sessions were the same.
|
||||
- If `sessionState == received` then it means that the new session we created is the one the sender used for sending message. We have successfully adopted the new session.
|
||||
- Delete all sessions except `PS`
|
||||
- `sessionState = none`
|
||||
- We can show `Session reset done`
|
@ -0,0 +1,25 @@
|
||||
/// Loki: Refer to Docs/SessionReset.md for explanations
|
||||
|
||||
#import "SessionCipher.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kNSNotificationName_SessionAdopted;
|
||||
extern NSString *const kNSNotificationKey_ContactPubKey;
|
||||
|
||||
@interface SessionCipher (Loki)
|
||||
|
||||
/**
|
||||
Decrypt the given `CipherMessage`.
|
||||
This function is a wrapper around `throws_decrypt:protocolContext:` and adds on the custom loki session handling ontop.
|
||||
Refer to SignalServiceKit/Loki/Docs/SessionReset.md for overview on how it works.
|
||||
|
||||
@param whisperMessage The cipher message.
|
||||
@param protocolContext The protocol context (YapDatabaseReadWriteTransaction)
|
||||
@return The decrypted data.
|
||||
*/
|
||||
- (NSData *)throws_lokiDecrypt:(id<CipherMessage>)whisperMessage protocolContext:(nullable id)protocolContext NS_SWIFT_UNAVAILABLE("throws objc exceptions");
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,181 @@
|
||||
/// Loki: Refer to Docs/SessionReset.md for explanations
|
||||
|
||||
#import "SessionCipher+Loki.h"
|
||||
#import "NSNotificationCenter+OWS.h"
|
||||
#import "PreKeyWhisperMessage.h"
|
||||
#import "OWSPrimaryStorage+Loki.h"
|
||||
#import "TSContactThread.h"
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NSString *const kNSNotificationName_SessionAdopted = @"kNSNotificationName_SessionAdopted";
|
||||
NSString *const kNSNotificationKey_ContactPubKey = @"kNSNotificationKey_ContactPubKey";
|
||||
|
||||
@interface SessionCipher ()
|
||||
|
||||
@property (nonatomic, readonly) NSString *recipientId;
|
||||
@property (nonatomic, readonly) int deviceId;
|
||||
|
||||
@property (nonatomic, readonly) id<SessionStore> sessionStore;
|
||||
@property (nonatomic, readonly) id<PreKeyStore> prekeyStore;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SessionCipher (Loki)
|
||||
|
||||
- (NSData *)throws_lokiDecrypt:(id<CipherMessage>)whisperMessage protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
// Our state before we decrypt the message
|
||||
SessionState *_Nullable state = [self getCurrentState:protocolContext];
|
||||
|
||||
// While decrypting our state may change internally
|
||||
NSData *plainText = [self throws_decrypt:whisperMessage protocolContext:protocolContext];
|
||||
|
||||
// Loki: Verify incoming friend request messages
|
||||
if (!state) {
|
||||
[self throws_verifyFriendRequestAcceptPreKeyForMessage:whisperMessage protocolContext:protocolContext];
|
||||
}
|
||||
|
||||
// Loki: Handle any session resets
|
||||
[self handleSessionReset:whisperMessage previousState:state protocolContext:protocolContext];
|
||||
|
||||
return plainText;
|
||||
}
|
||||
|
||||
/// Get the current session state
|
||||
- (SessionState *_Nullable)getCurrentState:(nullable id)protocolContext {
|
||||
SessionRecord *record = [self.sessionStore loadSession:self.recipientId deviceId:self.deviceId protocolContext:protocolContext];
|
||||
SessionState *state = record.sessionState;
|
||||
|
||||
// Check if session is initialized
|
||||
if (!state.hasSenderChain) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// Handle any loki session reset stuff
|
||||
- (void)handleSessionReset:(id<CipherMessage>)whisperMessage
|
||||
previousState:(SessionState *_Nullable)previousState
|
||||
protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
// Don't bother doing anything if we didn't have a session before
|
||||
if (!previousState) {
|
||||
return;
|
||||
}
|
||||
|
||||
OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]);
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
// Get the thread
|
||||
TSContactThread *thread = [TSContactThread getThreadWithContactId:self.recipientId transaction:transaction];
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail early if no session reset is in progress
|
||||
if (thread.sessionResetState == TSContactThreadSessionResetStateNone) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL sessionResetReceived = thread.sessionResetState == TSContactThreadSessionResetStateRequestReceived;
|
||||
SessionState *_Nullable currentState = [self getCurrentState:protocolContext];
|
||||
|
||||
// Check if our previous state and our current state differ
|
||||
if (!currentState || ![currentState.aliceBaseKey isEqualToData:previousState.aliceBaseKey]) {
|
||||
|
||||
if (sessionResetReceived) {
|
||||
// The other user used an old session to contact us.
|
||||
// Wait for them to use a new one
|
||||
[self restoreSession:previousState protocolContext:protocolContext];
|
||||
} else {
|
||||
// Our session reset went through successfully
|
||||
// We had initiated a session reset and got a different session back from the user
|
||||
[self deleteAllSessionsExcept:currentState protocolContext:protocolContext];
|
||||
[self notifySessionAdopted];
|
||||
}
|
||||
|
||||
} else if (sessionResetReceived) {
|
||||
// Our session reset went through successfully
|
||||
// We got a message with the same session from the other user
|
||||
[self deleteAllSessionsExcept:previousState protocolContext:protocolContext];
|
||||
[self notifySessionAdopted];
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a notification about a new session being adopted
|
||||
- (void)notifySessionAdopted
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationNameAsync:kNSNotificationName_SessionAdopted
|
||||
object:nil
|
||||
userInfo:@{
|
||||
kNSNotificationKey_ContactPubKey : self.recipientId,
|
||||
}];
|
||||
}
|
||||
|
||||
/// Delete all other sessions except the given one
|
||||
- (void)deleteAllSessionsExcept:(SessionState *)state protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
SessionRecord *record = [self.sessionStore loadSession:self.recipientId deviceId:self.deviceId protocolContext:protocolContext];
|
||||
[record removePreviousSessionStates];
|
||||
[record setState:state];
|
||||
|
||||
[self.sessionStore storeSession:self.recipientId
|
||||
deviceId:self.deviceId
|
||||
session:record
|
||||
protocolContext:protocolContext];
|
||||
}
|
||||
|
||||
/// Set the given session as the active one while archiving the old one
|
||||
- (void)restoreSession:(SessionState *)state protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
SessionRecord *record = [self.sessionStore loadSession:self.recipientId deviceId:self.deviceId protocolContext:protocolContext];
|
||||
|
||||
// Remove the state from previous session states
|
||||
[record.previousSessionStates enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(SessionState *obj, NSUInteger idx, BOOL *stop) {
|
||||
if ([state.aliceBaseKey isEqualToData:obj.aliceBaseKey]) {
|
||||
[record.previousSessionStates removeObjectAtIndex:idx];
|
||||
*stop = true;
|
||||
}
|
||||
}];
|
||||
|
||||
// Promote it so the previous state gets archived
|
||||
[record promoteState:state];
|
||||
|
||||
[self.sessionStore storeSession:self.recipientId
|
||||
deviceId:self.deviceId
|
||||
session:record
|
||||
protocolContext:protocolContext];
|
||||
}
|
||||
|
||||
/// Check that we have matching prekeys in the case of a `PreKeyWhisperMessage`
|
||||
/// This is so that we don't trigger a false friend request accept on unknown contacts
|
||||
- (void)throws_verifyFriendRequestAcceptPreKeyForMessage:(id<CipherMessage>)whisperMessage protocolContext:(nullable id)protocolContext {
|
||||
OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadTransaction class]]);
|
||||
YapDatabaseReadTransaction *transaction = protocolContext;
|
||||
|
||||
/// We only want to look at `PreKeyWhisperMessage`
|
||||
if (![whisperMessage isKindOfClass:[PreKeyWhisperMessage class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
/// We need the primary storage to access contact prekeys
|
||||
if (![self.prekeyStore isKindOfClass:[OWSPrimaryStorage class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreKeyWhisperMessage *preKeyMessage = whisperMessage;
|
||||
OWSPrimaryStorage *primaryStorage = self.prekeyStore;
|
||||
|
||||
PreKeyRecord *_Nullable storedPreKey = [primaryStorage getPreKeyForContact:self.recipientId transaction:transaction];
|
||||
if(!storedPreKey) {
|
||||
OWSRaiseException(@"LokiInvalidPreKey", @"Received a friend request from a pubkey for which no prekey bundle was created");
|
||||
}
|
||||
|
||||
if (storedPreKey.Id != preKeyMessage.prekeyID) {
|
||||
OWSRaiseException(@"LokiPreKeyIdsDontMatch", @"Received a preKeyWhisperMessage (friend request accept) from an unknown source");
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
Loading…
Reference in New Issue