Merge remote-tracking branch 'upstream/dev' into libsession-integration

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt
#	libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
This commit is contained in:
Morgan Pretty 2023-05-31 14:48:46 +10:00
commit 796fdc6d1b
32 changed files with 372 additions and 385 deletions

View File

@ -14,6 +14,7 @@ import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Conversions import org.session.libsession.utilities.Conversions
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
import org.session.libsignal.crypto.kdf.HKDFv3 import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -278,7 +279,7 @@ object FullBackupExporter {
return false return false
} }
private class BackupFrameOutputStream : Closeable, Flushable { private class BackupFrameOutputStream(outputStream: OutputStream, passphrase: String) : Closeable, Flushable {
private val outputStream: OutputStream private val outputStream: OutputStream
private var cipher: Cipher private var cipher: Cipher
@ -289,7 +290,7 @@ object FullBackupExporter {
private var counter: Int = 0 private var counter: Int = 0
constructor(outputStream: OutputStream, passphrase: String) : super() { init {
try { try {
val salt = Util.getSecretBytes(32) val salt = Util.getSecretBytes(32)
val key = BackupUtil.computeBackupKey(passphrase, salt) val key = BackupUtil.computeBackupKey(passphrase, salt)
@ -381,18 +382,24 @@ object FullBackupExporter {
private fun writeStream(inputStream: InputStream) { private fun writeStream(inputStream: InputStream) {
try { try {
Conversions.intToByteArray(iv, 0, counter++) Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) val remainder = synchronized(CIPHER_LOCK) {
mac.update(iv) cipher.init(
val buffer = ByteArray(8192) Cipher.ENCRYPT_MODE,
var read: Int SecretKeySpec(cipherKey, "AES"),
while (inputStream.read(buffer).also { read = it } != -1) { IvParameterSpec(iv)
val ciphertext = cipher.update(buffer, 0, read) )
if (ciphertext != null) { mac.update(iv)
outputStream.write(ciphertext) val buffer = ByteArray(8192)
mac.update(ciphertext) var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
val ciphertext = cipher.update(buffer, 0, read)
if (ciphertext != null) {
outputStream.write(ciphertext)
mac.update(ciphertext)
}
} }
cipher.doFinal()
} }
val remainder = cipher.doFinal()
outputStream.write(remainder) outputStream.write(remainder)
mac.update(remainder) mac.update(remainder)
val attachmentDigest = mac.doFinal() val attachmentDigest = mac.doFinal()
@ -414,8 +421,10 @@ object FullBackupExporter {
private fun write(out: OutputStream, frame: BackupFrame) { private fun write(out: OutputStream, frame: BackupFrame) {
try { try {
Conversions.intToByteArray(iv, 0, counter++) Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) val frameCiphertext = synchronized(CIPHER_LOCK) {
val frameCiphertext = cipher.doFinal(frame.toByteArray()) cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
cipher.doFinal(frame.toByteArray())
}
val frameMac = mac.doFinal(frameCiphertext) val frameMac = mac.doFinal(frameCiphertext)
val length = Conversions.intToByteArray(frameCiphertext.size + 10) val length = Conversions.intToByteArray(frameCiphertext.size + 10)
out.write(length) out.write(length)

View File

@ -12,6 +12,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Conversions import org.session.libsession.utilities.Conversions
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
import org.session.libsignal.crypto.kdf.HKDFv3 import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -243,7 +244,7 @@ object FullBackupImporter {
val split = ByteUtil.split(derived, 32, 32) val split = ByteUtil.split(derived, 32, 32)
cipherKey = split[0] cipherKey = split[0]
macKey = split[1] macKey = split[1]
cipher = Cipher.getInstance("AES/CTR/NoPadding") cipher = synchronized(CIPHER_LOCK) { Cipher.getInstance("AES/CTR/NoPadding") }
mac = Mac.getInstance("HmacSHA256") mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(macKey, "HmacSHA256")) mac.init(SecretKeySpec(macKey, "HmacSHA256"))
counter = Conversions.byteArrayToInt(iv) counter = Conversions.byteArrayToInt(iv)
@ -269,20 +270,26 @@ object FullBackupImporter {
var length = length var length = length
try { try {
Conversions.intToByteArray(iv, 0, counter++) Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) val plaintext = synchronized(CIPHER_LOCK) {
mac.update(iv) cipher.init(
val buffer = ByteArray(8192) Cipher.DECRYPT_MODE,
while (length > 0) { SecretKeySpec(cipherKey, "AES"),
val read = inputStream.read(buffer, 0, Math.min(buffer.size, length)) IvParameterSpec(iv)
if (read == -1) throw IOException("File ended early!") )
mac.update(buffer, 0, read) mac.update(iv)
val plaintext = cipher.update(buffer, 0, read) val buffer = ByteArray(8192)
if (plaintext != null) { while (length > 0) {
out.write(plaintext, 0, plaintext.size) val read = inputStream.read(buffer, 0, Math.min(buffer.size, length))
if (read == -1) throw IOException("File ended early!")
mac.update(buffer, 0, read)
val plaintext = cipher.update(buffer, 0, read)
if (plaintext != null) {
out.write(plaintext, 0, plaintext.size)
}
length -= read
} }
length -= read cipher.doFinal()
} }
val plaintext = cipher.doFinal()
if (plaintext != null) { if (plaintext != null) {
out.write(plaintext, 0, plaintext.size) out.write(plaintext, 0, plaintext.size)
} }
@ -325,8 +332,10 @@ object FullBackupImporter {
throw IOException("Bad MAC") throw IOException("Bad MAC")
} }
Conversions.intToByteArray(iv, 0, counter++) Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) val plaintext = synchronized(CIPHER_LOCK) {
val plaintext = cipher.doFinal(frame, 0, frame.size - 10) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
cipher.doFinal(frame, 0, frame.size - 10)
}
BackupFrame.parseFrom(plaintext) BackupFrame.parseFrom(plaintext)
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {

View File

@ -1,70 +0,0 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSeparatorBinding
import org.thoughtcrime.securesms.util.toPx
import org.session.libsession.utilities.ThemeUtil
class LabeledSeparatorView : RelativeLayout {
private lateinit var binding: ViewSeparatorBinding
private val path = Path()
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.STROKE
result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal)
result.strokeWidth = toPx(1, resources).toFloat()
result.isAntiAlias = true
result
}
// region Lifecycle
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()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
setUpViewHierarchy()
}
private fun setUpViewHierarchy() {
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(binding.root, layoutParams)
setWillNotDraw(false)
}
// endregion
// region Updating
override fun onDraw(c: Canvas) {
super.onDraw(c)
val w = width.toFloat()
val h = height.toFloat()
val hMargin = toPx(16, resources).toFloat()
path.reset()
path.moveTo(0.0f, h / 2)
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
path.moveTo(binding.titleTextView.right + hMargin, h / 2)
path.lineTo(w, h / 2)
path.close()
c.drawPath(path, paint)
}
// endregion
}

View File

@ -270,6 +270,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
var searchViewItem: MenuItem? = null var searchViewItem: MenuItem? = null
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private var emojiPickerVisible = false
private val isScrolledToBottom: Boolean private val isScrolledToBottom: Boolean
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
@ -521,17 +522,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
}) })
binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
showScrollToBottomButtonIfApplicable()
}
} }
// called from onCreate // called from onCreate
private fun setUpToolBar() { private fun setUpToolBar() {
setSupportActionBar(binding?.toolbar) val binding = binding ?: return
setSupportActionBar(binding.toolbar)
val actionBar = supportActionBar ?: return val actionBar = supportActionBar ?: return
val recipient = viewModel.recipient ?: return val recipient = viewModel.recipient ?: return
actionBar.title = "" actionBar.title = ""
actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setHomeButtonEnabled(true) actionBar.setHomeButtonEnabled(true)
binding!!.toolbarContent.conversationTitleView.text = when { binding.toolbarContent.conversationTitleView.text = when {
recipient.isLocalNumber -> getString(R.string.note_to_self) recipient.isLocalNumber -> getString(R.string.note_to_self)
else -> recipient.toShortString() else -> recipient.toShortString()
} }
@ -541,13 +547,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
R.dimen.small_profile_picture_size R.dimen.small_profile_picture_size
} }
val size = resources.getDimension(sizeID).roundToInt() val size = resources.getDimension(sizeID).roundToInt()
binding!!.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size) binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
binding!!.toolbarContent.profilePictureView.root.glide = glide binding.toolbarContent.profilePictureView.root.glide = glide
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
val profilePictureView = binding!!.toolbarContent.profilePictureView.root val profilePictureView = binding.toolbarContent.profilePictureView.root
viewModel.recipient?.let { recipient -> viewModel.recipient?.let(profilePictureView::update)
profilePictureView.update(recipient)
}
} }
// called from onCreate // called from onCreate
@ -986,8 +990,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val binding = binding ?: return val binding = binding ?: return
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
binding.typingIndicatorViewContainer.isVisible showScrollToBottomButtonIfApplicable()
showOrHideScrollToBottomButton()
val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: RecyclerView.NO_POSITION val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: RecyclerView.NO_POSITION
if (!firstLoad.get() && firstVisiblePosition != RecyclerView.NO_POSITION) { if (!firstLoad.get() && firstVisiblePosition != RecyclerView.NO_POSITION) {
if (firstVisiblePosition == 0) { if (firstVisiblePosition == 0) {
@ -1033,8 +1036,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
private fun showOrHideScrollToBottomButton(show: Boolean = true) { private fun showScrollToBottomButtonIfApplicable() {
binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0 binding?.scrollToBottomButton?.isVisible = !emojiPickerVisible && !isScrolledToBottom && adapter.itemCount > 0
} }
private fun updateUnreadCountIndicator() { private fun updateUnreadCountIndicator() {
@ -1206,21 +1209,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Log.e("Loki", "Failed to show emoji picker", e) Log.e("Loki", "Failed to show emoji picker", e)
return return
} }
val binding = binding ?: return
emojiPickerVisible = true
ViewUtil.hideKeyboard(this, visibleMessageView) ViewUtil.hideKeyboard(this, visibleMessageView)
binding?.reactionsShade?.isVisible = true binding.reactionsShade.isVisible = true
showOrHideScrollToBottomButton(false) binding.scrollToBottomButton.isVisible = false
binding?.conversationRecyclerView?.suppressLayout(true) binding.conversationRecyclerView.suppressLayout(true)
reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message)) reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message))
reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener { reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener {
override fun startHide() { override fun startHide() {
binding?.reactionsShade?.let { emojiPickerVisible = false
binding.reactionsShade.let {
ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE) ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE)
} }
showOrHideScrollToBottomButton(true) showScrollToBottomButtonIfApplicable()
} }
override fun onHide() { override fun onHide() {
binding?.conversationRecyclerView?.suppressLayout(false) binding.conversationRecyclerView.suppressLayout(false)
WindowUtil.setLightStatusBarFromTheme(this@ConversationActivityV2); WindowUtil.setLightStatusBarFromTheme(this@ConversationActivityV2);
WindowUtil.setLightNavigationBarFromTheme(this@ConversationActivityV2); WindowUtil.setLightNavigationBarFromTheme(this@ConversationActivityV2);

View File

@ -1,13 +1,13 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.os.Build; import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties; import android.security.keystore.KeyProperties;
import android.util.Base64; import android.util.Base64;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
@ -45,8 +45,6 @@ public final class KeyStoreHelper {
private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "SignalSecret"; private static final String KEY_ALIAS = "SignalSecret";
private static final Object lock = new Object();
public static SealedData seal(@NonNull byte[] input) { public static SealedData seal(@NonNull byte[] input) {
SecretKey secretKey = getOrCreateKeyStoreEntry(); SecretKey secretKey = getOrCreateKeyStoreEntry();
@ -54,7 +52,7 @@ public final class KeyStoreHelper {
// Cipher operations are not thread-safe so we synchronize over them through doFinal to // Cipher operations are not thread-safe so we synchronize over them through doFinal to
// prevent crashes with quickly repeated encrypt/decrypt operations // prevent crashes with quickly repeated encrypt/decrypt operations
// https://github.com/mozilla-mobile/android-components/issues/5342 // https://github.com/mozilla-mobile/android-components/issues/5342
synchronized (lock) { synchronized (CIPHER_LOCK) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.init(Cipher.ENCRYPT_MODE, secretKey);
@ -75,7 +73,7 @@ public final class KeyStoreHelper {
// Cipher operations are not thread-safe so we synchronize over them through doFinal to // Cipher operations are not thread-safe so we synchronize over them through doFinal to
// prevent crashes with quickly repeated encrypt/decrypt operations // prevent crashes with quickly repeated encrypt/decrypt operations
// https://github.com/mozilla-mobile/android-components/issues/5342 // https://github.com/mozilla-mobile/android-components/issues/5342
synchronized (lock) { synchronized (CIPHER_LOCK) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
@ -208,7 +206,5 @@ public final class KeyStoreHelper {
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING); return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
} }
} }
} }
} }

View File

@ -289,7 +289,7 @@ public class RecipientDatabase extends Database {
notifyRecipientListeners(); notifyRecipientListeners();
} }
public void setBlocked(@NonNull List<Recipient> recipients, boolean blocked) { public void setBlocked(@NonNull Iterable<Recipient> recipients, boolean blocked) {
SQLiteDatabase db = getWritableDatabase(); SQLiteDatabase db = getWritableDatabase();
db.beginTransaction(); db.beginTransaction();
try { try {

View File

@ -1572,7 +1572,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms)) DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms))
} }
override fun setBlocked(recipients: List<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) { override fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase() val recipientDb = DatabaseComponent.get(context).recipientDatabase()
recipientDb.setBlocked(recipients, isBlocked) recipientDb.setBlocked(recipients, isBlocked)
recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient -> recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient ->

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.logging; package org.thoughtcrime.securesms.logging;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.utilities.Conversions; import org.session.libsession.utilities.Conversions;
@ -66,15 +68,17 @@ class LogFile {
byte[] plaintext = entry.getBytes(); byte[] plaintext = entry.getBytes();
try { try {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); synchronized (CIPHER_LOCK) {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer));
int cipherLength = cipher.getOutputSize(plaintext.length); int cipherLength = cipher.getOutputSize(plaintext.length);
byte[] ciphertext = ciphertextBuffer.get(cipherLength); byte[] ciphertext = ciphertextBuffer.get(cipherLength);
cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
outputStream.write(ivBuffer); outputStream.write(ivBuffer);
outputStream.write(Conversions.intToByteArray(cipherLength)); outputStream.write(Conversions.intToByteArray(cipherLength));
outputStream.write(ciphertext, 0, cipherLength); outputStream.write(ciphertext, 0, cipherLength);
}
outputStream.flush(); outputStream.flush();
} catch (ShortBufferException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { } catch (ShortBufferException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
@ -134,10 +138,11 @@ class LogFile {
Util.readFully(inputStream, ciphertext, length); Util.readFully(inputStream, ciphertext, length);
try { try {
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); synchronized (CIPHER_LOCK) {
byte[] plaintext = cipher.doFinal(ciphertext, 0, length); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer));
byte[] plaintext = cipher.doFinal(ciphertext, 0, length);
return new String(plaintext); return new String(plaintext);
}
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.preferences
import android.app.AlertDialog import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.isVisible import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -11,58 +10,26 @@ import network.loki.messenger.databinding.ActivityBlockedContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
@AndroidEntryPoint @AndroidEntryPoint
class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener { class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
lateinit var binding: ActivityBlockedContactsBinding lateinit var binding: ActivityBlockedContactsBinding
val viewModel: BlockedContactsViewModel by viewModels() val viewModel: BlockedContactsViewModel by viewModels()
val adapter = BlockedContactsAdapter() val adapter: BlockedContactsAdapter by lazy { BlockedContactsAdapter(viewModel) }
override fun onClick(v: View?) { fun unblock() {
if (v === binding.unblockButton && adapter.getSelectedItems().isNotEmpty()) { // show dialog
val contactsToUnblock = adapter.getSelectedItems() val title = viewModel.getTitle(this)
// show dialog
val title = if (contactsToUnblock.size == 1) {
getString(R.string.Unblock_dialog__title_single, contactsToUnblock.first().name)
} else {
getString(R.string.Unblock_dialog__title_multiple)
}
val message = if (contactsToUnblock.size == 1) { val message = viewModel.getMessage(this)
getString(R.string.Unblock_dialog__message, contactsToUnblock.first().name)
} else {
val stringBuilder = StringBuilder()
val iterator = contactsToUnblock.iterator()
var numberAdded = 0
while (iterator.hasNext() && numberAdded < 3) {
val nextRecipient = iterator.next()
if (numberAdded > 0) stringBuilder.append(", ")
stringBuilder.append(nextRecipient.name)
numberAdded++
}
val overflow = contactsToUnblock.size - numberAdded
if (overflow > 0) {
stringBuilder.append(" ")
val string = resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow)
stringBuilder.append(string.format(overflow))
}
getString(R.string.Unblock_dialog__message, stringBuilder.toString())
}
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(title) .setTitle(title)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.continue_2) { d, _ -> .setPositiveButton(R.string.continue_2) { _, _ -> viewModel.unblock() }
viewModel.unblock(contactsToUnblock) .setNegativeButton(R.string.cancel) { _, _ -> }
d.dismiss() .show()
}
.setNegativeButton(R.string.cancel) { d, _ ->
d.dismiss()
}
.show()
}
} }
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
@ -73,15 +40,15 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnCli
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
viewModel.subscribe(this) viewModel.subscribe(this)
.observe(this) { newState -> .observe(this) { state ->
adapter.submitList(newState.blockedContacts) adapter.submitList(state.items)
val isEmpty = newState.blockedContacts.isEmpty() binding.emptyStateMessageTextView.isVisible = state.emptyStateMessageTextViewVisible
binding.emptyStateMessageTextView.isVisible = isEmpty binding.nonEmptyStateGroup.isVisible = state.nonEmptyStateGroupVisible
binding.nonEmptyStateGroup.isVisible = !isEmpty binding.unblockButton.isEnabled = state.unblockButtonEnabled
} }
binding.unblockButton.setOnClickListener(this) binding.unblockButton.setOnClickListener { unblock() }
} }
}
}

