Skip to content

Leaderboards — Client-only games

Score Reporting

Start by encrypting the score. This is crucial to make it harder for hackers to tamper with your leaderboard.

    export async function encryptScore(score, encryptionKey) {
    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const algorithm = { name: 'AES-GCM', iv: iv };

    const keyBytes = new Uint8Array(
    atob(encryptionKey)
    .split('')
    .map((c) => c.charCodeAt(0)),
    );

    const cryptoKey = await window.crypto.subtle.importKey('raw', keyBytes, algorithm, false, ['encrypt']);

    const dataBuffer = new TextEncoder().encode(score.toString());
    const encryptedBuffer = await window.crypto.subtle.encrypt(algorithm, cryptoKey, dataBuffer);

    const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encryptedBuffer), iv.length);

    return btoa(String.fromCharCode(...combined));
    }
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;

public static string EncryptScore(float score, string encryptionKey)
{
    // generate random IV
    byte[] iv = new byte[12];
    using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
    {
        rng.GetBytes(iv);
    }

    byte[] key = Convert.FromBase64String(encryptionKey);
    byte[] plaintext = Encoding.UTF8.GetBytes(score.ToString(CultureInfo.InvariantCulture));

    using (var aes = Aes.Create())
    {
        aes.Key = key;
        aes.Mode = CipherMode.ECB;
        aes.Padding = PaddingMode.None;

        using (var encryptor = aes.CreateEncryptor())
        {
            // CTR mode encryption
            byte[] ciphertext = new byte[plaintext.Length];
            byte[] counter = new byte[16];
            Buffer.BlockCopy(iv, 0, counter, 0, 12);
            counter[15] = 1; // Start counter at 1

            for (int i = 0; i < plaintext.Length; i += 16)
            {
                byte[] keystream = encryptor.TransformFinalBlock(counter, 0, 16);
                int blockSize = Math.Min(16, plaintext.Length - i);
                for (int j = 0; j < blockSize; j++)
                {
                    ciphertext[i + j] = (byte)(plaintext[i + j] ^ keystream[j]);
                }

                // increment counter (32-bit big-endian)
                for (int k = 15; k >= 12; k--)
                {
                    if (++counter[k] != 0)
                        break;
                }
            }

            // combine: IV + ciphertext + Unity marker
            byte[] result = new byte[iv.Length + ciphertext.Length + 1];
            Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
            Buffer.BlockCopy(ciphertext, 0, result, iv.Length, ciphertext.Length);
            result[result.Length - 1] = 0x55; // Unity marker byte

            string base64Result = Convert.ToBase64String(result);
            return base64Result;
        }
    }
}

Afterwards, submit the score. You need to pass both the encrypted and the plain score:

const encryptionKey = 'your-32-byte-base64-key-here';

// Encrypt the score
const finalScore = 152.1;
const encryptedScore = await encryptScore(finalScore, encryptionKey);

// Submit the score
CrazyGames.SDK.user.submitScore({
    encryptedScore: encryptedScore,
    score: finalScore,
});
private string encryptionKey = "your-32-byte-base64-key-here";

// Encrypt the score
float finalScore = 152.1f;
string encryptedScore = EncryptScore(finalScore, encryptionKey);

// Submit to leaderboard
CrazySDK.User.SubmitScore(encryptedScore, finalScore);

Testing

You can test your leaderboard integration in our preview tool on the Developer Portal.

  • When your game submits a score, you'll see a submitScore message in the logs and in browser console.
  • To avoid hacking, the server response is always successful and validation is applied in our back-end.

Leaderboard QA Tool

Ask AI