signalapp/Signal-Server · error · BackupPermissionException
Only primary device can set backup-id
Error message
Only primary device can set backup-id
What it means
BackupAuthManager.commitBackupId only allows the account's primary device to set (commit) a backup-id. When a linked (non-primary) device calls it, a BackupPermissionException is thrown to prevent linked devices from taking over backup configuration.
Solutions
- Perform backup-id setup on the primary device; route the request through the primary or have the primary do it first.
- Check Device#isPrimary() client-side before calling commit and show an appropriate error otherwise.
- Ensure the authenticated device in the request is the primary; if not, return a 403-style response to the user.
Example fix
// before
backupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);
// after
if (!device.isPrimary()) {
throw new WebApplicationException("backup setup must be done from primary device", 403);
}
backupAuthManager.commitBackupId(account, device, messagesReq, mediaReq); Defensive patterns
Strategy: try-catch
Validate before calling
if (!device.isPrimary()) {
throw new WebApplicationException("backup setup must be performed from the primary device", 403);
} Type guard
boolean isPrimary(Device d) {
return d != null && d.isPrimary();
} Try / catch
try {
backupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);
} catch (BackupPermissionException e) {
return Response.status(403).entity("primary device required").build();
} Prevention
- Check Device#isPrimary() on the client before initiating backup setup
- Direct linked devices to a read-only or delegated backup flow
- Test multi-device flows so secondary devices never hit primary-only endpoints
When it happens
Trigger: A linked device (device.isPrimary() == false) invoking PUT /v1/backups/ or commitBackupId to set a new backup-id / backup auth credential request.
Common situations: Client SDKs performing backup setup on a secondary device (desktop/iPad link); fleet rollouts where the wrong device performs initialization; testing with a linked device assuming parity with primary.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- credential does not support the requested operation
- wrong credential type for the requested operation
- Must set at least one of message/media credential requests
- receipt credential presentation verification failed
- Invalid sourceObject size
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/70711b6e6a44e536.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java:107
/**
* 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
* media backups
* @throws RateLimitExceededException If too many backup-ids have been committed
*/
public void commitBackupId(
final Account account,
final Device device,
final Optional<BackupAuthCredentialRequest> messagesBackupCredentialRequest,
final Optional<BackupAuthCredentialRequest> mediaBackupCredentialRequest)
throws RateLimitExceededException, BackupPermissionException, BackupInvalidArgumentException {
if (!device.isPrimary()) {
throw new BackupPermissionException("Only primary device can set backup-id");
}
if (messagesBackupCredentialRequest.isEmpty() && mediaBackupCredentialRequest.isEmpty()) {
throw new BackupInvalidArgumentException("Must set at least one of message/media credential requests");
}
final byte[] storedMessageCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)
.orElse(null);
final byte[] storedMediaCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MEDIA)
.orElse(null);
// If the provided credential request is null, we want to set to the existing request
final byte[] targetMessageCredentialRequest = messagesBackupCredentialRequest
.map(BackupAuthCredentialRequest::serialize)
.orElse(storedMessageCredentialRequest);
final byte[] targetMediaCredentialRequest = mediaBackupCredentialRequest
.map(BackupAuthCredentialRequest::serialize)
.orElse(storedMediaCredentialRequest);View on GitHub (pinned to 100ab61c82)