mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-27 12:15:50 +01:00
Add initial SVRB support.
This commit is contained in:
committed by
Cody Henthorne
parent
f6ab408fc8
commit
5aeca1deb1
@@ -24,6 +24,7 @@ import org.whispersystems.signalservice.internal.delete
|
||||
import org.whispersystems.signalservice.internal.get
|
||||
import org.whispersystems.signalservice.internal.post
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.put
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
@@ -362,6 +363,24 @@ class ArchiveApi(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves auth credentials that can be used to perform SVRB operations.
|
||||
*
|
||||
* GET /v1/archives/auth/svrb
|
||||
* - 200: Success
|
||||
* - 400: Bad arguments, or made on an authenticated channel
|
||||
* - 401: Bad presentation, invalid public key signature, no matching backupId on the server, or the credential was of the wrong type (messages/media)
|
||||
* - 403: Forbidden
|
||||
*/
|
||||
fun getSvrBAuthorization(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MessageBackupKey>): NetworkResult<AuthCredentials> {
|
||||
return getCredentialPresentation(aci, archiveServiceAccess)
|
||||
.map { it.toArchiveCredentialPresentation().toHeaders() }
|
||||
.then { headers ->
|
||||
val request = WebSocketRequestMessage.get("/v1/archives/auth/svrb", headers)
|
||||
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, AuthCredentials::class)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCredentialPresentation(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult<CredentialPresentationData> {
|
||||
return NetworkResult.fromLocal {
|
||||
val zkCredential = getZkCredential(aci, archiveServiceAccess)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.whispersystems.signalservice.api.backup
|
||||
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey
|
||||
@@ -28,11 +29,13 @@ class MessageBackupKey(override val value: ByteArray) : BackupKey {
|
||||
|
||||
/**
|
||||
* The cryptographic material used to encrypt a backup.
|
||||
*
|
||||
* @param forwardSecrecyToken Should be present for any backup located on the archive CDN. Absent for other uses (i.e. link+sync).
|
||||
*/
|
||||
fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial {
|
||||
fun deriveBackupSecrets(aci: ACI, forwardSecrecyToken: BackupForwardSecrecyToken?): BackupKeyMaterial {
|
||||
val backupId = deriveBackupId(aci)
|
||||
val libsignalBackupKey = LibSignalBackupKey(value)
|
||||
val libsignalMessageMessageBackupKey = LibSignalMessageBackupKey(libsignalBackupKey, backupId.value)
|
||||
val libsignalMessageMessageBackupKey = LibSignalMessageBackupKey(libsignalBackupKey, backupId.value, forwardSecrecyToken)
|
||||
|
||||
return BackupKeyMaterial(
|
||||
id = backupId,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import org.signal.libsignal.attest.AttestationFailedException
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.messagebackup.BackupKey
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.libsignal.net.NetworkException
|
||||
import org.signal.libsignal.net.NetworkProtocolException
|
||||
import org.signal.libsignal.net.SvrB
|
||||
import org.signal.libsignal.net.SvrBRestoreResponse
|
||||
import org.signal.libsignal.net.SvrBStoreResponse
|
||||
import org.signal.libsignal.svr.DataMissingException
|
||||
import org.signal.libsignal.svr.RestoreFailedException
|
||||
import org.signal.libsignal.svr.SvrException
|
||||
import org.whispersystems.signalservice.api.CancelationException
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
/**
|
||||
* A collection of operations for interacting with SVRB, the SVR enclave that provides forward secrecy for backups.
|
||||
*/
|
||||
class SvrBApi(private val network: Network) {
|
||||
|
||||
/**
|
||||
* See [SvrB.createNewBackupChain].
|
||||
*
|
||||
* Call this the first time you ever interact with SVRB. Gives you a secret data to persist and use for future calls.
|
||||
*
|
||||
* Note that this doesn't actually make a network call, it just needs a [Network] to get the environment.
|
||||
*/
|
||||
fun createNewBackupChain(auth: AuthCredentials, backupKey: MessageBackupKey): ByteArray {
|
||||
return network
|
||||
.svrB(auth.username(), auth.password())
|
||||
.createNewBackupChain(BackupKey(backupKey.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* See [SvrB.store].
|
||||
*
|
||||
* Handling this one is funny because the underlying protocols don't use status codes, instead favoring complex results.
|
||||
* As a result, responses are only [NetworkResult.Success] and [NetworkResult.NetworkError], with errors being accounted for
|
||||
* in the success case via the sealed result class.
|
||||
*/
|
||||
fun store(auth: AuthCredentials, backupKey: MessageBackupKey, previousSecretData: ByteArray): StoreResult {
|
||||
return try {
|
||||
val result = network
|
||||
.svrB(auth.username(), auth.password())
|
||||
.store(BackupKey(backupKey.value), previousSecretData)
|
||||
.get()
|
||||
|
||||
when (val exception = result.exceptionOrNull()) {
|
||||
null -> StoreResult.Success(result.getOrThrow())
|
||||
is NetworkException -> StoreResult.NetworkError(exception)
|
||||
is NetworkProtocolException -> StoreResult.NetworkError(exception)
|
||||
is SvrException -> StoreResult.SvrError(exception)
|
||||
else -> StoreResult.UnknownError(exception)
|
||||
}
|
||||
} catch (e: CancelationException) {
|
||||
StoreResult.UnknownError(e)
|
||||
} catch (e: ExecutionException) {
|
||||
StoreResult.UnknownError(e)
|
||||
} catch (e: InterruptedException) {
|
||||
StoreResult.UnknownError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See [SvrB.restore]
|
||||
*
|
||||
* Handling this one is funny because the underlying protocols don't use status codes, instead favoring complex results.
|
||||
* As a result, responses are only [NetworkResult.Success] and [NetworkResult.NetworkError], with errors being accounted for
|
||||
* in the success case via the sealed result class.
|
||||
*/
|
||||
fun restore(auth: AuthCredentials, backupKey: MessageBackupKey, forwardSecrecyMetadata: ByteArray): RestoreResult {
|
||||
return try {
|
||||
val result = network
|
||||
.svrB(auth.username(), auth.password())
|
||||
.restore(BackupKey(backupKey.value), forwardSecrecyMetadata)
|
||||
.get()
|
||||
|
||||
when (val exception = result.exceptionOrNull()) {
|
||||
null -> RestoreResult.Success(result.getOrThrow())
|
||||
is NetworkException -> RestoreResult.NetworkError(exception)
|
||||
is NetworkProtocolException -> RestoreResult.NetworkError(exception)
|
||||
is DataMissingException -> RestoreResult.DataMissingError
|
||||
is RestoreFailedException -> RestoreResult.RestoreFailedError(exception.triesRemaining)
|
||||
is SvrException -> RestoreResult.SvrError(exception)
|
||||
is AttestationFailedException -> RestoreResult.SvrError(exception)
|
||||
else -> RestoreResult.UnknownError(exception)
|
||||
}
|
||||
} catch (e: CancelationException) {
|
||||
RestoreResult.UnknownError(e)
|
||||
} catch (e: ExecutionException) {
|
||||
RestoreResult.UnknownError(e)
|
||||
} catch (e: InterruptedException) {
|
||||
RestoreResult.UnknownError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <ResultType, FinalType> CompletableFuture<Result<ResultType>>.toNetworkResult(resultMapper: (Result<ResultType>) -> FinalType): NetworkResult<FinalType> {
|
||||
return try {
|
||||
val result = this.get()
|
||||
return when (val exception = result.exceptionOrNull()) {
|
||||
is NetworkException -> NetworkResult.NetworkError(exception)
|
||||
is NetworkProtocolException -> NetworkResult.NetworkError(exception)
|
||||
else -> NetworkResult.Success(resultMapper(result))
|
||||
}
|
||||
} catch (e: CancelationException) {
|
||||
NetworkResult.Success(resultMapper(Result.failure(e)))
|
||||
} catch (e: ExecutionException) {
|
||||
NetworkResult.Success(resultMapper(Result.failure(e)))
|
||||
} catch (e: InterruptedException) {
|
||||
NetworkResult.Success(resultMapper(Result.failure(e)))
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StoreResult {
|
||||
data class Success(val data: SvrBStoreResponse) : StoreResult()
|
||||
data class NetworkError(val exception: IOException) : StoreResult()
|
||||
data class SvrError(val throwable: Throwable) : StoreResult()
|
||||
data class UnknownError(val throwable: Throwable) : StoreResult()
|
||||
}
|
||||
|
||||
sealed class RestoreResult {
|
||||
data class Success(val data: SvrBRestoreResponse) : RestoreResult()
|
||||
data class NetworkError(val exception: IOException) : RestoreResult()
|
||||
data object DataMissingError : RestoreResult()
|
||||
data class RestoreFailedError(val triesRemaining: Int) : RestoreResult()
|
||||
data class SvrError(val throwable: Throwable) : RestoreResult()
|
||||
data class UnknownError(val throwable: Throwable) : RestoreResult()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user