@ -14,21 +14,29 @@ import PromiseKit
public final class ClosedGroupsProtocol : NSObject {
public static let isSharedSenderKeysEnabled = true
// / - N o t e : I t ' s r e c o m m e n d e d t o b a t c h f e t c h t h e d e v i c e l i n k s f o r t h e g i v e n s e t o f m e m b e r s b e f o r e i n v o k i n g t h i s , t o a v o i d
// / th e m e s s a g e s e n d i n g p i p e l i n e ma k i n g a r e q u e s t f o r e a c h m e m b e r .
// / - N o t e : I t ' s r e c o m m e n d e d t o b a t c h f e t c h t h e d e v i c e l i n k s f o r t h e g i v e n s e t o f m e m b e r s b e f o r e i n v o k i n g t h i s , t o a v o i d t h e m e s s a g e s e n d i n g p i p e l i n e
// / ma k i n g a r e q u e s t f o r e a c h m e m b e r .
public static func createClosedGroup ( name : String , members membersAsSet : Set < String > , transaction : YapDatabaseReadWriteTransaction ) -> TSGroupThread {
// P r e p a r e
var membersAsSet = membersAsSet
let messageSenderJobQueue = SSKEnvironment . shared . messageSenderJobQueue
let userPublicKey = getUserHexEncodedPublicKey ( )
// G e n e r a t e a k e y p a i r f o r t h e g r o u p
let groupKeyPair = Curve25519 . generateKeyPair ( )
let groupPublicKey = groupKeyPair . hexEncodedPublicKey
// E n s u r e t h e c u r r e n t u s e r ' s m a s t e r d e v i c e i s in c l u d e d i n t h e m e m b e r l i s t
let groupPublicKey = groupKeyPair . hexEncodedPublicKey // I n c l u d e s t h e " 0 5 " p r e f i x
// E n s u r e t h e c u r r e n t u s e r ' s m a s t e r d e v i c e i s th e o n e t h a t ' s in c l u d e d i n t h e m e m b e r l i s t
membersAsSet . remove ( userPublicKey )
membersAsSet . insert ( UserDefaults . standard [ . masterHexEncodedPublicKey ] ? ? userPublicKey )
// C r e a t e r a t c h e t s f o r a l l u s e r s i n v o l v e d
let members = [ String ] ( membersAsSet ) // O n t h e r e c e i v i n g s i d e i t ' s a s s u m e d t h a t t h e m e m b e r l i s t a n d c h a i n k e y l i s t a r e o r d e r e d t h e s a m e
let ratchets = members . map {
// C r e a t e r a t c h e t s f o r a l l m e m b e r s ( a n d t h e i r l i n k e d d e v i c e s ) . T h e s o r t i n g t h a t h a p p e n s i s n e e d e d b e c a u s e t h e r e c e i v i n g e n d a s s u m e s t h a t t h e m e m b e r
// l i s t a n d s e n d e r k e y l i s t a r e o r d e r e d t h e s a m e .
var membersAndLinkedDevicesAsSet : Set < String > = [ ]
for member in membersAsSet {
let deviceLinks = OWSPrimaryStorage . shared ( ) . getDeviceLinks ( for : member , in : transaction )
membersAndLinkedDevicesAsSet . formUnion ( deviceLinks . flatMap { [ $0 . master . hexEncodedPublicKey , $0 . slave . hexEncodedPublicKey ] } )
}
let members = [ String ] ( membersAsSet ) . sorted ( )
let membersAndLinkedDevices = [ String ] ( membersAndLinkedDevicesAsSet ) . sorted ( )
let ratchets = membersAndLinkedDevices . map {
SharedSenderKeysImplementation . shared . generateRatchet ( for : groupPublicKey , senderPublicKey : $0 , using : transaction )
}
// C r e a t e t h e g r o u p
@ -39,15 +47,15 @@ public final class ClosedGroupsProtocol : NSObject {
thread . usesSharedSenderKeys = true
thread . save ( with : transaction )
SSKEnvironment . shared . profileManager . addThread ( toProfileWhitelist : thread )
// E s t a b l i s h s e s s i o n s i f n e e d e d ( s h o u l d n ' t b e n e c e s s a r y u n d e r n o r m a l c i r c u m s t a n c e s a s
// t h e u s e r c a n o n l y p i c k f r o m e x i s t i n g c o n t a c t s )
establishSessionsIfNeeded ( with : members , using : transaction )
// S e n d a c l o s e d g r o u p u p d a t e m e s s a g e t o a l l m e m b e r s i n v o l v e d u s i n g e s t a b l i s h e d c h a n n e l s
// E s t a b l i s h s e s s i o n s i f n e e d e d
establishSessionsIfNeeded ( with : members , using : transaction ) // N o t ` m e m b e r s A n d L i n k e d D e v i c e s ` a s t h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e a l r e a d y
// S e n d a c l o s e d g r o u p u p d a t e m e s s a g e t o a l l m e m b e r s ( a n d t h e i r l i n k e d d e v i c e s ) u s i n g e s t a b l i s h e d c h a n n e l s
let senderKeys = ratchets . map { ClosedGroupSenderKey ( chainKey : Data ( hex : $0 . chainKey ) , keyIndex : $0 . keyIndex ) }
for member in members {
for member in members { // N o t ` m e m b e r s A n d L i n k e d D e v i c e s ` a s t h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e a l r e a d y
let thread = TSContactThread . getOrCreateThread ( withContactId : member , transaction : transaction )
thread . save ( with : transaction )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . new ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name , groupPrivateKey : groupKeyPair . privateKey , senderKeys : senderKeys , members : members , admins : admins )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . new ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name ,
groupPrivateKey : groupKeyPair . privateKey , senderKeys : senderKeys , members : members , admins : admins )
let closedGroupUpdateMessage = ClosedGroupUpdateMessage ( thread : thread , kind : closedGroupUpdateMessageKind )
messageSenderJobQueue . add ( message : closedGroupUpdateMessage , transaction : transaction )
}
@ -76,28 +84,35 @@ public final class ClosedGroupsProtocol : NSObject {
// A d d t h e m e m b e r s t o t h e m e m b e r l i s t
var members = group . groupMemberIds
members . append ( contentsOf : newMembersAsSet )
// E s t a b l i s h s e s s i o n s i f n e e d e d ( s h o u l d n ' t b e n e c e s s a r y u n d e r n o r m a l c i r c u m s t a n c e s a s
// t h e u s e r c a n o n l y p i c k f r o m e x i s t i n g c o n t a c t s )
establishSessionsIfNeeded ( with : members , using : transaction )
// G e n e r a t e r a t c h e t s f o r t h e n e w m e m b e r s
let newMembers = [ String ] ( newMembersAsSet ) // O n t h e r e c e i v i n g s i d e i t ' s a s s u m e d t h a t t h e m e m b e r l i s t a n d c h a i n k e y l i s t a r e o r d e r e d t h e s a m e
let ratchets = newMembers . map {
// G e n e r a t e r a t c h e t s f o r t h e n e w m e m b e r s ( a n d t h e i r l i n k e d d e v i c e s ) . T h e s o r t i n g t h a t h a p p e n s i s n e e d e d b e c a u s e t h e r e c e i v i n g e n d a s s u m e s t h a t t h e m e m b e r
// l i s t a n d s e n d e r k e y l i s t a r e o r d e r e d t h e s a m e .
var newMembersAndLinkedDevicesAsSet : Set < String > = [ ]
for member in newMembersAsSet {
let deviceLinks = OWSPrimaryStorage . shared ( ) . getDeviceLinks ( for : member , in : transaction )
newMembersAndLinkedDevicesAsSet . formUnion ( deviceLinks . flatMap { [ $0 . master . hexEncodedPublicKey , $0 . slave . hexEncodedPublicKey ] } )
}
members = members . sorted ( )
let newMembersAndLinkedDevices = [ String ] ( newMembersAndLinkedDevicesAsSet ) . sorted ( )
let ratchets = newMembersAndLinkedDevices . map {
SharedSenderKeysImplementation . shared . generateRatchet ( for : groupPublicKey , senderPublicKey : $0 , using : transaction )
}
// S e n d a c l o s e d g r o u p u p d a t e m e s s a g e t o t h e e x i s t i n g m e m b e r s w i t h t h e n e w m e m b e r s ' r a t c h e t s ( t h i s m e s s a g e i s
// a i m e d a t t h e g r o u p )
// S e n d a c l o s e d g r o u p u p d a t e m e s s a g e t o t h e e x i s t i n g m e m b e r s w i t h t h e n e w m e m b e r s ' r a t c h e t s ( t h i s m e s s a g e i s a i m e d a t t h e g r o u p )
let senderKeys = ratchets . map { ClosedGroupSenderKey ( chainKey : Data ( hex : $0 . chainKey ) , keyIndex : $0 . keyIndex ) }
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . info ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name , senderKeys : senderKeys , members : members , admins : admins )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . info ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name , senderKeys : senderKeys ,
members : members , admins : admins )
let closedGroupUpdateMessage = ClosedGroupUpdateMessage ( thread : thread , kind : closedGroupUpdateMessageKind )
messageSenderJobQueue . add ( message : closedGroupUpdateMessage , transaction : transaction )
// S e n d c l o s e d g r o u p u p d a t e m e s s a g e s t o t h e n e w m e m b e r s u s i n g e s t a b l i s h e d c h a n n e l s
// E s t a b l i s h s e s s i o n s i f n e e d e d
establishSessionsIfNeeded ( with : [ String ] ( newMembersAsSet ) , using : transaction ) // N o t ` n e w M e m b e r s A n d L i n k e d D e v i c e s ` a s t h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e a l r e a d y
// S e n d c l o s e d g r o u p u p d a t e m e s s a g e s t o t h e n e w m e m b e r s ( a n d t h e i r l i n k e d d e v i c e s ) u s i n g e s t a b l i s h e d c h a n n e l s
let allSenderKeys = Storage . getAllClosedGroupRatchets ( for : groupPublicKey ) . map { // T h i s i n c l u d e s t h e n e w l y g e n e r a t e d r a t c h e t s
ClosedGroupSenderKey ( chainKey : Data ( hex : $0 . chainKey ) , keyIndex : $0 . keyIndex )
}
for member in newMembers {
for member in newMembers AsSet { // N o t ` n e w M e m b e r s A n d L i n k e d D e v i c e s ` a s t h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e a l r e a d y
let thread = TSContactThread . getOrCreateThread ( contactId : member )
thread . save ( with : transaction )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . new ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name , groupPrivateKey : Data ( hex : groupPrivateKey ) , senderKeys : allSenderKeys , members : members , admins : admins )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . new ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name ,
groupPrivateKey : Data ( hex : groupPrivateKey ) , senderKeys : allSenderKeys , members : members , admins : admins )
let closedGroupUpdateMessage = ClosedGroupUpdateMessage ( thread : thread , kind : closedGroupUpdateMessageKind )
messageSenderJobQueue . add ( message : closedGroupUpdateMessage , transaction : transaction )
}
@ -115,12 +130,12 @@ public final class ClosedGroupsProtocol : NSObject {
}
public static func removeMembers ( _ membersToRemove : Set < String > , from groupPublicKey : String , using transaction : YapDatabaseReadWriteTransaction ) {
// P r e p a r e
let userPublicKey = getUserHexEncodedPublicKey ( )
let isUserLeaving = membersToRemove . contains ( userPublicKey )
guard ! isUserLeaving || membersToRemove . count = = 1 else {
return print ( " [Loki] Can't remove self and others simultaneously. " )
}
// P r e p a r e
let messageSenderJobQueue = SSKEnvironment . shared . messageSenderJobQueue
let groupID = LKGroupUtilities . getEncodedClosedGroupID ( groupPublicKey )
guard let thread = TSGroupThread . fetch ( uniqueId : groupID , transaction : transaction ) else {
@ -137,26 +152,26 @@ public final class ClosedGroupsProtocol : NSObject {
}
indexes . forEach { members . remove ( at : $0 ) }
// S e n d t h e u p d a t e t o t h e g r o u p ( d o n ' t i n c l u d e n e w r a t c h e t s a s e v e r y o n e s h o u l d g e n e r a t e n e w r a t c h e t s i n d i v i d u a l l y )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . info ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name , senderKeys : [ ] , members : members , admins : admins )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . info ( groupPublicKey : Data ( hex : groupPublicKey ) , name : name , senderKeys : [ ] ,
members : members , admins : admins )
let closedGroupUpdateMessage = ClosedGroupUpdateMessage ( thread : thread , kind : closedGroupUpdateMessageKind )
messageSenderJobQueue . add ( message : closedGroupUpdateMessage , transaction : transaction )
// D e l e t e a l l r a t c h e t s ( i t ' s i m p o r t a n t t h a t t h i s h a p p e n s a f t e r s e n d i n g o u t t h e u p d a t e )
Storage . removeAllClosedGroupRatchets ( for : groupPublicKey , using : transaction )
// R e m o v e t h e g r o u p f r o m t h e u s e r ' s s e t o f p u b l i c k e y s t o p o l l f o r i f t h e u s e r i s l e a v i n g . O t h e r w i s e g e n e r a t e
// a n e w r a t c h e t a n d s e n d i t o u t t o a l l me m b e r s ( m i n u s t h e r e m o v e d o n e s ) u s i n g e s t a b l i s h e d c h a n n e l s .
// R e m o v e t h e g r o u p f r o m t h e u s e r ' s s e t o f p u b l i c k e y s t o p o l l f o r i f t h e u s e r i s l e a v i n g . O t h e r w i s e g e n e r a t e a n e w r a t c h e t a n d s e n d i t o u t t o a l l
// me m b e r s ( m i n u s t h e r e m o v e d o n e s ) a n d t h e i r l i n k e d d e v i c e s u s i n g e s t a b l i s h e d c h a n n e l s .
if isUserLeaving {
Storage . removeClosedGroupPrivateKey ( for : groupPublicKey , using : transaction )
} else {
// E s t a b l i s h s e s s i o n s i f n e e d e d ( s h o u l d n ' t b e n e c e s s a r y u n d e r n o r m a l c i r c u m s t a n c e s a s
// s e s s i o n s w o u l d ' v e a l r e a d y b e e n e s t a b l i s h e d p r e v i o u s l y )
establishSessionsIfNeeded ( with : members , using : transaction )
// S e n d o u t t h e u s e r ' s n e w r a t c h e t t o a l l m e m b e r s ( m i n u s t h e r e m o v e d o n e s ) u s i n g e s t a b l i s h e d c h a n n e l s
let newRatchet = SharedSenderKeysImplementation . shared . generateRatchet ( for : groupPublicKey , senderPublicKey : userPublicKey , using : transaction )
let newSenderKey = ClosedGroupSenderKey ( chainKey : Data ( hex : newRatchet . chainKey ) , keyIndex : newRatchet . keyIndex )
for member in members {
// E s t a b l i s h s e s s i o n s i f n e e d e d
establishSessionsIfNeeded ( with : members , using : transaction ) // T h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e
// S e n d o u t t h e u s e r ' s n e w r a t c h e t t o a l l m e m b e r s ( m i n u s t h e r e m o v e d o n e s ) a n d t h e i r l i n k e d d e v i c e s u s i n g e s t a b l i s h e d c h a n n e l s
let userRatchet = SharedSenderKeysImplementation . shared . generateRatchet ( for : groupPublicKey , senderPublicKey : userPublicKey , using : transaction )
let userSenderKey = ClosedGroupSenderKey ( chainKey : Data ( hex : userRatchet . chainKey ) , keyIndex : userRatchet . keyIndex )
for member in members { // T h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e
let thread = TSContactThread . getOrCreateThread ( withContactId : member , transaction : transaction )
thread . save ( with : transaction )
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . senderKey ( groupPublicKey : Data ( hex : groupPublicKey ) , senderKey : new SenderKey)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage . Kind . senderKey ( groupPublicKey : Data ( hex : groupPublicKey ) , senderKey : user SenderKey)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage ( thread : thread , kind : closedGroupUpdateMessageKind )
messageSenderJobQueue . add ( message : closedGroupUpdateMessage , transaction : transaction )
}
@ -210,12 +225,13 @@ public final class ClosedGroupsProtocol : NSObject {
let infoMessage = TSInfoMessage ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , in : thread , messageType : . typeGroupUpdate )
infoMessage . save ( with : transaction )
// E s t a b l i s h s e s s i o n s i f n e e d e d
establishSessionsIfNeeded ( with : members , using : transaction )
establishSessionsIfNeeded ( with : members , using : transaction ) // T h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e
}
// / I n v o k e d u p o n r e c e i v i n g a g r o u p u p d a t e . A g r o u p u p d a t e i s s e n t o u t w h e n a g r o u p ' s n a m e i s c h a n g e d , w h e n n e w u s e r s
// / a r e a d d e d , w h e n u s e r s l e a v e o r a r e k i c k e d , o r i f t h e g r o u p a d m i n s a r e c h a n g e d .
private static func handleInfoMessage ( _ closedGroupUpdate : SSKProtoDataMessageClosedGroupUpdate , from senderPublicKey : String , using transaction : YapDatabaseReadWriteTransaction ) {
// / I n v o k e d u p o n r e c e i v i n g a g r o u p u p d a t e . A g r o u p u p d a t e i s s e n t o u t w h e n a g r o u p ' s n a m e i s c h a n g e d , w h e n n e w u s e r s a r e a d d e d , w h e n u s e r s l e a v e o r a r e
// / k i c k e d , o r i f t h e g r o u p a d m i n s a r e c h a n g e d .
private static func handleInfoMessage ( _ closedGroupUpdate : SSKProtoDataMessageClosedGroupUpdate , from senderPublicKey : String ,
using transaction : YapDatabaseReadWriteTransaction ) {
// U n w r a p t h e m e s s a g e
let groupPublicKey = closedGroupUpdate . groupPublicKey . toHexString ( )
let name = closedGroupUpdate . name
@ -237,19 +253,15 @@ public final class ClosedGroupsProtocol : NSObject {
return print ( " [Loki] Ignoring closed group update from non-admin. " )
}
// E s t a b l i s h s e s s i o n s i f n e e d e d ( i t ' s i m p o r t a n t t h a t t h i s h a p p e n s b e f o r e t h e c o d e b e l o w )
establishSessionsIfNeeded ( with : members , using : transaction )
// P a r s e o u t a n y n e w m e m b e r s a n d s t o r e t h e i r r a t c h e t s ( i t ' s i m p o r t a n t t h a t
// t h i s h a p p e n s b e f o r e t h e c o d e b e l o w )
establishSessionsIfNeeded ( with : members , using : transaction ) // T h i s i n t e r n a l l y t a k e s c a r e o f m u l t i d e v i c e
// P a r s e o u t a n y n e w m e m b e r s a n d s t o r e t h e i r r a t c h e t s ( i t ' s i m p o r t a n t t h a t t h i s h a p p e n s b e f o r e t h e c o d e b e l o w )
let oldMembers = group . groupMemberIds
let newMembers = members . filter { ! oldMembers . contains ( $0 ) }
if newMembers . count = = senderKeys . count { // I f s o m e o n e l e f t o r w a s k i c k e d t h e m e s s a g e w o n ' t h a v e a n y s e n d e r k e y s
zip ( newMembers , senderKeys ) . forEach { ( member , senderKey ) in
let ratchet = ClosedGroupRatchet ( chainKey : senderKey . chainKey . toHexString ( ) , keyIndex : UInt ( senderKey . keyIndex ) , messageKeys : [ ] )
Storage . setClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : member , ratchet : ratchet , using : transaction )
}
zip ( newMembers , senderKeys ) . forEach { ( member , senderKey ) in
let ratchet = ClosedGroupRatchet ( chainKey : senderKey . chainKey . toHexString ( ) , keyIndex : UInt ( senderKey . keyIndex ) , messageKeys : [ ] )
Storage . setClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : member , ratchet : ratchet , using : transaction )
}
// D e l e t e a l l r a t c h e t s a n d s e n d o u t t h e u s e r ' s n e w r a t c h e t u s i n g e s t a b l i s h e d
// c h a n n e l s i f a n y m e m b e r o f t h e g r o u p l e f t o r w a s r e m o v e d
// D e l e t e a l l r a t c h e t s a n d s e n d o u t t h e u s e r ' s n e w r a t c h e t u s i n g e s t a b l i s h e d c h a n n e l s i f a n y m e m b e r o f t h e g r o u p l e f t o r w a s r e m o v e d
if Set ( members ) . intersection ( oldMembers ) != Set ( oldMembers ) {
Storage . removeAllClosedGroupRatchets ( for : groupPublicKey , using : transaction )
let userPublicKey = getUserHexEncodedPublicKey ( )