View File

@ -10,38 +10,30 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.BlockedContactLayoutBinding import network.loki.messenger.databinding.BlockedContactLayoutBinding
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.adapter.SelectableItem
class BlockedContactsAdapter: ListAdapter<Recipient,BlockedContactsAdapter.ViewHolder>(RecipientDiffer()) { typealias SelectableRecipient = SelectableItem<Recipient>
class RecipientDiffer: DiffUtil.ItemCallback<Recipient>() { class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdapter<SelectableRecipient,BlockedContactsAdapter.ViewHolder>(RecipientDiffer()) {
override fun areItemsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem === newItem
override fun areContentsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem == newItem class RecipientDiffer: DiffUtil.ItemCallback<SelectableRecipient>() {
override fun areItemsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.item.address == new.item.address
override fun areContentsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.isSelected == new.isSelected
override fun getChangePayload(old: SelectableRecipient, new: SelectableRecipient) = new.isSelected
} }
private val selectedItems = mutableListOf<Recipient>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
LayoutInflater.from(parent.context)
fun getSelectedItems() = selectedItems .inflate(R.layout.blocked_contact_layout, parent, false)
.let(::ViewHolder)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.blocked_contact_layout, parent, false)
return ViewHolder(itemView)
}
private fun toggleSelection(recipient: Recipient, isSelected: Boolean, position: Int) {
if (isSelected) {
selectedItems -= recipient
} else {
selectedItems += recipient
}
notifyItemChanged(position)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val recipient = getItem(position) holder.bind(getItem(position), viewModel::toggle)
val isSelected = recipient in selectedItems }
holder.bind(recipient, isSelected) {
toggleSelection(recipient, isSelected, position) override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
} if (payloads.isEmpty()) holder.bind(getItem(position), viewModel::toggle)
else holder.select(getItem(position).isSelected)
} }
override fun onViewRecycled(holder: ViewHolder) { override fun onViewRecycled(holder: ViewHolder) {
@ -54,15 +46,18 @@ class BlockedContactsAdapter: ListAdapter<Recipient,BlockedContactsAdapter.ViewH
val glide = GlideApp.with(itemView) val glide = GlideApp.with(itemView)
val binding = BlockedContactLayoutBinding.bind(itemView) val binding = BlockedContactLayoutBinding.bind(itemView)
fun bind(recipient: Recipient, isSelected: Boolean, toggleSelection: () -> Unit) { fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
binding.recipientName.text = recipient.name binding.recipientName.text = selectable.item.name
with (binding.profilePictureView.root) { with (binding.profilePictureView.root) {
glide = this@ViewHolder.glide glide = this@ViewHolder.glide
update(recipient) update(selectable.item)
} }
binding.root.setOnClickListener { toggleSelection() } binding.root.setOnClickListener { toggle(selectable) }
binding.selectButton.isSelected = selectable.isSelected
}
fun select(isSelected: Boolean) {
binding.selectButton.isSelected = isSelected binding.selectButton.isSelected = isSelected
} }
} }
}
}

