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

  1. Confirm the challenge was encrypted to the server's current public key fingerprint (passbolt serverkey fingerprint config)
  2. Run `gpg --list-secret-keys` as the web server user to ensure the server private key exists in its keyring
  3. Check error logs for the underlying Exception message logged before this error
  4. Re-fetch the server public key on the client and re-encrypt the challenge
  5. 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

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


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)