@ -25,7 +25,9 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext
import org.whispersystems.signalservice.loki.api.SnodeAPI
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation
import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey
import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey
@ -38,7 +40,13 @@ import kotlin.jvm.Throws
object ClosedGroupsProtocol {
object ClosedGroupsProtocol {
val isSharedSenderKeysEnabled = true
val isSharedSenderKeysEnabled = true
val groupSizeLimit = 20
val groupSizeLimit = 20
sealed class Error ( val description : String ) : Exception ( ) {
object NoThread : Error ( " Couldn't find a thread associated with the given group public key " )
object NoPrivateKey : Error ( " Couldn't find a private key associated with the given group public key. " )
object InvalidUpdate : Error ( " Invalid group update. " )
}
public fun createClosedGroup ( context : Context , name : String , members : Collection < String > ) : Promise < String , Exception > {
public fun createClosedGroup ( context : Context , name : String , members : Collection < String > ) : Promise < String , Exception > {
val deferred = deferred < String , Exception > ( )
val deferred = deferred < String , Exception > ( )
Thread {
Thread {
@ -98,118 +106,135 @@ object ClosedGroupsProtocol {
val name = group . title
val name = group . title
val oldMembers = group . members . map { it . serialize ( ) } . toSet ( )
val oldMembers = group . members . map { it . serialize ( ) } . toSet ( )
val newMembers = oldMembers . minus ( userPublicKey )
val newMembers = oldMembers . minus ( userPublicKey )
update ( context , groupPublicKey , newMembers , name )
return update ( context , groupPublicKey , newMembers , name ) . get ( )
}
}
public fun update ( context : Context , groupPublicKey : String , members : Collection < String > , name : String ) {
public fun update ( context : Context , groupPublicKey : String , members : Collection < String > , name : String ) : Promise < Unit , Exception > {
val userPublicKey = TextSecurePreferences . getLocalNumber ( context )
val deferred = deferred < Unit , Exception > ( )
val sskDatabase = DatabaseFactory . getSSKDatabase ( context )
Thread {
val groupDB = DatabaseFactory . getGroupDatabase ( context )
val userPublicKey = TextSecurePreferences . getLocalNumber ( context )
val groupID = doubleEncodeGroupID ( groupPublicKey )
val sskDatabase = DatabaseFactory . getSSKDatabase ( context )
val group = groupDB . getGroup ( groupID ) . orNull ( )
val groupDB = DatabaseFactory . getGroupDatabase ( context )
if ( group == null ) {
val groupID = doubleEncodeGroupID ( groupPublicKey )
Log . d ( " Loki " , " Can't update nonexistent closed group. " )
val group = groupDB . getGroup ( groupID ) . orNull ( )
return
if ( group == null ) {
}
Log . d ( " Loki " , " Can't update nonexistent closed group. " )
val oldMembers = group . members . map { it . serialize ( ) } . toSet ( )
return @Thread deferred . reject ( Error . NoThread )
val newMembers = members . minus ( oldMembers )
val membersAsData = members . map { Hex . fromStringCondensed ( it ) }
val admins = group . admins . map { it . serialize ( ) }
val adminsAsData = admins . map { Hex . fromStringCondensed ( it ) }
val groupPrivateKey = DatabaseFactory . getSSKDatabase ( context ) . getClosedGroupPrivateKey ( groupPublicKey )
if ( groupPrivateKey == null ) {
Log . d ( " Loki " , " Couldn't get private key for closed group. " )
return
}
val wasAnyUserRemoved = members . toSet ( ) . intersect ( oldMembers ) != oldMembers . toSet ( )
val removedMembers = oldMembers . minus ( members )
val isUserLeaving = removedMembers . contains ( userPublicKey )
var newSenderKeys = listOf < ClosedGroupSenderKey > ( )
if ( wasAnyUserRemoved ) {
if ( isUserLeaving && removedMembers . count ( ) != 1 ) {
Log . d ( " Loki " , " Can't remove self and others simultaneously. " )
return
}
}
// Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
val oldMembers = group . members . map { it . serialize ( ) } . toSet ( )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . Info ( Hex . fromStringCondensed ( groupPublicKey ) ,
val newMembers = members . minus ( oldMembers )
name , setOf ( ) , membersAsData , adminsAsData )
val membersAsData = members . map { Hex . fromStringCondensed ( it ) }
val job = ClosedGroupUpdateMessageSendJob ( groupPublicKey , closedGroupUpdateKind )
val admins = group . admins . map { it . serialize ( ) }
job . setContext ( context )
val adminsAsData = admins . map { Hex . fromStringCondensed ( it ) }
job . onRun ( ) // Run the job immediately
val groupPrivateKey = DatabaseFactory . getSSKDatabase ( context ) . getClosedGroupPrivateKey ( groupPublicKey )
// Delete all ratchets (it's important that this happens * after * sending out the update)
if ( groupPrivateKey == null ) {
sskDatabase . removeAllClosedGroupRatchets ( groupPublicKey )
Log . d ( " Loki " , " Couldn't get private key for closed group. " )
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
return @Thread deferred . reject ( Error . NoPrivateKey )
// send it out to all members (minus the removed ones) using established channels.
}
if ( isUserLeaving ) {
val wasAnyUserRemoved = members . toSet ( ) . intersect ( oldMembers ) != oldMembers . toSet ( )
sskDatabase . removeClosedGroupPrivateKey ( groupPublicKey )
val removedMembers = oldMembers . minus ( members )
groupDB . setActive ( groupID , false )
val isUserLeaving = removedMembers . contains ( userPublicKey )
groupDB . remove ( groupID , Address . fromSerialized ( userPublicKey ) )
var newSenderKeys = listOf < ClosedGroupSenderKey > ( )
// Notify the PN server
if ( wasAnyUserRemoved ) {
LokiPushNotificationManager . performOperation ( context , ClosedGroupOperation . Unsubscribe , groupPublicKey , userPublicKey )
if ( isUserLeaving && removedMembers . count ( ) != 1 ) {
} else {
Log . d ( " Loki " , " Can't remove self and others simultaneously. " )
return @Thread deferred . reject ( Error . InvalidUpdate )
}
// Establish sessions if needed
// Establish sessions if needed
establishSessionsWithMembersIfNeeded ( context , members )
establishSessionsWithMembersIfNeeded ( context , members )
// Send closed group update messages to any new members using established channels
// Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
for ( member in new Members) {
for ( member in old Members) {
@Suppress ( " NAME_SHADOWING " )
@Suppress ( " NAME_SHADOWING " )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . New ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . Info ( Hex . fromStringCondensed ( groupPublicKey ) ,
Hex . fromStringCondensed ( groupPrivateKey ) , lis tOf( ) , membersAsData , adminsAsData )
name , se tOf( ) , membersAsData , adminsAsData )
@Suppress ( " NAME_SHADOWING " )
@Suppress ( " NAME_SHADOWING " )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
job . setContext ( context )
job . onRun ( ) // Run the job immediately
}
}
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
val allOldRatchets = sskDatabase . getAllClosedGroupRatchets ( groupPublicKey , ClosedGroupRatchetCollectionType . Current )
val userRatchet = SharedSenderKeysImplementation . shared . generateRatchet ( groupPublicKey , userPublicKey )
for ( pair in allOldRatchets ) {
val userSenderKey = ClosedGroupSenderKey ( Hex . fromStringCondensed ( userRatchet . chainKey ) , userRatchet . keyIndex , Hex . fromStringCondensed ( userPublicKey ) )
val senderPublicKey = pair . first
for ( member in members ) {
val ratchet = pair . second
if ( member == userPublicKey ) { continue }
val collection = ClosedGroupRatchetCollectionType . Old
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderPublicKey , ratchet , collection )
}
// Delete all ratchets (it's important that this happens * after * sending out the update)
sskDatabase . removeAllClosedGroupRatchets ( groupPublicKey , ClosedGroupRatchetCollectionType . Current )
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
// send it out to all members (minus the removed ones) using established channels.
if ( isUserLeaving ) {
sskDatabase . removeClosedGroupPrivateKey ( groupPublicKey )
groupDB . setActive ( groupID , false )
groupDB . remove ( groupID , Address . fromSerialized ( userPublicKey ) )
// Notify the PN server
LokiPushNotificationManager . performOperation ( context , ClosedGroupOperation . Unsubscribe , groupPublicKey , userPublicKey )
} else {
// Send closed group update messages to any new members using established channels
for ( member in newMembers ) {
@Suppress ( " NAME_SHADOWING " )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . New ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
Hex . fromStringCondensed ( groupPrivateKey ) , listOf ( ) , membersAsData , adminsAsData )
@Suppress ( " NAME_SHADOWING " )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
}
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
val userRatchet = SharedSenderKeysImplementation . shared . generateRatchet ( groupPublicKey , userPublicKey )
val userSenderKey = ClosedGroupSenderKey ( Hex . fromStringCondensed ( userRatchet . chainKey ) , userRatchet . keyIndex , Hex . fromStringCondensed ( userPublicKey ) )
for ( member in members ) {
if ( member == userPublicKey ) { continue }
@Suppress ( " NAME_SHADOWING " )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . SenderKey ( Hex . fromStringCondensed ( groupPublicKey ) , userSenderKey )
@Suppress ( " NAME_SHADOWING " )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
}
}
} else if ( newMembers . isNotEmpty ( ) ) {
// Generate ratchets for any new members
newSenderKeys = newMembers . map { publicKey ->
val ratchet = SharedSenderKeysImplementation . shared . generateRatchet ( groupPublicKey , publicKey )
ClosedGroupSenderKey ( Hex . fromStringCondensed ( ratchet . chainKey ) , ratchet . keyIndex , Hex . fromStringCondensed ( publicKey ) )
}
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . Info ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
newSenderKeys , membersAsData , adminsAsData )
val job = ClosedGroupUpdateMessageSendJob ( groupPublicKey , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
// Establish sessions if needed
establishSessionsWithMembersIfNeeded ( context , newMembers )
// Send closed group update messages to the new members using established channels
var allSenderKeys = sskDatabase . getAllClosedGroupSenderKeys ( groupPublicKey , ClosedGroupRatchetCollectionType . Current )
allSenderKeys = allSenderKeys . union ( newSenderKeys )
for ( member in newMembers ) {
@Suppress ( " NAME_SHADOWING " )
@Suppress ( " NAME_SHADOWING " )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . SenderKey ( Hex . fromStringCondensed ( groupPublicKey ) , userSenderKey )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . New ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
Hex . fromStringCondensed ( groupPrivateKey ) , allSenderKeys , membersAsData , adminsAsData )
@Suppress ( " NAME_SHADOWING " )
@Suppress ( " NAME_SHADOWING " )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
}
}
}
} else {
} else if ( newMembers . isNotEmpty ( ) ) {
val allSenderKeys = sskDatabase . getAllClosedGroupSenderKeys ( groupPublicKey , ClosedGroupRatchetCollectionType . Current )
// Generate ratchets for any new members
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . Info ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
newSenderKeys = newMembers . map { publicKey ->
allSenderKeys , membersAsData , adminsAsData )
val ratchet = SharedSenderKeysImplementation . shared . generateRatchet ( groupPublicKey , publicKey )
val job = ClosedGroupUpdateMessageSendJob ( groupPublicKey , closedGroupUpdateKind )
ClosedGroupSenderKey ( Hex . fromStringCondensed ( ratchet . chainKey ) , ratchet . keyIndex , Hex . fromStringCondensed ( publicKey ) )
}
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . Info ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
newSenderKeys , membersAsData , adminsAsData )
val job = ClosedGroupUpdateMessageSendJob ( groupPublicKey , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
// Establish sessions if needed
establishSessionsWithMembersIfNeeded ( context , newMembers )
// Send closed group update messages to the new members using established channels
var allSenderKeys = sskDatabase . getAllClosedGroupSenderKeys ( groupPublicKey ) ;
allSenderKeys = allSenderKeys . union ( newSenderKeys )
for ( member in newMembers ) {
@Suppress ( " NAME_SHADOWING " )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . New ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
Hex . fromStringCondensed ( groupPrivateKey ) , allSenderKeys , membersAsData , adminsAsData )
@Suppress ( " NAME_SHADOWING " )
val job = ClosedGroupUpdateMessageSendJob ( member , closedGroupUpdateKind )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
}
}
} else {
// Update the group
val allSenderKeys = sskDatabase . getAllClosedGroupSenderKeys ( groupPublicKey ) ;
groupDB . updateTitle ( groupID , name )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . Info ( Hex . fromStringCondensed ( groupPublicKey ) , name ,
if ( !is UserLeaving ) {
allSenderKeys , membersAsData , adminsAsData )
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
val job = ClosedGroupUpdateMessageSendJob ( groupPublicKey , closedGroupUpdateKind )
groupDB . updateMembers ( groupID , members . map { Address . fromSerialized ( it ) } )
ApplicationContext . getInstance ( context ) . jobManager . add ( job )
}
}
// Notify the user
// Update the group
val infoType = if ( isUserLeaving ) GroupContext . Type . QUIT else GroupContext . Type . UPDATE
groupDB . updateTitle ( groupID , name )
val threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( Recipient . from ( context , Address . fromSerialized ( groupID ) , false ) )
if ( !is UserLeaving ) {
insertOutgoingInfoMessage ( context , groupID , infoType , name , members , admins , threadID )
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
deferred . resolve ( Unit )
groupDB . updateMembers ( groupID , members . map { Address . fromSerialized ( it ) } )
} . start ( )
}
return deferred . promise
// Notify the user
val threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( Recipient . from ( context , Address . fromSerialized ( groupID ) , false ) )
insertOutgoingInfoMessage ( context , groupID , GroupContext . Type . UPDATE , name , members , admins , threadID )
}
}
@JvmStatic
@JvmStatic
@ -270,7 +295,7 @@ object ClosedGroupsProtocol {
senderKeys . forEach { senderKey ->
senderKeys . forEach { senderKey ->
if ( ! members . contains ( senderKey . publicKey . toHexString ( ) ) ) { return @forEach }
if ( ! members . contains ( senderKey . publicKey . toHexString ( ) ) ) { return @forEach }
val ratchet = ClosedGroupRatchet ( senderKey . chainKey . toHexString ( ) , senderKey . keyIndex , listOf ( ) )
val ratchet = ClosedGroupRatchet ( senderKey . chainKey . toHexString ( ) , senderKey . keyIndex , listOf ( ) )
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderKey . publicKey . toHexString ( ) , ratchet )
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderKey . publicKey . toHexString ( ) , ratchet , ClosedGroupRatchetCollectionType . Current )
}
}
// Sort out any discrepancies between the provided sender keys and what's required
// Sort out any discrepancies between the provided sender keys and what's required
val missingSenderKeys = members . toSet ( ) . subtract ( senderKeys . map { Hex . toStringCondensed ( it . publicKey ) } )
val missingSenderKeys = members . toSet ( ) . subtract ( senderKeys . map { Hex . toStringCondensed ( it . publicKey ) } )
@ -340,7 +365,7 @@ object ClosedGroupsProtocol {
// Store the ratchets for any new members (it's important that this happens before the code below)
// Store the ratchets for any new members (it's important that this happens before the code below)
senderKeys . forEach { senderKey ->
senderKeys . forEach { senderKey ->
val ratchet = ClosedGroupRatchet ( senderKey . chainKey . toHexString ( ) , senderKey . keyIndex , listOf ( ) )
val ratchet = ClosedGroupRatchet ( senderKey . chainKey . toHexString ( ) , senderKey . keyIndex , listOf ( ) )
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderKey . publicKey . toHexString ( ) , ratchet )
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderKey . publicKey . toHexString ( ) , ratchet , ClosedGroupRatchetCollectionType . Current )
}
}
// Delete all ratchets and either:
// Delete all ratchets and either:
// • Send out the user's new ratchet using established channels if other members of the group left or were removed
// • Send out the user's new ratchet using established channels if other members of the group left or were removed
@ -349,7 +374,14 @@ object ClosedGroupsProtocol {
val wasAnyUserRemoved = members . toSet ( ) . intersect ( oldMembers ) != oldMembers . toSet ( )
val wasAnyUserRemoved = members . toSet ( ) . intersect ( oldMembers ) != oldMembers . toSet ( )
val wasSenderRemoved = ! members . contains ( senderPublicKey )
val wasSenderRemoved = ! members . contains ( senderPublicKey )
if ( wasAnyUserRemoved ) {
if ( wasAnyUserRemoved ) {
sskDatabase . removeAllClosedGroupRatchets ( groupPublicKey )
val allOldRatchets = sskDatabase . getAllClosedGroupRatchets ( groupPublicKey , ClosedGroupRatchetCollectionType . Current )
for ( pair in allOldRatchets ) {
@Suppress ( " NAME_SHADOWING " ) val senderPublicKey = pair . first
val ratchet = pair . second
val collection = ClosedGroupRatchetCollectionType . Old
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderPublicKey , ratchet , collection )
}
sskDatabase . removeAllClosedGroupRatchets ( groupPublicKey , ClosedGroupRatchetCollectionType . Current )
if ( wasCurrentUserRemoved ) {
if ( wasCurrentUserRemoved ) {
sskDatabase . removeClosedGroupPrivateKey ( groupPublicKey )
sskDatabase . removeClosedGroupPrivateKey ( groupPublicKey )
groupDB . setActive ( groupID , false )
groupDB . setActive ( groupID , false )
@ -399,7 +431,7 @@ object ClosedGroupsProtocol {
// Respond to the request
// Respond to the request
Log . d ( " Loki " , " Responding to sender key request from: $senderPublicKey . " )
Log . d ( " Loki " , " Responding to sender key request from: $senderPublicKey . " )
ApplicationContext . getInstance ( context ) . sendSessionRequestIfNeeded ( senderPublicKey )
ApplicationContext . getInstance ( context ) . sendSessionRequestIfNeeded ( senderPublicKey )
val userRatchet = DatabaseFactory . getSSKDatabase ( context ) . getClosedGroupRatchet ( groupPublicKey , userPublicKey )
val userRatchet = DatabaseFactory . getSSKDatabase ( context ) . getClosedGroupRatchet ( groupPublicKey , userPublicKey , ClosedGroupRatchetCollectionType . Current )
?: SharedSenderKeysImplementation . shared . generateRatchet ( groupPublicKey , userPublicKey )
?: SharedSenderKeysImplementation . shared . generateRatchet ( groupPublicKey , userPublicKey )
val userSenderKey = ClosedGroupSenderKey ( Hex . fromStringCondensed ( userRatchet . chainKey ) , userRatchet . keyIndex , Hex . fromStringCondensed ( userPublicKey ) )
val userSenderKey = ClosedGroupSenderKey ( Hex . fromStringCondensed ( userRatchet . chainKey ) , userRatchet . keyIndex , Hex . fromStringCondensed ( userPublicKey ) )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . SenderKey ( Hex . fromStringCondensed ( groupPublicKey ) , userSenderKey )
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob . Kind . SenderKey ( Hex . fromStringCondensed ( groupPublicKey ) , userSenderKey )
@ -424,7 +456,7 @@ object ClosedGroupsProtocol {
// Store the sender key
// Store the sender key
Log . d ( " Loki " , " Received a sender key from: $senderPublicKey . " )
Log . d ( " Loki " , " Received a sender key from: $senderPublicKey . " )
val ratchet = ClosedGroupRatchet ( senderKey . chainKey . toHexString ( ) , senderKey . keyIndex , listOf ( ) )
val ratchet = ClosedGroupRatchet ( senderKey . chainKey . toHexString ( ) , senderKey . keyIndex , listOf ( ) )
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderPublicKey , ratchet )
sskDatabase . setClosedGroupRatchet ( groupPublicKey , senderPublicKey , ratchet , ClosedGroupRatchetCollectionType . Current )
}
}
@JvmStatic
@JvmStatic