View File

@ -17,9 +17,11 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.util.adapter.SelectableItem
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -29,7 +31,9 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage)
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED) private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
private val _contacts = MutableLiveData(BlockedContactsViewState(emptyList())) private val _state = MutableLiveData(BlockedContactsViewState())
val state get() = _state.value!!
fun subscribe(context: Context): LiveData<BlockedContactsViewState> { fun subscribe(context: Context): LiveData<BlockedContactsViewState> {
executor.launch(IO) { executor.launch(IO) {
@ -45,21 +49,74 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage)
} }
executor.launch(IO) { executor.launch(IO) {
for (update in listUpdateChannel) { for (update in listUpdateChannel) {
val blockedContactState = BlockedContactsViewState(storage.blockedContacts().sortedBy { it.name }) val blockedContactState = state.copy(
blockedContacts = storage.blockedContacts().sortedBy { it.name }
)
withContext(Main) { withContext(Main) {
_contacts.value = blockedContactState _state.value = blockedContactState
} }
} }
} }
return _contacts return _state
} }
fun unblock(toUnblock: List<Recipient>) { fun unblock() {
storage.setBlocked(toUnblock, false) storage.setBlocked(state.selectedItems, false)
_state.value = state.copy(selectedItems = emptySet())
}
fun select(selectedItem: Recipient, isSelected: Boolean) {
_state.value = state.run {
if (isSelected) copy(selectedItems = selectedItems + selectedItem)
else copy(selectedItems = selectedItems - selectedItem)
}
}
fun getTitle(context: Context): String =
if (state.selectedItems.size == 1) {
context.getString(R.string.Unblock_dialog__title_single, state.selectedItems.first().name)
} else {
context.getString(R.string.Unblock_dialog__title_multiple)
}
fun getMessage(context: Context): String {
if (state.selectedItems.size == 1) {
return context.getString(R.string.Unblock_dialog__message, state.selectedItems.first().name)
}
val stringBuilder = StringBuilder()
val iterator = state.selectedItems.iterator()
var numberAdded = 0
while (iterator.hasNext() && numberAdded < 3) {
val nextRecipient = iterator.next()
if (numberAdded > 0) stringBuilder.append(", ")
stringBuilder.append(nextRecipient.name)
numberAdded++
}
val overflow = state.selectedItems.size - numberAdded
if (overflow > 0) {
stringBuilder.append(" ")
val string = context.resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow)
stringBuilder.append(string.format(overflow))
}
return context.getString(R.string.Unblock_dialog__message, stringBuilder.toString())
}
fun toggle(selectable: SelectableItem<Recipient>) {
_state.value = state.run {
if (selectable.item in selectedItems) copy(selectedItems = selectedItems - selectable.item)
else copy(selectedItems = selectedItems + selectable.item)
}
} }
data class BlockedContactsViewState( data class BlockedContactsViewState(
val blockedContacts: List<Recipient> val blockedContacts: List<Recipient> = emptyList(),
) val selectedItems: Set<Recipient> = emptySet()
) {
val items = blockedContacts.map { SelectableItem(it, it in selectedItems) }
} val unblockButtonEnabled get() = selectedItems.isNotEmpty()
val emptyStateMessageTextViewVisible get() = blockedContacts.isEmpty()
val nonEmptyStateGroupVisible get() = blockedContacts.isNotEmpty()
}
}

