Merge branch 'dev' into open-group-invitations

This commit is contained in:
Niels Andriesse 2021-05-17 11:42:27 +10:00
commit 11e223f5d8
57 changed files with 716 additions and 633 deletions

View File

@ -158,8 +158,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 162 def canonicalVersionCode = 163
def canonicalVersionName = "1.10.3" def canonicalVersionName = "1.10.4"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -254,6 +254,10 @@
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" /> android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
</activity> </activity>
<activity
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight"/>
<activity <activity
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity" android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@ -42,6 +42,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.snode.SnodeModule; import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.IdentityKeyUtil; import org.session.libsession.utilities.IdentityKeyUtil;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
@ -50,6 +51,7 @@ import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsignal.service.api.util.StreamDetails; import org.session.libsignal.service.api.util.StreamDetails;
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import org.session.libsignal.utilities.ThreadUtils;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
@ -68,7 +70,6 @@ import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker; import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager; import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
import org.thoughtcrime.securesms.loki.api.PublicChatManager; import org.thoughtcrime.securesms.loki.api.PublicChatManager;
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
@ -91,8 +92,10 @@ import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.voiceengine.WebRtcAudioManager; import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils; import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.util.Date; import java.util.Date;
@ -101,6 +104,7 @@ import java.util.Set;
import dagger.ObjectGraph; import dagger.ObjectGraph;
import kotlin.Unit; import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlinx.coroutines.Job; import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
@ -173,8 +177,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this);
MessagingModuleConfiguration.Companion.configure(this, MessagingModuleConfiguration.Companion.configure(this,
DatabaseFactory.getStorage(this), DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this), DatabaseFactory.getAttachmentProvider(this));
new SessionProtocolImpl(this));
SnodeModule.Companion.configure(apiDB, broadcaster); SnodeModule.Companion.configure(apiDB, broadcaster);
if (userPublicKey != null) { if (userPublicKey != null) {
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB); MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
@ -481,21 +484,31 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
} }
private void resubmitProfilePictureIfNeeded() { private void resubmitProfilePictureIfNeeded() {
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
// at a certain interval to ensure it's always available.
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return; if (userPublicKey == null) return;
long now = new Date().getTime(); long now = new Date().getTime();
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this); long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return; if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
AsyncTask.execute(() -> { ThreadUtils.queue(() -> {
String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this); // Don't generate a new profile key here; we do that when the user changes their profile picture
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey); String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
try { try {
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey)); // Read the file into a byte array
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length()); InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> { ByteArrayOutputStream baos = new ByteArrayOutputStream();
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime()); int count;
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt()); byte[] buffer = new byte[1024];
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey); while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) {
baos.write(buffer, 0, count);
}
baos.flush();
byte[] profilePicture = baos.toByteArray();
// Re-upload it
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
// Update the last profile picture upload date
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
return Unit.INSTANCE; return Unit.INSTANCE;
}); });
} catch (Exception exception) { } catch (Exception exception) {

View File

@ -108,28 +108,28 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return null // TODO: Implement return null // TODO: Implement
} }
override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
val database = DatabaseFactory.getAttachmentDatabase(context) val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id, val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
attachmentStream.contentType, attachmentStream.contentType,
attachmentKey, attachmentKey,
Optional.of(Util.toIntExact(attachmentStream.length)), Optional.of(Util.toIntExact(attachmentStream.length)),
attachmentStream.preview, attachmentStream.preview,
attachmentStream.width, attachmentStream.height, attachmentStream.width, attachmentStream.height,
Optional.fromNullable(uploadResult.digest), Optional.fromNullable(uploadResult.digest),
attachmentStream.fileName, attachmentStream.fileName,
attachmentStream.voiceNote, attachmentStream.voiceNote,
attachmentStream.caption, attachmentStream.caption,
uploadResult.url); uploadResult.url);
val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get() val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get()
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment) database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment)
} }
override fun updateAttachmentAfterUploadFailed(attachmentId: Long) { override fun handleFailedAttachmentUpload(attachmentId: Long) {
val database = DatabaseFactory.getAttachmentDatabase(context) val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId) database.handleFailedAttachmentUpload(databaseAttachment.attachmentId)
} }
override fun getMessageID(serverID: Long): Long? { override fun getMessageID(serverID: Long): Long? {
@ -230,23 +230,24 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? { fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? {
if (TextUtils.isEmpty(location)) { return null } if (TextUtils.isEmpty(location)) { return null }
if (TextUtils.isEmpty(key)) { return null } // `key` can be empty in an open group context (no encryption means no encryption key)
return try { return try {
val id: Long = location!!.toLong() val id = location!!.toLong()
val key: ByteArray = Base64.decode(key!!) val key = Base64.decode(key!!)
SignalServiceAttachmentPointer(id, SignalServiceAttachmentPointer(
contentType, id,
key, contentType,
Optional.of(Util.toIntExact(size)), key,
Optional.absent(), Optional.of(Util.toIntExact(size)),
width, Optional.absent(),
height, width,
Optional.fromNullable(digest), height,
Optional.fromNullable(fileName), Optional.fromNullable(digest),
isVoiceNote, Optional.fromNullable(fileName),
Optional.fromNullable(caption), isVoiceNote,
url) Optional.fromNullable(caption),
url
)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }

View File

@ -384,6 +384,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel()); PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
} else if (openGroupV2 != null) { } else if (openGroupV2 != null) {
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom()); PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen")
|| openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) {
View openGroupGuidelinesView = findViewById(R.id.open_group_guidelines_view);
openGroupGuidelinesView.setVisibility(View.VISIBLE);
}
} }
View rootView = findViewById(R.id.rootView); View rootView = findViewById(R.id.rootView);

View File

@ -546,7 +546,7 @@ public class ConversationFragment extends Fragment
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser) .deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
.success(l -> { .success(l -> {
for (MessageRecord messageRecord : messageRecords) { for (MessageRecord messageRecord : messageRecords) {
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
if (l.contains(serverID)) { if (l.contains(serverID)) {
if (messageRecord.isMms()) { if (messageRecord.isMms()) {
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId()); DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
@ -569,7 +569,7 @@ public class ConversationFragment extends Fragment
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer()) .deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
.success(l -> { .success(l -> {
for (MessageRecord messageRecord : messageRecords) { for (MessageRecord messageRecord : messageRecords) {
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
if (serverID != null && serverID.equals(serverId)) { if (serverID != null && serverID.equals(serverId)) {
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms()); MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
break; break;

View File

@ -393,7 +393,7 @@ public class AttachmentDatabase extends Database {
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
} }
public void updateAttachmentAfterUploadFailed(@NonNull AttachmentId id) { public void handleFailedAttachmentUpload(@NonNull AttachmentId id) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();

View File

@ -351,7 +351,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server) DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server)
} }
override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean { override fun isDuplicateMessage(timestamp: Long, sender: String): Boolean {
return getReceivedMessageTimestamps().contains(timestamp) return getReceivedMessageTimestamps().contains(timestamp)
} }

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.app.Application; import android.app.Application;
import android.text.TextUtils; import android.text.TextUtils;
@ -39,7 +38,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName();
private static final int MAX_PROFILE_SIZE_BYTES = 20 * 1024 * 1024; private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024;
private static final String KEY_PROFILE_AVATAR = "profile_avatar"; private static final String KEY_PROFILE_AVATAR = "profile_avatar";
private static final String KEY_ADDRESS = "address"; private static final String KEY_ADDRESS = "address";
@ -51,18 +50,17 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) { public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
this(new Job.Parameters.Builder() this(new Job.Parameters.Builder()
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.HOURS.toMillis(1)) .setLifespan(TimeUnit.HOURS.toMillis(1))
.setMaxAttempts(3) .setMaxAttempts(10)
.build(), .build(),
recipient, recipient,
profileAvatar); profileAvatar);
} }
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) { private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) {
super(parameters); super(parameters);
this.recipient = recipient; this.recipient = recipient;
this.profileAvatar = profileAvatar; this.profileAvatar = profileAvatar;
} }
@ -70,9 +68,10 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
@Override @Override
public @NonNull public @NonNull
Data serialize() { Data serialize() {
return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar) return new Data.Builder()
.putString(KEY_ADDRESS, recipient.getAddress().serialize()) .putString(KEY_PROFILE_AVATAR, profileAvatar)
.build(); .putString(KEY_ADDRESS, recipient.getAddress().serialize())
.build();
} }
@Override @Override

View File

@ -24,7 +24,6 @@ import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -315,23 +314,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val threadID = thread.threadId val threadID = thread.threadId
val recipient = thread.recipient val recipient = thread.recipient
val threadDB = DatabaseFactory.getThreadDatabase(this) val threadDB = DatabaseFactory.getThreadDatabase(this)
val dialogMessage: String val message: String
if (recipient.isGroupRecipient) { if (recipient.isGroupRecipient) {
val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull() val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull()
if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) { if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
dialogMessage = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
} else { } else {
dialogMessage = resources.getString(R.string.activity_home_leave_group_dialog_message) message = resources.getString(R.string.activity_home_leave_group_dialog_message)
} }
} else { } else {
dialogMessage = resources.getString(R.string.activity_home_delete_conversation_dialog_message) message = resources.getString(R.string.activity_home_delete_conversation_dialog_message)
} }
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
dialog.setMessage(dialogMessage) dialog.setMessage(message)
dialog.setPositiveButton(R.string.yes) { _, _ -> dialog.setPositiveButton(R.string.yes) { _, _ ->
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
val context = this@HomeActivity as Context val context = this@HomeActivity as Context
// Cancel any outstanding jobs
DatabaseFactory.getSessionJobDatabase(context).cancelPendingMessageSendJobs(threadID)
// Send a leave group message if this is an active closed group // Send a leave group message if this is an active closed group
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) { if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
var isClosedGroup: Boolean var isClosedGroup: Boolean
@ -350,34 +350,28 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
return@launch return@launch
} }
} }
// Delete the conversation
withContext(Dispatchers.IO) { val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) if (v1OpenGroup != null) {
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
if (publicChat != null) { apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context) apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server)
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server)
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server)
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) ApplicationContext.getInstance(context).publicChatManager
.removeChat(v1OpenGroup.server, v1OpenGroup.channel)
OpenGroupAPI.leave(publicChat.channel, publicChat.server) } else if (v2OpenGroup != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
ApplicationContext.getInstance(context).publicChatManager apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server)
.removeChat(publicChat.server, publicChat.channel) apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server)
} else if (openGroupV2 != null) { ApplicationContext.getInstance(context).publicChatManager
val apiDB = DatabaseFactory.getLokiAPIDatabase(context) .removeChat(v2OpenGroup.server, v2OpenGroup.room)
apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server) } else {
apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server) threadDB.deleteConversation(threadID)
ApplicationContext.getInstance(context).publicChatManager
.removeChat(openGroupV2.server, openGroupV2.room)
} else {
threadDB.deleteConversation(threadID)
}
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
} }
// Update the badge count
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
// Notify the user // Notify the user
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()

