{"record":{"id":"70711b6e6a44e536","repo":"signalapp/Signal-Server","slug":"only-primary-device-can-set-backup-id","errorCode":null,"errorMessage":"Only primary device can set backup-id","messagePattern":"Only primary device can set backup-id","errorType":"exception","errorClass":"BackupPermissionException","httpStatus":403,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java","lineNumber":107,"sourceCode":"  /**\n   * Store credential requests containing blinded backup-ids for future use.\n   *\n   * @param account                         The account using the backup-id\n   * @param device                          The device setting the account backup-id\n   * @param messagesBackupCredentialRequest A request containing the blinded backup-id the client will use to upload\n   *                                        message backups\n   * @param mediaBackupCredentialRequest    A request containing the blinded backup-id the client will use to upload\n   *                                        media backups\n   * @throws RateLimitExceededException If too many backup-ids have been committed\n   */\n  public void commitBackupId(\n      final Account account,\n      final Device device,\n      final Optional<BackupAuthCredentialRequest> messagesBackupCredentialRequest,\n      final Optional<BackupAuthCredentialRequest> mediaBackupCredentialRequest)\n      throws RateLimitExceededException, BackupPermissionException, BackupInvalidArgumentException {\n    if (!device.isPrimary()) {\n      throw new BackupPermissionException(\"Only primary device can set backup-id\");\n    }\n\n    if (messagesBackupCredentialRequest.isEmpty() && mediaBackupCredentialRequest.isEmpty()) {\n      throw new BackupInvalidArgumentException(\"Must set at least one of message/media credential requests\");\n    }\n\n    final byte[] storedMessageCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)\n        .orElse(null);\n    final byte[] storedMediaCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MEDIA)\n        .orElse(null);\n\n    // If the provided credential request is null, we want to set to the existing request\n    final byte[] targetMessageCredentialRequest = messagesBackupCredentialRequest\n        .map(BackupAuthCredentialRequest::serialize)\n        .orElse(storedMessageCredentialRequest);\n    final byte[] targetMediaCredentialRequest = mediaBackupCredentialRequest\n        .map(BackupAuthCredentialRequest::serialize)\n        .orElse(storedMediaCredentialRequest);","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java#L89-L125","documentation":"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.","triggerScenarios":"A linked device (device.isPrimary() == false) invoking PUT /v1/backups/ or commitBackupId to set a new backup-id / backup auth credential request.","commonSituations":"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.","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."],"exampleFix":"// before\nbackupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);\n// after\nif (!device.isPrimary()) {\n  throw new WebApplicationException(\"backup setup must be done from primary device\", 403);\n}\nbackupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);","handlingStrategy":"try-catch","validationCode":"if (!device.isPrimary()) {\n  throw new WebApplicationException(\"backup setup must be performed from the primary device\", 403);\n}","typeGuard":"boolean isPrimary(Device d) {\n  return d != null && d.isPrimary();\n}","tryCatchPattern":"try {\n  backupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);\n} catch (BackupPermissionException e) {\n  return Response.status(403).entity(\"primary device required\").build();\n}","preventionTips":["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"],"tags":["authorization","backup","device-permissions"],"backgroundTag":"permission-denied","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}