View File

@ -0,0 +1,3 @@
package org.thoughtcrime.securesms.util.adapter
data class SelectableItem<T>(val item: T, val isSelected: Boolean)

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="?android:textColorTertiary"/>
<item android:color="@color/destructive"/>
</selector>

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape <ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/button_destructive">
android:shape="rectangle"> <item>
<shape android:shape="rectangle">
<solid android:color="@color/transparent" /> <solid android:color="?colorPrimary"/>
<corners android:radius="@dimen/medium_button_corner_radius" />
<corners android:radius="@dimen/medium_button_corner_radius" /> <stroke
android:color="@color/button_destructive"
<stroke android:width="@dimen/border_thickness" android:color="@color/destructive" /> android:width="@dimen/border_thickness" />
</shape> </shape>
</item>
</ripple>

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape <ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" android:color="?prominentButtonColor">
android:shape="rectangle"> <item>
<shape android:shape="rectangle">
<solid android:color="@color/transparent" /> <solid android:color="?colorPrimary"/>
<corners android:radius="@dimen/medium_button_corner_radius" />
<corners android:radius="@dimen/medium_button_corner_radius" /> <stroke
android:color="?prominentButtonColor"
<stroke android:width="@dimen/border_thickness" android:color="?prominentButtonColor" /> android:width="@dimen/border_thickness" />
</shape> </shape>
</item>
</ripple>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<stroke android:color="?colorDividerBackground" android:width="1dp"/>
<corners android:radius="16dp"/>
<solid android:color="?colorPrimary"/>
</shape>

