signalapp/Signal-Server · error · IllegalArgumentException

PNI identity key must not be provided if existing account…

Error message

PNI identity key must not be provided if existing account does not have a phone number

What it means

During account creation/update, AccountsManager throws IllegalArgumentException("PNI identity key must not be provided if existing account does not have a phone number") when a caller supplies a PNI identity key for an account that has no phone number (and thus no PNI). The PNI identity key only makes sense for accounts with a phone-number identity.

Solutions

  1. Only include pniIdentityKey in the request when the target account has a phone number (account.getPhoneNumberIdentifier().isPresent())
  2. Check existingAccount.getPhoneNumberIdentifier().isPresent() before passing the key
  3. Update older clients/tools to omit the PNI key for non-PNI accounts
  4. Fix test fixtures to match the account shape

Example fix

// before
manager.changeNumber(account, number, pniIdentityKey, pniSigningKey, attributes); // always passes key
// after
final Optional<IdentityKey> effectivePniKey = account.getPhoneNumberIdentifier().isPresent()
    ? Optional.of(pniIdentityKey) : Optional.empty();
manager.changeNumber(account, number, effectivePniKey, pniSigningKey, attributes);
Defensive patterns

Strategy: validation

Validate before calling

// Only pass PNI identity key when the account has a PNI
final boolean hasPni = existingAccount.getPhoneNumberIdentifier().isPresent();
if (!hasPni && pniIdentityKey.isPresent()) {
  throw new WebApplicationException(Response.status(400).build());
}

Type guard

static boolean canAcceptPniIdentityKey(Account account, Optional<IdentityKey> pniKey) {
  return pniKey.isEmpty() || account.getPhoneNumberIdentifier().isPresent();
}

Try / catch

try {
  accounts.changeNumber(...);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("PNI identity key must not be provided")) {
    return Response.status(400).entity(e.getMessage()).build();
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling changeNumber or account creation flows passing a pniIdentityKey Optional.of(...) while the existing account lacks a phone number.

Common situations: Client migration code written before PNI existed being applied to legacy/no-PNI accounts; copy-paste in account-update endpoints passing all keys unconditionally; tests using fixtures mixing old and new account shapes.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java:593

      @Nullable final String userAgent) {

    final Account recoveredAccount = accountLockManager.withSingleAccountLock(existingAccount, () -> {
      final Account account = new Account();
      account.setAccountIdentifier(existingAccount.getAccountIdentifier());

      if (existingAccount.getNumber().isPresent()) {
        account.setNumber(existingAccount.getNumber().get(),
            existingAccount.getPhoneNumberIdentifier()
                .orElseThrow(() -> new AssertionError("Accounts that have a phone number must also have a PNI")));

        account.setPhoneNumberIdentityKey(maybePniIdentityKey
            .orElseThrow(() -> new IllegalArgumentException("PNI identity key must be provided if existing account has a phone number")));

        account.setRegistrationLockFromAttributes(accountAttributes);
        account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
      } else {
        if (maybePniIdentityKey.isPresent()) {
          throw new IllegalArgumentException("PNI identity key must not be provided if existing account does not have a phone number");
        }

        final byte[] authCredentialSalt = new byte[AUTH_CREDENTIAL_SALT_SIZE];
        SECURE_RANDOM.nextBytes(authCredentialSalt);

        account.setAuthCredentialSalt(authCredentialSalt);
      }

      account.setIdentityKey(aciIdentityKey);
      account.addDevice(primaryDeviceSpec.toDevice(Device.PRIMARY_ID, clock, aciIdentityKey));
      account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
      account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
      account.setAccountRecoveryPassword(accountAttributes.recoveryPassword().orElseThrow(() ->
          new IllegalArgumentException("Must specify a recovery password when reclaiming an existing account")));

      reclaimAccount(account, existingAccount, primaryDeviceSpec, accountAttributes);

      return account;

View on GitHub (pinned to 100ab61c82)