passbolt/passbolt_api · error · Cake\Http\Exception\BadRequestException
The challenge cannot be decrypted.
Error message
The challenge cannot be decrypted.
What it means
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.
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
Example fix
// before: encrypted with stale/cached server key
$challenge = encrypt($oldServerFingerprint, $payload);
// after: fetch current server key first
$serverKey = $httpClient->get('/auth/verify.json')->serverKey;
$challenge = encrypt($serverKey->fingerprint, $payload); Defensive patterns
Strategy: validation
Validate before calling
if (!challenge.startsWith('-----BEGIN PGP MESSAGE-----')) throw new Error('challenge must be an armored PGP message encrypted to the server key'); Type guard
null
Try / catch
try { await login(challenge); } catch (e) { if (e.status === 400 && /cannot be decrypted/.test(e.message)) { await refreshServerKeyAndRetry(); } } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Could not import the user OpenPGP key.
- The challenge is invalid. Deserialization failed.
- The challenge is invalid. Validation Failed.
- The domain is invalid.
- The domain is invalid. Expected
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/f56f249248f18957.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php:317
* @throws \InvalidArgumentException if the challenge is missing
* @throws \Cake\Http\Exception\BadRequestException if the challenge is invalid
* @return string
*/
public function verifyChallenge(): string
{
// Sanity check
$armoredChallenge = $this->request->getData('challenge');
$this->assertArmoredChallenge($armoredChallenge);
// Decrypt and verify signature
try {
$clearTextChallenge = $this->gpg->decrypt($armoredChallenge, true);
} catch (InvalidSignatureException $exception) {
Log::error($exception->getMessage());
throw new InvalidUserSignatureException(__('The user signature could not be verified.'));
} catch (Exception $exception) {
Log::error($exception->getMessage());
throw new BadRequestException(__('The challenge cannot be decrypted.'));
}
// Deserialize JSON
try {
$jsonChallenge = json_decode($clearTextChallenge, true, 2, JSON_THROW_ON_ERROR);
[
'version' => $version,
'domain' => $domain,
'verify_token' => $verifyToken,
'verify_token_expiry' => $verifyTokenExpiry,
] = $jsonChallenge;
} catch (Exception $exception) {
Log::error($exception->getMessage() . "\n" . $clearTextChallenge);
throw new BadRequestException(__('The challenge is invalid. Deserialization failed.'));
}
// Challenge sanity check
View on GitHub (pinned to 31c1bbc10f)