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
- Re-derive and resend the correct subscriberId/HMAC pair on the client, ensuring both are persisted atomically together.
- 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.
- Verify the client is not URL-encoding/base64-encoding the subscriberId differently than when it was created.
- 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
- Persist subscriberUser and HMAC key atomically on the client
- Never regenerate credentials on retry — reuse the same pair
- Use one canonical base64/URL encoding for subscriberId everywhere
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
- must not use authenticated connection for subscriber…
- access key length must be 16
- Invalid combined unidentified sender access keys
- cannot create payment methods with payment type
- 400 Bad Request (UNSUPPORTED_LEVEL)
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)