View File

@ -4,28 +4,37 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
<androidx.recyclerview.widget.RecyclerView android:id="@+id/cardView"
android:id="@+id/recyclerView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/unblockButton" app:layout_constraintBottom_toTopOf="@+id/unblockButton"
android:layout_width="match_parent" app:cardCornerRadius="?preferenceCornerRadius"
android:layout_height="0dp" app:cardElevation="0dp"
android:background="@drawable/preference_single_no_padding" app:cardBackgroundColor="?colorSettingsBackground"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_marginHorizontal="@dimen/medium_spacing" android:layout_marginHorizontal="@dimen/medium_spacing"
android:layout_marginVertical="@dimen/large_spacing" android:layout_marginVertical="@dimen/large_spacing"
/> android:layout_width="match_parent"
android:layout_height="0dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.cardview.widget.CardView>
<TextView <TextView
android:id="@+id/emptyStateMessageTextView" android:id="@+id/emptyStateMessageTextView"
android:visibility="gone" android:visibility="gone"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@+id/recyclerView" app:layout_constraintTop_toTopOf="@+id/cardView"
android:layout_marginTop="@dimen/medium_spacing" android:layout_marginTop="@dimen/medium_spacing"
app:layout_constraintStart_toStartOf="@+id/recyclerView" app:layout_constraintStart_toStartOf="@+id/cardView"
app:layout_constraintEnd_toEndOf="@+id/recyclerView" app:layout_constraintEnd_toEndOf="@+id/cardView"
android:text="@string/blocked_contacts_empty_state" android:text="@string/blocked_contacts_empty_state"
/> />
@ -38,7 +47,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recyclerView" app:layout_constraintTop_toBottomOf="@+id/cardView"
android:id="@+id/unblockButton" android:id="@+id/unblockButton"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginVertical="@dimen/large_spacing" android:layout_marginVertical="@dimen/large_spacing"
@ -49,6 +58,6 @@
android:id="@+id/nonEmptyStateGroup" android:id="@+id/nonEmptyStateGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:constraint_referenced_ids="unblockButton,recyclerView"/> app:constraint_referenced_ids="unblockButton,cardView"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -61,7 +61,7 @@
</RelativeLayout> </RelativeLayout>
<org.thoughtcrime.securesms.components.LabeledSeparatorView <include layout="@layout/view_separator"
android:id="@+id/separatorView" android:id="@+id/separatorView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="32dp"

