{"record":{"id":"91ab92c08894aae0","repo":"signalapp/Signal-Server","slug":"must-specify-a-pni-associated-identity-key-when-re","errorCode":null,"errorMessage":"Must specify a PNI-associated identity key when recovering an account by identifier","messagePattern":"Must specify a PNI-associated identity key when recovering an account by identifier","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":412,"sourceCode":"      final String password,\n      final RegistrationRequest registrationRequest,\n      final String userAgent,\n      final String signalAgent) throws RegistrationLockFailureException, RateLimitExceededException {\n\n    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {\n      throw new BadRequestException(\"login purchases are not enabled\");\n    }\n\n    if (ArrayUtils.isEmpty(registrationRequest.recoveryPassword())) {\n      throw new BadRequestException(\"Recovery password required for authentication when recovering an account by identifier\");\n    }\n\n    if (registrationRequest.accountAttributes().recoveryPassword().isEmpty()) {\n      throw new BadRequestException(\"Recovery password required for for storage when recovering an account by identifier\");\n    }\n\n    if (registrationRequest.pniIdentityKey() == null) {\n      throw new BadRequestException(\"Must specify a PNI-associated identity key when recovering an account by identifier\");\n    }\n\n    final Account existingAccount = accounts.getByAccountIdentifier(accountIdentifier)\n            .orElseThrow(ForbiddenException::new);\n\n    final boolean passwordVerified = existingAccount.getAccountRecoveryPassword()\n        .map(saltedRecoveryPasswordHash -> PhoneNumberRecoveryPasswordsManager.verify(saltedRecoveryPasswordHash, registrationRequest.recoveryPassword()))\n        .orElse(false);\n\n    if (!passwordVerified) {\n      throw new ForbiddenException();\n    }\n\n    checkTotp(existingAccount, registrationRequest.totp());\n\n    if (!registrationRequest.skipDeviceTransfer() && existingAccount.hasCapability(DeviceCapability.TRANSFER)) {\n      // If a device transfer is possible, clients must explicitly opt out of a transfer (i.e. after prompting the user)\n      // before we'll let them recover an account and start \"from scratch\"","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L394-L430","documentation":"recoverAccount requires a PNI (phone-number identity) identity key when recovering an account by identifier, since recovery re-establishes the PNI identity. If registrationRequest.pniIdentityKey() is null the controller throws a BadRequestException (HTTP 400) before loading the existing account.","triggerScenarios":"Submitting a recovery-by-identifier registration request without the pniIdentityKey field populated (null after deserialization).","commonSituations":"Older clients predating PNI that only send an ACI identity key; hand-written API requests omitting pniIdentityKey; serialization bugs where the key object fails to deserialize into the request DTO.","solutions":["Generate and include the PNI identity key (IdentityKey for the PNI pair) in the RegistrationRequest.","Upgrade the client to a PNI-aware version — pre-PNI clients cannot perform identifier recovery against this server API.","Check request JSON deserialization logs if the key was sent but arrived null (field-name mismatch, wrong nesting).","Update API test harnesses to always set pniIdentityKey alongside aciIdentityKey."],"exampleFix":"// before\nnew RegistrationRequest(attrs, aciIdentityKey, null, recoveryPassword);\n// after\nnew RegistrationRequest(attrs, aciIdentityKey, pniIdentityKey, recoveryPassword);","handlingStrategy":"validation","validationCode":"if (request.pniIdentityKey() == null) {\n  throw new IllegalArgumentException(\"pniIdentityKey is required for recovery by identifier\");\n}","typeGuard":"boolean hasPniIdentityKey(RegistrationRequest r) {\n  return r.pniIdentityKey() != null;\n}","tryCatchPattern":"try {\n  recoverAccount(request);\n} catch (BadRequestException e) {\n  if (e.getMessage().contains(\"PNI-associated identity key\")) {\n    generatePniIdentityKeyAndRetry();\n  } else throw e;\n}","preventionTips":["Always generate and send both ACI and PNI identity keys in registration/recovery requests.","Require a PNI-capable client version; pre-PNI clients cannot use this endpoint.","Check deserialization logs when the key is sent but arrives null."],"tags":["account-recovery","missing-field","bad-request","identity-key","pni"],"backgroundTag":"missing-required-argument","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"}