remove shared sender keys

This commit is contained in:
Ryan ZHAO 2021-02-18 14:14:05 +11:00
parent 568fddf91d
commit 9d0831b874
29 changed files with 120 additions and 678 deletions

View File

@ -75,7 +75,6 @@ import org.thoughtcrime.securesms.loki.api.PublicChatManager;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
@ -182,12 +181,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
SessionResetImplementation sessionResetImpl = new SessionResetImplementation(this);
MessagingConfiguration.Companion.configure(this,
DatabaseFactory.getStorage(this),
sskDatabase,
DatabaseFactory.getAttachmentProvider(this),
new SessionProtocolImpl(this));
if (userPublicKey != null) {
@ -507,8 +504,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
return Unit.INSTANCE;
});
SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this);
ClosedGroupPoller.Companion.configureIfNeeded(this, sskDatabase);
ClosedGroupPoller.Companion.configureIfNeeded(this);
closedGroupPoller = ClosedGroupPoller.Companion.getShared();
}

View File

@ -1098,16 +1098,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
builder.setPositiveButton(R.string.yes, (dialog, which) -> {
Recipient groupRecipient = getRecipient();
String groupPublicKey;
boolean isSSKBasedClosedGroup;
boolean isClosedGroup;
try {
groupPublicKey = HexEncodingKt.toHexString(GroupUtil.doubleDecodeGroupID(groupRecipient.getAddress().toString()));
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey);
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(this).isClosedGroup(groupPublicKey);
} catch (IOException e) {
groupPublicKey = null;
isSSKBasedClosedGroup = false;
isClosedGroup = false;
}
try {
if (isSSKBasedClosedGroup) {
if (isClosedGroup) {
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey);
initializeEnabledCheck();
} else {

View File

@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SessionJobDatabase;
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
public class DatabaseFactory {
@ -67,7 +66,6 @@ public class DatabaseFactory {
private final LokiThreadDatabase lokiThreadDatabase;
private final LokiUserDatabase lokiUserDatabase;
private final LokiBackupFilesDatabase lokiBackupFilesDatabase;
private final SharedSenderKeysDatabase sskDatabase;
private final SessionJobDatabase sessionJobDatabase;
// Refactor
@ -176,10 +174,6 @@ public class DatabaseFactory {
return getInstance(context).lokiBackupFilesDatabase;
}
public static SharedSenderKeysDatabase getSSKDatabase(Context context) {
return getInstance(context).sskDatabase;
}
public static SessionJobDatabase getSessionJobDatabase(Context context) {
return getInstance(context).sessionJobDatabase;
}
@ -229,7 +223,6 @@ public class DatabaseFactory {
this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper);
this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper);
this.lokiBackupFilesDatabase = new LokiBackupFilesDatabase(context, databaseHelper);
this.sskDatabase = new SharedSenderKeysDatabase(context, databaseHelper);
this.storage = new Storage(context, databaseHelper);
this.attachmentProvider = new DatabaseAttachmentProvider(context, databaseHelper);
this.sessionJobDatabase = new SessionJobDatabase(context, databaseHelper);

View File

@ -417,9 +417,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
override fun isClosedGroup(publicKey: String): Boolean {
val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(publicKey)
val isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(publicKey)
val address = Address.fromSerialized(publicKey)
return address.isClosedGroup || isSSKBasedClosedGroup
return address.isClosedGroup || isClosedGroup
}
override fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair> {
@ -431,7 +431,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
override fun getAllClosedGroupPublicKeys(): Set<String> {
return DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
return DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
}
override fun addClosedGroupPublicKey(groupPublicKey: String) {

View File

@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration;
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@ -131,9 +130,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand());
db.execSQL(SharedSenderKeysDatabase.getCreateCurrentClosedGroupRatchetTableCommand());
db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand());
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -186,8 +182,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV12) {
db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command());
db.execSQL(SharedSenderKeysDatabase.getCreateCurrentClosedGroupRatchetTableCommand());
db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand());
db.execSQL(ClosedGroupsMigration.getCreateCurrentClosedGroupRatchetTableCommand());
db.execSQL(ClosedGroupsMigration.getCreateClosedGroupPrivateKeyTableCommand());
}
if (oldVersion < lokiV13) {
@ -199,7 +195,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < lokiV15) {
db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand());
db.execSQL(ClosedGroupsMigration.getCreateOldClosedGroupRatchetTableCommand());
}
if (oldVersion < lokiV16) {

View File

@ -98,7 +98,6 @@ public class SignalCommunicationModule {
Optional.of(new MessageSenderEventListener(context)),
TextSecurePreferences.getLocalNumber(context),
DatabaseFactory.getLokiAPIDatabase(context),
DatabaseFactory.getSSKDatabase(context),
DatabaseFactory.getLokiThreadDatabase(context),
DatabaseFactory.getLokiMessageDatabase(context),
null, // DatabaseFactory.getLokiPreKeyBundleDatabase(context)

View File

@ -248,7 +248,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context);
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, apiDB, UnidentifiedAccessUtil.getCertificateValidator());
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, new SessionProtocolImpl(context), sessionResetProtocol, apiDB, UnidentifiedAccessUtil.getCertificateValidator());
SignalServiceContent content = cipher.decrypt(envelope);

View File