View File

@ -7,6 +7,7 @@
android:paddingHorizontal="@dimen/medium_spacing" android:paddingHorizontal="@dimen/medium_spacing"
android:paddingVertical="@dimen/small_spacing" android:paddingVertical="@dimen/small_spacing"
android:gravity="center_vertical" android:gravity="center_vertical"
android:background="?selectableItemBackground"
android:id="@+id/backgroundContainer"> android:id="@+id/backgroundContainer">
<include layout="@layout/view_profile_picture" <include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView" android:id="@+id/profilePictureView"

View File

@ -70,7 +70,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/promptTextView"> app:layout_constraintTop_toBottomOf="@id/promptTextView">
<org.thoughtcrime.securesms.components.LabeledSeparatorView <include layout="@layout/view_separator"
android:id="@+id/separatorView" android:id="@+id/separatorView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="32dp"

View File

@ -1,17 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<TextView <View
android:id="@+id/titleTextView" android:layout_gravity="center"
android:background="?colorDividerBackground"
android:layout_width="match_parent"
android:layout_height="1dp"/>
<FrameLayout
android:layout_gravity="center"
android:background="@drawable/view_separator"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_centerInParent="true"
android:gravity="center"
android:textColor="?android:textColorTertiary"
android:textSize="@dimen/small_font_size"
android:text="@string/your_session_id" />
</RelativeLayout> <TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?android:textColorTertiary"
android:textSize="@dimen/small_font_size"
android:text="@string/your_session_id" />
</FrameLayout>
</FrameLayout>

View File

@ -112,7 +112,7 @@
<style name="Widget.Session.Button.Common.DestructiveOutline"> <style name="Widget.Session.Button.Common.DestructiveOutline">
<item name="android:background">@drawable/destructive_outline_button_medium_background</item> <item name="android:background">@drawable/destructive_outline_button_medium_background</item>
<item name="android:textColor">@color/destructive</item> <item name="android:textColor">@color/button_destructive</item>
<item name="android:drawableTint">?android:textColorPrimary</item> <item name="android:drawableTint">?android:textColorPrimary</item>
</style> </style>

View File

