mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
remove shared sender keys
This commit is contained in:
parent
568fddf91d
commit
9d0831b874
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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))) {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()} ]"
|
||||
}
|
||||
}
|
@ -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()} ]"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
public interface SharedSenderKeysImplementationDelegate {
|
||||
|
||||
public fun requestSenderKey(groupPublicKey: String, senderPublicKey: String)
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user