View File

@ -10,8 +10,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.GridLayout
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.* import androidx.fragment.app.*
@ -42,9 +42,6 @@ import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
import org.thoughtcrime.securesms.loki.viewmodel.State import org.thoughtcrime.securesms.loki.viewmodel.State
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private val viewModel by viewModels<DefaultGroupsViewModel>()
private val adapter = JoinPublicChatActivityAdapter(this) private val adapter = JoinPublicChatActivityAdapter(this)
// region Lifecycle // region Lifecycle
@ -83,23 +80,18 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
} }
fun joinPublicChatIfPossible(url: String) { fun joinPublicChatIfPossible(url: String) {
// add http if just an IP style / host style URL is entered but leave it if scheme is included // Add "http" if not entered explicitly
val properString = if (!url.startsWith("http")) "http://$url" else url val stringWithExplicitScheme = if (!url.startsWith("http")) "http://$url" else url
val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() val url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
val room = url.pathSegments().firstOrNull()
val room = httpUrl.pathSegments().firstOrNull() val publicKey = url.queryParameter("public_key")
val publicKey = httpUrl.queryParameter("public_key")
val isV2OpenGroup = !room.isNullOrEmpty() val isV2OpenGroup = !room.isNullOrEmpty()
showLoader() showLoader()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
val (threadID, groupID) = if (isV2OpenGroup) { val (threadID, groupID) = if (isV2OpenGroup) {
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply { val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply {
if (httpUrl.port() != 80 || httpUrl.port() != 443) { if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server
// non-standard port, add to server
this.port(httpUrl.port())
}
}.build() }.build()
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!) val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity) val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
@ -107,21 +99,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
threadID to groupID threadID to groupID
} else { } else {
val channel: Long = 1 val channel: Long = 1
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel) val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, stringWithExplicitScheme, channel)
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity) val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray()) val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
threadID to groupID threadID to groupID
} }
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// go to the new conversation and finish this one val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)) openConversationActivity(this@JoinPublicChatActivity, threadID, recipient)
finish() finish()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("JoinPublicChatActivity", "Failed to join open group.", e) Log.e("Loki", "Couldn't join open group.", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
hideLoader() hideLoader()
Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
@ -175,19 +165,40 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
// region Enter Chat URL Fragment // region Enter Chat URL Fragment
class EnterChatURLFragment : Fragment() { class EnterChatURLFragment : Fragment() {
private val viewModel by activityViewModels<DefaultGroupsViewModel>() private val viewModel by activityViewModels<DefaultGroupsViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
defaultRoomsContainer.isVisible = state is State.Success
defaultRoomsLoader.isVisible = state is State.Loading
when (state) {
State.Loading -> {
// TODO: Show a loader
}
is State.Error -> {
// TODO: Hide the loader
}
is State.Success -> {
populateDefaultGroups(state.value)
}
}
}
}
private fun populateDefaultGroups(groups: List<DefaultGroup>) { private fun populateDefaultGroups(groups: List<DefaultGroup>) {
defaultRoomsGridLayout.removeAllViews() defaultRoomsGridLayout.removeAllViews()
defaultRoomsGridLayout.useDefaultMargins = false
groups.forEach { defaultGroup -> groups.forEach { defaultGroup ->
val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip
val drawable = defaultGroup.image?.let { bytes -> val drawable = defaultGroup.image?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size) val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
RoundedBitmapDrawableFactory.create(resources,bitmap).apply { RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
isCircular = true isCircular = true
} }
@ -197,35 +208,14 @@ class EnterChatURLFragment : Fragment() {
chip.setOnClickListener { chip.setOnClickListener {
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
} }
defaultRoomsGridLayout.addView(chip) defaultRoomsGridLayout.addView(chip)
} }
if (groups.size and 1 != 0) { if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
// add a filler weight 1 view
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
} }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
defaultRoomsParent.isVisible = state is State.Success
defaultRoomsLoader.isVisible = state is State.Loading
when (state) {
State.Loading -> {
// show a loader here probs
}
is State.Error -> {
// hide the loader and the
}
is State.Success -> {
populateDefaultGroups(state.value)
}
}
}
}
// region Convenience // region Convenience
private fun joinPublicChatIfPossible() { private fun joinPublicChatIfPossible() {
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager

View File

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.loki.activities
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_open_group_guidelines)
communityGuidelinesTextView.text = """
In order for our open group to be a fun environment, full of robust and constructive discussion, please follow these four simple rules:
1. Keep conversations on-topic and add value to the discussion (no referral links, spamming, or off-topic discussion).
2. You don't have to love everyone, but be civil (no baiting, excessively partisan arguments, threats, and so on; use common sense).
3. Do not be a shill. Comparison and criticism is reasonable, but blatant shilling is not.
4. Don't post explicit content, be it excessive offensive language, or content which is sexual or violent in nature.
If you break these rules, youll be warned by an admin. If your behaviour doesnt improve, you will be removed from the open group.
If you see or experience any destructive behaviour, please contact an admin.
SCAMMER WARNING
Trust only those with an admin tag in the chat. No admin will ever DM you first. No admin will ever message you for Oxen coins.
""".trimIndent()
}
// endregion
}

View File

@ -23,18 +23,15 @@ import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.avatars.AvatarHelper import org.session.libsession.messaging.avatars.AvatarHelper
import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.open_groups.OpenGroupAPI import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.preferences.ProfileKeyUtil import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.api.util.StreamDetails
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection
@ -51,7 +48,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
@ -127,7 +123,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
AvatarSelection.REQUEST_CODE_AVATAR -> { AvatarSelection.REQUEST_CODE_AVATAR -> {
if (resultCode != Activity.RESULT_OK) { return } if (resultCode != Activity.RESULT_OK) {
return
}
val outputFile = Uri.fromFile(File(cacheDir, "cropped")) val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
var inputFile: Uri? = data?.data var inputFile: Uri? = data?.data
if (inputFile == null && tempFile != null) { if (inputFile == null && tempFile != null) {
@ -136,7 +134,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar) AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
} }
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> { AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
if (resultCode != Activity.RESULT_OK) { return } if (resultCode != Activity.RESULT_OK) {
return
}
AsyncTask.execute { AsyncTask.execute {
try { try {
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
@ -186,42 +186,28 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
val profilePicture = profilePictureToBeUploaded val profilePicture = profilePictureToBeUploaded
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture && profilePicture != null) {
val storageAPI = FileServerAPI.shared promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
val deferred = deferred<Unit, Exception>()
AsyncTask.execute {
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream) {
TextSecurePreferences.setLastProfilePictureUpload(this@SettingsActivity, Date().time)
}
TextSecurePreferences.setProfilePictureURL(this, url)
deferred.resolve(Unit)
}
promises.add(deferred.promise)
} }
val compoundPromise = all(promises)
all(promises).bind { compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
// updating the profile name or picture if (isUpdatingProfilePicture && profilePicture != null) {
if (profilePicture != null || displayName != null) { AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
task { TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
if (isUpdatingProfilePicture && profilePicture != null) { TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
}
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
}
} else {
Promise.of(Unit)
} }
}.alwaysUi { if (profilePicture != null || displayName != null) {
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
}
}
compoundPromise.alwaysUi {
if (displayName != null) { if (displayName != null) {
btnGroupNameDisplay.text = displayName btnGroupNameDisplay.text = displayName
} }
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture && profilePicture != null) {
profilePictureView.recycle() // clear cached image before update tje profilePictureView profilePictureView.recycle() // Clear the cached image before updating
profilePictureView.update() profilePictureView.update()
} }
displayNameToBeUploaded = null displayNameToBeUploaded = null

View File

@ -11,6 +11,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
@ -138,9 +139,10 @@ class PublicChatManager(private val context: Context) {
val groupId = OpenGroup.getId(channel, server) val groupId = OpenGroup.getId(channel, server)
val threadId = GroupManager.getOpenGroupThreadID(groupId, context) val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize() val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
GroupManager.deleteGroup(groupAddress, context) ThreadUtils.queue {
GroupManager.deleteGroup(groupAddress, context) // Must be invoked on a background thread
Util.runOnMain { startPollersIfNeeded() } Util.runOnMain { startPollersIfNeeded() }
}
} }
fun removeChat(server: String, room: String) { fun removeChat(server: String, room: String) {

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
@ -98,11 +99,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) { override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2) val contentValues = ContentValues(3)
contentValues.put(Companion.messageID, messageID) contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID) contentValues.put(Companion.serverID, serverID)
contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE) contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE)
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString())) database.insertWithOnConflict(messageIDTable, null, contentValues, CONFLICT_REPLACE)
} }
fun getOriginalThreadID(messageID: Long): Long { fun getOriginalThreadID(messageID: Long): Long {
@ -114,11 +115,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) { fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2) val contentValues = ContentValues(3)
contentValues.put(Companion.messageID, messageID) contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID) contentValues.put(Companion.serverID, serverID)
contentValues.put(Companion.threadID, threadID) contentValues.put(Companion.threadID, threadID)
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString())) database.insertWithOnConflict(messageThreadMappingTable, null, contentValues, CONFLICT_REPLACE)
} }
fun getErrorMessage(messageID: Long): String? { fun getErrorMessage(messageID: Long): String? {

View File

@ -71,6 +71,30 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
} }
} }
fun cancelPendingMessageSendJobs(threadID: Long) {
val database = databaseHelper.writableDatabase
val attachmentUploadJobKeys = mutableListOf<String>()
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
val job = jobFromCursor(cursor) as AttachmentUploadJob?
if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) }
}
val messageSendJobKeys = mutableListOf<String>()
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( MessageSendJob.KEY )) { cursor ->
val job = jobFromCursor(cursor) as MessageSendJob?
if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) }
}
if (attachmentUploadJobKeys.isNotEmpty()) {
val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ")
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString ))
}
if (messageSendJobKeys.isNotEmpty()) {
val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ")
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString ))
}
}
fun isJobCanceled(job: Job): Boolean { fun isJobCanceled(job: Job): Boolean {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
var cursor: android.database.Cursor? = null var cursor: android.database.Cursor? = null

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.session.libsession.messaging.sending_receiving.*
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
@ -15,12 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.GroupRecord
@ -195,7 +191,7 @@ object ClosedGroupsProtocolV2 {
} }
if (userPublicKey in admins) { if (userPublicKey in admins) {
// send current encryption key to the latest added members // send current encryption key to the latest added members
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) { if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.") Log.d("Loki", "Couldn't get encryption key pair for closed group.")
@ -330,7 +326,7 @@ object ClosedGroupsProtocolV2 {
// Find our wrapper and decrypt it if possible // Find our wrapper and decrypt it if possible
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray() val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
val plaintext = SessionProtocolImpl(context).decrypt(encryptedKeyPair, userKeyPair).first val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
// Parse it // Parse it
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))

View File

@ -35,11 +35,10 @@ object OpenGroupUtilities {
val groupInfo = OpenGroupAPIV2.getInfo(room,server).get() val groupInfo = OpenGroupAPIV2.getInfo(room,server).get()
val application = ApplicationContext.getInstance(context) val application = ApplicationContext.getInstance(context)
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
storage.removeLastDeletionServerId(room, server) storage.removeLastDeletionServerId(room, server)
storage.removeLastMessageServerId(room, server) storage.removeLastMessageServerId(room, server)
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
return group return group
} }

View File

@ -20,5 +20,4 @@ class DefaultGroupsViewModel : ViewModel() {
}.onStart { }.onStart {
emit(State.Loading) emit(State.Loading)
}.asLiveData() }.asLiveData()
} }

