{"record":{"id":"f56f249248f18957","repo":"passbolt/passbolt_api","slug":"the-challenge-cannot-be-decrypted","errorCode":null,"errorMessage":"The challenge cannot be decrypted.","messagePattern":"The challenge cannot be decrypted\\.","errorType":"exception","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php","lineNumber":317,"sourceCode":"     * @throws \\InvalidArgumentException if the challenge is missing\n     * @throws \\Cake\\Http\\Exception\\BadRequestException if the challenge is invalid\n     * @return string\n     */\n    public function verifyChallenge(): string\n    {\n        // Sanity check\n        $armoredChallenge = $this->request->getData('challenge');\n        $this->assertArmoredChallenge($armoredChallenge);\n\n        // Decrypt and verify signature\n        try {\n            $clearTextChallenge = $this->gpg->decrypt($armoredChallenge, true);\n        } catch (InvalidSignatureException $exception) {\n            Log::error($exception->getMessage());\n            throw new InvalidUserSignatureException(__('The user signature could not be verified.'));\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage());\n            throw new BadRequestException(__('The challenge cannot be decrypted.'));\n        }\n\n        // Deserialize JSON\n        try {\n            $jsonChallenge = json_decode($clearTextChallenge, true, 2, JSON_THROW_ON_ERROR);\n            [\n                'version' => $version,\n                'domain' => $domain,\n                'verify_token' => $verifyToken,\n                'verify_token_expiry' => $verifyTokenExpiry,\n            ] = $jsonChallenge;\n        } catch (Exception $exception) {\n            Log::error($exception->getMessage() . \"\\n\" . $clearTextChallenge);\n            throw new BadRequestException(__('The challenge is invalid. Deserialization failed.'));\n        }\n\n        // Challenge sanity check\n","sourceCodeStart":299,"sourceCodeEnd":335,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php#L299-L335","documentation":"verifyChallenge decrypts the armored challenge with signature verification. Any decryption failure other than InvalidSignatureException (wrong recipient key, malformed PGP message, missing server private key) surfaces as this BadRequestException.","triggerScenarios":"POST /auth/jwt/login where $this->gpg->decrypt($armoredChallenge, true) throws a generic Exception: challenge encrypted for a different server key, corrupted armored block, server gpg keychain missing the private key, or passphrase/key setup broken.","commonSituations":"Server key regenerated or restored from backup without the private key in the keyring; client cached an old server public key; full_plus_url vs armored payload corruption through JSON encoding; GNUPGHOME misconfigured so decrypt has no secret key.","solutions":["Confirm the challenge was encrypted to the server's current public key fingerprint (passbolt serverkey fingerprint config)","Run `gpg --list-secret-keys` as the web server user to ensure the server private key exists in its keyring","Check error logs for the underlying Exception message logged before this error","Re-fetch the server public key on the client and re-encrypt the challenge","Validate the armored challenge is not truncated or re-encoded (e.g. by JSON/base64 round-trips) in transit"],"exampleFix":"// before: encrypted with stale/cached server key\n$challenge = encrypt($oldServerFingerprint, $payload);\n// after: fetch current server key first\n$serverKey = $httpClient->get('/auth/verify.json')->serverKey;\n$challenge = encrypt($serverKey->fingerprint, $payload);","handlingStrategy":"validation","validationCode":"if (!challenge.startsWith('-----BEGIN PGP MESSAGE-----')) throw new Error('challenge must be an armored PGP message encrypted to the server key');","typeGuard":"null","tryCatchPattern":"try { await login(challenge); } catch (e) { if (e.status === 400 && /cannot be decrypted/.test(e.message)) { await refreshServerKeyAndRetry(); } }","preventionTips":["Encrypt the challenge to the current server public key fetched from /auth/verify.json","Ensure the web-server user's GNUPGHOME contains the server private key","Avoid body re-encoding that could corrupt armored newlines","Run passbolt healthcheck after any key/backup restore"],"tags":["gpg","decryption","jwt","challenge"],"backgroundTag":"decryption-failed","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}