signalapp/Signal-Server · error · SubscriptionForbiddenException

subscriberId mismatch

Error message

subscriberId mismatch

What it means

updateSubscriber throws SubscriptionForbiddenException("subscriberId mismatch") when subscriptions.get() returns PASSWORD_MISMATCH: the subscriberUser exists but the presented HMAC does not match the stored one. It means the client is using credentials (username/HMAC pair) that do not correspond to the stored subscription record.

Solutions

  1. Re-derive and resend the correct subscriberId/HMAC pair on the client, ensuring both are persisted atomically together.
  2. If the subscriberId is lost, call updateSubscriber with createPermitted=true and a freshly generated subscriberUser/hmac to create a new record instead of guessing the old credentials.
  3. Verify the client is not URL-encoding/base64-encoding the subscriberId differently than when it was created.
  4. As a last resort, delete the DDB record for that subscriberUser so a fresh create can succeed.

Example fix

// before
manager.updateSubscriber(SubscriberCredentials.process(oldSubscriberId, staleHmac, clock), true);
// after
byte[] subscriberUser = generateNewSubscriberUser();
byte[] hmac = computeHmac(subscriberUser);
manager.updateSubscriber(SubscriberCredentials.process(subscriberUser, hmac, clock), true);
Defensive patterns

Strategy: validation

Validate before calling

byte[] stored = keyStore.loadSubscriberHmac(subscriberUser);
if (stored == null || !MessageDigest.isEqual(stored, hmac)) {
  regenerateCredentials(); // before calling updateSubscriber
}

Try / catch

try {
  manager.updateSubscriber(creds, createPermitted);
} catch (SubscriptionForbiddenException e) {
  log.warn("subscriberId HMAC mismatch — regenerate credentials and retry with createPermitted=true");
}

Prevention

When it happens

Trigger: Calling SubscriptionManager.updateSubscriber with SubscriberCredentials whose hmac() fails verification against the stored record for subscriberUser() (Subscriptions.GetResult.PASSWORD_MISMATCH).

Common situations: Client regenerated its subscriberId locally but kept an old stored one (or vice versa) after a reinstall; key material out of sync between client devices; a different client trying to update someone else's subscription id; truncated/corrupted subscriberId sent over the wire.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/749c35b658c73e07. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java:116

  /**
   * Create or update a subscriber in the subscriptions table
   * <p>
   * If the subscriber does not exist, a subscriber with the provided credentials will be created. If the subscriber
   * already exists, its last access time will be updated.
   *
   * @param subscriberCredentials Subscriber credentials derived from the subscriberId
   * @param createPermitted Whether creating a new subscriber is permitted if one does not exist
   * @throws SubscriptionForbiddenException if the subscriber credentials were incorrect
   * @throws SubscriberIdCreationNotPermittedException if a new subscriber ID would be created, but the caller does not permit it
   */
  public void updateSubscriber(final SubscriberCredentials subscriberCredentials, final boolean createPermitted)
      throws SubscriptionForbiddenException, SubscriberIdCreationNotPermittedException {
    final Subscriptions.GetResult getResult =
        subscriptions.get(subscriberCredentials.subscriberUser(), subscriberCredentials.hmac());

    if (getResult == Subscriptions.GetResult.PASSWORD_MISMATCH) {
      throw new SubscriptionForbiddenException("subscriberId mismatch");
    } else if (getResult == Subscriptions.GetResult.NOT_STORED) {

      if (!createPermitted) {
        throw new SubscriberIdCreationNotPermittedException();
      }

      // create a customer and write it to ddb
      final Subscriptions.Record updatedRecord = subscriptions.create(subscriberCredentials.subscriberUser(),
          subscriberCredentials.hmac(),
          subscriberCredentials.now());
      if (updatedRecord == null) {
        throw new SubscriptionForbiddenException("subscriberId mismatch");
      }
    } else {
      // already exists so just touch access time and return
      subscriptions.accessedAt(subscriberCredentials.subscriberUser(), subscriberCredentials.now());
    }
  }

View on GitHub (pinned to 100ab61c82)