View File

@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.loki.views
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity
import org.thoughtcrime.securesms.loki.utilities.push
class OpenGroupGuidelinesView : FrameLayout {
constructor(context: Context) : super(context) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setUpViewHierarchy()
}
private fun setUpViewHierarchy() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
addView(contentView)
readButton.setOnClickListener {
val activity = context as ConversationActivity
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
activity.push(intent)
}
}
}

View File

@ -56,7 +56,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textColor="@color/text" android:textColor="@color/text"
android:text="Youll be notified of new messages reliably and immediately using Googles notification servers. The contents of your messages, and who youre messaging, are never exposed to Google." /> android:text="Youll be notified of new messages reliably and immediately using Googles notification servers." />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -94,7 +94,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textColor="@color/text" android:textColor="@color/text"
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." /> android:text="Session will occasionally check for new messages in the background." />
</org.thoughtcrime.securesms.loki.views.PNModeView> </org.thoughtcrime.securesms.loki.views.PNModeView>

View File

@ -33,23 +33,27 @@
<LinearLayout <LinearLayout
android:visibility="gone" android:visibility="gone"
android:paddingHorizontal="24dp" android:id="@+id/defaultRoomsContainer"
android:id="@+id/defaultRoomsParent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:layout_marginVertical="16dp" android:layout_marginVertical="16dp"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:paddingHorizontal="24dp"
android:text="@string/activity_join_public_chat_join_rooms" android:text="@string/activity_join_public_chat_join_rooms"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<GridLayout <GridLayout
android:id="@+id/defaultRoomsGridLayout" android:id="@+id/defaultRoomsGridLayout"
android:columnCount="2" android:columnCount="2"
android:paddingHorizontal="16dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
<View <View

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/large_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:textSize="@dimen/large_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Community Guidelines" />
<TextView
android:id="@+id/communityGuidelinesTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:layout_marginBottom="@dimen/large_spacing"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text" />
</LinearLayout>
</ScrollView>

View File

@ -56,7 +56,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textColor="@color/text" android:textColor="@color/text"
android:text="Youll be notified of new messages reliably and immediately using Googles notification servers. The contents of your messages, and who youre messaging, are never exposed to Google." /> android:text="Youll be notified of new messages reliably and immediately using Googles notification servers." />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -94,7 +94,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textColor="@color/text" android:textColor="@color/text"
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." /> android:text="Session will occasionally check for new messages in the background." />
</org.thoughtcrime.securesms.loki.views.PNModeView> </org.thoughtcrime.securesms.loki.views.PNModeView>

View File

@ -134,6 +134,12 @@
android:background="?android:dividerHorizontal" android:background="?android:dividerHorizontal"
android:elevation="1dp" /> android:elevation="1dp" />
<org.thoughtcrime.securesms.loki.views.OpenGroupGuidelinesView
android:id="@+id/open_group_guidelines_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<FrameLayout <FrameLayout
android:id="@+id/fragment_content" android:id="@+id/fragment_content"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,14 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.chip.Chip
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:theme="@style/Theme.MaterialComponents.DayNight" android:theme="@style/Theme.MaterialComponents.DayNight"
style="?attr/chipStyle" style="?attr/chipStyle"
app:chipStartPadding="6dp" app:chipStartPadding="4dp"
app:chipBackgroundColor="@color/open_group_chip_color"
android:layout_columnWeight="1" android:layout_columnWeight="1"
android:layout_marginHorizontal="2dp"
tools:text="Main Group" tools:text="Main Group"
android:ellipsize="end" android:ellipsize="end"
tools:layout_width="wrap_content" tools:layout_width="wrap_content"
app:chipMinTouchTargetSize="0dp"
android:layout_margin="4dp"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="52dp" /> android:layout_height="wrap_content" />

View File

@ -33,23 +33,27 @@
<LinearLayout <LinearLayout
android:visibility="gone" android:visibility="gone"
android:paddingHorizontal="24dp" android:id="@+id/defaultRoomsContainer"
android:id="@+id/defaultRoomsParent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:layout_marginVertical="16dp" android:layout_marginVertical="16dp"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:paddingHorizontal="24dp"
android:text="@string/activity_join_public_chat_join_rooms" android:text="@string/activity_join_public_chat_join_rooms"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<GridLayout <GridLayout
android:id="@+id/defaultRoomsGridLayout" android:id="@+id/defaultRoomsGridLayout"
android:columnCount="2" android:columnCount="2"
android:paddingHorizontal="16dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
<View <View

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/cell_background"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingLeft="12dp"
android:paddingTop="@dimen/small_spacing"
android:paddingBottom="@dimen/small_spacing"
android:orientation="horizontal">
<View
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_marginRight="@dimen/small_spacing"
android:background="@color/accent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
android:textStyle="bold"
android:text="Pinned message" />
<TextView
android:layout_width="wrap_content"
android:maxWidth="260dp"
android:layout_height="wrap_content"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
android:maxLines="2"
android:text="Community guidelines" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:minWidth="@dimen/small_spacing" />
<Button
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/readButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/small_button_height"
android:layout_marginRight="12dp"
android:textSize="@dimen/small_font_size"
android:textStyle="normal"
android:text="Read" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="?android:dividerHorizontal" />
</LinearLayout>

View File

@ -22,6 +22,7 @@
<color name="new_conversation_button_collapsed_background">#F5F5F5</color> <color name="new_conversation_button_collapsed_background">#F5F5F5</color>
<color name="pn_option_background">#FCFCFC</color> <color name="pn_option_background">#FCFCFC</color>
<color name="fake_chat_bubble_background">#F5F5F5</color> <color name="fake_chat_bubble_background">#F5F5F5</color>
<color name="open_group_chip_color">#0D000000</color>
<color name="default_background_start">#ffffff</color> <color name="default_background_start">#ffffff</color>

View File

@ -31,6 +31,7 @@
<color name="pn_option_background">#1B1B1B</color> <color name="pn_option_background">#1B1B1B</color>
<color name="pn_option_border">#212121</color> <color name="pn_option_border">#212121</color>
<color name="paths_building">#FFCE3A</color> <color name="paths_building">#FFCE3A</color>
<color name="open_group_chip_color">#0DFFFFFF</color>
<array name="profile_picture_placeholder_colors"> <array name="profile_picture_placeholder_colors">
<item>#5ff8b0</item> <item>#5ff8b0</item>

View File

@ -27,7 +27,7 @@
android:dependency="pref_key_enable_notifications" android:dependency="pref_key_enable_notifications"
android:key="pref_key_use_fcm" android:key="pref_key_use_fcm"
android:title="Use Fast Mode" android:title="Use Fast Mode"
android:summary="Youll be notified of new messages reliably and immediately using Googles notification servers. The contents of your messages, and who youre messaging, are never exposed to Google." android:summary="Youll be notified of new messages reliably and immediately using Googles notification servers."
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>

View File

