mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
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:
commit
796fdc6d1b
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 ->
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package org.thoughtcrime.securesms.util.adapter
|
||||||
|
|
||||||
|
data class SelectableItem<T>(val item: T, val isSelected: Boolean)
|
5
app/src/main/res/color/button_destructive.xml
Normal file
5
app/src/main/res/color/button_destructive.xml
Normal 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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
6
app/src/main/res/drawable/view_separator.xml
Normal file
6
app/src/main/res/drawable/view_separator.xml
Normal 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>
|
@ -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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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()
|
||||||
|
@ -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();
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user