@ -246,21 +246,21 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
val admins = members.toSet() //TODO For now, consider all the users to be admins.
var isSSKBasedClosedGroup: Boolean
var isClosedGroup: Boolean
var groupPublicKey: String?
try {
groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey)
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(this).isClosedGroup(groupPublicKey)
} catch (e: IOException) {
groupPublicKey = null
isSSKBasedClosedGroup = false
isClosedGroup = false
}
if (members.isEmpty()) {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocolV2.groupSizeLimit else legacyGroupSizeLimit
val maxGroupMembers = if (isClosedGroup) ClosedGroupsProtocolV2.groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) {
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
@ -273,7 +273,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
}
if (isSSKBasedClosedGroup) {
if (isClosedGroup) {
isLoading = true
loaderContainer.fadeIn()
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {

View File

@ -167,7 +167,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val apiDB = DatabaseFactory.getLokiAPIDatabase(this)
val threadDB = DatabaseFactory.getLokiThreadDatabase(this)
val userDB = DatabaseFactory.getLokiUserDatabase(this)
val sskDatabase = DatabaseFactory.getSSKDatabase(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
val sessionResetImpl = SessionResetImplementation(this)
if (userPublicKey != null) {
@ -315,7 +314,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val threadID = thread.threadId
val recipient = thread.recipient
val threadDB = DatabaseFactory.getThreadDatabase(this)
val isClosedGroup = recipient.address.isClosedGroup
val dialogMessage: String
if (recipient.isGroupRecipient) {
val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull()
@ -333,17 +331,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val context = this@HomeActivity as Context
// Send a leave group message if this is an active closed group
if (isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
var isSSKBasedClosedGroup: Boolean
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
var isClosedGroup: Boolean
var groupPublicKey: String?
try {
groupPublicKey = GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)
} catch (e: IOException) {
groupPublicKey = null
isSSKBasedClosedGroup = false
isClosedGroup = false
}
if (isSSKBasedClosedGroup) {
if (isClosedGroup) {
ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!)
} else {
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()

View File

@ -76,8 +76,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
promises.add(privateChatsPromise)
// Closed groups
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
ClosedGroupPoller.configureIfNeeded(context, sskDatabase)
ClosedGroupPoller.configureIfNeeded(context)
promises.addAll(ClosedGroupPoller.shared.pollOnce())
// Open Groups

View File

@ -7,14 +7,14 @@ import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase
import org.session.libsignal.utilities.successBackground
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
import org.session.libsignal.service.loki.api.SnodeAPI
import org.session.libsignal.service.loki.api.SwarmAPI
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
import org.thoughtcrime.securesms.database.DatabaseFactory
class ClosedGroupPoller private constructor(private val context: Context, private val database: SharedSenderKeysDatabase) {
class ClosedGroupPoller private constructor(private val context: Context) {
private var isPolling = false
private val handler: Handler by lazy { Handler() }
@ -32,9 +32,9 @@ class ClosedGroupPoller private constructor(private val context: Context, privat
public lateinit var shared: ClosedGroupPoller
public fun configureIfNeeded(context: Context, sskDatabase: SharedSenderKeysDatabase) {
public fun configureIfNeeded(context: Context) {
if (::shared.isInitialized) { return; }
shared = ClosedGroupPoller(context, sskDatabase)
shared = ClosedGroupPoller(context)
}
}
// endregion
@ -66,7 +66,7 @@ class ClosedGroupPoller private constructor(private val context: Context, privat
// region Private API
private fun poll(): List<Promise<Unit, Exception>> {
if (!isPolling) { return listOf() }
val publicKeys = database.getAllClosedGroupPublicKeys()
val publicKeys = DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
return publicKeys.map { publicKey ->
val promise = SwarmAPI.shared.getSwarm(publicKey).bind { swarm ->
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure

View File

@ -53,7 +53,7 @@ object LokiPushNotificationManager {
}
}
// Unsubscribe from all closed groups
val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
val allClosedGroupPublicKeys = DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
allClosedGroupPublicKeys.forEach { closedGroup ->
performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
@ -84,7 +84,7 @@ object LokiPushNotificationManager {
}
}
// Subscribe to all closed groups
val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
val allClosedGroupPublicKeys = DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
allClosedGroupPublicKeys.forEach { closedGroup ->
performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey)
}

View File

@ -16,6 +16,7 @@ import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsignal.utilities.Hex
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
import java.util.*
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {
@ -454,6 +455,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}.toSet()
}
override fun isClosedGroup(groupPublicKey: String): Boolean {
if (!PublicKeyValidation.isValid(groupPublicKey)) { return false }
return getAllClosedGroupPublicKeys().contains(groupPublicKey)
}
fun removeClosedGroupPublicKey(groupPublicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(closedGroupPublicKeysTable, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey))

View File

@ -1,143 +0,0 @@
package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues
import android.content.Context
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
import org.session.libsignal.utilities.Hex
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.database.DatabaseFactory
class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), SharedSenderKeysDatabaseProtocol {
companion object {
// Shared
public val closedGroupPublicKey = "closed_group_public_key"
// Ratchets
private val oldClosedGroupRatchetTable = "old_closed_group_ratchet_table"
private val currentClosedGroupRatchetTable = "closed_group_ratchet_table"
private val senderPublicKey = "sender_public_key"
private val chainKey = "chain_key"
private val keyIndex = "key_index"
private val messageKeys = "message_keys"
@JvmStatic val createOldClosedGroupRatchetTableCommand
= "CREATE TABLE $oldClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
// Private keys
@JvmStatic val createCurrentClosedGroupRatchetTableCommand
= "CREATE TABLE $currentClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
// Private keys
public val closedGroupPrivateKeyTable = "closed_group_private_key_table"
public val closedGroupPrivateKey = "closed_group_private_key"
@JvmStatic val createClosedGroupPrivateKeyTableCommand
= "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);"
}
private fun getTable(collection: ClosedGroupRatchetCollectionType): String {
return when (collection) {
ClosedGroupRatchetCollectionType.Old -> oldClosedGroupRatchetTable
ClosedGroupRatchetCollectionType.Current -> currentClosedGroupRatchetTable
}
}
// region Ratchets & Sender Keys
override fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, collection: ClosedGroupRatchetCollectionType): ClosedGroupRatchet? {
val database = databaseHelper.readableDatabase
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?"
return database.get(getTable(collection), query, arrayOf( groupPublicKey, senderPublicKey )) { cursor ->
val chainKey = cursor.getString(Companion.chainKey)
val keyIndex = cursor.getInt(Companion.keyIndex)
val messageKeys = cursor.getString(Companion.messageKeys).split("-")
ClosedGroupRatchet(chainKey, keyIndex, messageKeys)
}
}
override fun setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, collection: ClosedGroupRatchetCollectionType) {
val database = databaseHelper.writableDatabase
val values = ContentValues()
values.put(Companion.closedGroupPublicKey, groupPublicKey)
values.put(Companion.senderPublicKey, senderPublicKey)
values.put(Companion.chainKey, ratchet.chainKey)
values.put(Companion.keyIndex, ratchet.keyIndex)
values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-"))
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?"
database.insertOrUpdate(getTable(collection), values, query, arrayOf( groupPublicKey, senderPublicKey ))
}
override fun removeAllClosedGroupRatchets(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType) {
val database = databaseHelper.writableDatabase
val query = "${Companion.closedGroupPublicKey} = ?"
database.delete(getTable(collection), query, arrayOf( groupPublicKey ))
}
override fun getAllClosedGroupRatchets(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType): Set<Pair<String, ClosedGroupRatchet>> {
val database = databaseHelper.readableDatabase
val query = "${Companion.closedGroupPublicKey} = ?"
return database.getAll(getTable(collection), query, arrayOf( groupPublicKey )) { cursor ->
val chainKey = cursor.getString(Companion.chainKey)
val keyIndex = cursor.getInt(Companion.keyIndex)
val messageKeys = cursor.getString(Companion.messageKeys).split("-")
val senderPublicKey = cursor.getString(Companion.senderPublicKey)
val ratchet = ClosedGroupRatchet(chainKey, keyIndex, messageKeys)
Pair(senderPublicKey, ratchet)
}.toSet()
}
override fun getAllClosedGroupSenderKeys(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType): Set<ClosedGroupSenderKey> {
return getAllClosedGroupRatchets(groupPublicKey, collection).map { pair ->
val senderPublicKey = pair.first
val ratchet = pair.second
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(senderPublicKey))
}.toSet()
}
// endregion
// region Public & Private Keys
override fun getClosedGroupPrivateKey(groupPublicKey: String): String? {
val database = databaseHelper.readableDatabase
val query = "${Companion.closedGroupPublicKey} = ?"
return database.get(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey )) { cursor ->
cursor.getString(Companion.closedGroupPrivateKey)
}
}
override fun setClosedGroupPrivateKey(groupPublicKey: String, groupPrivateKey: String) {
val database = databaseHelper.writableDatabase
val values = ContentValues()
values.put(Companion.closedGroupPublicKey, groupPublicKey)
values.put(Companion.closedGroupPrivateKey, groupPrivateKey)
val query = "${Companion.closedGroupPublicKey} = ?"
database.insertOrUpdate(closedGroupPrivateKeyTable, values, query, arrayOf( groupPublicKey ))
}
override fun removeClosedGroupPrivateKey(groupPublicKey: String) {
val database = databaseHelper.writableDatabase
val query = "${Companion.closedGroupPublicKey} = ?"
database.delete(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey ))
}
override fun getAllClosedGroupPublicKeys(): Set<String> {
val database = databaseHelper.readableDatabase
val result = mutableSetOf<String>()
result.addAll(database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
cursor.getString(Companion.closedGroupPublicKey)
}.filter {
PublicKeyValidation.isValid(it)
})
result.addAll(DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys())
return result
}
// endregion
override fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean {
if (!PublicKeyValidation.isValid(groupPublicKey)) { return false }
return getAllClosedGroupPublicKeys().contains(groupPublicKey)
}
// endregion
}

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.loki.protocol
import android.content.ContentValues
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase
import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getAll
import org.thoughtcrime.securesms.loki.utilities.getString
@ -18,17 +17,39 @@ import java.util.*
object ClosedGroupsMigration {
fun perform(database: net.sqlcipher.database.SQLiteDatabase) {
val publicKeys = database.getAll(SharedSenderKeysDatabase.closedGroupPrivateKeyTable, null, null) { cursor ->
cursor.getString(SharedSenderKeysDatabase.closedGroupPublicKey)
public val closedGroupPublicKey = "closed_group_public_key"
// Ratchets
private val oldClosedGroupRatchetTable = "old_closed_group_ratchet_table"
private val currentClosedGroupRatchetTable = "closed_group_ratchet_table"
private val senderPublicKey = "sender_public_key"
private val chainKey = "chain_key"
private val keyIndex = "key_index"
private val messageKeys = "message_keys"
@JvmStatic val createOldClosedGroupRatchetTableCommand
= "CREATE TABLE $oldClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
// Private keys
@JvmStatic val createCurrentClosedGroupRatchetTableCommand
= "CREATE TABLE $currentClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
// Private keys
public val closedGroupPrivateKeyTable = "closed_group_private_key_table"
public val closedGroupPrivateKey = "closed_group_private_key"
@JvmStatic val createClosedGroupPrivateKeyTableCommand
= "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);"
fun perform(database: net.sqlcipher.database.SQLiteDatabase) {
val publicKeys = database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
cursor.getString(closedGroupPublicKey)
}.filter {
PublicKeyValidation.isValid(it)
}
val keyPairs = mutableListOf<ECKeyPair>()
for (publicKey in publicKeys) {
val query = "${SharedSenderKeysDatabase.closedGroupPublicKey} = ?"
val privateKey = database.get(SharedSenderKeysDatabase.closedGroupPrivateKeyTable, query, arrayOf( publicKey )) { cursor ->
cursor.getString(SharedSenderKeysDatabase.closedGroupPrivateKey)
val query = "${closedGroupPublicKey} = ?"
val privateKey = database.get(closedGroupPrivateKeyTable, query, arrayOf( publicKey )) { cursor ->
cursor.getString(closedGroupPrivateKey)
}
val keyPair = ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(publicKey.removing05PrefixIfNeeded())), DjbECPrivateKey(Hex.fromStringCondensed(privateKey)))
keyPairs.add(keyPair)

View File

@ -2,16 +2,11 @@ package org.session.libsession.messaging
import android.content.Context
import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.libsignal.loki.SessionResetProtocol
import org.session.libsignal.libsignal.state.*
import org.session.libsignal.metadata.certificate.CertificateValidator
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
class MessagingConfiguration(
val context: Context,
val storage: StorageProtocol,
val sskDatabase: SharedSenderKeysDatabaseProtocol,
val messageDataProvider: MessageDataProvider,
val sessionProtocol: SessionProtocol)
{
@ -20,12 +15,11 @@ class MessagingConfiguration(
fun configure(context: Context,
storage: StorageProtocol,
sskDatabase: SharedSenderKeysDatabaseProtocol,
messageDataProvider: MessageDataProvider,
sessionProtocol: SessionProtocol
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingConfiguration(context, storage, sskDatabase, messageDataProvider, sessionProtocol)
shared = MessagingConfiguration(context, storage, messageDataProvider, sessionProtocol)
}
}
}

View File

@ -47,7 +47,7 @@ object PushNotificationAPI {
}
}
// Unsubscribe from all closed groups
val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
val allClosedGroupPublicKeys = MessagingConfiguration.shared.storage.getAllClosedGroupPublicKeys()
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()!!
allClosedGroupPublicKeys.forEach { closedGroup ->
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
@ -77,7 +77,7 @@ object PushNotificationAPI {
}
}
// Subscribe to all closed groups
val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
val allClosedGroupPublicKeys = MessagingConfiguration.shared.storage.getAllClosedGroupPublicKeys()
allClosedGroupPublicKeys.forEach { closedGroup ->
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
}

View File

@ -61,7 +61,7 @@ class ClosedGroupPoller {
// region Private API
private fun poll(): List<Promise<Unit, Exception>> {
if (!isPolling) { return listOf() }
val publicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
val publicKeys = MessagingConfiguration.shared.storage.getAllClosedGroupPublicKeys()
return publicKeys.map { publicKey ->
val promise = SnodeAPI.getSwarm(publicKey).bind { swarm ->
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure

View File

@ -75,7 +75,6 @@ import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol
import org.session.libsignal.service.loki.database.LokiPreKeyBundleDatabaseProtocol;
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol;
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol;
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol;
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities;
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
import org.session.libsignal.service.loki.utilities.Broadcaster;
@ -121,7 +120,6 @@ public class SignalServiceMessageSender {
// Loki
private final String userPublicKey;
private final LokiAPIDatabaseProtocol apiDatabase;
private final SharedSenderKeysDatabaseProtocol sskDatabase;
private final LokiThreadDatabaseProtocol threadDatabase;
private final LokiMessageDatabaseProtocol messageDatabase;
private final LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase;
@ -151,7 +149,6 @@ public class SignalServiceMessageSender {
Optional<EventListener> eventListener,
String userPublicKey,
LokiAPIDatabaseProtocol apiDatabase,
SharedSenderKeysDatabaseProtocol sskDatabase,
LokiThreadDatabaseProtocol threadDatabase,
LokiMessageDatabaseProtocol messageDatabase,
LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase,
@ -161,7 +158,7 @@ public class SignalServiceMessageSender {
LokiOpenGroupDatabaseProtocol openGroupDatabase,
Broadcaster broadcaster)
{
this(urls, new StaticCredentialsProvider(user, password, null), store, userAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, userPublicKey, apiDatabase, sskDatabase, threadDatabase, messageDatabase, preKeyBundleDatabase, sessionProtocolImpl, sessionResetImpl, userDatabase, openGroupDatabase, broadcaster);
this(urls, new StaticCredentialsProvider(user, password, null), store, userAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, userPublicKey, apiDatabase, threadDatabase, messageDatabase, preKeyBundleDatabase, sessionProtocolImpl, sessionResetImpl, userDatabase, openGroupDatabase, broadcaster);
}
public SignalServiceMessageSender(SignalServiceConfiguration urls,
@ -174,7 +171,6 @@ public class SignalServiceMessageSender {
Optional<EventListener> eventListener,
String userPublicKey,
LokiAPIDatabaseProtocol apiDatabase,
SharedSenderKeysDatabaseProtocol sskDatabase,
LokiThreadDatabaseProtocol threadDatabase,
LokiMessageDatabaseProtocol messageDatabase,
LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase,
@ -193,7 +189,6 @@ public class SignalServiceMessageSender {
this.eventListener = eventListener;
this.userPublicKey = userPublicKey;
this.apiDatabase = apiDatabase;
this.sskDatabase = sskDatabase;
this.threadDatabase = threadDatabase;
this.messageDatabase = messageDatabase;
this.preKeyBundleDatabase = preKeyBundleDatabase;
@ -1181,9 +1176,9 @@ public class SignalServiceMessageSender {
PushTransportDetails transportDetails = new PushTransportDetails(3);
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
boolean isSSKBasedClosedGroup = sskDatabase.isSSKBasedClosedGroup(publicKey);
boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey);
String encryptionPublicKey;
if (isSSKBasedClosedGroup) {
if (isClosedGroup) {
ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey);
encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair);
} else {
@ -1191,7 +1186,7 @@ public class SignalServiceMessageSender {
}
byte[] ciphertext = sessionProtocolImpl.encrypt(transportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
String body = Base64.encodeBytes(ciphertext);
int type = isSSKBasedClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
OutgoingPushMessage message = new OutgoingPushMessage(type, 1, 0, body);
messages.add(message);

View File

@ -88,8 +88,6 @@ import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupUtilities;
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol;
import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage;
import java.io.ByteArrayInputStream;
@ -115,7 +113,6 @@ public class SignalServiceCipher {
private final SignalProtocolStore signalProtocolStore;
private final SessionResetProtocol sessionResetProtocol;
private final SharedSenderKeysDatabaseProtocol sskDatabase;
private final SignalServiceAddress localAddress;
private final SessionProtocol sessionProtocolImpl;
private final LokiAPIDatabaseProtocol apiDB;
@ -123,7 +120,6 @@ public class SignalServiceCipher {
public SignalServiceCipher(SignalServiceAddress localAddress,
SignalProtocolStore signalProtocolStore,
SharedSenderKeysDatabaseProtocol sskDatabase,
SessionResetProtocol sessionResetProtocol,
SessionProtocol sessionProtocolImpl,
LokiAPIDatabaseProtocol apiDB,
@ -131,56 +127,55 @@ public class SignalServiceCipher {
{
this.signalProtocolStore = signalProtocolStore;
this.sessionResetProtocol = sessionResetProtocol;
this.sskDatabase = sskDatabase;
this.localAddress = localAddress;
this.sessionProtocolImpl = sessionProtocolImpl;
this.apiDB = apiDB;
this.certificateValidator = certificateValidator;
}
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess,
byte[] unpaddedMessage)
throws UntrustedIdentityException, InvalidKeyException, IOException
{
if (unidentifiedAccess.isPresent() && sskDatabase.isSSKBasedClosedGroup(destination.getName())) {
String userPublicKey = localAddress.getNumber();
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(userPublicKey, 1);
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, signalProtocolAddress);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
byte[] plaintext = transportDetails.getPaddedMessageBody(unpaddedMessage);
byte[] ciphertext = ClosedGroupUtilities.encrypt(plaintext, destination.getName(), userPublicKey);
String body = Base64.encodeBytes(ciphertext);
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
return new OutgoingPushMessage(Type.CLOSED_GROUP_CIPHERTEXT_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
} else if (unidentifiedAccess.isPresent()) {
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, new SignalProtocolAddress(localAddress.getNumber(), 1));
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
String body = Base64.encodeBytes(ciphertext);
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
} else {
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
String body = Base64.encodeBytes(message.serialize());
int type;
switch (message.getType()) {
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
case CiphertextMessage.FALLBACK_MESSAGE_TYPE: type = Type.FALLBACK_MESSAGE_VALUE; break;
case CiphertextMessage.CLOSED_GROUP_CIPHERTEXT: type = Type.CLOSED_GROUP_CIPHERTEXT_VALUE; break;
default: throw new AssertionError("Bad type: " + message.getType());
}
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
}
}
// public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
// Optional<UnidentifiedAccess> unidentifiedAccess,
// byte[] unpaddedMessage)
// throws UntrustedIdentityException, InvalidKeyException, IOException
// {
// if (unidentifiedAccess.isPresent() && sskDatabase.isSSKBasedClosedGroup(destination.getName())) {
// String userPublicKey = localAddress.getNumber();
// SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(userPublicKey, 1);
// SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, signalProtocolAddress);
// PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
// byte[] plaintext = transportDetails.getPaddedMessageBody(unpaddedMessage);
// byte[] ciphertext = ClosedGroupUtilities.encrypt(plaintext, destination.getName(), userPublicKey);
// String body = Base64.encodeBytes(ciphertext);
// int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
// return new OutgoingPushMessage(Type.CLOSED_GROUP_CIPHERTEXT_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
// } else if (unidentifiedAccess.isPresent()) {
// SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, new SignalProtocolAddress(localAddress.getNumber(), 1));
// PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
// byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
// String body = Base64.encodeBytes(ciphertext);
// int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
//
// return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
// } else {
// SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
// PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
// CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
// int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
// String body = Base64.encodeBytes(message.serialize());
//
// int type;
//
// switch (message.getType()) {
// case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
// case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
// case CiphertextMessage.FALLBACK_MESSAGE_TYPE: type = Type.FALLBACK_MESSAGE_VALUE; break;
// case CiphertextMessage.CLOSED_GROUP_CIPHERTEXT: type = Type.CLOSED_GROUP_CIPHERTEXT_VALUE; break;
// default: throw new AssertionError("Bad type: " + message.getType());
// }
//
// return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
// }
// }
/**
* Decrypt a received {@link SignalServiceEnvelope}

View File

@ -11,9 +11,8 @@ import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.PushTransportDetails
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
class LokiServiceCipher(localAddress: SignalServiceAddress, private val signalProtocolStore: SignalProtocolStore, private val sskDatabase: SharedSenderKeysDatabaseProtocol, sessionProtocolImpl: SessionProtocol, sessionResetProtocol: SessionResetProtocol, apiDB: LokiAPIDatabaseProtocol, certificateValidator: CertificateValidator?) : SignalServiceCipher(localAddress, signalProtocolStore, sskDatabase, sessionResetProtocol, sessionProtocolImpl, apiDB, certificateValidator) {
class LokiServiceCipher(localAddress: SignalServiceAddress, private val signalProtocolStore: SignalProtocolStore, sessionProtocolImpl: SessionProtocol, sessionResetProtocol: SessionResetProtocol, apiDB: LokiAPIDatabaseProtocol, certificateValidator: CertificateValidator?) : SignalServiceCipher(localAddress, signalProtocolStore, sessionResetProtocol, sessionProtocolImpl, apiDB, certificateValidator) {
private val userPrivateKey get() = signalProtocolStore.identityKeyPair.privateKey.serialize()

View File

@ -37,4 +37,5 @@ interface LokiAPIDatabaseProtocol {
fun getUserX25519KeyPair(): ECKeyPair
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): List<ECKeyPair>
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
fun isClosedGroup(groupPublicKey: String): Boolean
}

View File

@ -1,22 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
import org.session.libsignal.service.loki.utilities.prettifiedDescription
public class ClosedGroupRatchet(public val chainKey: String, public val keyIndex: Int, public val messageKeys: List<String>) {
override fun equals(other: Any?): Boolean {
return if (other is ClosedGroupRatchet) {
chainKey == other.chainKey && keyIndex == other.keyIndex && messageKeys == other.messageKeys
} else {
false
}
}
override fun hashCode(): Int {
return chainKey.hashCode() xor keyIndex.hashCode() xor messageKeys.hashCode()
}
override fun toString(): String {
return "[ chainKey : $chainKey, keyIndex : $keyIndex, messageKeys : ${messageKeys.prettifiedDescription()} ]"
}
}

View File

@ -1,56 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
import com.google.protobuf.ByteString
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Hex
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.utilities.toHexString
public class ClosedGroupSenderKey(public val chainKey: ByteArray, public val keyIndex: Int, public val publicKey: ByteArray) {
companion object {
public fun fromJSON(jsonAsString: String): ClosedGroupSenderKey? {
try {
val json = JsonUtil.fromJson(jsonAsString, Map::class.java)
val chainKey = Hex.fromStringCondensed(json["chainKey"] as String)
val keyIndex = json["keyIndex"] as Int
val publicKey = Hex.fromStringCondensed(json["publicKey"] as String)
return ClosedGroupSenderKey(chainKey, keyIndex, publicKey)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't parse closed group sender key from: $jsonAsString.")
return null
}
}
}
public fun toJSON(): String {
val json = mapOf( "chainKey" to chainKey.toHexString(), "keyIndex" to keyIndex, "publicKey" to publicKey.toHexString() )
return JsonUtil.toJson(json)
}
public fun toProto(): SignalServiceProtos.ClosedGroupUpdate.SenderKey {
val builder = SignalServiceProtos.ClosedGroupUpdate.SenderKey.newBuilder()
builder.chainKey = ByteString.copyFrom(chainKey)
builder.keyIndex = keyIndex
builder.publicKey = ByteString.copyFrom(publicKey)
return builder.build()
}
override fun equals(other: Any?): Boolean {
return if (other is ClosedGroupSenderKey) {
chainKey.contentEquals(other.chainKey) && keyIndex == other.keyIndex && publicKey.contentEquals(other.publicKey)
} else {
false
}
}
override fun hashCode(): Int {
return chainKey.hashCode() xor keyIndex.hashCode() xor publicKey.hashCode()
}
override fun toString(): String {
return "[ chainKey : ${chainKey.toHexString()}, keyIndex : $keyIndex, messageKeys : ${publicKey.toHexString()} ]"
}
}

View File

@ -1,78 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
import com.google.protobuf.ByteString
import org.whispersystems.curve25519.Curve25519
import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage
import org.session.libsignal.utilities.Hex
import org.session.libsignal.libsignal.util.Pair
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.utilities.DecryptionUtilities
import org.session.libsignal.service.loki.api.utilities.EncryptionUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
public object ClosedGroupUtilities {
sealed class Error(val description: String) : Exception() {
object InvalidGroupPublicKey : Error("Invalid group public key.")
object NoData : Error("Received an empty envelope.")
object NoGroupPrivateKey : Error("Missing group private key.")
object ParsingFailed : Error("Couldn't parse closed group ciphertext message.")
}
@JvmStatic
public fun encrypt(data: ByteArray, groupPublicKey: String, userPublicKey: String): ByteArray {
// 1. ) Encrypt the data with the user's sender key
val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(data, groupPublicKey, userPublicKey)
val ivAndCiphertext = ciphertextAndKeyIndex.first
val keyIndex = ciphertextAndKeyIndex.second
val x0 = ClosedGroupCiphertextMessage(ivAndCiphertext, Hex.fromStringCondensed(userPublicKey), keyIndex);
// 2. ) Encrypt the result for the group's public key to hide the sender public key and key index
val x1 = EncryptionUtilities.encryptForX25519PublicKey(x0.serialize(), groupPublicKey.removing05PrefixIfNeeded())
// 3. ) Wrap the result
return SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.newBuilder()
.setCiphertext(ByteString.copyFrom(x1.ciphertext))
.setEphemeralPublicKey(ByteString.copyFrom(x1.ephemeralPublicKey))
.build().toByteArray()
}
@JvmStatic
public fun decrypt(envelope: SignalServiceEnvelope): Pair<ByteArray, String> {
// 1. ) Check preconditions
val groupPublicKey = envelope.source
if (groupPublicKey == null || !SharedSenderKeysImplementation.shared.isClosedGroup(groupPublicKey)) {
throw Error.InvalidGroupPublicKey
}
val data = envelope.content
if (data.count() == 0) {
throw Error.NoData
}
val groupPrivateKey = SharedSenderKeysImplementation.shared.getKeyPair(groupPublicKey)?.privateKey?.serialize()
if (groupPrivateKey == null) {
throw Error.NoGroupPrivateKey
}
// 2. ) Parse the wrapper
val x0 = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data)
val ivAndCiphertext = x0.ciphertext.toByteArray()
val ephemeralPublicKey = x0.ephemeralPublicKey.toByteArray()
// 3. ) Decrypt the data inside
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(ephemeralPublicKey, groupPrivateKey)
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
val symmetricKey = mac.doFinal(ephemeralSharedSecret)
val x1 = DecryptionUtilities.decryptUsingAESGCM(ivAndCiphertext, symmetricKey)
// 4. ) Parse the closed group ciphertext message
val x2 = ClosedGroupCiphertextMessage.from(x1)
if (x2 == null) {
throw Error.ParsingFailed
}
val senderPublicKey = x2.senderPublicKey.toHexString()
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
val plaintext = SharedSenderKeysImplementation.shared.decrypt(x2.ivAndCiphertext, groupPublicKey, senderPublicKey, x2.keyIndex)
// 6. ) Return
return Pair(plaintext, senderPublicKey)
}
}

View File

@ -1,25 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
enum class ClosedGroupRatchetCollectionType { Old, Current }
interface SharedSenderKeysDatabaseProtocol {
// region Ratchets & Sender Keys
fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, collection: ClosedGroupRatchetCollectionType): ClosedGroupRatchet?
fun setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, collection: ClosedGroupRatchetCollectionType)
fun removeAllClosedGroupRatchets(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType)
fun getAllClosedGroupRatchets(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType): Set<Pair<String, ClosedGroupRatchet>>
fun getAllClosedGroupSenderKeys(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType): Set<ClosedGroupSenderKey>
// endregion
// region Private & Public Keys
fun getClosedGroupPrivateKey(groupPublicKey: String): String?
fun setClosedGroupPrivateKey(groupPublicKey: String, groupPrivateKey: String)
fun removeClosedGroupPrivateKey(groupPublicKey: String)
fun getAllClosedGroupPublicKeys(): Set<String>
// endregion
// region Convenience
fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean
// endregion
}

View File

@ -1,217 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.libsignal.util.ByteUtil
import org.session.libsignal.utilities.Hex
import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.api.utilities.EncryptionUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
public final class SharedSenderKeysImplementation(private val database: SharedSenderKeysDatabaseProtocol, private val delegate: SharedSenderKeysImplementationDelegate) {
private val gcmTagSize = 128
private val ivSize = 12
// A quick overview of how shared sender key based closed groups work:
//
// • When a user creates a group, they generate a key pair for the group along with a ratchet for
// every member of the group. They bundle this together with some other group info such as the group
// name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of
// the group. Note that because a user can only pick from their existing contacts when selecting
// the group members they shouldn't need to establish sessions before being able to send the
// `ClosedGroupUpdateMessage`.
// • After the group is created, every user polls for the public key associated with the group.
// • Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all
// other members of the group they don't yet have a session with for reasons outlined below.
// • When a user sends a message they step their ratchet and use the resulting message key to encrypt
// the message.
// • When another user receives that message, they step the ratchet associated with the sender and
// use the resulting message key to decrypt the message.
// • When a user leaves or is kicked from a group, all members must generate new ratchets to ensure that
// removed users can't decrypt messages going forward. To this end every user deletes all ratchets
// associated with the group in question upon receiving a group update message that indicates that
// a user left. They then generate a new ratchet for themselves and send it out to all members of
// the group. The user should already have established sessions with all other members at this point
// because of the behavior outlined a few points above.
// • When a user adds a new member to the group, they generate a ratchet for that new member and
// send that bundled in a `ClosedGroupUpdateMessage` to the group. They send a
// `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of
// every other member of the group to the user that joined.
// region Initialization
companion object {
public lateinit var shared: SharedSenderKeysImplementation
public fun configureIfNeeded(database: SharedSenderKeysDatabaseProtocol, delegate: SharedSenderKeysImplementationDelegate) {
if (::shared.isInitialized) { return; }
shared = SharedSenderKeysImplementation(database, delegate)
}
}
// endregion
// region Error
public class LoadingFailed(val groupPublicKey: String, val senderPublicKey: String)
: Exception("Couldn't get ratchet for closed group with public key: $groupPublicKey, sender public key: $senderPublicKey.")
public class MessageKeyMissing(val targetKeyIndex: Int, val groupPublicKey: String, val senderPublicKey: String)
: Exception("Couldn't find message key for old key index: $targetKeyIndex, public key: $groupPublicKey, sender public key: $senderPublicKey.")
public class GenericRatchetingException : Exception("An error occurred.")
// endregion
// region Private API
private fun hmac(key: ByteArray, input: ByteArray): ByteArray {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(key, "HmacSHA256"))
return mac.doFinal(input)
}
private fun step(ratchet: ClosedGroupRatchet): ClosedGroupRatchet {
val nextMessageKey = hmac(Hex.fromStringCondensed(ratchet.chainKey), ByteArray(1) { 1.toByte() })
val nextChainKey = hmac(Hex.fromStringCondensed(ratchet.chainKey), ByteArray(1) { 2.toByte() })
val nextKeyIndex = ratchet.keyIndex + 1
val messageKeys = ratchet.messageKeys + listOf( nextMessageKey.toHexString() )
return ClosedGroupRatchet(nextChainKey.toHexString(), nextKeyIndex, messageKeys)
}
/**
* Sync. Don't call from the main thread.
*/
private fun stepRatchetOnce(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet {
val ratchet = database.getClosedGroupRatchet(groupPublicKey, senderPublicKey, ClosedGroupRatchetCollectionType.Current)
if (ratchet == null) {
val exception = LoadingFailed(groupPublicKey, senderPublicKey)
Log.d("Loki", exception.message ?: "An error occurred.")
throw exception
}
try {
val result = step(ratchet)
database.setClosedGroupRatchet(groupPublicKey, senderPublicKey, result, ClosedGroupRatchetCollectionType.Current)
return result
} catch (exception: Exception) {
Log.d("Loki", "Couldn't step ratchet due to error: $exception.")
throw exception
}
}
private fun stepRatchet(groupPublicKey: String, senderPublicKey: String, targetKeyIndex: Int, isRetry: Boolean = false): ClosedGroupRatchet {
val collection = if (isRetry) ClosedGroupRatchetCollectionType.Old else ClosedGroupRatchetCollectionType.Current
val ratchet = database.getClosedGroupRatchet(groupPublicKey, senderPublicKey, collection)
if (ratchet == null) {
val exception = LoadingFailed(groupPublicKey, senderPublicKey)
Log.d("Loki", exception.message ?: "An error occurred.")
throw exception
}
if (targetKeyIndex < ratchet.keyIndex) {
// There's no need to advance the ratchet if this is invoked for an old key index
if (ratchet.messageKeys.count() <= targetKeyIndex) {
val exception = MessageKeyMissing(targetKeyIndex, groupPublicKey, senderPublicKey)
Log.d("Loki", exception.message ?: "An error occurred.")
throw exception
}
return ratchet
} else {
var currentKeyIndex = ratchet.keyIndex
var result: ClosedGroupRatchet = ratchet // Explicitly typed because otherwise the compiler has trouble inferring that this can't be null
while (currentKeyIndex < targetKeyIndex) {
try {
result = step(result)
currentKeyIndex = result.keyIndex
} catch (exception: Exception) {
Log.d("Loki", "Couldn't step ratchet due to error: $exception.")
throw exception
}
}
val collection = if (isRetry) ClosedGroupRatchetCollectionType.Old else ClosedGroupRatchetCollectionType.Current
database.setClosedGroupRatchet(groupPublicKey, senderPublicKey, result, collection)
return result
}
}
// endregion
// region Public API
public fun generateRatchet(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet {
val rootChainKey = Util.getSecretBytes(32).toHexString()
val ratchet = ClosedGroupRatchet(rootChainKey, 0, listOf())
database.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, ClosedGroupRatchetCollectionType.Current)
return ratchet
}
public fun encrypt(plaintext: ByteArray, groupPublicKey: String, senderPublicKey: String): Pair<ByteArray, Int> {
val ratchet: ClosedGroupRatchet
try {
ratchet = stepRatchetOnce(groupPublicKey, senderPublicKey)
} catch (exception: Exception) {
if (exception is LoadingFailed) {
delegate.requestSenderKey(groupPublicKey, senderPublicKey)
}
throw exception
}
val iv = Util.getSecretBytes(ivSize)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val messageKey = ratchet.messageKeys.last()
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(Hex.fromStringCondensed(messageKey), "AES"), GCMParameterSpec(gcmTagSize, iv))
return Pair(ByteUtil.combine(iv, cipher.doFinal(plaintext)), ratchet.keyIndex)
}
public fun decrypt(ivAndCiphertext: ByteArray, groupPublicKey: String, senderPublicKey: String, keyIndex: Int, isRetry: Boolean = false): ByteArray {
val ratchet: ClosedGroupRatchet
try {
ratchet = stepRatchet(groupPublicKey, senderPublicKey, keyIndex, isRetry)
} catch (exception: Exception) {
if (!isRetry) {
return decrypt(ivAndCiphertext, groupPublicKey, senderPublicKey, keyIndex, true)
} else {
if (exception is LoadingFailed) {
delegate.requestSenderKey(groupPublicKey, senderPublicKey)
}
throw exception
}
}
val iv = ivAndCiphertext.sliceArray(0 until ivSize)
val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count())
val messageKeys = ratchet.messageKeys
val lastNMessageKeys: List<String>
if (messageKeys.count() > 16) { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order
lastNMessageKeys = messageKeys.subList(messageKeys.lastIndex - 16, messageKeys.lastIndex)
} else {
lastNMessageKeys = messageKeys
}
if (lastNMessageKeys.isEmpty()) {
throw MessageKeyMissing(keyIndex, groupPublicKey, senderPublicKey)
}
var exception: Exception? = null
for (messageKey in lastNMessageKeys.reversed()) { // Reversed because most likely the last one is the one we need
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(Hex.fromStringCondensed(messageKey), "AES"), GCMParameterSpec(EncryptionUtilities.gcmTagSize, iv))
try {
return cipher.doFinal(ciphertext)
} catch (e: Exception) {
exception = e
}
}
if (!isRetry) {
return decrypt(ivAndCiphertext, groupPublicKey, senderPublicKey, keyIndex, true)
} else {
delegate.requestSenderKey(groupPublicKey, senderPublicKey)
throw exception ?: GenericRatchetingException()
}
}
public fun isClosedGroup(publicKey: String): Boolean {
return database.getAllClosedGroupPublicKeys().contains(publicKey)
}
public fun getKeyPair(groupPublicKey: String): ECKeyPair? {
val privateKey = database.getClosedGroupPrivateKey(groupPublicKey) ?: return null
return ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(groupPublicKey.removing05PrefixIfNeeded())),
DjbECPrivateKey(Hex.fromStringCondensed(privateKey)))
}
// endregion
}

View File

@ -1,6 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
public interface SharedSenderKeysImplementationDelegate {
public fun requestSenderKey(groupPublicKey: String, senderPublicKey: String)
}

View File

@ -3,23 +3,20 @@ package org.session.libsignal.service.loki.protocol.sessionmanagement
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.libsignal.loki.SessionResetProtocol
import org.session.libsignal.libsignal.loki.SessionResetStatus
import org.session.libsignal.libsignal.state.SignalProtocolStore
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.SignalServiceMessageSender
import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
public class SessionManagementProtocol(private val sessionResetImpl: SessionResetProtocol, private val sskDatabase: SharedSenderKeysDatabaseProtocol,
private val delegate: SessionManagementProtocolDelegate) {
public class SessionManagementProtocol(private val sessionResetImpl: SessionResetProtocol, private val delegate: SessionManagementProtocolDelegate) {
// region Initialization
companion object {
public lateinit var shared: SessionManagementProtocol
public fun configureIfNeeded(sessionResetImpl: SessionResetProtocol, sskDatabase: SharedSenderKeysDatabaseProtocol, delegate: SessionManagementProtocolDelegate) {
public fun configureIfNeeded(sessionResetImpl: SessionResetProtocol, delegate: SessionManagementProtocolDelegate) {
if (::shared.isInitialized) { return; }
shared = SessionManagementProtocol(sessionResetImpl, sskDatabase, delegate)
shared = SessionManagementProtocol(sessionResetImpl, delegate)
}
}
// endregion