@ -29,8 +29,8 @@ interface MessageDataProvider {
fun isOutgoingMessage(timestamp: Long): Boolean fun isOutgoingMessage(timestamp: Long): Boolean
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
fun updateAttachmentAfterUploadFailed(attachmentId: Long) fun handleFailedAttachmentUpload(attachmentId: Long)
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>

View File

@ -2,13 +2,11 @@ package org.session.libsession.messaging
import android.content.Context import android.content.Context
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
class MessagingModuleConfiguration( class MessagingModuleConfiguration(
val context: Context, val context: Context,
val storage: StorageProtocol, val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider, val messageDataProvider: MessageDataProvider
val sessionProtocol: SessionProtocol
) { ) {
companion object { companion object {
@ -16,11 +14,10 @@ class MessagingModuleConfiguration(
fun configure(context: Context, fun configure(context: Context,
storage: StorageProtocol, storage: StorageProtocol,
messageDataProvider: MessageDataProvider, messageDataProvider: MessageDataProvider
sessionProtocol: SessionProtocol
) { ) {
if (Companion::shared.isInitialized) { return } if (Companion::shared.isInitialized) { return }
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, sessionProtocol) shared = MessagingModuleConfiguration(context, storage, messageDataProvider)
} }
} }
} }

View File

@ -92,7 +92,7 @@ interface StorageProtocol {
fun removeLastDeletionServerId(room: String, server: String) fun removeLastDeletionServerId(room: String, server: String)
// Message Handling // Message Handling
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean fun isDuplicateMessage(timestamp: Long, sender: String): Boolean
fun getReceivedMessageTimestamps(): Set<Long> fun getReceivedMessageTimestamps(): Set<Long>
fun addReceivedMessageTimestamp(timestamp: Long) fun addReceivedMessageTimestamp(timestamp: Long)
fun removeReceivedMessageTimestamps(timestamps: Set<Long>) fun removeReceivedMessageTimestamps(timestamps: Set<Long>)

View File

@ -15,8 +15,8 @@ import org.session.libsignal.utilities.logging.Log
object FileServerAPIV2 { object FileServerAPIV2 {
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" private const val SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
const val DEFAULT_SERVER = "http://88.99.175.227" const val SERVER = "http://88.99.175.227"
sealed class Error(message: String) : Exception(message) { sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.") object ParsingFailed : Error("Invalid response.")
@ -43,7 +43,7 @@ object FileServerAPIV2 {
} }
private fun send(request: Request): Promise<Map<*, *>, Exception> { private fun send(request: Request): Promise<Map<*, *>, Exception> {
val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val url = HttpUrl.parse(SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(url.scheme()) .scheme(url.scheme())
.host(url.host()) .host(url.host())
@ -64,7 +64,7 @@ object FileServerAPIV2 {
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
} }
if (request.useOnionRouting) { if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e -> return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), SERVER, SERVER_PUBLIC_KEY).fail { e ->
Log.e("Loki", "File server request failed.", e) Log.e("Loki", "File server request failed.", e)
} }
} else { } else {

View File

@ -36,28 +36,29 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
override fun execute() { override fun execute() {
val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val handleFailure: (java.lang.Exception) -> Unit = { exception -> val handleFailure: (java.lang.Exception) -> Unit = { exception ->
if (exception == Error.NoAttachment) { if (exception == Error.NoAttachment) {
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception) this.handlePermanentFailure(exception)
} else if (exception == DotNetAPI.Error.ParsingFailed) { } else if (exception == DotNetAPI.Error.ParsingFailed) {
// No need to retry if the response is invalid. Most likely this means we (incorrectly) // No need to retry if the response is invalid. Most likely this means we (incorrectly)
// got a "Cannot GET ..." error from the file server. // got a "Cannot GET ..." error from the file server.
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception) this.handlePermanentFailure(exception)
} else { } else {
this.handleFailure(exception) this.handleFailure(exception)
} }
} }
try { try {
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
?: return handleFailure(Error.NoAttachment) ?: return handleFailure(Error.NoAttachment)
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
val tempFile = createTempFile() val tempFile = createTempFile()
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID) val threadID = storage.getThreadIdForMms(databaseMessageID)
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString()) val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
val stream = if (openGroupV2 == null) { val inputStream = if (openGroupV2 == null) {
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null) DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
// Assume we're retrieving an attachment for an open group server if the digest is not set // Assume we're retrieving an attachment for an open group server if the digest is not set
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) { if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
@ -67,13 +68,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
} else { } else {
val url = HttpUrl.parse(attachment.url)!! val url = HttpUrl.parse(attachment.url)!!
val fileId = url.pathSegments().last() val fileID = url.pathSegments().last()
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let { OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
tempFile.writeBytes(it) tempFile.writeBytes(it)
} }
FileInputStream(tempFile) FileInputStream(tempFile)
} }
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream) messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
tempFile.delete() tempFile.delete()
handleSuccess() handleSuccess()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -3,8 +3,12 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import nl.komponents.kovenant.Promise
import okhttp3.MultipartBody
import okio.Buffer
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
@ -15,9 +19,11 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
import org.session.libsignal.service.internal.crypto.PaddingInputStream import org.session.libsignal.service.internal.crypto.PaddingInputStream
import org.session.libsignal.service.internal.push.PushAttachmentData import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.PlaintextOutputStreamFactory import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import java.util.*
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job { class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
override var delegate: JobDelegate? = null override var delegate: JobDelegate? = null
@ -45,27 +51,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
override fun execute() { override fun execute() {
try { try {
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID) val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment) ?: return handleFailure(Error.NoAttachment)
val usePadding = false val v2OpenGroup = storage.getV2OpenGroup(threadID)
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID) val v1OpenGroup = storage.getOpenGroup(threadID)
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID) if (v2OpenGroup != null) {
val server = openGroupV2?.server ?: openGroup?.server ?: FileServerAPI.shared.server val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
val attachmentKey = Util.getSecretBytes(64) }
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream } else if (v1OpenGroup == null) {
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) {
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory() FileServerAPIV2.upload(it)
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener) }
val uploadResult = if (openGroupV2 != null) { handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
val dataBytes = attachmentData.data.readBytes() } else { // V1 open group
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get() val server = v1OpenGroup.server
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf()) val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream,
} else { attachment.length, PlaintextOutputStreamFactory(), attachment.listener)
FileServerAPI.shared.uploadAttachment(server, attachmentData) val result = FileServerAPI.shared.uploadAttachment(server, pushData)
handleSuccess(attachment, ByteArray(0), result)
} }
handleSuccess(attachment, attachmentKey, uploadResult)
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
if (e == Error.NoAttachment) { if (e == Error.NoAttachment) {
this.handlePermanentFailure(e) this.handlePermanentFailure(e)
@ -77,17 +85,49 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
} }
} }
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, DotNetAPI.UploadResult> {
// Key
val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0)
// Length
val rawLength = attachment.length
val length = if (encrypt) {
val paddedLength = PaddingInputStream.getPaddedSize(rawLength)
AttachmentCipherOutputStream.getCiphertextLength(paddedLength)
} else {
attachment.length
}
// In & out streams
// PaddingInputStream adds padding as data is read out from it. AttachmentCipherOutputStream
// encrypts as it writes data.
val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream
val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory()
// Create a digesting request body but immediately read it out to a buffer. Doing this makes
// it easier to deal with inputStream and outputStreamFactory.
val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener)
val contentType = "application/octet-stream"
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, pad.listener)
Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.")
val b = Buffer()
drb.writeTo(b)
val data = b.readByteArray()
// Upload the data
val id = upload(data).get()
val digest = drb.transmittedDigest
// Return
return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest))
}
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
Log.d(TAG, "Attachment uploaded successfully.") Log.d(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this) delegate?.handleJobSucceeded(this)
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult) MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
} }
private fun handlePermanentFailure(e: Exception) { private fun handlePermanentFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed permanently due to error: $this.") Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
delegate?.handleJobFailedPermanently(this, e) delegate?.handleJobFailedPermanently(this, e)
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID) MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
failAssociatedMessageSendJob(e) failAssociatedMessageSendJob(e)
} }

View File

@ -130,12 +130,7 @@ class JobQueue : JobDelegate {
} }
// Message send jobs waiting for the attachment to upload // Message send jobs waiting for the attachment to upload
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) { if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
val retryInterval: Long = 1000 * 4
Log.i("Loki", "Message send job waiting for attachment upload to finish.") Log.i("Loki", "Message send job waiting for attachment upload to finish.")
timer.schedule(delay = retryInterval) {
Log.i("Loki", "Retrying ${job::class.simpleName}.")
queue.offer(job)
}
return return
} }
// Regular job failure // Regular job failure

View File

@ -348,7 +348,7 @@ object OpenGroupAPIV2 {
// region General // region General
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> { fun compactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) } val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val requests = rooms.mapNotNull { room -> val requests = rooms.mapNotNull { room ->

View File

@ -1,26 +1,29 @@
package org.thoughtcrime.securesms.loki.api package org.session.libsession.messaging.sending_receiving
import android.content.Context
import android.util.Log import android.util.Log
import com.goterl.lazycode.lazysodium.LazySodiumAndroid import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.interfaces.Box import com.goterl.lazycode.lazysodium.interfaces.Box
import com.goterl.lazycode.lazysodium.interfaces.Sign import com.goterl.lazycode.lazysodium.interfaces.Sign
import org.session.libsignal.utilities.Hex
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsession.utilities.KeyPairUtilities import org.session.libsignal.utilities.Hex
class SessionProtocolImpl(private val context: Context) : SessionProtocol { object MessageDecrypter {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> { /**
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
*
* @param ciphertext the data to decrypt.
* @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group.
*
* @return the padded plaintext.
*/
public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
val signatureSize = Sign.BYTES val signatureSize = Sign.BYTES
@ -32,9 +35,9 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey) sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("Loki", "Couldn't decrypt message due to error: $exception.") Log.d("Loki", "Couldn't decrypt message due to error: $exception.")
throw SessionProtocol.Exception.DecryptionFailed throw MessageReceiver.Error.DecryptionFailed
} }
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw SessionProtocol.Exception.DecryptionFailed } if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw MessageReceiver.Error.DecryptionFailed }
// 2. ) Get the message parts // 2. ) Get the message parts
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size) val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize) val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
@ -43,10 +46,10 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey) val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
try { try {
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey) val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
if (!isValid) { throw SessionProtocol.Exception.InvalidSignature } if (!isValid) { throw MessageReceiver.Error.InvalidSignature }
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("Loki", "Couldn't verify message signature due to error: $exception.") Log.d("Loki", "Couldn't verify message signature due to error: $exception.")
throw SessionProtocol.Exception.InvalidSignature throw MessageReceiver.Error.InvalidSignature
} }
// 4. ) Get the sender's X25519 public key // 4. ) Get the sender's X25519 public key
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES) val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)

View File

