XTLS/Xray-core · error

decrypt verify token: %w

Error message

decrypt verify token: %w

What it means

RSA PKCS#1 v1.5 decryption of the encrypted verify token failed. The verify token is the 4 random bytes the server sent in its encryption request, echoed back encrypted with the server public key (in this fork it also carries the password suffix). Decryption failure means the ciphertext does not match the server's private key or is malformed.

Source

Thrown at transport/internet/finalmask/xmc/server.go:199

			return fmt.Errorf("bad encrypt response packet id")
		}

		err = pkt.readFields(&encryptedSharedSecret, &encryptedVerifyToken)
		if err != nil {
			return fmt.Errorf("read encrypt response: %w", err)
		}

		sharedSecret, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedSharedSecret)
		if err != nil {
			return fmt.Errorf("decrypt shared secret: %w", err)
		}
		if len(sharedSecret) != 16 {
			return fmt.Errorf("bad shared secret length: %d", len(sharedSecret))
		}

		decryptedVerifyToken, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken)
		if err != nil {
			return fmt.Errorf("decrypt verify token: %w", err)
		}

		if len(decryptedVerifyToken) < 4 || !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) {
			return fmt.Errorf("verify token mismatch")
		}

		c.reader, err = newCryptoReader(c.reader, sharedSecret)
		if err != nil {
			return fmt.Errorf("new crypto reader: %w", err)
		}

		c.writer, err = newCryptoWriter(c.writer, sharedSecret)
		if err != nil {
			return fmt.Errorf("new crypto writer: %w", err)
		}

		// verify password
		receivedPassword := decryptedVerifyToken[4:]

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Compare len(encryptedVerifyToken) with the RSA modulus size — an off-by-framing truncation shows up immediately.
  2. Confirm the client encrypts both fields with the exact public key bytes from the encryption request packet of THIS session.
  3. Treat as a failed authentication: disconnect and count the event; repeated hits from one IP indicate probing.
  4. If both this and error 961 fire together, suspect key mismatch; if only this fires, suspect field framing.

Example fix

// before
decryptedVerifyToken, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken)
if err != nil {
    return fmt.Errorf("decrypt verify token: %w", err)
}

// after: add size context for triage
decryptedVerifyToken, err := rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken)
if err != nil {
    return fmt.Errorf("decrypt verify token (ct len %d): %w", len(encryptedVerifyToken), err)
}
Defensive patterns

Strategy: validation

Validate before calling

if len(encryptedVerifyToken) != c.rsaPrivateKey.Size() {
    return fmt.Errorf("verify token ciphertext has wrong size %d", len(encryptedVerifyToken))
}

Try / catch

if _, err := rsa.DecryptPKCS1v15(rand.Reader, key, encToken); err != nil {
    return errors.New("verify token rejected") // generic message, no crypto detail
}

Prevention

When it happens

Trigger: Client encrypted the token with a different public key than the one in this connection's encryption request; the encryptedVerifyToken field was truncated or empty from a framing bug; hostile random bytes. Note the shared secret decrypted fine at this point, so a failure here specifically implicates the second field or a partially-replayed handshake.

Common situations: Handshake replay where an attacker reuses an old shared-secret ciphertext but a fresh garbage token; client library bug serializing two RSA blocks; key rotation races.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/f05dbb9eedd72cfb. Report an issue: GitHub.