Forbid linked devices from setting backup-ids
This commit is contained in:
parent
5de848bf38
commit
9dfe51eac4
|
@ -34,6 +34,7 @@ import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
|||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
|
@ -85,6 +86,7 @@ public class BackupAuthManager {
|
|||
* Store credential requests containing blinded backup-ids for future use.
|
||||
*
|
||||
* @param account The account using the backup-id
|
||||
* @param device The device setting the account backup-id
|
||||
* @param messagesBackupCredentialRequest A request containing the blinded backup-id the client will use to upload
|
||||
* message backups
|
||||
* @param mediaBackupCredentialRequest A request containing the blinded backup-id the client will use to upload
|
||||
|
@ -92,12 +94,17 @@ public class BackupAuthManager {
|
|||
* @return A future that completes when the credentialRequest has been stored
|
||||
* @throws RateLimitExceededException If too many backup-ids have been committed
|
||||
*/
|
||||
public CompletableFuture<Void> commitBackupId(final Account account,
|
||||
public CompletableFuture<Void> commitBackupId(
|
||||
final Account account,
|
||||
final Device device,
|
||||
final BackupAuthCredentialRequest messagesBackupCredentialRequest,
|
||||
final BackupAuthCredentialRequest mediaBackupCredentialRequest) {
|
||||
if (configuredBackupLevel(account).isEmpty()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
|
||||
}
|
||||
if (!device.isPrimary()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Only primary device can set backup-id").asRuntimeException();
|
||||
}
|
||||
final byte[] serializedMessageCredentialRequest = messagesBackupCredentialRequest.serialize();
|
||||
final byte[] serializedMediaCredentialRequest = mediaBackupCredentialRequest.serialize();
|
||||
|
||||
|
|
|
@ -135,13 +135,14 @@ public class ArchiveController {
|
|||
""")
|
||||
@ApiResponse(responseCode = "204", description = "The backup-id was set")
|
||||
@ApiResponse(responseCode = "400", description = "The provided backup auth credential request was invalid")
|
||||
@ApiResponse(responseCode = "403", description = "The device did not have permission to set the backup-id. Only the primary device can set the backup-id for an account")
|
||||
@ApiResponse(responseCode = "429", description = "Rate limited. Too many attempts to change the backup-id have been made")
|
||||
public CompletionStage<Response> setBackupId(
|
||||
@Mutable @Auth final AuthenticatedDevice account,
|
||||
@Valid @NotNull final SetBackupIdRequest setBackupIdRequest) throws RateLimitExceededException {
|
||||
|
||||
return this.backupAuthManager
|
||||
.commitBackupId(account.getAccount(), setBackupIdRequest.messagesBackupAuthCredentialRequest,
|
||||
.commitBackupId(account.getAccount(), account.getAuthenticatedDevice(),
|
||||
setBackupIdRequest.messagesBackupAuthCredentialRequest,
|
||||
setBackupIdRequest.mediaBackupAuthCredentialRequest)
|
||||
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
|
|||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
@ -60,9 +61,15 @@ public class BackupsGrpcService extends ReactorBackupsGrpc.BackupsImplBase {
|
|||
BackupAuthCredentialRequest::new,
|
||||
request.getMediaBackupAuthCredentialRequest().toByteArray());
|
||||
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
return authenticatedAccount()
|
||||
.flatMap(account -> Mono.fromFuture(
|
||||
backupAuthManager.commitBackupId(account, messagesCredentialRequest, mediaCredentialRequest)))
|
||||
.flatMap(account -> {
|
||||
final Device device = account
|
||||
.getDevice(authenticatedDevice.deviceId())
|
||||
.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException);
|
||||
return Mono.fromFuture(
|
||||
backupAuthManager.commitBackupId(account, device, messagesCredentialRequest, mediaCredentialRequest));
|
||||
})
|
||||
.thenReturn(SetBackupIdResponse.getDefaultInstance());
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
|||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.ExperimentHelper;
|
||||
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
||||
|
@ -119,7 +120,7 @@ public class BackupAuthManagerTest {
|
|||
final BackupAuthCredentialRequest messagesCredentialRequest = backupAuthTestUtil.getRequest(messagesBackupKey, aci);
|
||||
final BackupAuthCredentialRequest mediaCredentialRequest = backupAuthTestUtil.getRequest(mediaBackupKey, aci);
|
||||
|
||||
authManager.commitBackupId(account, messagesCredentialRequest, mediaCredentialRequest).join();
|
||||
authManager.commitBackupId(account, primaryDevice(), messagesCredentialRequest, mediaCredentialRequest).join();
|
||||
|
||||
verify(account).setBackupCredentialRequests(messagesCredentialRequest.serialize(), mediaCredentialRequest.serialize());
|
||||
}
|
||||
|
@ -135,6 +136,7 @@ public class BackupAuthManagerTest {
|
|||
|
||||
final ThrowableAssert.ThrowingCallable commit = () ->
|
||||
authManager.commitBackupId(account,
|
||||
primaryDevice(),
|
||||
backupAuthTestUtil.getRequest(messagesBackupKey, aci),
|
||||
backupAuthTestUtil.getRequest(mediaBackupKey, aci)).join();
|
||||
if (backupLevel == null) {
|
||||
|
@ -147,6 +149,24 @@ public class BackupAuthManagerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void commitRequiresPrimary() {
|
||||
final BackupAuthManager authManager = create(BackupLevel.FREE);
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(aci);
|
||||
when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account));
|
||||
|
||||
final ThrowableAssert.ThrowingCallable commit = () ->
|
||||
authManager.commitBackupId(account,
|
||||
linkedDevice(),
|
||||
backupAuthTestUtil.getRequest(messagesBackupKey, aci),
|
||||
backupAuthTestUtil.getRequest(mediaBackupKey, aci)).join();
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(commit)
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.PERMISSION_DENIED);
|
||||
}
|
||||
|
||||
@CartesianTest
|
||||
void getBackupAuthCredentials(@CartesianTest.Enum final BackupLevel backupLevel,
|
||||
@CartesianTest.Enum final BackupCredentialType credentialType) {
|
||||
|
@ -504,7 +524,7 @@ public class BackupAuthManagerTest {
|
|||
: storedMediaCredential;
|
||||
|
||||
final boolean expectRateLimit = (changeMedia || changeMessage) && rateLimitBackupId;
|
||||
final CompletableFuture<Void> future = authManager.commitBackupId(account, newMessagesCredential, newMediaCredential);
|
||||
final CompletableFuture<Void> future = authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential);
|
||||
if (expectRateLimit) {
|
||||
CompletableFutureTestUtil.assertFailsWithCause(RateLimitExceededException.class, future);
|
||||
} else {
|
||||
|
@ -538,7 +558,7 @@ public class BackupAuthManagerTest {
|
|||
|
||||
// We should get rate limited iff we are out of paid media changes and we changed the media backup-id
|
||||
final boolean expectRateLimit = changeMedia && paid && rateLimitPaidMedia;
|
||||
final CompletableFuture<Void> future = authManager.commitBackupId(account, newMessagesCredential, newMediaCredential);
|
||||
final CompletableFuture<Void> future = authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential);
|
||||
if (expectRateLimit) {
|
||||
CompletableFutureTestUtil.assertFailsWithCause(RateLimitExceededException.class, future);
|
||||
} else {
|
||||
|
@ -562,6 +582,17 @@ public class BackupAuthManagerTest {
|
|||
return account;
|
||||
}
|
||||
|
||||
private Device primaryDevice() {
|
||||
final Device device = mock(Device.class);
|
||||
when(device.isPrimary()).thenReturn(true);
|
||||
return device;
|
||||
}
|
||||
|
||||
private Device linkedDevice() {
|
||||
final Device device = mock(Device.class);
|
||||
when(device.isPrimary()).thenReturn(false);
|
||||
return device;
|
||||
}
|
||||
|
||||
private static String experimentName(@Nullable BackupLevel backupLevel) {
|
||||
return switch (backupLevel) {
|
||||
|
|
|
@ -157,7 +157,7 @@ public class ArchiveControllerTest {
|
|||
|
||||
@Test
|
||||
public void setBackupId() {
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("v1/archives/backupid")
|
||||
|
@ -170,7 +170,7 @@ public class ArchiveControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(backupAuthManager).commitBackupId(AuthHelper.VALID_ACCOUNT,
|
||||
verify(backupAuthManager).commitBackupId(AuthHelper.VALID_ACCOUNT, AuthHelper.VALID_DEVICE,
|
||||
backupAuthTestUtil.getRequest(messagesBackupKey, aci),
|
||||
backupAuthTestUtil.getRequest(mediaBackupKey, aci));
|
||||
}
|
||||
|
@ -275,9 +275,9 @@ public class ArchiveControllerTest {
|
|||
@MethodSource
|
||||
public void setBackupIdException(final Exception ex, final boolean sync, final int expectedStatus) {
|
||||
if (sync) {
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenThrow(ex);
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any(), any())).thenThrow(ex);
|
||||
} else {
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.failedFuture(ex));
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any(), any())).thenReturn(CompletableFuture.failedFuture(ex));
|
||||
}
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("v1/archives/backupid")
|
||||
|
|
|
@ -57,6 +57,7 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
|||
import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.EnumMapUtil;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
|
||||
|
@ -69,7 +70,8 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
|||
backupAuthTestUtil.getRequest(mediaBackupKey, AUTHENTICATED_ACI);
|
||||
final BackupAuthCredentialRequest messagesAuthCredRequest =
|
||||
backupAuthTestUtil.getRequest(messagesBackupKey, AUTHENTICATED_ACI);
|
||||
private final Account account = mock(Account.class);
|
||||
private Account account;
|
||||
private Device device;
|
||||
|
||||
@Mock
|
||||
private BackupAuthManager backupAuthManager;
|
||||
|
@ -83,14 +85,19 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
|||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
account = mock(Account.class);
|
||||
device = mock(Device.class);
|
||||
when(device.isPrimary()).thenReturn(true);
|
||||
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
|
||||
when(account.getDevice(AUTHENTICATED_DEVICE_ID)).thenReturn(Optional.of(device));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void setBackupId() {
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
authenticatedServiceStub().setBackupId(
|
||||
SetBackupIdRequest.newBuilder()
|
||||
|
@ -98,7 +105,7 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
|||
.setMessagesBackupAuthCredentialRequest(ByteString.copyFrom(messagesAuthCredRequest.serialize()))
|
||||
.build());
|
||||
|
||||
verify(backupAuthManager).commitBackupId(account, messagesAuthCredRequest, mediaAuthCredRequest);
|
||||
verify(backupAuthManager).commitBackupId(account, device, messagesAuthCredRequest, mediaAuthCredRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -147,9 +154,10 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
|||
@MethodSource
|
||||
void setBackupIdException(final Exception ex, final boolean sync, final Status expected) {
|
||||
if (sync) {
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenThrow(ex);
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any(), any())).thenThrow(ex);
|
||||
} else {
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.failedFuture(ex));
|
||||
when(backupAuthManager.commitBackupId(any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(ex));
|
||||
}
|
||||
|
||||
GrpcTestUtils.assertStatusException(
|
||||
|
|
Loading…
Reference in New Issue