commitment(), publicKey() and proof(), which compute the bot's answers to the proof of identity the game server asks for on each game connection, with when to call each one and every error.
Once a game session is open, the game server asks the client to prove who it is, in three exchanges. asterobot:verification computes what the bot answers with. Your script sends the messages, and the secrets behind the answers stay inside Asterobot: your code only ever sees the values that go on the wire.
Only a Full socket bot needs it. On a MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client that someone plays), the Dofus client answers these exchanges itself, and a script answering too would send each answer twice.
Most scripts never import this module. @asterobot:base answers the three exchanges for every package that depends on it, on a Full socket bot, and leaves them to the Dofus client on a MITM bot. This page is for writing such a package yourself, or for understanding what base sends.
import { commitment, publicKey, proof } from "asterobot:verification";
These are the module's only exports. There's no verification object to import.
| Export | Returns | What it gives you |
|---|---|---|
commitment() |
A string, right away | The id of ClientIdRequest |
publicKey() |
A string, right away | The challengeKey of ClientChallengeInitRequest |
proof() |
A string, right away | The proof of ClientChallengeProofRequest |
Every value is a whole number written in base 10, which is how the three messages carry them. None of the functions returns a promise.
The three exchanges
| The server sends | Answer with | Carrying |
|---|---|---|
ServerVerificationEvent |
ClientIdRequest |
id: commitment() |
ServerSessionReadyEvent |
ClientChallengeInitRequest |
challengeKey: publicKey() |
ServerChallengeEvent |
ClientChallengeProofRequest |
proof: proof(traffic.payload?.value) |
Answer each one as soon as it arrives, and only when it arrives: the Dofus client sends each of these requests from the handler of the message that asks for it, and from nowhere else.
When they come
ServerSessionReadyEventcloses the burst of messages that follows an accepted identification, once per connection.ServerVerificationEvent, thenServerChallengeEvent, come later and again from time to time: in captures, the first came about five and a half minutes into a session, and more followed. The client's code shows the poll can also arrive right after the connection opens, before the server has answered the identification. Register the handlers above the identification block, as the sample below does, so none can be missed.- The challenge comes with a value or without one. Both happen, even on the same game launch. Without a value, the event is empty, and
proof()works the challenge out itself.
Send them with a uid
The Dofus client sends these three requests with a uid of its own, and nothing ever answers them. Send them the same way, with correlated: true:
await send("ClientIdRequest", { id: commitment() }, { correlated: true });
A plain send() would put -1 where the client puts a uid. A request() would wait for a response that never comes, and reject with context deadline exceeded once its timeout runs out. Sending explains the three ways to send.
Answer at once
Send each answer as soon as its event arrives. The server waits for each answer before its next step, so the three can't cross.
commitment()
commitment() returns a string.
Each call draws a new random secret, keeps it inside Asterobot, and returns the value that proves it without revealing it. That secret replaces the previous one and is what the next proof() uses. So call it once for every ServerVerificationEvent, and never ahead of time: the Dofus client does the same, and a second call before the challenge makes the proof use the newer secret.
Every call returns a different value.
publicKey()
publicKey() returns a string.
It returns the bot's public key, which the server checks every proof against. It's the same value every time, and it belongs to the bot rather than to your script. It stays the same when the script stops and plays again, and across the bot's reconnections, as the key of a Dofus client stays the same for as long as the client runs. Captures confirm it: a client's proofs on two connections of one game launch both verify against the key it sent on the first.
A bot gets a new key when Asterobot starts again, and when a change of its Ankama account or of its game version makes Asterobot rebuild it.
proof()
proof(challenge?) returns a string.
| Parameter | Type | Description |
|---|---|---|
challenge |
string, undefined or null |
The value of the ServerChallengeEvent you're answering, exactly as it arrived |
Pass traffic.payload?.value. The field is optional, so it's absent when the server sent no value, and proof() then answers the way the Dofus client does in that case: it works the challenge out itself, from the values of the exchange and the device identifier the bot sent when it signed in (see Device identifier). A bot that didn't sign in on its own uses an empty one.
proof() draws nothing: it answers with the secret of the latest commitment(). Before the first commitment() of a bot, that secret is zero, which is also what the Dofus client answers with when a challenge comes before any ServerVerificationEvent.
A complete example
This script answers the three exchanges on a Full socket bot, and leaves them to the Dofus client on a MITM bot:
import { session, botWarn } from "asterobot:bot";
import { on, send } from "asterobot:protocol";
import { commitment, publicKey, proof } from "asterobot:verification";
async function answer(type, payload) {
try {
// With a uid, as the Dofus client sends these three.
await send(type, payload, { correlated: true });
} catch (error) {
// An error that escapes a handler stops the whole script.
botWarn(`Couldn't send ${type}:`, String(error));
}
}
export default async function behavior(launch) {
// On a MITM bot the Dofus client answers these itself.
if (!session.shared) {
on("ServerVerificationEvent", () => answer("ClientIdRequest", { id: commitment() }));
on("ServerSessionReadyEvent", () => answer("ClientChallengeInitRequest", { challengeKey: publicKey() }));
on("ServerChallengeEvent", (traffic) => {
try {
answer("ClientChallengeProofRequest", { proof: proof(traffic.payload?.value) });
} catch (error) {
// proof() throws for a value that isn't a number, and an error
// thrown in a handler stops the whole script.
botWarn("Couldn't answer the challenge:", String(error));
}
});
}
// A new game connection starts with this message. MITM bots never launch
// with "initial": the Dofus client has already identified their session.
if (launch.reason === "initial") {
await send("IdentificationRequest", {
ticketKey: session.gameToken,
languageCode: session.language,
});
}
}
A script that stops and plays again in the middle of a session keeps working with it: the key is the bot's, and a challenge is answered with the secret of the bot's latest commitment(), whichever run of the script made it.
Checked against the real client
The computation was read out of the Dofus client's code: the 768-bit group of RFC 2409 section 6.1 with G = 2, the proof (k + c·x) mod Q, and the challenge the client works out when the server sends none. The server checks a proof with G^proof = id × challengeKey^c (mod P).
Asterobot's tests check all of it against the real client too. They hold two checks a Dofus client answered, taken from captures of one game launch:
| Check | Challenge | Result |
|---|---|---|
| 1 | None: the client worked it out | The client's proof verifies against the challenge proof() works out |
| 2 | A value from the server | The client's proof verifies against that value |
So the group, the proof and both kinds of challenge are the client's, and every answer these functions give passes the same check. The tests also cover the path from a script: a script answering the three events as the sample above does passes the check on both kinds of challenge.
Errors
| Error | Type | Cause |
|---|---|---|
verification.proof challenge must be a decimal string, or undefined when the server sent none |
TypeError |
challenge is neither a string, undefined nor null |
verification: the challenge is not a decimal integer: "<challenge>" |
Error | challenge isn't a whole number written in base 10. Spaces around it and a leading sign are accepted. |
String(error) gives TypeError: or GoError: followed by the text.
The three functions throw when the top level of a module calls them while Asterobot reads the package's declarations, with an error that ends with not available while reading a package's declarations: see Declarations. Call them from your handlers.
The Dofus protocol reference describes the six messages of the exchange.