@ -217,7 +217,7 @@ interface StorageProtocol {
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean) fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
fun deleteReactions(messageId: Long, mms: Boolean) fun deleteReactions(messageId: Long, mms: Boolean)
fun setBlocked(recipients: List<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false) fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
fun setRecipientHash(recipient: Recipient, recipientHash: String?) fun setRecipientHash(recipient: Recipient, recipientHash: String?)
fun blockedContacts(): List<Recipient> fun blockedContacts(): List<Recipient>

View File

@ -1,6 +1,7 @@
package org.session.libsession.utilities package org.session.libsession.utilities
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Util import org.session.libsignal.utilities.Util
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
@ -27,9 +28,11 @@ internal object AESGCM {
internal fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray { internal fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray {
val iv = ivAndCiphertext.sliceArray(0 until ivSize) val iv = ivAndCiphertext.sliceArray(0 until ivSize)
val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count()) val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count())
val cipher = Cipher.getInstance("AES/GCM/NoPadding") synchronized(CIPHER_LOCK) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) val cipher = Cipher.getInstance("AES/GCM/NoPadding")
return cipher.doFinal(ciphertext) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
return cipher.doFinal(ciphertext)
}
} }
/** /**
@ -47,9 +50,11 @@ internal object AESGCM {
*/ */
internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray { internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray {
val iv = Util.getSecretBytes(ivSize) val iv = Util.getSecretBytes(ivSize)
val cipher = Cipher.getInstance("AES/GCM/NoPadding") synchronized(CIPHER_LOCK) {
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) val cipher = Cipher.getInstance("AES/GCM/NoPadding")
return ByteUtil.combine(iv, cipher.doFinal(plaintext)) cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
return ByteUtil.combine(iv, cipher.doFinal(plaintext))
}
} }
/** /**

View File

@ -5,6 +5,7 @@ import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
@ColorInt @ColorInt
fun Context.getColorFromAttr( fun Context.getColorFromAttr(
@ -17,4 +18,4 @@ fun Context.getColorFromAttr(
} }
val RecyclerView.isScrolledToBottom: Boolean val RecyclerView.isScrolledToBottom: Boolean
get() = computeVerticalScrollOffset() + computeVerticalScrollExtent() >= computeVerticalScrollRange() get() = max(0, computeVerticalScrollOffset()) + computeVerticalScrollExtent() >= computeVerticalScrollRange()

View File

@ -0,0 +1,8 @@
package org.session.libsignal.crypto;
public class CipherUtil {
// Cipher operations are not thread-safe so we synchronize over them through doFinal to
// prevent crashes with quickly repeated encrypt/decrypt operations
// https://github.com/mozilla-mobile/android-components/issues/5342
public static final Object CIPHER_LOCK = new Object();
}

View File

@ -1,45 +0,0 @@
package org.session.libsignal.crypto
import org.whispersystems.curve25519.Curve25519
import org.session.libsignal.utilities.Util
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object DiffieHellman {
private val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
private val curve = Curve25519.getInstance(Curve25519.BEST)
private val ivSize = 16
@JvmStatic @Throws
fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray {
val iv = Util.getSecretBytes(ivSize)
val ivSpec = IvParameterSpec(iv)
val secretKeySpec = SecretKeySpec(symmetricKey, "AES")
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec)
val ciphertext = cipher.doFinal(plaintext)
return iv + ciphertext
}
@JvmStatic @Throws
fun encrypt(plaintext: ByteArray, publicKey: ByteArray, privateKey: ByteArray): ByteArray {
val symmetricKey = curve.calculateAgreement(publicKey, privateKey)
return encrypt(plaintext, symmetricKey)
}
@JvmStatic @Throws
fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray {
val iv = ivAndCiphertext.sliceArray(0 until ivSize)
val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.size)
val ivSpec = IvParameterSpec(iv)
val secretKeySpec = SecretKeySpec(symmetricKey, "AES")
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec)
return cipher.doFinal(ciphertext)
}
@JvmStatic @Throws
fun decrypt(ivAndCiphertext: ByteArray, publicKey: ByteArray, privateKey: ByteArray): ByteArray {
val symmetricKey = curve.calculateAgreement(publicKey, privateKey)
return decrypt(ivAndCiphertext, symmetricKey)
}
}

View File

@ -39,9 +39,7 @@ public abstract class HKDF {
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(salt, "HmacSHA256")); mac.init(new SecretKeySpec(salt, "HmacSHA256"));
return mac.doFinal(inputKeyMaterial); return mac.doFinal(inputKeyMaterial);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@ -73,9 +71,7 @@ public abstract class HKDF {
} }
return results.toByteArray(); return results.toByteArray();
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }

View File

@ -6,6 +6,8 @@
package org.session.libsignal.streams; package org.session.libsignal.streams;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import org.session.libsignal.exceptions.InvalidMacException; import org.session.libsignal.exceptions.InvalidMacException;
import org.session.libsignal.exceptions.InvalidMessageException; import org.session.libsignal.exceptions.InvalidMessageException;
import org.session.libsignal.utilities.Util; import org.session.libsignal.utilities.Util;
@ -92,19 +94,15 @@ public class AttachmentCipherInputStream extends FilterInputStream {
byte[] iv = new byte[BLOCK_SIZE]; byte[] iv = new byte[BLOCK_SIZE];
readFully(iv); readFully(iv);
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); synchronized (CIPHER_LOCK) {
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv)); this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
}
this.done = false; this.done = false;
this.totalRead = 0; this.totalRead = 0;
this.totalDataSize = totalDataSize; this.totalDataSize = totalDataSize;
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@ -141,15 +139,12 @@ public class AttachmentCipherInputStream extends FilterInputStream {
private int readFinal(byte[] buffer, int offset, int length) throws IOException { private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try { try {
int flourish = cipher.doFinal(buffer, offset); synchronized (CIPHER_LOCK) {
int flourish = cipher.doFinal(buffer, offset);
done = true; done = true;
return flourish; return flourish;
} catch (IllegalBlockSizeException e) { }
throw new IOException(e); } catch (IllegalBlockSizeException | ShortBufferException | BadPaddingException e) {
} catch (BadPaddingException e) {
throw new IOException(e);
} catch (ShortBufferException e) {
throw new IOException(e); throw new IOException(e);
} }
} }
@ -234,9 +229,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
throw new InvalidMacException("Digest doesn't match!"); throw new InvalidMacException("Digest doesn't match!");
} }
} catch (IOException e) { } catch (IOException | ArithmeticException e) {
throw new InvalidMacException(e);
} catch (ArithmeticException e) {
throw new InvalidMacException(e); throw new InvalidMacException(e);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new AssertionError(e); throw new AssertionError(e);

View File

@ -6,6 +6,8 @@
package org.session.libsignal.streams; package org.session.libsignal.streams;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import org.session.libsignal.utilities.Util; import org.session.libsignal.utilities.Util;
import java.io.IOException; import java.io.IOException;
@ -68,16 +70,17 @@ public class AttachmentCipherOutputStream extends DigestingOutputStream {
@Override @Override
public void flush() throws IOException { public void flush() throws IOException {
try { try {
byte[] ciphertext = cipher.doFinal(); byte[] ciphertext;
synchronized (CIPHER_LOCK) {
ciphertext = cipher.doFinal();
}
byte[] auth = mac.doFinal(ciphertext); byte[] auth = mac.doFinal(ciphertext);
super.write(ciphertext); super.write(ciphertext);
super.write(auth); super.write(auth);
super.flush(); super.flush();
} catch (IllegalBlockSizeException e) { } catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@ -97,9 +100,7 @@ public class AttachmentCipherOutputStream extends DigestingOutputStream {
private Cipher initializeCipher() { private Cipher initializeCipher() {
try { try {
return Cipher.getInstance("AES/CBC/PKCS5Padding"); return Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }

View File

@ -1,5 +1,7 @@
package org.session.libsignal.streams; package org.session.libsignal.streams;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import org.session.libsignal.utilities.Util; import org.session.libsignal.utilities.Util;
import java.io.FilterInputStream; import java.io.FilterInputStream;
@ -62,23 +64,23 @@ public class ProfileCipherInputStream extends FilterInputStream {
byte[] ciphertext = new byte[outputLength / 2]; byte[] ciphertext = new byte[outputLength / 2];
int read = in.read(ciphertext, 0, ciphertext.length); int read = in.read(ciphertext, 0, ciphertext.length);
if (read == -1) { synchronized (CIPHER_LOCK) {
if (cipher.getOutputSize(0) > outputLength) { if (read == -1) {
throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength); if (cipher.getOutputSize(0) > outputLength) {
} throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
}
finished = true; finished = true;
return cipher.doFinal(output, outputOffset); return cipher.doFinal(output, outputOffset);
} else { } else {
if (cipher.getOutputSize(read) > outputLength) { if (cipher.getOutputSize(read) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength); throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
} }
return cipher.update(ciphertext, 0, read, output, outputOffset); return cipher.update(ciphertext, 0, read, output, outputOffset);
}
} }
} catch (IllegalBlockSizeException e) { } catch (IllegalBlockSizeException | ShortBufferException e) {
throw new AssertionError(e);
} catch(ShortBufferException e) {
throw new AssertionError(e); throw new AssertionError(e);
} catch (BadPaddingException e) { } catch (BadPaddingException e) {
throw new IOException(e); throw new IOException(e);

View File

@ -1,5 +1,7 @@
package org.session.libsignal.streams; package org.session.libsignal.streams;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
@ -54,20 +56,24 @@ public class ProfileCipherOutputStream extends DigestingOutputStream {
byte[] input = new byte[1]; byte[] input = new byte[1];
input[0] = (byte)b; input[0] = (byte)b;
byte[] output = cipher.update(input); byte[] output;
synchronized (CIPHER_LOCK) {
output = cipher.update(input);
}
super.write(output); super.write(output);
} }
@Override @Override
public void flush() throws IOException { public void flush() throws IOException {
try { try {
byte[] output = cipher.doFinal(); byte[] output;
synchronized (CIPHER_LOCK) {
output = cipher.doFinal();
}
super.write(output); super.write(output);
super.flush(); super.flush();
} catch (BadPaddingException e) { } catch (BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }