mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 12:24:00 +01:00
Migrate to libsignal getDevices/removeDevice.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Greyson Parrelli
parent
70e768ba05
commit
c0283fb75f
@@ -5,18 +5,18 @@
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.crypto.DeviceName
|
||||
import org.signal.core.util.crypto.DeviceNameCipher
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.roundedString
|
||||
import org.signal.libsignal.net.RequestResult
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.CoroutineJob
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LeastActiveLinkedDevice
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
@@ -32,7 +32,7 @@ class LinkedDeviceInactiveCheckJob private constructor(
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.build()
|
||||
) : Job(parameters) {
|
||||
) : CoroutineJob(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(LinkedDeviceInactiveCheckJob::class.java)
|
||||
@@ -61,7 +61,7 @@ class LinkedDeviceInactiveCheckJob private constructor(
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
override suspend fun doRun(): Result {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
Log.i(TAG, "Not registered, skipping.")
|
||||
return Result.success()
|
||||
@@ -72,14 +72,11 @@ class LinkedDeviceInactiveCheckJob private constructor(
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val devices = try {
|
||||
AppDependencies
|
||||
.linkDeviceApi
|
||||
.getDevices()
|
||||
.successOrThrow()
|
||||
.filter { it.id != SignalServiceAddress.DEFAULT_DEVICE_ID }
|
||||
} catch (e: IOException) {
|
||||
return Result.retry(defaultBackoff())
|
||||
val devices = when (val result = AppDependencies.linkDeviceApi.getDevices()) {
|
||||
is RequestResult.Success -> result.result.filter { it.id != SignalServiceAddress.DEFAULT_DEVICE_ID }
|
||||
is RequestResult.RetryableNetworkError -> return Result.retry(defaultBackoff())
|
||||
is RequestResult.ApplicationError -> throw result.cause
|
||||
is RequestResult.NonSuccess -> error("Code branch is unreachable")
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) {
|
||||
@@ -93,16 +90,16 @@ class LinkedDeviceInactiveCheckJob private constructor(
|
||||
}
|
||||
|
||||
val leastActiveDevice: LeastActiveLinkedDevice? = devices
|
||||
.filter { it.name != null }
|
||||
.filter { it.encryptedName.isNotEmpty() }
|
||||
.minByOrNull { it.lastSeen }
|
||||
?.let {
|
||||
val nameProto = DeviceName.ADAPTER.decode(Base64.decode(it.getName()))
|
||||
val nameProto = DeviceName.ADAPTER.decode(it.encryptedName)
|
||||
val decryptedBytes = DeviceNameCipher.decryptDeviceName(nameProto, AppDependencies.protocolStore.aci().identityKeyPair) ?: return@let null
|
||||
val name = String(decryptedBytes)
|
||||
|
||||
LeastActiveLinkedDevice(
|
||||
name = name,
|
||||
lastActiveTimestamp = it.lastSeen
|
||||
lastActiveTimestamp = it.lastSeen.toEpochMilli()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import org.signal.libsignal.net.LinkedDevice as LibSignalLinkedDevice
|
||||
|
||||
/**
|
||||
* Repository for linked devices and its various actions (linking, unlinking, listing).
|
||||
@@ -53,31 +54,35 @@ object LinkDeviceRepository {
|
||||
|
||||
suspend fun removeDevice(deviceId: Int): Boolean {
|
||||
return when (val result = AppDependencies.linkDeviceApi.removeDevice(deviceId)) {
|
||||
is NetworkResult.Success -> {
|
||||
is RequestResult.Success -> {
|
||||
LinkedDeviceInactiveCheckJob.enqueue()
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unable to remove device", result.getCause())
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "Unable to remove device", result.networkError)
|
||||
false
|
||||
}
|
||||
is RequestResult.ApplicationError -> throw result.cause
|
||||
is RequestResult.NonSuccess -> error("Code branch is unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDevices(): List<Device>? {
|
||||
suspend fun loadDevices(): List<Device>? {
|
||||
return when (val result = AppDependencies.linkDeviceApi.getDevices()) {
|
||||
is NetworkResult.Success -> {
|
||||
is RequestResult.Success -> {
|
||||
result
|
||||
.result
|
||||
.filter { d: DeviceInfo -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID }
|
||||
.map { deviceInfo: DeviceInfo -> deviceInfo.toDevice() }
|
||||
.filter { it.id != SignalServiceAddress.DEFAULT_DEVICE_ID }
|
||||
.map { it.toLocalDevice() }
|
||||
.sortedBy { it.createdMillis }
|
||||
.toList()
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unable to load device", result.getCause())
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "Unable to load device", result.networkError)
|
||||
null
|
||||
}
|
||||
is RequestResult.ApplicationError -> throw result.cause
|
||||
is RequestResult.NonSuccess -> error("Code branch is unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +94,10 @@ object LinkDeviceRepository {
|
||||
lastSeen = response.lastSeen
|
||||
registrationId = response.registrationId
|
||||
createdAtCiphertext = response.createdAtCiphertext
|
||||
}.toDevice()
|
||||
}.toLocalDevice()
|
||||
}
|
||||
|
||||
private fun DeviceInfo.toDevice(): Device {
|
||||
private fun DeviceInfo.toLocalDevice(): Device {
|
||||
val createdAt = this.getPlaintextCreatedAt()
|
||||
val defaultDevice = Device(getId(), getName(), createdAt, getLastSeen(), getRegistrationId())
|
||||
try {
|
||||
@@ -120,11 +125,54 @@ object LinkDeviceRepository {
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
private fun LibSignalLinkedDevice.toLocalDevice(): Device {
|
||||
val createdAt = getPlaintextCreatedAt()
|
||||
val defaultDevice = Device(this.id, Base64.encodeWithPadding(this.encryptedName), createdAt, this.lastSeen.toEpochMilli(), this.registrationId)
|
||||
try {
|
||||
if (this.encryptedName.size < 4) {
|
||||
Log.w(TAG, "Invalid LinkedDevice name.")
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
val deviceName = DeviceName.ADAPTER.decode(this.encryptedName)
|
||||
if (deviceName.ciphertext == null || deviceName.ephemeralPublic == null || deviceName.syntheticIv == null) {
|
||||
Log.w(TAG, "Got a DeviceName that wasn't properly populated.")
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
val plaintext = DeviceNameCipher.decryptDeviceName(deviceName, SignalStore.account.aciIdentityKey)
|
||||
if (plaintext == null) {
|
||||
Log.w(TAG, "Failed to decrypt device name.")
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
return Device(id, String(plaintext), createdAt, lastSeen.toEpochMilli(), registrationId)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed while reading the protobuf.", e)
|
||||
}
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
private fun DeviceInfo.getPlaintextCreatedAt(): Long? {
|
||||
return try {
|
||||
val associatedData = byteArrayOf(getId().toByte()) + getRegistrationId().toByteArray()
|
||||
val associatedData = byteArrayOf(getId().toByte()) + this.getRegistrationId().toByteArray()
|
||||
val createdAtPlaintext = SignalStore.account.aciIdentityKey.privateKey.open(
|
||||
ciphertext = Base64.decode(getCreatedAtCiphertext().toByteArray()),
|
||||
ciphertext = Base64.decode(this.getCreatedAtCiphertext().toByteArray()),
|
||||
info = DECRYPTION_INFO,
|
||||
associatedData = associatedData
|
||||
)
|
||||
ByteBuffer.wrap(createdAtPlaintext).getLong()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed while reading the protobuf.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun LibSignalLinkedDevice.getPlaintextCreatedAt(): Long? {
|
||||
return try {
|
||||
val associatedData = byteArrayOf(this.id.toByte()) + this.registrationId.toByteArray()
|
||||
val createdAtPlaintext = SignalStore.account.aciIdentityKey.privateKey.open(
|
||||
ciphertext = this.createdAtCiphertext,
|
||||
info = DECRYPTION_INFO,
|
||||
associatedData = associatedData
|
||||
)
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ fun <T : Any> RequestResult<T, Nothing>.successOrThrow(): T {
|
||||
return when (this) {
|
||||
is RequestResult.Success -> result
|
||||
is RequestResult.RetryableNetworkError -> throw networkError
|
||||
is RequestResult.NonSuccess -> error("Branch is unreachable")
|
||||
is RequestResult.NonSuccess -> error("Code branch is unreachable")
|
||||
is RequestResult.ApplicationError -> throw when (val error = cause) {
|
||||
is IOException, is RuntimeException -> error
|
||||
else -> RuntimeException(error)
|
||||
|
||||
@@ -14,12 +14,13 @@ import org.signal.core.models.backup.MediaRootBackupKey
|
||||
import org.signal.core.models.backup.MessageBackupKey
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.urlEncode
|
||||
import org.signal.libsignal.net.AuthDevicesService
|
||||
import org.signal.libsignal.net.RequestResult
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.signal.network.NetworkResult
|
||||
import org.signal.network.websocket.WebSocketRequestMessage
|
||||
import org.signal.network.websocket.delete
|
||||
import org.signal.network.websocket.get
|
||||
import org.signal.network.websocket.put
|
||||
import org.whispersystems.signalservice.api.fromWebSocketRequest
|
||||
@@ -29,15 +30,14 @@ import org.whispersystems.signalservice.api.link.SetLinkedDeviceTransferArchiveR
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveError
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningMessage
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.push.DeviceInfoList
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningVersion
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.signal.libsignal.net.LinkedDevice as LibSignalLinkedDevice
|
||||
|
||||
/**
|
||||
* Class to interact with device-linking endpoints.
|
||||
@@ -47,29 +47,19 @@ class LinkDeviceApi(
|
||||
) {
|
||||
/**
|
||||
* Fetches a list of linked devices.
|
||||
*
|
||||
* GET /v1/devices
|
||||
*
|
||||
* - 200: Success
|
||||
*/
|
||||
fun getDevices(): NetworkResult<List<DeviceInfo>> {
|
||||
val request = WebSocketRequestMessage.get("/v1/devices")
|
||||
return NetworkResult
|
||||
.fromWebSocketRequest(authWebSocket, request, DeviceInfoList::class)
|
||||
.map { it.getDevices() }
|
||||
suspend fun getDevices(): RequestResult<List<LibSignalLinkedDevice>, Nothing> {
|
||||
return authWebSocket.runCatchingWithChatConnection { connection ->
|
||||
AuthDevicesService(connection).getDevices()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and unlink a linked device.
|
||||
*
|
||||
* DELETE /v1/devices/{id}
|
||||
*
|
||||
* - 200: Success
|
||||
*/
|
||||
suspend fun removeDevice(deviceId: Int): NetworkResult<Unit> {
|
||||
val request = WebSocketRequestMessage.delete("/v1/devices/$deviceId")
|
||||
return NetworkResult.fromWebSocketSuspend(NetworkResult.DefaultWebSocketConverter(Unit::class)) {
|
||||
authWebSocket.requestSuspend(request)
|
||||
suspend fun removeDevice(deviceId: Int): RequestResult<Unit, Nothing> {
|
||||
return authWebSocket.runCatchingWithChatConnection { connection ->
|
||||
AuthDevicesService(connection).removeDevice(deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user