Response token
Prove the answer came from ModKey and belongs to this exact request.
The success answer carries a token; rebuild it on the client from the values you sent and compare.
Keep in mind
- Lowercase hex
- Every algorithm answers a lowercase hex digest.
- The secret stays home
- Set it in Connect Settings > Request > Secret. It is never a request field.
- Default source
- New software signs
{{game}}-{{key}}-{{serial}}-{{secret}}withmd5. - Test endpoint
- Test Connect always signs with
hmac-sha256and a separate prefix, see test endpoint.
Algorithms
| Algorithm | Token |
|---|---|
md5 |
hex(MD5(source)) |
sha256 |
hex(SHA256(source)) |
sha512 |
hex(SHA512(source)) |
hmac-sha256 |
hex(HMAC-SHA256(source, CONNECT_SECRET)) |
Building the source
The source is a template you choose in Connect Settings. These placeholders are filled with the values of the request:
| Placeholder | Value |
|---|---|
{{game}} |
Game sent in the request |
{{key}} |
License key sent in the request |
{{serial}} |
Device id sent in the request |
{{secret}} |
Your Connect secret, never sent over the wire |
{{soft_id}} |
Software id |
Default source and an example
{{game}}-{{key}}-{{serial}}-{{secret}}
pubg-XXXX-XXXX-XXXX-XXXX-device-id-<CONNECT_SECRET>
Verifying
- Read the token from the response.
- Build source_template from the same values sent in the request.
- Calculate the selected hash.
- Compare both values in constant time.
The samples below use the recommended hmac-sha256 with the source {{soft_id}}|{{game}}|{{key}}|{{serial}}.
$source = "{$soft}|{$game}|{$key}|{$serial}";
$expected = hash_hmac('sha256', $source, CONNECT_SECRET);
if (! hash_equals($expected, $answer['data']['token'])) {
throw new RuntimeException('Bad token');
}
import hashlib, hmac
source = f"{soft}|{game}|{key}|{serial}"
expected = hmac.new(CONNECT_SECRET.encode(), source.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, answer["data"]["token"]):
raise ValueError("Bad token")
import { createHmac, timingSafeEqual } from "node:crypto";
const source = `${soft}|${game}|${key}|${serial}`;
const expected = createHmac("sha256", CONNECT_SECRET).update(source).digest("hex");
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(answer.data.token))) {
throw new Error("Bad token");
}
#include <openssl/hmac.h>
#include <openssl/crypto.h>
std::string source = soft + "|" + game + "|" + key + "|" + serial;
unsigned char mac[32];
unsigned int length = 0;
HMAC(EVP_sha256(), secret.data(), secret.size(),
reinterpret_cast<const unsigned char*>(source.data()), source.size(), mac, &length);
std::string expected = to_hex(mac, length);
bool ok = expected.size() == token.size()
&& CRYPTO_memcmp(expected.data(), token.data(), token.size()) == 0;
Keep {{secret}} out of the source
This signature source includes CONNECT_SECRET, and the success response returns the source in real. To keep the secret private, use hmac-sha256 with a source without {{secret}}. For example {{soft_id}}|{{game}}|{{key}}|{{serial}}.