{"record":{"id":"0c44ca119e71e621","repo":"signalapp/Signal-Server","slug":"recovery-password-could-not-be-verified","errorCode":null,"errorMessage":"recovery password could not be verified","messagePattern":"recovery password could not be verified","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java","lineNumber":144,"sourceCode":"            .type(MediaType.APPLICATION_JSON_TYPE)\n            .entity(new MismatchedDevicesResponse(e.getMismatchedDevices().missingDeviceIds(),\n                e.getMismatchedDevices().extraDeviceIds()))\n            .build());\n      }\n    } catch (final IllegalArgumentException e) {\n      throw new BadRequestException(e);\n    } catch (final MessageTooLargeException e) {\n      throw new WebApplicationException(Response.Status.REQUEST_ENTITY_TOO_LARGE);\n    } catch (final MessageDeliveryNotAllowedException e) {\n      throw new ServiceUnavailableException();\n    } catch (final UnverifiedRegistrationSessionException e) {\n      throw new NotAuthorizedException(\"registration session is unverified\");\n    } catch (final InvalidRegistrationSessionException e) {\n      throw new BadRequestException(e.getMessage());\n    } catch (final IOException e) {\n      throw new ServiceUnavailableException(e.getMessage());\n    } catch (final RecoveryPasswordVerificationFailedException e) {\n      throw new ForbiddenException(\"recovery password could not be verified\");\n    }\n  }\n\n  @PUT\n  @Path(\"/phone_number_discoverability\")\n  @Consumes(MediaType.APPLICATION_JSON)\n  @Produces(MediaType.APPLICATION_JSON)\n  @Operation(summary = \"Sets whether the account should be discoverable by phone number in the directory.\")\n  @ApiResponse(responseCode = \"204\", description = \"The setting was successfully updated.\")\n  public void setPhoneNumberDiscoverability(\n      @Auth AuthenticatedDevice auth,\n      @NotNull @Valid PhoneNumberDiscoverabilityRequest phoneNumberDiscoverability) {\n\n    accountsManager.update(auth.accountIdentifier(), account -> {\n      if (account.getNumber().isEmpty()) {\n        throw new BadRequestException();\n      }\n","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java#L126-L162","documentation":"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').","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nchangeNumberWithRecoveryPassword(staleRecoveryPassword);\n// after\nrecoveryPassword = promptUserForRecoveryPassword();\nchangeNumberWithRecoveryPassword(recoveryPassword);","handlingStrategy":"try-catch","validationCode":"if (!recoveryPassword || recoveryPassword.length === 0) throw new Error('recovery password required');","typeGuard":null,"tryCatchPattern":"try { await changeNumberWithRecoveryPassword(pw); } catch (e) { if (e.status === 403 && e.message.includes('recovery password')) { pw = await promptUserAgain(); return changeNumberWithRecoveryPassword(pw); } throw e; }","preventionTips":["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"],"tags":["authentication","recovery-password","forbidden"],"backgroundTag":"recovery-password-verification-failed","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}