@ -13,7 +13,7 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
object MessageSenderEncryption { object MessageEncrypter {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
@ -25,7 +25,7 @@ object MessageSenderEncryption {
* *
* @return the encrypted message. * @return the encrypted message.
*/ */
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{ internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())

View File

@ -10,35 +10,24 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
object MessageReceiver { object MessageReceiver {
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>() internal sealed class Error(message: String) : Exception(message) {
internal sealed class Error(val description: String) : Exception(description) {
object DuplicateMessage: Error("Duplicate message.") object DuplicateMessage: Error("Duplicate message.")
object InvalidMessage: Error("Invalid message.") object InvalidMessage: Error("Invalid message.")
object UnknownMessage: Error("Unknown message type.") object UnknownMessage: Error("Unknown message type.")
object UnknownEnvelopeType: Error("Unknown envelope type.") object UnknownEnvelopeType: Error("Unknown envelope type.")
object NoUserX25519KeyPair: Error("Couldn't find user X25519 key pair.") object DecryptionFailed : Exception("Couldn't decrypt message.")
object NoUserED25519KeyPair: Error("Couldn't find user ED25519 key pair.")
object InvalidSignature: Error("Invalid message signature.") object InvalidSignature: Error("Invalid message signature.")
object NoData: Error("Received an empty envelope.") object NoData: Error("Received an empty envelope.")
object SenderBlocked: Error("Received a message from a blocked user.") object SenderBlocked: Error("Received a message from a blocked user.")
object NoThread: Error("Couldn't find thread for message.") object NoThread: Error("Couldn't find thread for message.")
object SelfSend: Error("Message addressed at self.") object SelfSend: Error("Message addressed at self.")
object ParsingFailed : Error("Couldn't parse ciphertext message.")
// Shared sender keys
object InvalidGroupPublicKey: Error("Invalid group public key.") object InvalidGroupPublicKey: Error("Invalid group public key.")
object NoGroupKeyPair: Error("Missing group key pair.") object NoGroupKeyPair: Error("Missing group key pair.")
internal val isRetryable: Boolean = when (this) { internal val isRetryable: Boolean = when (this) {
is DuplicateMessage -> false is DuplicateMessage, is InvalidMessage, is UnknownMessage,
is InvalidMessage -> false is UnknownEnvelopeType, is InvalidSignature, is NoData,
is UnknownMessage -> false is SenderBlocked, is SelfSend -> false
is UnknownEnvelopeType -> false
is InvalidSignature -> false
is NoData -> false
is NoThread -> false
is SenderBlocked -> false
is SelfSend -> false
else -> true else -> true
} }
} }
@ -46,13 +35,15 @@ object MessageReceiver {
internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair<Message, SignalServiceProtos.Content> { internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair<Message, SignalServiceProtos.Content> {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey() val userPublicKey = storage.getUserPublicKey()
val isOpenGroupMessage = openGroupServerID != null val isOpenGroupMessage = (openGroupServerID != null)
// Parse the envelope // Parse the envelope
val envelope = SignalServiceProtos.Envelope.parseFrom(data) val envelope = SignalServiceProtos.Envelope.parseFrom(data)
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
// for this issue. // for this issue.
if (storage.isMessageDuplicated(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) throw Error.DuplicateMessage if (storage.isDuplicateMessage(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) {
throw Error.DuplicateMessage
}
// Decrypt the contents // Decrypt the contents
val ciphertext = envelope.content ?: throw Error.NoData val ciphertext = envelope.content ?: throw Error.NoData
var plaintext: ByteArray? = null var plaintext: ByteArray? = null
@ -65,7 +56,7 @@ object MessageReceiver {
when (envelope.type) { when (envelope.type) {
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair() val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), userX25519KeyPair) val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair)
plaintext = decryptionResult.first plaintext = decryptionResult.first
sender = decryptionResult.second sender = decryptionResult.second
} }
@ -81,7 +72,7 @@ object MessageReceiver {
var encryptionKeyPair = encryptionKeyPairs.removeLast() var encryptionKeyPair = encryptionKeyPairs.removeLast()
fun decrypt() { fun decrypt() {
try { try {
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), encryptionKeyPair) val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), encryptionKeyPair)
plaintext = decryptionResult.first plaintext = decryptionResult.first
sender = decryptionResult.second sender = decryptionResult.second
} catch (e: Exception) { } catch (e: Exception) {
@ -100,9 +91,9 @@ object MessageReceiver {
} }
} }
// Don't process the envelope any further if the message has been handled already // Don't process the envelope any further if the message has been handled already
if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage if (storage.isDuplicateMessage(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
// Don't process the envelope any further if the sender is blocked // Don't process the envelope any further if the sender is blocked
if (isBlock(sender!!)) throw Error.SenderBlocked if (isBlocked(sender!!)) throw Error.SenderBlocked
// Parse the proto // Parse the proto
val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext)) val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
// Parse the message // Parse the message
@ -113,7 +104,7 @@ object MessageReceiver {
ExpirationTimerUpdate.fromProto(proto) ?: ExpirationTimerUpdate.fromProto(proto) ?:
ConfigurationMessage.fromProto(proto) ?: ConfigurationMessage.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
// Ignore self sends if needed // Ignore self send if needed
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
// Guard against control messages in open groups // Guard against control messages in open groups
if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage

View File

@ -1,11 +0,0 @@
package org.session.libsession.messaging.sending_receiving
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsignal.libsignal.ecc.ECKeyPair
object MessageReceiverDecryption {
internal fun decryptWithSessionProtocol(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
return MessagingModuleConfiguration.shared.sessionProtocol.decrypt(ciphertext, x25519KeyPair)
}
}

View File

@ -14,7 +14,6 @@ import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.* import org.session.libsession.messaging.messages.visible.*
import org.session.libsession.messaging.open_groups.* import org.session.libsession.messaging.open_groups.*
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.RawResponsePromise import org.session.libsession.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
@ -27,6 +26,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import java.lang.IllegalStateException
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
@ -37,8 +37,6 @@ object MessageSender {
sealed class Error(val description: String) : Exception(description) { sealed class Error(val description: String) : Exception(description) {
object InvalidMessage : Error("Invalid message.") object InvalidMessage : Error("Invalid message.")
object ProtoConversionFailed : Error("Couldn't convert message to proto.") object ProtoConversionFailed : Error("Couldn't convert message to proto.")
object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.")
object NoUserX25519KeyPair : Error("Couldn't find user X25519 key pair.")
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.") object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
object SigningFailed : Error("Couldn't sign message.") object SigningFailed : Error("Couldn't sign message.")
object EncryptionFailed : Error("Couldn't encrypt message.") object EncryptionFailed : Error("Couldn't encrypt message.")
@ -46,17 +44,10 @@ object MessageSender {
// Closed groups // Closed groups
object NoThread : Error("Couldn't find a thread associated with the given group public key.") object NoThread : Error("Couldn't find a thread associated with the given group public key.")
object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.") object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.")
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
object InvalidClosedGroupUpdate : Error("Invalid group update.") object InvalidClosedGroupUpdate : Error("Invalid group update.")
// Precondition
class PreconditionFailure(val reason: String): Error(reason)
internal val isRetryable: Boolean = when (this) { internal val isRetryable: Boolean = when (this) {
is InvalidMessage -> false is InvalidMessage, ProtoConversionFailed, InvalidClosedGroupUpdate -> false
is ProtoConversionFailed -> false
is ProofOfWorkCalculationFailed -> false
is InvalidClosedGroupUpdate -> false
else -> true else -> true
} }
} }
@ -76,7 +67,9 @@ object MessageSender {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey() val userPublicKey = storage.getUserPublicKey()
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ if (message.sentTimestamp == null) {
message.sentTimestamp = System.currentTimeMillis() // Visible messages will already have their sent timestamp set
}
message.sender = userPublicKey message.sender = userPublicKey
val isSelfSend = (message.recipient == userPublicKey) val isSelfSend = (message.recipient == userPublicKey)
// Set the failure handler (need it here already for precondition failure handling) // Set the failure handler (need it here already for precondition failure handling)
@ -91,8 +84,7 @@ object MessageSender {
when (destination) { when (destination) {
is Destination.Contact -> message.recipient = destination.publicKey is Destination.Contact -> message.recipient = destination.publicKey
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
is Destination.OpenGroup, is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
} }
// Validate the message // Validate the message
if (!message.isValid()) { throw Error.InvalidMessage } if (!message.isValid()) { throw Error.InvalidMessage }
@ -125,13 +117,12 @@ object MessageSender {
// Encrypt the serialized protobuf // Encrypt the serialized protobuf
val ciphertext: ByteArray val ciphertext: ByteArray
when (destination) { when (destination) {
is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey) is Destination.Contact -> ciphertext = MessageEncrypter.encrypt(plaintext, destination.publicKey)
is Destination.ClosedGroup -> { is Destination.ClosedGroup -> {
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!! val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey) ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
} }
is Destination.OpenGroup, is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
} }
// Wrap the result // Wrap the result
val kind: SignalServiceProtos.Envelope.Type val kind: SignalServiceProtos.Envelope.Type
@ -145,8 +136,7 @@ object MessageSender {
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
senderPublicKey = destination.groupPublicKey senderPublicKey = destination.groupPublicKey
} }
is Destination.OpenGroup, is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
} }
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
// Send the result // Send the result
@ -201,7 +191,9 @@ object MessageSender {
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> { private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } if (message.sentTimestamp == null) {
message.sentTimestamp = System.currentTimeMillis()
}
message.sender = storage.getUserPublicKey() message.sender = storage.getUserPublicKey()
// Set the failure handler (need it here already for precondition failure handling) // Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) { fun handleFailure(error: Exception) {
@ -210,18 +202,15 @@ object MessageSender {
} }
try { try {
when (destination) { when (destination) {
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!") is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
is Destination.OpenGroup -> { is Destination.OpenGroup -> {
message.recipient = "${destination.server}.${destination.channel}" message.recipient = "${destination.server}.${destination.channel}"
val server = destination.server val server = destination.server
val channel = destination.channel val channel = destination.channel
// Validate the message // Validate the message
if (message !is VisibleMessage || !message.isValid()) { if (message !is VisibleMessage || !message.isValid()) {
throw Error.InvalidMessage throw Error.InvalidMessage
} }
// Convert the message to an open group message // Convert the message to an open group message
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run { val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
throw Error.InvalidMessage throw Error.InvalidMessage
@ -239,7 +228,6 @@ object MessageSender {
message.recipient = "${destination.server}.${destination.room}" message.recipient = "${destination.server}.${destination.room}"
val server = destination.server val server = destination.server
val room = destination.room val room = destination.room
// Attach the user's profile if needed // Attach the user's profile if needed
if (message is VisibleMessage) { if (message is VisibleMessage) {
val displayName = storage.getUserDisplayName()!! val displayName = storage.getUserDisplayName()!!
@ -251,20 +239,17 @@ object MessageSender {
message.profile = Profile(displayName) message.profile = Profile(displayName)
} }
} }
// Validate the message // Validate the message
if (message !is VisibleMessage || !message.isValid()) { if (message !is VisibleMessage || !message.isValid()) {
throw Error.InvalidMessage throw Error.InvalidMessage
} }
val proto = message.toProto()!! val proto = message.toProto()!!
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray()) val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
val openGroupMessage = OpenGroupMessageV2( val openGroupMessage = OpenGroupMessageV2(
sender = message.sender, sender = message.sender,
sentTimestamp = message.sentTimestamp!!, sentTimestamp = message.sentTimestamp!!,
base64EncodedData = Base64.encodeBytes(plaintext), base64EncodedData = Base64.encodeBytes(plaintext),
) )
OpenGroupAPIV2.send(openGroupMessage,room,server).success { OpenGroupAPIV2.send(openGroupMessage,room,server).success {
message.openGroupServerMessageID = it.serverID message.openGroupServerMessageID = it.serverID
handleSuccessfulMessageSend(message, destination) handleSuccessfulMessageSend(message, destination)
@ -272,7 +257,6 @@ object MessageSender {
}.fail { }.fail {
handleFailure(it) handleFailure(it)
} }
} }
} }
} catch (exception: Exception) { } catch (exception: Exception) {
@ -285,7 +269,7 @@ object MessageSender {
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) { fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return val messageID = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
// Ignore future self-sends // Ignore future self-sends
storage.addReceivedMessageTimestamp(message.sentTimestamp!!) storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
// Track the open group server message ID // Track the open group server message ID
@ -293,7 +277,7 @@ object MessageSender {
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray()) val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded)) val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
if (threadID != null && threadID >= 0) { if (threadID != null && threadID >= 0) {
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage()) storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
} }
} }
// Mark the message as sent // Mark the message as sent
@ -323,16 +307,16 @@ object MessageSender {
// Convenience // Convenience
@JvmStatic @JvmStatic
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) { fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!) val attachmentIDs = messageDataProvider.getAttachmentIDsFor(message.id!!)
message.attachmentIDs.addAll(attachmentIDs) message.attachmentIDs.addAll(attachmentIDs)
message.quote = Quote.from(quote) message.quote = Quote.from(quote)
message.linkPreview = LinkPreview.from(linkPreview) message.linkPreview = LinkPreview.from(linkPreview)
message.linkPreview?.let { message.linkPreview?.let { linkPreview ->
if (it.attachmentID == null) { if (linkPreview.attachmentID == null) {
dataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID ->
message.linkPreview!!.attachmentID = it message.linkPreview!!.attachmentID = attachmentID
message.attachmentIDs.remove(it) message.attachmentIDs.remove(attachmentID)
} }
} }
} }

View File

@ -26,7 +26,8 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
const val groupSizeLimit = 100 const val groupSizeLimit = 100
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> { fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>() val deferred = deferred<String, Exception>()
@ -45,7 +46,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
val admins = setOf( userPublicKey ) val admins = setOf( userPublicKey )
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis()) null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
storage.setProfileSharing(Address.fromSerialized(groupID), true) storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Send a closed group update message to all members individually // Send a closed group update message to all members individually
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData) val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
@ -179,13 +180,11 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
throw Error.InvalidClosedGroupUpdate throw Error.InvalidClosedGroupUpdate
} }
// Save the new group members // Save the new group members
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
// Update the zombie list // Update the zombie list
val oldZombies = storage.getZombieMember(groupID) val oldZombies = storage.getZombieMember(groupID)
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) }) storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val name = group.title val name = group.title
// Send the update to the group // Send the update to the group
@ -194,17 +193,14 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind) val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID)) send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Send the new encryption key pair to the remaining group members.
// Send the new encryption key pair to the remaining group members // At this stage we know the user is admin, no need to test.
// At this stage we know the user is admin, no need to test
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers) generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
// Notify the user // Notify the user
// We don't display zombie members in the notification as users have already been notified when those members left
// Insert an outgoing notification
// we don't display zombie members in the notification as users have already been notified when those members left
val notificationMembers = membersToRemove.minus(oldZombies) val notificationMembers = membersToRemove.minus(oldZombies)
if (notificationMembers.isNotEmpty()) { if (notificationMembers.isNotEmpty()) {
// no notification to display when only zombies have been removed // No notification to display when only zombies have been removed
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, notificationMembers, admins, threadID, sentTime) storage.insertOutgoingInfoMessage(context, groupID, infoType, name, notificationMembers, admins, threadID, sentTime)
@ -259,16 +255,16 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
} }
// Generate the new encryption key pair // Generate the new encryption key pair
val newKeyPair = Curve.generateKeyPair() val newKeyPair = Curve.generateKeyPair()
// replace call will not succeed if no value already set // Replace call will not succeed if no value already set
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent()) pendingKeyPairs.putIfAbsent(groupPublicKey,Optional.absent())
do { do {
// make sure we set the pendingKeyPair or wait until it is not null // Make sure we set the pending key pair or wait until it is not null
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair))) } while (!pendingKeyPairs.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
// Distribute it // Distribute it
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success { sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
// Store it * after * having sent out the message to the group // Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent() pendingKeyPairs[groupPublicKey] = Optional.absent()
} }
} }
@ -279,7 +275,7 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
val plaintext = proto.build().toByteArray() val plaintext = proto.build().toByteArray()
val wrappers = targetMembers.map { publicKey -> val wrappers = targetMembers.map { publicKey ->
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
} }
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers) val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
@ -307,14 +303,14 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
return return
} }
// Get the latest encryption key pair // Get the latest encryption key pair
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
// Send it // Send it
val proto = SignalServiceProtos.KeyPair.newBuilder() val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize()) proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
val plaintext = proto.build().toByteArray() val plaintext = proto.build().toByteArray()
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.") Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper)) val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))

