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
- Confirm and re-enter the correct recovery password on the client before retrying.
- Re-sync account state so the client uses the current recovery password (it may have been rotated).
- Fall back to change-number via a verified registration session (SMS challenge) instead of recovery password.
- 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
- Re-prompt users instead of silently retrying with a cached recovery password
- Sync recovery-password rotations from other devices
- Offer registration-session verification as an alternate path
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
- recovery password could not be verified
- Operation requires unauthenticated access
- must not use authenticated connection for anonymous…
- must not use authenticated connection for call quality…
- Endpoint requires unauthenticated access
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)