Integrate call links create/update/read apis.

This commit is contained in:
Alex Hart
2023-05-19 10:28:29 -03:00
committed by Nicholas Tinsley
parent 4d6d31d624
commit 5a38143987
60 changed files with 1986 additions and 191 deletions

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.service.webrtc
import com.google.protobuf.ByteString
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.RemotePeer
@@ -56,11 +55,17 @@ object CallEventSyncMessageUtil {
event: CallEvent.Event
): CallEvent {
val recipient = Recipient.resolved(recipientId)
val isGroupCall = recipient.isGroup
val conversationId: ByteString = if (isGroupCall) {
recipient.requireGroupId().decodedId.toProtoByteString()
} else {
recipient.requireServiceId().toByteString()
val callType = when {
recipient.isCallLink -> CallEvent.Type.AD_HOC_CALL
recipient.isGroup -> CallEvent.Type.GROUP_CALL
isVideoCall -> CallEvent.Type.VIDEO_CALL
else -> CallEvent.Type.AUDIO_CALL
}
val conversationId: ByteString = when {
recipient.isCallLink -> recipient.requireCallLinkRoomId().encodeForProto()
recipient.isGroup -> ByteString.copyFrom(recipient.requireGroupId().decodedId)
else -> recipient.requireServiceId().toByteString()
}
return CallEvent
@@ -68,13 +73,7 @@ object CallEventSyncMessageUtil {
.setConversationId(conversationId)
.setId(callId)
.setTimestamp(timestamp)
.setType(
when {
isGroupCall -> CallEvent.Type.GROUP_CALL
isVideoCall -> CallEvent.Type.VIDEO_CALL
else -> CallEvent.Type.AUDIO_CALL
}
)
.setType(callType)
.setDirection(if (isOutgoing) CallEvent.Direction.OUTGOING else CallEvent.Direction.INCOMING)
.setEvent(event)
.build()

View File

@@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
@@ -81,6 +82,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -992,6 +994,10 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
}
}
public @NonNull SignalCallLinkManager getCallLinkManager() {
return new SignalCallLinkManager(Objects.requireNonNull(callManager));
}
private void processSendMessageFailureWithChangeDetection(@NonNull RemotePeer remotePeer,
@NonNull ProcessAction failureProcessAction)
{

View File

@@ -0,0 +1,57 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.signal.ringrtc.CallLinkRootKey
/**
* Holds onto the credentials for a given call link.
*/
@Parcelize
data class CallLinkCredentials(
val linkKeyBytes: ByteArray,
val adminPassBytes: ByteArray?
) : Parcelable {
val roomId: CallLinkRoomId by lazy {
CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(linkKeyBytes))
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CallLinkCredentials
if (!linkKeyBytes.contentEquals(other.linkKeyBytes)) return false
if (adminPassBytes != null) {
if (other.adminPassBytes == null) return false
if (!adminPassBytes.contentEquals(other.adminPassBytes)) return false
} else if (other.adminPassBytes != null) return false
return true
}
override fun hashCode(): Int {
var result = linkKeyBytes.contentHashCode()
result = 31 * result + (adminPassBytes?.contentHashCode() ?: 0)
return result
}
companion object {
/**
* Generate a new call link credential for creating a new call.
*/
fun generate(): CallLinkCredentials {
return CallLinkCredentials(
CallLinkRootKey.generate().keyBytes,
CallLinkRootKey.generateAdminPasskey()
)
}
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
import android.os.Parcelable
import com.google.protobuf.ByteString
import kotlinx.parcelize.Parcelize
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.util.Base64
@Parcelize
class CallLinkRoomId private constructor(private val roomId: ByteArray) : Parcelable {
fun serialize(): String = Base64.encodeBytes(roomId)
fun encodeForProto(): ByteString = ByteString.copyFrom(roomId)
companion object {
@JvmStatic
fun fromBytes(byteArray: ByteArray): CallLinkRoomId {
return CallLinkRoomId(byteArray)
}
fun fromCallLinkRootKey(callLinkRootKey: CallLinkRootKey): CallLinkRoomId {
return CallLinkRoomId(callLinkRootKey.deriveRoomId())
}
}
}

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
/**
* Result type for call link creation.
*/
sealed interface CreateCallLinkResult {
data class Success(
val credentials: CallLinkCredentials,
val state: SignalCallLinkState
) : CreateCallLinkResult
data class Failure(
val status: Short
) : CreateCallLinkResult
}

View File

@@ -0,0 +1,17 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
/**
* Result type for call link reads.
*/
sealed interface ReadCallLinkResult {
data class Success(
val callLinkState: SignalCallLinkState
) : ReadCallLinkResult
data class Failure(val status: Short) : ReadCallLinkResult
}

View File

@@ -0,0 +1,250 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.isAbsent
import org.signal.core.util.logging.Log
import org.signal.core.util.or
import org.signal.libsignal.zkgroup.GenericServerPublicParams
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialPresentation
import org.signal.libsignal.zkgroup.calllinks.CallLinkSecretParams
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredential
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialPresentation
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequestContext
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState
import org.signal.ringrtc.CallLinkState.Restrictions
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.internal.ServiceResponse
import java.io.IOException
/**
* Call Link manager which encapsulates CallManager and provides a stable interface.
*
* We can remove the outer sealed class once we have the final, working builds from core.
*/
class SignalCallLinkManager(
private val callManager: CallManager
) {
private val genericServerPublicParams: GenericServerPublicParams = GenericServerPublicParams(
ApplicationDependencies.getSignalServiceNetworkAccess()
.getConfiguration()
.genericServerPublicParams
)
private fun requestCreateCallLinkCredentailPresentation(
linkRootKey: ByteArray,
roomId: ByteArray
): CreateCallLinkCredentialPresentation {
val userUuid = Recipient.self().requireServiceId().uuid()
val requestContext = CreateCallLinkCredentialRequestContext.forRoom(roomId)
val request = requestContext.request
Log.d(TAG, "Requesting call link credential response.")
val serviceResponse: ServiceResponse<CreateCallLinkCredentialResponse> = ApplicationDependencies.getCallLinksService().getCreateCallLinkAuthCredential(request)
if (serviceResponse.result.isAbsent()) {
throw IOException("Failed to create credential response", serviceResponse.applicationError.or(serviceResponse.executionError).get())
}
Log.d(TAG, "Requesting call link credential.")
val createCallLinkCredential: CreateCallLinkCredential = requestContext.receiveResponse(
serviceResponse.result.get(),
userUuid,
genericServerPublicParams
)
Log.d(TAG, "Requesting and returning call link presentation.")
return createCallLinkCredential.present(
roomId,
userUuid,
genericServerPublicParams,
CallLinkSecretParams.deriveFromRootKey(linkRootKey)
)
}
private fun requestCallLinkAuthCredentialPresentation(
linkRootKey: ByteArray
): CallLinkAuthCredentialPresentation {
return ApplicationDependencies.getGroupsV2Authorization().getCallLinkAuthorizationForToday(
genericServerPublicParams,
CallLinkSecretParams.deriveFromRootKey(linkRootKey)
)
}
fun createCallLink(
callLinkCredentials: CallLinkCredentials
): Single<CreateCallLinkResult> {
return Single.create { emitter ->
Log.d(TAG, "Generating keys.")
val rootKey = CallLinkRootKey(callLinkCredentials.linkKeyBytes)
val adminPassKey: ByteArray = requireNotNull(callLinkCredentials.adminPassBytes)
val roomId: ByteArray = rootKey.deriveRoomId()
Log.d(TAG, "Generating credential.")
val credentialPresentation = try {
requestCreateCallLinkCredentailPresentation(
rootKey.keyBytes,
roomId
)
} catch (e: Exception) {
Log.e(TAG, "Failed to create call link credential.", e)
emitter.onError(e)
return@create
}
Log.d(TAG, "Creating call link.")
val publicParams = CallLinkSecretParams.deriveFromRootKey(rootKey.keyBytes).publicParams
// Credential
callManager.createCallLink(
SignalStore.internalValues().groupCallingServer(),
credentialPresentation.serialize(),
rootKey,
adminPassKey,
publicParams.serialize()
) { result ->
if (result.isSuccess) {
Log.d(TAG, "Successfully created call link.")
emitter.onSuccess(
CreateCallLinkResult.Success(
credentials = CallLinkCredentials(rootKey.keyBytes, adminPassKey),
state = result.value!!.toAppState()
)
)
} else {
Log.w(TAG, "Failed to create call link with failure status ${result.status}")
emitter.onSuccess(CreateCallLinkResult.Failure(result.status))
}
}
}
}
fun readCallLink(
credentials: CallLinkCredentials
): Single<ReadCallLinkResult> {
return Single.create { emitter ->
callManager.readCallLink(
SignalStore.internalValues().groupCallingServer(),
requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes).serialize(),
CallLinkRootKey(credentials.linkKeyBytes)
) {
if (it.isSuccess) {
emitter.onSuccess(ReadCallLinkResult.Success(it.value!!.toAppState()))
} else {
Log.w(TAG, "Failed to read call link with failure status ${it.status}")
emitter.onSuccess(ReadCallLinkResult.Failure(it.status))
}
}
}
}
fun updateCallLinkName(
credentials: CallLinkCredentials,
name: String
): Single<UpdateCallLinkResult> {
if (credentials.adminPassBytes == null) {
return Single.just(UpdateCallLinkResult.NotAuthorized)
}
return Single.create { emitter ->
val credentialPresentation = requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes)
callManager.updateCallLinkName(
SignalStore.internalValues().groupCallingServer(),
credentialPresentation.serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.adminPassBytes,
name
) { result ->
if (result.isSuccess) {
emitter.onSuccess(UpdateCallLinkResult.Success(result.value!!.toAppState()))
} else {
emitter.onSuccess(UpdateCallLinkResult.Failure(result.status))
}
}
}
}
fun updateCallLinkRestrictions(
credentials: CallLinkCredentials,
restrictions: Restrictions
): Single<UpdateCallLinkResult> {
if (credentials.adminPassBytes == null) {
return Single.just(UpdateCallLinkResult.NotAuthorized)
}
return Single.create { emitter ->
val credentialPresentation = requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes)
callManager.updateCallLinkRestrictions(
SignalStore.internalValues().groupCallingServer(),
credentialPresentation.serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.adminPassBytes,
restrictions
) { result ->
if (result.isSuccess) {
emitter.onSuccess(UpdateCallLinkResult.Success(result.value!!.toAppState()))
} else {
emitter.onSuccess(UpdateCallLinkResult.Failure(result.status))
}
}
}
}
fun updateCallLinkRevoked(
credentials: CallLinkCredentials,
revoked: Boolean
): Single<UpdateCallLinkResult> {
if (credentials.adminPassBytes == null) {
return Single.just(UpdateCallLinkResult.NotAuthorized)
}
return Single.create { emitter ->
val credentialPresentation = requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes)
callManager.updateCallLinkRevoked(
SignalStore.internalValues().groupCallingServer(),
credentialPresentation.serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.adminPassBytes,
revoked
) { result ->
if (result.isSuccess) {
emitter.onSuccess(UpdateCallLinkResult.Success(result.value!!.toAppState()))
} else {
emitter.onSuccess(UpdateCallLinkResult.Failure(result.status))
}
}
}
}
companion object {
private val TAG = Log.tag(SignalCallLinkManager::class.java)
private fun CallLinkState.toAppState(): SignalCallLinkState {
return SignalCallLinkState(
name = name,
expiration = expiration,
restrictions = restrictions,
revoked = hasBeenRevoked()
)
}
}
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
import org.signal.ringrtc.CallLinkState.Restrictions
import java.time.Instant
/**
* Adapter class between our app code and RingRTC CallLinkState.
*/
data class SignalCallLinkState(
val name: String = "",
val restrictions: Restrictions = Restrictions.UNKNOWN,
@get:JvmName("hasBeenRevoked") val revoked: Boolean = false,
val expiration: Instant = Instant.MAX
)

View File

@@ -0,0 +1,21 @@
/**
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc.links
/**
* Result type for call link updates.
*/
sealed interface UpdateCallLinkResult {
data class Success(
val state: SignalCallLinkState
) : UpdateCallLinkResult
class Failure(
val status: Short
) : UpdateCallLinkResult
object NotAuthorized : UpdateCallLinkResult
}