View File

@ -35,7 +35,7 @@ import java.security.MessageDigest
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
internal fun MessageReceiver.isBlock(publicKey: String): Boolean { internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
return recipient.isBlocked return recipient.isBlocked
@ -96,12 +96,9 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
} }
} }
// Data Extraction Notification handling
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) { private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
// we don't handle data extraction messages for groups (they shouldn't be sent, but in case we filter them here too) // We don't handle data extraction messages for groups (they shouldn't be sent, but just in case we filter them here too)
if (message.groupPublicKey != null) return if (message.groupPublicKey != null) return
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val senderPublicKey = message.sender!! val senderPublicKey = message.sender!!
val notification: DataExtractionNotificationInfoMessage = when(message.kind) { val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
@ -112,20 +109,20 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!) storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
} }
// Configuration message handling
private fun handleConfigurationMessage(message: ConfigurationMessage) { private fun handleConfigurationMessage(message: ConfigurationMessage) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return if (TextSecurePreferences.getConfigurationMessageSynced(context)
&& !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return
val userPublicKey = storage.getUserPublicKey() val userPublicKey = storage.getUserPublicKey()
if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return
TextSecurePreferences.setConfigurationMessageSynced(context, true) TextSecurePreferences.setConfigurationMessageSynced(context, true)
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!) TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
for (closeGroup in message.closedGroups) { for (closedGroup in message.closedGroups) {
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
} }
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL } val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
@ -137,14 +134,12 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
TextSecurePreferences.setProfileName(context, message.displayName) TextSecurePreferences.setProfileName(context, message.displayName)
storage.setDisplayName(userPublicKey, message.displayName) storage.setDisplayName(userPublicKey, message.displayName)
} }
if (message.profileKey.isNotEmpty()) { if (message.profileKey.isNotEmpty() && !message.profilePicture.isNullOrEmpty()
&& TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
val profileKey = Base64.encodeBytes(message.profileKey) val profileKey = Base64.encodeBytes(message.profileKey)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey) ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey) storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
// handle profile photo storage.setUserProfilePictureUrl(message.profilePicture!!)
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
storage.setUserProfilePictureUrl(message.profilePicture!!)
}
} }
storage.addContacts(message.contacts) storage.addContacts(message.contacts)
} }
@ -159,41 +154,32 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val userPublicKey = storage.getUserPublicKey() val userPublicKey = storage.getUserPublicKey()
// Get or create thread // Get or create thread
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
// exist. This is intentional, but it's very non-obvious.
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
?: message.sender!!, message.groupPublicKey, openGroupID) ?: message.sender!!, message.groupPublicKey, openGroupID)
if (threadID < 0) { if (threadID < 0) {
// thread doesn't exist, should only be reached in a case where we are processing open group messages for no longer existent thread // Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
throw MessageReceiver.Error.NoThread throw MessageReceiver.Error.NoThread
} }
val openGroup = threadID.let { val openGroup = threadID.let {
storage.getOpenGroup(it.toString()) storage.getOpenGroup(it.toString())
} }
// Update profile if needed // Update profile if needed
val newProfile = message.profile val profile = message.profile
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
if (newProfile != null && userPublicKey != message.sender && openGroup == null) {
val profileManager = SSKEnvironment.shared.profileManager val profileManager = SSKEnvironment.shared.profileManager
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false) val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
val displayName = newProfile.displayName!! val displayName = profile.displayName!!
if (displayName.isNotEmpty()) { if (displayName.isNotEmpty()) {
profileManager.setDisplayName(context, recipient, displayName) profileManager.setDisplayName(context, recipient, displayName)
} }
if (newProfile.profileKey?.isNotEmpty() == true if (profile.profileKey?.isNotEmpty() == true && profile.profilePictureURL?.isNotEmpty() == true
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) { && (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profile.profileKey))) {
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!) profileManager.setProfileKey(context, recipient, profile.profileKey!!)
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN) profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
val newUrl = newProfile.profilePictureURL profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
if (!newUrl.isNullOrEmpty()) {
profileManager.setProfilePictureURL(context, recipient, newUrl)
if (userPublicKey == message.sender) {
profileManager.updateOpenGroupProfilePicturesIfNeeded(context)
}
}
} }
} }
// Parse quote if needed // Parse quote if needed
@ -201,10 +187,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
if (message.quote != null && proto.dataMessage.hasQuote()) { if (message.quote != null && proto.dataMessage.hasQuote()) {
val quote = proto.dataMessage.quote val quote = proto.dataMessage.quote
val author = Address.fromSerialized(quote.author) val author = Address.fromSerialized(quote.author)
val messageInfo = MessagingModuleConfiguration.shared.messageDataProvider.getMessageForQuote(quote.id, author) val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
if (messageInfo != null) { if (messageInfo != null) {
val attachments = if (messageInfo.second) MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
quoteModel = QuoteModel(quote.id, author, MessagingModuleConfiguration.shared.messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments) quoteModel = QuoteModel(quote.id, author, messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
} else { } else {
quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList)) quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
} }
@ -225,6 +212,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
} }
} }
} }
// Parse attachments if needed
val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto -> val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto ->
val attachment = Attachment.fromProto(proto) val attachment = Attachment.fromProto(proto)
if (!attachment.isValid()) { if (!attachment.isValid()) {
@ -233,7 +221,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
return@mapNotNull attachment return@mapNotNull attachment
} }
} }
// Parse stickers if needed
// Persist the message // Persist the message
message.threadID = threadID message.threadID = threadID
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
@ -247,7 +234,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
} }
val openGroupServerID = message.openGroupServerMessageID val openGroupServerID = message.openGroupServerMessageID
if (openGroupServerID != null) { if (openGroupServerID != null) {
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !message.isMediaMessage()) val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
} }
// Cancel any typing indicators if needed // Cancel any typing indicators if needed
cancelTypingIndicatorsIfNeeded(message.sender!!) cancelTypingIndicatorsIfNeeded(message.sender!!)
@ -278,7 +266,6 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!) handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
} }
// Parameter @sender:String is just for inserting incoming info message
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) { private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
@ -290,11 +277,10 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else { } else {
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
val userPublicKey = TextSecurePreferences.getLocalNumber(context) val userPublicKey = TextSecurePreferences.getLocalNumber(context)
// Notify the user // Notify the user
if (userPublicKey == sender) { if (userPublicKey == sender) {
// sender is a linked device
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
} else { } else {
@ -322,11 +308,11 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
// Unwrap the message // Unwrap the message
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run { val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group encryption key pair for nonexistent group.")
return return
} }
if (!group.isActive) { if (!group.isActive) {
Log.d("Loki", "Ignoring closed group info message for inactive group") Log.d("Loki", "Ignoring closed group encryption key pair for inactive group.")
return return
} }
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) { if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
@ -336,7 +322,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
// Find our wrapper and decrypt it if possible // Find our wrapper and decrypt it if possible
val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray() val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
// Parse it // Parse it
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
@ -347,7 +333,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
return return
} }
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey) storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
Log.d("Loki", "Received a new closed group encryption key pair") Log.d("Loki", "Received a new closed group encryption key pair.")
} }
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) { private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
@ -360,11 +346,11 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
// Check that the sender is a member of the group (before the update) // Check that the sender is a member of the group (before the update)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run { val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group update for nonexistent group.")
return return
} }
if (!group.isActive) { if (!group.isActive) {
Log.d("Loki", "Ignoring closed group info message for inactive group") Log.d("Loki", "Ignoring closed group update for inactive group.")
return return
} }
// Check common group update logic // Check common group update logic
@ -375,10 +361,8 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
val admins = group.admins.map { it.serialize() } val admins = group.admins.map { it.serialize() }
val name = kind.name val name = kind.name
storage.updateTitle(groupID, name) storage.updateTitle(groupID, name)
// Notify the user // Notify the user
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
// sender is a linked device
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!)
} else { } else {
@ -395,11 +379,11 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val groupPublicKey = message.groupPublicKey ?: return val groupPublicKey = message.groupPublicKey ?: return
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run { val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group update for nonexistent group.")
return return
} }
if (!group.isActive) { if (!group.isActive) {
Log.d("Loki", "Ignoring closed group info message for inactive group") Log.d("Loki", "Ignoring closed group update for inactive group.")
return return
} }
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
@ -411,19 +395,28 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val updateMembers = kind.members.map { it.toByteArray().toHexString() } val updateMembers = kind.members.map { it.toByteArray().toHexString() }
val newMembers = members + updateMembers val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Notify the user // Notify the user
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
// sender is a linked device
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!)
} else { } else {
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!) storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!)
} }
if (userPublicKey in admins) { if (userPublicKey in admins) {
// send current encryption key to the latest added members // Send the latest encryption key pair to the added members if the current user is the admin of the group
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() //
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) // This fixes a race condition where:
// • A member removes another member.
// • A member adds someone to the group and sends them the latest group key pair.
// • The admin is offline during all of this.
// • When the admin comes back online they see the member removed message and generate + distribute a new key pair,
// but they don't know about the added member yet.
// • Now they see the member added message.
//
// Without the code below, the added member(s) would never get the key pair that was generated by the admin when they saw
// the member removed message.
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) { if (encryptionKeyPair == null) {
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.") android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else { } else {
@ -448,65 +441,54 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
val groupPublicKey = message.groupPublicKey ?: return val groupPublicKey = message.groupPublicKey ?: return
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run { val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group update for nonexistent group.")
return return
} }
if (!group.isActive) { if (!group.isActive) {
Log.d("Loki", "Ignoring closed group info message for inactive group.") Log.d("Loki", "Ignoring closed group update for inactive group.")
return return
} }
val name = group.title val name = group.title
// Check common group update logic // Check common group update logic
val members = group.members.map { it.serialize() } val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.toString() } val admins = group.admins.map { it.toString() }
val removedMembers = kind.members.map { it.toByteArray().toHexString() }
// Users that are part of this remove update
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
// Check that the admin wasn't removed // Check that the admin wasn't removed
if (updateMembers.contains(admins.first())) { if (removedMembers.contains(admins.first())) {
Log.d("Loki", "Ignoring invalid closed group update.") Log.d("Loki", "Ignoring invalid closed group update.")
return return
} }
// Check that the message was sent by the group admin // Check that the message was sent by the group admin
if (!admins.contains(senderPublicKey)) { if (!admins.contains(senderPublicKey)) {
Log.d("Loki", "Ignoring invalid closed group update.") Log.d("Loki", "Ignoring invalid closed group update.")
return return
} }
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
// If admin leaves the group is disbanded // If the admin leaves the group is disbanded
val didAdminLeave = admins.any { it in updateMembers } val didAdminLeave = admins.any { it in removedMembers }
// newMembers to save is old members minus removed members val newMembers = members - removedMembers
val newMembers = members - updateMembers // A user should be posting a MEMBERS_LEFT in case they leave, so this shouldn't be encountered
// user should be posting MEMBERS_LEFT so this should not be encountered val senderLeft = senderPublicKey in removedMembers
val senderLeft = senderPublicKey in updateMembers
if (senderLeft) { if (senderLeft) {
android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey") Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.")
} }
val wasCurrentUserRemoved = userPublicKey in updateMembers val wasCurrentUserRemoved = userPublicKey in removedMembers
// Admin should send a MEMBERS_LEFT message but handled here just in case
// admin should send a MEMBERS_LEFT message but handled here in case
if (didAdminLeave || wasCurrentUserRemoved) { if (didAdminLeave || wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else { } else {
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
} }
// update zombie members // Update zombie members
val zombies = storage.getZombieMember(groupID) val zombies = storage.getZombieMember(groupID)
storage.updateZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) }) storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
val type = if (senderLeft) SignalServiceGroup.Type.QUIT
else SignalServiceGroup.Type.MEMBER_REMOVED
// Notify the user // Notify the user
// we don't display zombie members in the notification as users have already been notified when those members left // We don't display zombie members in the notification as users have already been notified when those members left
val notificationMembers = updateMembers.minus(zombies) val notificationMembers = removedMembers.minus(zombies)
if (notificationMembers.isNotEmpty()) { if (notificationMembers.isNotEmpty()) {
// no notification to display when only zombies have been removed // No notification to display when only zombies have been removed
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
// sender is a linked device
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!) storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!)
} else { } else {
@ -528,11 +510,11 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
val groupPublicKey = message.groupPublicKey ?: return val groupPublicKey = message.groupPublicKey ?: return
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run { val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group update for nonexistent group.")
return return
} }
if (!group.isActive) { if (!group.isActive) {
Log.d("Loki", "Ignoring closed group info message for inactive group") Log.d("Loki", "Ignoring closed group update for inactive group.")
return return
} }
val name = group.title val name = group.title
@ -546,19 +528,16 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
val didAdminLeave = admins.contains(senderPublicKey) val didAdminLeave = admins.contains(senderPublicKey)
val updatedMemberList = members - senderPublicKey val updatedMemberList = members - senderPublicKey
val userLeft = (userPublicKey == senderPublicKey) val userLeft = (userPublicKey == senderPublicKey)
if (didAdminLeave || userLeft) { if (didAdminLeave || userLeft) {
// admin left the group of linked device left the group
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else { } else {
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
// update zombie members // Update zombie members
val zombies = storage.getZombieMember(groupID) val zombies = storage.getZombieMember(groupID)
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) }) storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
} }
// Notify the user // Notify the user
if (userLeft) { if (userLeft) {
//sender is a linked device
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
} else { } else {
@ -566,9 +545,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
} }
} }
private fun isValidGroupUpdate(group: GroupRecord, private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
sentTimestamp: Long,
senderPublicKey: String): Boolean {
val oldMembers = group.members.map { it.serialize() } val oldMembers = group.members.map { it.serialize() }
// Check that the message isn't from before the group was created // Check that the message isn't from before the group was created
if (group.formationTimestamp > sentTimestamp) { if (group.formationTimestamp > sentTimestamp) {

View File

@ -70,11 +70,11 @@ class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val e
isPollOngoing = true isPollOngoing = true
val server = openGroups.first().server // assume all the same server val server = openGroups.first().server // assume all the same server
val rooms = openGroups.map { it.room } val rooms = openGroups.map { it.room }
return OpenGroupAPIV2.getCompactPoll(rooms = rooms, server).successBackground { results -> return OpenGroupAPIV2.compactPoll(rooms = rooms, server).successBackground { results ->
results.forEach { (room, results) -> results.forEach { (room, results) ->
val serverRoomId = "$server.$room" val serverRoomId = "$server.$room"
handleDeletedMessages(serverRoomId,results.deletions)
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll) handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
handleDeletedMessages(serverRoomId,results.deletions)
} }
}.always { }.always {
isPollOngoing = false isPollOngoing = false
@ -120,7 +120,11 @@ class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val e
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId -> val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId ->
messagingModule.messageDataProvider.getMessageID(serverId, threadId) val id = messagingModule.messageDataProvider.getMessageID(serverId, threadId)
if (id == null) {
Log.d("Loki", "Couldn't find server ID $serverId")
}
id
} }
deletedMessageIDs.forEach { (messageId, isSms) -> deletedMessageIDs.forEach { (messageId, isSms) ->
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms) MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)

View File

@ -12,16 +12,15 @@ import javax.crypto.spec.SecretKeySpec
@WorkerThread @WorkerThread
internal object AESGCM { internal object AESGCM {
internal data class EncryptionResult(
internal val ciphertext: ByteArray,
internal val symmetricKey: ByteArray,
internal val ephemeralPublicKey: ByteArray
)
internal val gcmTagSize = 128 internal val gcmTagSize = 128
internal val ivSize = 12 internal val ivSize = 12
internal data class EncryptionResult(
internal val ciphertext: ByteArray,
internal val symmetricKey: ByteArray,
internal val ephemeralPublicKey: ByteArray
)
/** /**
* Sync. Don't call from the main thread. * Sync. Don't call from the main thread.
*/ */

View File

@ -5,7 +5,6 @@ import okhttp3.Request
import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.file_server.FileServerAPIV2 import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.api.messages.SignalServiceAttachment
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
@ -42,7 +41,7 @@ object DownloadUtilities {
@JvmStatic @JvmStatic
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
if (url.contains(FileServerAPIV2.DEFAULT_SERVER)) { if (url.contains(FileServerAPIV2.SERVER)) {
val httpUrl = HttpUrl.parse(url)!! val httpUrl = HttpUrl.parse(url)!!
val fileId = httpUrl.pathSegments().last() val fileId = httpUrl.pathSegments().last()
try { try {

View File

@ -0,0 +1,46 @@
package org.session.libsession.utilities
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import okio.Buffer
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
import org.session.libsignal.service.internal.push.ProfileAvatarData
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory
import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.ThreadUtils
import java.io.ByteArrayInputStream
import java.util.*
object ProfilePictureUtilities {
fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
ThreadUtils.queue {
val inputStream = ByteArrayInputStream(profilePicture)
val outputStream = ProfileCipherOutputStream.getCiphertextLength(profilePicture.size.toLong())
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
val pad = ProfileAvatarData(inputStream, outputStream, "image/jpeg", ProfileCipherOutputStreamFactory(profileKey))
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, pad.contentType, pad.dataLength, null)
val b = Buffer()
drb.writeTo(b)
val data = b.readByteArray()
var id: Long = 0
try {
id = retryIfNeeded(4) {
FileServerAPIV2.upload(data)
}.get()
} catch (e: Exception) {
deferred.reject(e)
}
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
val url = "${FileServerAPIV2.SERVER}/files/$id"
TextSecurePreferences.setProfilePictureURL(context, url)
deferred.resolve(Unit)
}
return deferred.promise
}
}

View File

@ -58,7 +58,6 @@ public enum MaterialColor {
private final String serialized; private final String serialized;
MaterialColor(@ColorRes int mainColor, @ColorRes int tintColor, @ColorRes int shadeColor, String serialized) { MaterialColor(@ColorRes int mainColor, @ColorRes int tintColor, @ColorRes int shadeColor, String serialized) {
this.mainColor = mainColor; this.mainColor = mainColor;
this.tintColor = tintColor; this.tintColor = tintColor;
@ -110,9 +109,9 @@ public enum MaterialColor {
} }
public boolean represents(Context context, int colorValue) { public boolean represents(Context context, int colorValue) {
return context.getResources().getColor(mainColor) == colorValue || return context.getResources().getColor(mainColor) == colorValue
context.getResources().getColor(tintColor) == colorValue || || context.getResources().getColor(tintColor) == colorValue
context.getResources().getColor(shadeColor) == colorValue; || context.getResources().getColor(shadeColor) == colorValue;
} }
public String serialize() { public String serialize() {

View File

@ -1,70 +0,0 @@
package org.session.libsession.utilities.color;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MaterialColors {
public static final MaterialColorList CONVERSATION_PALETTE = new MaterialColorList(new ArrayList<>(Arrays.asList(
MaterialColor.PLUM,
MaterialColor.CRIMSON,
MaterialColor.VERMILLION,
MaterialColor.VIOLET,
MaterialColor.BLUE,
MaterialColor.INDIGO,
MaterialColor.FOREST,
MaterialColor.WINTERGREEN,
MaterialColor.TEAL,
MaterialColor.BURLAP,
MaterialColor.TAUPE,
MaterialColor.STEEL
)));
public static class MaterialColorList {
private final List<MaterialColor> colors;
private MaterialColorList(List<MaterialColor> colors) {
this.colors = colors;
}
public MaterialColor get(int index) {
return colors.get(index);
}
public int size() {
return colors.size();
}
public @Nullable MaterialColor getByColor(Context context, int colorValue) {
for (MaterialColor color : colors) {
if (color.represents(context, colorValue)) {
return color;
}
}
return null;
}
public int[] asConversationColorArray(@NonNull Context context) {
int[] results = new int[colors.size()];
int index = 0;
for (MaterialColor color : colors) {
results[index++] = color.toConversationColor(context);
}
return results;
}
}
}

View File

@ -1,12 +1,10 @@
package org.session.libsession.utilities.color.spans; package org.session.libsession.utilities.color.spans;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.style.MetricAffectingSpan; import android.text.style.MetricAffectingSpan;
public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan { public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan {
private final float relativeSize; private final float relativeSize;
public CenterAlignedRelativeSizeSpan(float relativeSize) { public CenterAlignedRelativeSizeSpan(float relativeSize) {

View File

@ -31,8 +31,6 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.Content;
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities;
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import java.util.ArrayList; import java.util.ArrayList;
@ -51,13 +49,10 @@ public class SignalServiceCipher {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = SignalServiceCipher.class.getSimpleName(); private static final String TAG = SignalServiceCipher.class.getSimpleName();
private final SessionProtocol sessionProtocolImpl;
private final LokiAPIDatabaseProtocol apiDB; private final LokiAPIDatabaseProtocol apiDB;
public SignalServiceCipher(SessionProtocol sessionProtocolImpl, public SignalServiceCipher(LokiAPIDatabaseProtocol apiDB)
LokiAPIDatabaseProtocol apiDB)
{ {
this.sessionProtocolImpl = sessionProtocolImpl;
this.apiDB = apiDB; this.apiDB = apiDB;
} }
@ -125,27 +120,7 @@ public class SignalServiceCipher {
protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException
{ {
byte[] paddedMessage; throw new IllegalStateException("This shouldn't be used anymore");
Metadata metadata;
if (envelope.isClosedGroupCiphertext()) {
String groupPublicKey = envelope.getSource();
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl);
paddedMessage = plaintextAndSenderPublicKey.getFirst();
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
} else if (envelope.isUnidentifiedSender()) {
ECKeyPair userX25519KeyPair = apiDB.getUserX25519KeyPair();
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(ciphertext, userX25519KeyPair);
paddedMessage = plaintextAndSenderPublicKey.getFirst();
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
} else {
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
}
byte[] data = PushTransportDetails.getStrippedPaddingMessageBody(paddedMessage);
return new Plaintext(metadata, data);
} }
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) throws ProtocolInvalidMessageException { private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) throws ProtocolInvalidMessageException {

View File

@ -1,6 +1,5 @@
package org.session.libsignal.service.internal.push.http; package org.session.libsignal.service.internal.push.http;
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream; import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
import org.session.libsignal.service.api.crypto.DigestingOutputStream; import org.session.libsignal.service.api.crypto.DigestingOutputStream;
@ -19,5 +18,4 @@ public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory
public DigestingOutputStream createFor(OutputStream wrap) throws IOException { public DigestingOutputStream createFor(OutputStream wrap) throws IOException {
return new AttachmentCipherOutputStream(key, wrap); return new AttachmentCipherOutputStream(key, wrap);
} }
} }

View File

@ -1,53 +0,0 @@
package org.session.libsignal.service.loki.api.crypto
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
interface SessionProtocol {
sealed class Exception(val description: String) : kotlin.Exception(description) {
// Encryption
object NoUserED25519KeyPair : Exception("Couldn't find user ED25519 key pair.")
object SigningFailed : Exception("Couldn't sign message.")
object EncryptionFailed : Exception("Couldn't encrypt message.")
// Decryption
object NoData : Exception("Received an empty envelope.")
object InvalidGroupPublicKey : Exception("Invalid group public key.")
object NoGroupKeyPair : Exception("Missing group key pair.")
object DecryptionFailed : Exception("Couldn't decrypt message.")
object InvalidSignature : Exception("Invalid message signature.")
}
/**
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
*
* @param ciphertext the data to decrypt.
* @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group.
*
* @return the padded plaintext.
*/
fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String>
}
object SessionProtocolUtilities {
fun decryptClosedGroupCiphertext(ciphertext: ByteArray, groupPublicKey: String, apiDB: LokiAPIDatabaseProtocol, sessionProtocolImpl: SessionProtocol): Pair<ByteArray, String> {
val encryptionKeyPairs = apiDB.getClosedGroupEncryptionKeyPairs(groupPublicKey).toMutableList()
if (encryptionKeyPairs.isEmpty()) { throw SessionProtocol.Exception.NoGroupKeyPair }
// Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than
// likely be the one we want) but try older ones in case that didn't work)
var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
fun decrypt(): Pair<ByteArray, String> {
try {
return sessionProtocolImpl.decrypt(ciphertext, encryptionKeyPair)
} catch(exception: Exception) {
if (encryptionKeyPairs.isNotEmpty()) {
encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
return decrypt()
} else {
throw exception
}
}
}
return decrypt()
}
}