signalapp/Signal-Server · error · ForbiddenException

recovery password could not be verified

Error message

recovery password could not be verified

What it means

When change-number is authorized via account recovery password instead of a verified registration session, the server verifies the supplied recovery password (account recovery password / SRP-based check). If RecoveryPasswordVerificationFailedException is thrown, the request is rejected with 403 ForbiddenException('recovery password could not be verified').

Solutions

  1. Confirm and re-enter the correct recovery password on the client before retrying.
  2. Re-sync account state so the client uses the current recovery password (it may have been rotated).
  3. Fall back to change-number via a verified registration session (SMS challenge) instead of recovery password.
  4. Update the client if the recovery-password derivation scheme changed between versions.

Example fix

// before
changeNumberWithRecoveryPassword(staleRecoveryPassword);
// after
recoveryPassword = promptUserForRecoveryPassword();
changeNumberWithRecoveryPassword(recoveryPassword);
Defensive patterns

Strategy: try-catch

Validate before calling

if (!recoveryPassword || recoveryPassword.length === 0) throw new Error('recovery password required');

Try / catch

try { await changeNumberWithRecoveryPassword(pw); } catch (e) { if (e.status === 403 && e.message.includes('recovery password')) { pw = await promptUserAgain(); return changeNumberWithRecoveryPassword(pw); } throw e; }

Prevention

When it happens

Trigger: PUT /v2/accounts/phone_number/{number} using recovery-password-based authorization where the supplied recovery password hash fails verification against the account's stored recovery password.

Common situations: User supplied the wrong recovery password, recovery password was reset/rotated on another device so the client's copy is stale, or the client derived the recovery-password proof with an outdated algorithm.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java:144

            .type(MediaType.APPLICATION_JSON_TYPE)
            .entity(new MismatchedDevicesResponse(e.getMismatchedDevices().missingDeviceIds(),
                e.getMismatchedDevices().extraDeviceIds()))
            .build());
      }
    } catch (final IllegalArgumentException e) {
      throw new BadRequestException(e);
    } catch (final MessageTooLargeException e) {
      throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE);
    } catch (final MessageDeliveryNotAllowedException e) {
      throw new ServiceUnavailableException();
    } catch (final UnverifiedRegistrationSessionException e) {
      throw new NotAuthorizedException("registration session is unverified");
    } catch (final InvalidRegistrationSessionException e) {
      throw new BadRequestException(e.getMessage());
    } catch (final IOException e) {
      throw new ServiceUnavailableException(e.getMessage());
    } catch (final RecoveryPasswordVerificationFailedException e) {
      throw new ForbiddenException("recovery password could not be verified");
    }
  }

  @PUT
  @Path("/phone_number_discoverability")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "Sets whether the account should be discoverable by phone number in the directory.")
  @ApiResponse(responseCode = "204", description = "The setting was successfully updated.")
  public void setPhoneNumberDiscoverability(
      @Auth AuthenticatedDevice auth,
      @NotNull @Valid PhoneNumberDiscoverabilityRequest phoneNumberDiscoverability) {

    accountsManager.update(auth.accountIdentifier(), account -> {
      if (account.getNumber().isEmpty()) {
        throw new BadRequestException();
      }

View on GitHub (pinned to 100ab61c82)