diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt index 7c211a6bc7..e6289c39cb 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt @@ -18,6 +18,7 @@ import com.bumptech.glide.RequestManager import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState import org.thoughtcrime.securesms.contactshare.Contact @@ -326,7 +327,7 @@ class V2ConversationItemShapeTest { override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit - override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit + override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) = Unit override fun onItemClick(item: MultiselectPart?) = Unit diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallLinkTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallLinkTableTest.kt index ff49dedf11..0a22616dda 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallLinkTableTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallLinkTableTest.kt @@ -81,7 +81,8 @@ class CallLinkTableTest { roomId = CallLinkRoomId.fromBytes(roomId), credentials = CallLinkCredentials( linkKeyBytes = roomId, - adminPassBytes = null + adminPassBytes = null, + epochBytes = null ), state = SignalCallLinkState(), deletionTimestamp = 0L diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt index 86158fef32..906654ca04 100644 --- a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt @@ -18,6 +18,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.logging.Log +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate @@ -292,7 +293,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } - override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) { + override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) { Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index b7bdf907d1..8f8feddfec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -11,6 +11,7 @@ import androidx.lifecycle.Observer; import com.bumptech.glide.RequestManager; +import org.signal.ringrtc.CallLinkEpoch; import org.signal.ringrtc.CallLinkRootKey; import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState; import org.thoughtcrime.securesms.contactshare.Contact; @@ -133,7 +134,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args); void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage); void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks); - void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey); + void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey, @Nullable CallLinkEpoch callLinkEpoch); void onShowSafetyTips(boolean forGroup); void onReportSpamLearnMoreClicked(); void onMessageRequestAcceptOptionsClicked(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/CallLinkArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/CallLinkArchiveExporter.kt index 41d06eb952..72c4631088 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/CallLinkArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/CallLinkArchiveExporter.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.backup.v2.database import android.database.Cursor +import okio.ByteString import okio.ByteString.Companion.toByteString import org.signal.core.util.nullIfEmpty import org.signal.ringrtc.CallLinkState @@ -40,6 +41,7 @@ class CallLinkArchiveExporter(private val cursor: Cursor) : Iterator { return Observable.create { emitter -> @@ -60,8 +69,13 @@ object CallLinks { return url.split("#").last().startsWith("key=") } + data class CallLinkParseResult( + val rootKey: CallLinkRootKey, + val epoch: CallLinkEpoch? + ) + @JvmStatic - fun parseUrl(url: String): CallLinkRootKey? { + fun parseUrl(url: String): CallLinkParseResult? { if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) { Log.w(TAG, "Invalid url prefix.") return null @@ -73,18 +87,33 @@ object CallLinks { return null } - val fragment = parts[1] - val fragmentParts = fragment.split("&") - val fragmentQuery = fragmentParts.associate { - val kv = it.split("=") - if (kv.size != 2) { - Log.w(TAG, "Invalid fragment keypair. Skipping.") + val fragmentQuery = mutableMapOf() + + try { + for (part in parts[1].split("&")) { + val kv = part.split("=") + // Make sure we don't have an empty key (i.e. handle the case + // of "a=0&&b=0", for example) + if (kv[0].isEmpty()) { + Log.w(TAG, "Invalid url: $url (empty key)") + return null + } + val key = URLDecoder.decode(kv[0], "utf8") + val value = when (kv.size) { + 1 -> null + 2 -> URLDecoder.decode(kv[1], "utf8") + else -> { + // Cannot have more than one value per key (i.e. handle the case + // of "a=0&b=0=1=2", for example. + Log.w(TAG, "Invalid url: $url (multiple values)") + return null + } + } + fragmentQuery += key to value } - - val key = URLDecoder.decode(kv[0], "utf8") - val value = URLDecoder.decode(kv[1], "utf8") - - key to value + } catch (_: UnsupportedEncodingException) { + Log.w(TAG, "Invalid url: $url") + return null } val key = fragmentQuery[ROOT_KEY] @@ -94,9 +123,13 @@ object CallLinks { } return try { - CallLinkRootKey(key) + val epoch = fragmentQuery[EPOCH]?.let { s -> CallLinkEpoch(s) } + CallLinkParseResult( + rootKey = CallLinkRootKey(key), + epoch = epoch + ) } catch (e: CallException) { - Log.w(TAG, "Invalid root key found in fragment query string.") + Log.w(TAG, "Invalid root key or epoch found in fragment query string.") null } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/SignalCallRow.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/SignalCallRow.kt index 8557db71d5..e9e571d5bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/SignalCallRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/SignalCallRow.kt @@ -53,7 +53,7 @@ import org.signal.core.ui.R as CoreUiR @Composable private fun SignalCallRowPreview() { val callLink = remember { - val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8)) + val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(0, 1, 2, 3), byteArrayOf(5, 6, 7, 8)) CallLinkTable.CallLink( recipientId = RecipientId.UNKNOWN, roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 3, 5, 7)), @@ -97,7 +97,7 @@ fun SignalCallRow( "https://signal.call.example.com" } else { remember(callLink.credentials) { - callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: "" + callLink.credentials?.let { CallLinks.url(it.linkKeyBytes, it.epochBytes) } ?: "" } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt index 469c20f56e..16dde42cb7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt @@ -163,7 +163,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment startActivity( ShareActivity.sendSimpleText( requireContext(), - getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes)) + getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes)) ) ) } @@ -177,7 +177,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = { when (it) { is EnsureCallLinkCreatedResult.Success -> { - Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes)) + Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes)) Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show() } @@ -192,7 +192,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment is EnsureCallLinkCreatedResult.Success -> { val mimeType = Intent.normalizeMimeType("text/plain") val shareIntent = ShareCompat.IntentBuilder(requireContext()) - .setText(CallLinks.url(viewModel.linkKeyBytes)) + .setText(CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes)) .setType(mimeType) .createChooserIntent() diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt index 87f382e833..71443dce38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt @@ -31,12 +31,12 @@ class CreateCallLinkViewModel( private val repository: CreateCallLinkRepository = CreateCallLinkRepository(), private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository() ) : ViewModel() { - private val credentials = CallLinkCredentials.generate() + private val initialCredentials = CallLinkCredentials.generate() private val _callLink: MutableState = mutableStateOf( CallLinkTable.CallLink( recipientId = RecipientId.UNKNOWN, - roomId = credentials.roomId, - credentials = credentials, + roomId = initialCredentials.roomId, + credentials = initialCredentials, state = SignalCallLinkState( name = "", restrictions = Restrictions.ADMIN_APPROVAL, @@ -48,7 +48,12 @@ class CreateCallLinkViewModel( ) val callLink: State = _callLink - val linkKeyBytes: ByteArray = credentials.linkKeyBytes + + val linkKeyBytes: ByteArray + get() = callLink.value.credentials!!.linkKeyBytes + + val epochBytes: ByteArray? + get() = callLink.value.credentials!!.epochBytes private val internalShowAlreadyInACall = MutableStateFlow(false) val showAlreadyInACall: StateFlow = internalShowAlreadyInACall @@ -59,7 +64,7 @@ class CreateCallLinkViewModel( private val disposables = CompositeDisposable() init { - disposables += CallLinks.watchCallLink(credentials.roomId) + disposables += CallLinks.watchCallLink(initialCredentials.roomId) .subscribeBy { _callLink.value = it } @@ -75,7 +80,7 @@ class CreateCallLinkViewModel( } fun commitCallLink(): Single { - return repository.ensureCallLinkCreated(credentials) + return repository.ensureCallLinkCreated(initialCredentials) .observeOn(AndroidSchedulers.mainThread()) } @@ -84,7 +89,7 @@ class CreateCallLinkViewModel( .flatMap { when (it) { is EnsureCallLinkCreatedResult.Success -> mutationRepository.setCallRestrictions( - credentials, + callLink.value.credentials!!, if (approveAllMembers) Restrictions.ADMIN_APPROVAL else Restrictions.NONE ) is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status)) @@ -104,7 +109,7 @@ class CreateCallLinkViewModel( .flatMap { when (it) { is EnsureCallLinkCreatedResult.Success -> mutationRepository.setCallName( - credentials, + callLink.value.credentials!!, callName ) is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt index ec7c2ba1aa..7cffe2c405 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt @@ -118,7 +118,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback { override fun onShareClicked() { val mimeType = Intent.normalizeMimeType("text/plain") val shareIntent = ShareCompat.IntentBuilder(requireContext()) - .setText(CallLinks.url(viewModel.rootKeySnapshot)) + .setText(CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot)) .setType(mimeType) .createChooserIntent() @@ -130,7 +130,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback { } override fun onCopyClicked() { - Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.rootKeySnapshot)) + Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot)) Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show() } @@ -138,7 +138,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback { startActivity( ShareActivity.sendSimpleText( requireContext(), - getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot)) + getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot)) ) ) } @@ -230,6 +230,7 @@ private fun CallLinkDetailsPreview() { val callLink = remember { val credentials = CallLinkCredentials( byteArrayOf(1, 2, 3, 4), + byteArrayOf(0, 1, 2, 3), byteArrayOf(3, 4, 5, 6) ) CallLinkTable.CallLink( diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt index 93863da59d..ac713cb19a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt @@ -40,6 +40,9 @@ class CallLinkDetailsViewModel( val rootKeySnapshot: ByteArray get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.") + val epochSnapshot: ByteArray? + get() = state.value.callLink?.credentials?.epochBytes + private val recipientSubject = BehaviorSubject.create() val recipientSnapshot: Recipient? get() = recipientSubject.value diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java index 93a1eee5e3..cbec416a70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java @@ -172,11 +172,11 @@ public class LinkPreviewView extends FrameLayout { spinner.setVisibility(GONE); noPreview.setVisibility(GONE); - CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl()); + CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(linkPreview.getUrl()); if (!Util.isEmpty(linkPreview.getTitle())) { title.setText(linkPreview.getTitle()); title.setVisibility(VISIBLE); - } else if (callLinkRootKey != null) { + } else if (linkParseResult != null) { title.setText(R.string.Recipient_signal_call); title.setVisibility(VISIBLE); } else { @@ -186,7 +186,7 @@ public class LinkPreviewView extends FrameLayout { if (showDescription && !Util.isEmpty(linkPreview.getDescription())) { description.setText(linkPreview.getDescription()); description.setVisibility(VISIBLE); - } else if (callLinkRootKey != null) { + } else if (linkParseResult != null) { description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call); description.setVisibility(VISIBLE); } else { @@ -221,14 +221,14 @@ public class LinkPreviewView extends FrameLayout { thumbnail.get().setImageResource(requestManager, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false); thumbnail.get().showSecondaryText(false); thumbnail.get().setOutlineEnabled(true); - } else if (callLinkRootKey != null) { + } else if (linkParseResult != null) { thumbnail.setVisibility(VISIBLE); thumbnailState.applyState(thumbnail); thumbnail.get().setImageDrawable( requestManager, new FallbackAvatarDrawable( getContext(), - new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes())) + new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(linkParseResult.getRootKey().getKeyBytes())) ).circleCrop() ); thumbnail.get().showSecondaryText(false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoViewModel.kt index f997668f94..195727c3aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoViewModel.kt @@ -34,6 +34,8 @@ class ControlsAndInfoViewModel( val rootKeySnapshot: ByteArray get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.") + val epochSnapshot: ByteArray? + get() = state.value.callLink?.credentials?.epochBytes fun setRecipient(recipient: Recipient) { if (recipient.isCallLink && callRecipientId != recipient.id) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallInfoCallbacks.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallInfoCallbacks.kt index 82e1c7c6e2..b432721980 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallInfoCallbacks.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallInfoCallbacks.kt @@ -31,7 +31,7 @@ class CallInfoCallbacks( override fun onShareLinkClicked() { val mimeType = Intent.normalizeMimeType("text/plain") val shareIntent = ShareCompat.IntentBuilder(activity) - .setText(CallLinks.url(controlsAndInfoViewModel.rootKeySnapshot)) + .setText(CallLinks.url(controlsAndInfoViewModel.rootKeySnapshot, controlsAndInfoViewModel.epochSnapshot)) .setType(mimeType) .createChooserIntent() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index fc610885f1..4a8b2b180a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -1172,14 +1172,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo //noinspection ConstantConditions LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); - CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl()); - if (callLinkRootKey != null) { + CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(linkPreview.getUrl()); + if (linkParseResult != null) { joinCallLinkStub.setVisibility(View.VISIBLE); joinCallLinkStub.get().setTextColor(ContextCompat.getColor(context, messageRecord.isOutgoing() ? R.color.signal_light_colorOnPrimary : R.color.signal_colorOnPrimaryContainer)); joinCallLinkStub.get().setBackgroundColor(ContextCompat.getColor(context, messageRecord.isOutgoing() ? R.color.signal_light_colorTransparent2 : R.color.signal_colorOnPrimary)); joinCallLinkStub.get().setOnClickListener(v -> { if (eventListener != null) { - eventListener.onJoinCallLink(callLinkRootKey); + eventListener.onJoinCallLink(linkParseResult.getRootKey(), linkParseResult.getEpoch()); } }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index d4d31e8eac..4367c0b661 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -103,6 +103,7 @@ import org.signal.core.util.logging.Log import org.signal.core.util.orNull import org.signal.core.util.setActionItemTint import org.signal.donations.InAppPaymentType +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey import org.thoughtcrime.securesms.BlockUnblockDialog import org.thoughtcrime.securesms.GroupMembersDialog @@ -3365,8 +3366,8 @@ class ConversationFragment : GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks) } - override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) { - CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey) { + override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) { + CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey, callLinkEpoch) { YouAreAlreadyInACallSnackbar.show(requireView()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt index dd4e8052e4..8b3eae1690 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt @@ -21,6 +21,7 @@ import org.signal.core.util.requireNonNullString import org.signal.core.util.select import org.signal.core.util.update import org.signal.core.util.withinTransaction +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey import org.signal.ringrtc.CallLinkState.Restrictions import org.thoughtcrime.securesms.calls.log.CallLogRow @@ -47,6 +48,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database const val TABLE_NAME = "call_link" const val ID = "_id" const val ROOT_KEY = "root_key" + const val EPOCH = "epoch" const val ROOM_ID = "room_id" const val ADMIN_KEY = "admin_key" const val NAME = "name" @@ -61,6 +63,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database CREATE TABLE $TABLE_NAME ( $ID INTEGER PRIMARY KEY, $ROOT_KEY BLOB, + $EPOCH BLOB, $ROOM_ID TEXT NOT NULL UNIQUE, $ADMIN_KEY BLOB, $NAME TEXT NOT NULL, @@ -128,6 +131,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database .values( contentValuesOf( ROOT_KEY to credentials.linkKeyBytes, + EPOCH to credentials.epochBytes, ADMIN_KEY to credentials.adminPassBytes ) ) @@ -188,7 +192,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database } fun getOrCreateCallLinkByRootKey( - callLinkRootKey: CallLinkRootKey + callLinkRootKey: CallLinkRootKey, + callLinkEpoch: CallLinkEpoch? ): CallLink { val roomId = CallLinkRoomId.fromBytes(callLinkRootKey.deriveRoomId()) val callLink = getCallLinkByRoomId(roomId) @@ -198,6 +203,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database roomId = roomId, credentials = CallLinkCredentials( linkKeyBytes = callLinkRootKey.keyBytes, + epochBytes = callLinkEpoch?.bytes, adminPassBytes = null ), state = SignalCallLinkState(), @@ -207,12 +213,26 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database insertCallLink(link) return getCallLinkByRoomId(roomId)!! } else { - callLink + if (callLink.credentials?.epoch != callLinkEpoch) { + overwriteEpoch(callLink, callLinkEpoch) + } else { + callLink + } } } + private fun overwriteEpoch(callLink: CallLink, callLinkEpoch: CallLinkEpoch?): CallLink { + val modifiedCallLink = callLink.copy( + deletionTimestamp = 0, + credentials = callLink.credentials!!.copy(epochBytes = callLinkEpoch?.bytes) + ) + updateCallLinkCredentials(modifiedCallLink.roomId, modifiedCallLink.credentials!!) + return modifiedCallLink + } + fun insertOrUpdateCallLinkByRootKey( callLinkRootKey: CallLinkRootKey, + callLinkEpoch: CallLinkEpoch?, adminPassKey: ByteArray?, deletionTimestamp: Long = 0L, storageId: StorageId? = null @@ -227,6 +247,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database roomId = roomId, credentials = CallLinkCredentials( linkKeyBytes = callLinkRootKey.keyBytes, + epochBytes = callLinkEpoch?.bytes, adminPassBytes = adminPassKey ), state = SignalCallLinkState(), @@ -253,7 +274,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database writableDatabase.update(TABLE_NAME) .values( ADMIN_KEY to adminPassKey, - ROOT_KEY to callLinkRootKey.keyBytes + ROOT_KEY to callLinkRootKey.keyBytes, + EPOCH to callLinkEpoch?.bytes ) .where("$ROOM_ID = ?", callLink.roomId.serialize()) .run() @@ -464,6 +486,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database RECIPIENT_ID to data.recipientId.takeIf { it != RecipientId.UNKNOWN }?.toLong(), ROOM_ID to data.roomId.serialize(), ROOT_KEY to data.credentials?.linkKeyBytes, + EPOCH to data.credentials?.epochBytes, ADMIN_KEY to data.credentials?.adminPassBytes ).apply { putAll(data.state.serialize()) @@ -487,6 +510,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database credentials = data.requireBlob(ROOT_KEY)?.let { linkKey -> CallLinkCredentials( linkKeyBytes = linkKey, + epochBytes = data.requireBlob(EPOCH), adminPassBytes = data.requireBlob(ADMIN_KEY) ) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 2c8c6cb8b3..1bf07d2a4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -139,6 +139,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V281_RemoveArchiveT import org.thoughtcrime.securesms.database.helpers.migration.V282_AddSnippetMessageIdColumnToThreadTable import org.thoughtcrime.securesms.database.helpers.migration.V283_ViewOnceRemoteDataCleanup import org.thoughtcrime.securesms.database.helpers.migration.V284_SetPlaceholderGroupFlag +import org.thoughtcrime.securesms.database.helpers.migration.V285_AddEpochToCallLinksTable import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase /** @@ -283,10 +284,11 @@ object SignalDatabaseMigrations { 281 to V281_RemoveArchiveTransferFile, 282 to V282_AddSnippetMessageIdColumnToThreadTable, 283 to V283_ViewOnceRemoteDataCleanup, - 284 to V284_SetPlaceholderGroupFlag + 284 to V284_SetPlaceholderGroupFlag, + 285 to V285_AddEpochToCallLinksTable ) - const val DATABASE_VERSION = 284 + const val DATABASE_VERSION = 285 @JvmStatic fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V285_AddEpochToCallLinksTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V285_AddEpochToCallLinksTable.kt new file mode 100644 index 0000000000..f27c8a951a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V285_AddEpochToCallLinksTable.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import org.thoughtcrime.securesms.database.SQLiteDatabase + +@Suppress("ClassName") +object V285_AddEpochToCallLinksTable : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE call_link ADD COLUMN epoch BLOB DEFAULT NULL") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshCallLinkDetailsJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshCallLinkDetailsJob.kt index 8eee7c2186..7081b7409f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshCallLinkDetailsJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshCallLinkDetailsJob.kt @@ -48,6 +48,7 @@ class RefreshCallLinkDetailsJob private constructor( val manager: SignalCallLinkManager = AppDependencies.signalCallManager.callLinkManager val credentials = CallLinkCredentials( linkKeyBytes = callLinkUpdate.rootKey!!.toByteArray(), + epochBytes = callLinkUpdate.epoch?.toByteArray(), adminPassBytes = callLinkUpdate.adminPasskey?.toByteArray() ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 1e9324a970..017a0f270b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -20,6 +20,7 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; +import org.signal.ringrtc.CallLinkEpoch; import org.signal.ringrtc.CallLinkRootKey; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.thoughtcrime.securesms.R; @@ -326,15 +327,20 @@ public class LinkPreviewRepository { @NonNull String callLinkUrl, @NonNull Callback callback) { - CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(callLinkUrl); - if (callLinkRootKey == null) { + CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(callLinkUrl); + if (linkParseResult == null) { callback.onError(Error.PREVIEW_NOT_AVAILABLE); return () -> { }; } + CallLinkEpoch epoch = linkParseResult.getEpoch(); + byte[] epochBytes = epoch != null ? epoch.getBytes() : null; + Disposable disposable = AppDependencies.getSignalCallManager() .getCallLinkManager() - .readCallLink(new CallLinkCredentials(callLinkRootKey.getKeyBytes(), null)) + .readCallLink(new CallLinkCredentials(linkParseResult.getRootKey().getKeyBytes(), + epochBytes, + null)) .observeOn(Schedulers.io()) .subscribe( result -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt index 821939d74b..89691b5ea5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import org.signal.core.util.logging.Log +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.FullScreenDialogFragment @@ -358,7 +359,7 @@ class MessageDetailsFragment : FullScreenDialogFragment(), MessageDetailsAdapter Log.w(TAG, "Not yet implemented!", Exception()) } - override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) { + override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) { Log.w(TAG, "Not yet implemented!", Exception()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index ae253ca5a5..de6e3ff323 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1370,6 +1370,7 @@ object SyncMessageProcessor { roomId, CallLinkCredentials( callLinkUpdate.rootKey!!.toByteArray(), + callLinkUpdate.epoch?.toByteArray(), callLinkUpdate.adminPasskey?.toByteArray() ) ) @@ -1381,6 +1382,7 @@ object SyncMessageProcessor { roomId = roomId, credentials = CallLinkCredentials( linkKeyBytes = callLinkRootKey.keyBytes, + epochBytes = callLinkUpdate.epoch?.toByteArray(), adminPassBytes = callLinkUpdate.adminPasskey?.toByteArray() ), state = SignalCallLinkState(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt index e41274245d..26248e1ab4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt @@ -70,7 +70,7 @@ class CallLinkPreJoinActionProcessor( serverPublicParams.endorsementPublicKey, callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, - null, + callLink.credentials.epoch, callLink.credentials.adminPassBytes, ByteArray(0), AUDIO_LEVELS_INTERVAL, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 2778386ea6..abc313aaae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -25,6 +25,7 @@ import org.signal.libsignal.zkgroup.calllinks.CallLinkSecretParams; import org.signal.libsignal.zkgroup.groups.GroupIdentifier; import org.signal.ringrtc.CallException; import org.signal.ringrtc.CallId; +import org.signal.ringrtc.CallLinkEpoch; import org.signal.ringrtc.CallLinkRootKey; import org.signal.ringrtc.CallManager; import org.signal.ringrtc.GroupCall; @@ -406,6 +407,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. } CallLinkRootKey callLinkRootKey = new CallLinkRootKey(callLink.getCredentials().getLinkKeyBytes()); + CallLinkEpoch callLinkEpoch = callLink.getCredentials().getEpoch(); GenericServerPublicParams genericServerPublicParams = new GenericServerPublicParams(AppDependencies.getSignalServiceNetworkAccess() .getConfiguration() .getGenericServerPublicParams()); @@ -417,7 +419,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. CallLinkSecretParams.deriveFromRootKey(callLinkRootKey.getKeyBytes()) ); - callManager.peekCallLinkCall(SignalStore.internal().getGroupCallingServer(), callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, null, peekInfo -> { + callManager.peekCallLinkCall(SignalStore.internal().getGroupCallingServer(), callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, callLinkEpoch, peekInfo -> { PeekInfo info = peekInfo.getValue(); if (info == null) { Log.w(TAG, "Failed to get peek info: " + peekInfo.getStatus()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/CallLinkCredentials.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/CallLinkCredentials.kt index 9d9e15effb..787e41ab8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/CallLinkCredentials.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/CallLinkCredentials.kt @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.service.webrtc.links import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey /** @@ -15,6 +16,7 @@ import org.signal.ringrtc.CallLinkRootKey @Parcelize data class CallLinkCredentials( val linkKeyBytes: ByteArray, + val epochBytes: ByteArray?, val adminPassBytes: ByteArray? ) : Parcelable { @@ -22,6 +24,10 @@ data class CallLinkCredentials( CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(linkKeyBytes)) } + val epoch: CallLinkEpoch? by lazy { + epochBytes?.let { CallLinkEpoch.fromBytes(it) } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -35,6 +41,12 @@ data class CallLinkCredentials( } else if (other.adminPassBytes != null) { return false } + if (epochBytes != null) { + if (other.epochBytes == null) return false + if (!epochBytes.contentEquals(other.epochBytes)) return false + } else if (other.epochBytes != null) { + return false + } return true } @@ -52,6 +64,7 @@ data class CallLinkCredentials( fun generate(): CallLinkCredentials { return CallLinkCredentials( CallLinkRootKey.generate().keyBytes, + null, CallLinkRootKey.generateAdminPasskey() ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/SignalCallLinkManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/SignalCallLinkManager.kt index 5ec9822b2d..8a00763d58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/SignalCallLinkManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/links/SignalCallLinkManager.kt @@ -120,9 +120,10 @@ class SignalCallLinkManager( ) { result -> if (result.isSuccess) { Log.d(TAG, "Successfully created call link.") + val epoch = result.value!!.epoch emitter.onSuccess( CreateCallLinkResult.Success( - credentials = CallLinkCredentials(rootKey.keyBytes, adminPassKey), + credentials = CallLinkCredentials(rootKey.keyBytes, epoch?.bytes, adminPassKey), state = result.value!!.toAppState() ) ) @@ -142,7 +143,7 @@ class SignalCallLinkManager( SignalStore.internal.groupCallingServer, requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes).serialize(), CallLinkRootKey(credentials.linkKeyBytes), - null + credentials.epoch ) { if (it.isSuccess) { emitter.onSuccess(ReadCallLinkResult.Success(it.value!!.toAppState())) @@ -169,7 +170,7 @@ class SignalCallLinkManager( SignalStore.internal.groupCallingServer, credentialPresentation.serialize(), CallLinkRootKey(credentials.linkKeyBytes), - null, + credentials.epoch, credentials.adminPassBytes, name ) { result -> @@ -197,7 +198,7 @@ class SignalCallLinkManager( SignalStore.internal.groupCallingServer, credentialPresentation.serialize(), CallLinkRootKey(credentials.linkKeyBytes), - null, + credentials.epoch, credentials.adminPassBytes, restrictions ) { result -> @@ -224,7 +225,7 @@ class SignalCallLinkManager( SignalStore.internal.groupCallingServer, credentialPresentation.serialize(), CallLinkRootKey(credentials.linkKeyBytes), - null, + credentials.epoch, credentials.adminPassBytes ) { result -> if (result.isSuccess && result.value == true) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt index 0fc7858984..1f9e978620 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt @@ -5,10 +5,12 @@ package org.thoughtcrime.securesms.storage +import okio.ByteString import okio.ByteString.Companion.toByteString import org.signal.core.util.isNotEmpty import org.signal.core.util.logging.Log import org.signal.core.util.toOptional +import org.signal.ringrtc.CallLinkEpoch import org.signal.ringrtc.CallLinkRootKey import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId @@ -46,8 +48,10 @@ class CallLinkRecordProcessor : DefaultStorageRecordProcessor { CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId()); - CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey); + CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey, epoch); if (callLink.getState().hasBeenRevoked()) { return Optional.empty(); diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index fe3a3df3ae..6ba10cb93f 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -344,6 +344,7 @@ message CallLink { string name = 3; Restrictions restrictions = 4; uint64 expirationMs = 5; + bytes epoch = 6; // May be absent/empty for older links } message AdHocCall { diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessorTest.kt index 06823d3479..5cbfa464b3 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessorTest.kt @@ -34,6 +34,7 @@ class CallLinkRecordProcessorTest { private val testSubject = CallLinkRecordProcessor() private val mockCredentials = CallLinkCredentials( "root key".toByteArray(), + "abcd".toByteArray(), "admin pass".toByteArray() ) diff --git a/libsignal-service/src/main/protowire/SignalService.proto b/libsignal-service/src/main/protowire/SignalService.proto index a14dacae38..a1fa8f9b06 100644 --- a/libsignal-service/src/main/protowire/SignalService.proto +++ b/libsignal-service/src/main/protowire/SignalService.proto @@ -628,6 +628,7 @@ message SyncMessage { optional bytes rootKey = 1; optional bytes adminPasskey = 2; optional Type type = 3; // defaults to UPDATE + optional bytes epoch = 4; } message CallLogEvent { @@ -874,4 +875,4 @@ message ConversationIdentifier { bytes threadGroupId = 2; string threadE164 = 3; } -} \ No newline at end of file +} diff --git a/libsignal-service/src/main/protowire/StorageService.proto b/libsignal-service/src/main/protowire/StorageService.proto index eaa145346c..2229805519 100644 --- a/libsignal-service/src/main/protowire/StorageService.proto +++ b/libsignal-service/src/main/protowire/StorageService.proto @@ -306,6 +306,7 @@ message CallLinkRecord { bytes rootKey = 1; bytes adminPasskey = 2; uint64 deletedAtTimestampMs = 3; + bytes epoch = 4; } message Recipient {