XTLS/Xray-core · error

verify token mismatch

Error message

verify token mismatch

What it means

The decrypted verify token's first 4 bytes do not equal the random token the server generated for this handshake (or the token is shorter than 4 bytes). This is the anti-replay, session-binding check of the Minecraft encryption handshake: a legitimate client echoes the exact bytes back. A mismatch means the response belongs to a different session, was replayed, or the client is broken/hostile.

Source

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

		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:]

		if subtle.ConstantTimeCompare(receivedPassword, []byte(c.password)) != 1 {
			writeDisconnectPacket(c.writer, `{"type":"translatable","translate":"multiplayer.disconnect.authservers_down"}`)
			return fmt.Errorf("bad password")

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Confirm verifyToken is generated and stored per connection (the code does rand.Read per handshake — verify no refactor made it shared).
  2. If writing a client, echo the token bytes verbatim: encrypt exactly the 4 bytes received plus the password, nothing more.
  3. Disconnect on mismatch — retrying with the same token would defeat the check's purpose.
  4. Log the expected and received bytes at trace level during client development to pinpoint echo bugs.

Example fix

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

// after: distinguish the two failure shapes for clearer logs
if len(decryptedVerifyToken) < 4 {
    return fmt.Errorf("verify token too short: %d bytes", len(decryptedVerifyToken))
}
if !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) {
    return fmt.Errorf("verify token mismatch")
}
Defensive patterns

Strategy: validation

Validate before calling

if len(decryptedVerifyToken) < 4 {
    return errors.New("verify token too short")
}
if !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) {
    return errors.New("verify token mismatch")
}

Prevention

When it happens

Trigger: Replaying a captured encryption-response packet from an earlier session; a client that echoes the wrong token bytes (endian/encoding bug); concurrency bug where two connections' tokens got crossed (e.g. shared state instead of per-connection verifyToken); decryptedVerifyToken shorter than 4 bytes.

Common situations: Attack tooling that records and replays handshakes; parallel connection tests where token state leaks between connections; client implementation that hashes or transforms the token instead of echoing it.

Related errors


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