GameJolt is a gaming platform/website that offers API access with features like achievements tracking for users, etc. I can use the API no problem on my tests but what I'm concerned about is the private key that is issued to my game. How do I ensure that it's protected from people that want to decompile my game? Once they get the API private key, they will be able to do anything with it.
-
\$\begingroup\$ Does this API key need to be present in game clients or does it suffice to use it from server infrastructure you control? \$\endgroup\$– Lars ViklundCommented Apr 2, 2017 at 18:26
-
\$\begingroup\$ @LarsViklund This can still be received by a hacker. \$\endgroup\$– KrythicCommented Apr 2, 2017 at 19:17
-
\$\begingroup\$ @Krythic I'm not advising to send the secret to the client. I was asking if the API use needed to be on the client at all. \$\endgroup\$– Lars ViklundCommented Apr 2, 2017 at 20:02
-
\$\begingroup\$ @LarsViklund I can probably proxy the API access from a backend that I control but I wanted to not to have to host anything myself if it can be helped \$\endgroup\$– user400424Commented Apr 4, 2017 at 2:04
3 Answers
This question has actually been asked several times, and the answer is always the same thing; you're not going to like it. It's impossible to keep things like this a secret. You can however, take measures to make it more difficult for your passwords to be discovered. Essentially, you could convert it to a byte array and then perform some kind of hashing algorithm on the bytes. This can also be reversed by anyone with enough time and patience, but so can anything else. What you're basically trying to do is increase the amount of time required to discover the internal passwords. If the time required is greater than what the "hacker" is willing to spend, they will fail, and your game will remain secure. An example of fairly simple byte encryption is as follows:
using System;
using System.IO;
using System.Text;
namespace Your.Engine.Framework
{
public class Obfuscator
{
private int _inext;
private int _inextp;
private int[] _seedArray = new int[56];
private int _seed;
public int Seed
{
get
{
return _seed;
}
set
{
_seed = value;
int subtraction = (_seed == Int32.MinValue) ? Int32.MaxValue : Math.Abs(_seed);
int mj = 0x9a4ec86 - subtraction;
_seedArray[0x37] = mj;
int mk = 1;
for (int i = 1; i < 0x37; i++)
{
int ii = (0x15 * i) % 0x37;
_seedArray[ii] = mk;
mk = mj - mk;
if (mk < 0x0)
{
mk += Int32.MaxValue;
}
mj = _seedArray[ii];
}
for (int k = 1; k < 0x5; k++)
{
for (int i = 1; i < 0x38; i++)
{
_seedArray[i] -= _seedArray[1 + (i + 0x1e) % 0x37];
if (_seedArray[i] < 0)
{
_seedArray[i] += Int32.MaxValue;
}
}
}
_inext = 0;
_inextp = 21;
}
}
public Obfuscator()
{
}
public Obfuscator(string seed)
{
SetSeed(seed);
}
public Obfuscator(int seed)
{
SetSeed(seed);
}
public void Reset()
{
this.Seed = this.Seed;
}
public void SetSeed(string seed)
{
this.Seed = GenerateHashCode(seed);
}
public void SetSeed(int seed)
{
this.Seed = seed;
}
private static unsafe int GenerateHashCode(string data)
{
fixed (char* str = data)
{
int num = 0x15051505;
int num2 = num;
int* numPtr = (int*)str;
for (int i = data.Length; i > 0; i -= 4)
{
num = (((num << 0x5) + num) + (num >> 0x1f)) ^ numPtr[0];
if (i <= 2)
{
break;
}
num2 = (((num2 << 0x5) + num2) + (num2 >> 0x1f)) ^ numPtr[1];
numPtr += 2;
}
return (num + (num2 * 0x5d588b65));
}
}
private int NextOffset()
{
int locINext = _inext;
int locINextp = _inextp;
if (++locINext >= 56)
{
locINext = 1;
}
if (++locINextp >= 56)
{
locINextp = 1;
}
int retVal = _seedArray[locINext] - _seedArray[locINextp];
if (retVal == Int32.MaxValue)
{
retVal--;
}
if (retVal < 0)
{
retVal += Int32.MaxValue;
}
_seedArray[locINext] = retVal;
_inext = locINext;
_inextp = locINextp;
return retVal;
}
public byte[] TransformString(string data)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
TransformBytes(bytes);
return bytes;
}
public void TransformFile(string inputPath, string outputPath)
{
byte[] inputBytes = File.ReadAllBytes(inputPath);
TransformBytes(inputBytes);
File.WriteAllBytes(outputPath, inputBytes);
}
public void TransformBytes(byte[] bytes)
{
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = (byte)(bytes[i] ^ NextOffset());
}
}
}
}
The above C# code was written for my game engine. It uses an XOR cipher and an inlined Random class. You take the password's GetHashCode(), run an xor cipher on using the hashcode on each byte, and then store the resulting byte array somewhere in your executable. In order for someone to decompile your password, they will need to first decompile this class successfully, figure out how it works, and just generally do a lot of work to convert everything. Using actual Obfuscation will greatly increase the time further, because the binaries will be simplified down to single letter variables, functions, and class names. It will lose all coherency, which will make it extremely difficult for someone to figure out what's going on.
-
4\$\begingroup\$ +1, good post. Still, the attacker doesn't need to figure out how the code works, they just need to know what it does (deliver the key) and then run it, so it delivers the key. \$\endgroup\$– PeterCommented Apr 2, 2017 at 21:14
A simple key based software solution is insufficient to solve this problem.
Even if you obfuscate the key, people can easily wait until you generated the key, stop the execution, and grab it from memory. Obfuscation of keys is only relevant in scenarios where attackers fully automate the extraction, e.g. to switch the key for in app payments before they re-release your game under their name.
If creating a user involves some kind of cost, you can try to detect and resolve compromised instructions on the server side, and ban offending user ids, but that's it.
Also, don't use the GameJolt global data storage, unless you want random players to wipe information provided by other players. Stick to the per-user features.
One way I handled this in the past was to relay API requests via my own server.
Instead of contacting the API service directly, my game would send a request to my server. My server could then validate the request, perform rate-limiting, etc.
Then, if it determined the request was valid and in-budget, it would generate the appropriate query to the actual API service, signing it with my API key. Then it would reply back to the game client with the relevant results.
This way, my API key lived solely on my own server under my control. Someone decompiling my game would not be able to send arbitrary API requests under my name, because the key is nowhere inside the client.
They could still spoof my client to route similar messages to my server, but at least I could control the set of operations they could ask to do that way, and block off parts of the API that I did not want to use, because my server offered no endpoint for it. I could also restrict the volume of requests so that one hacker trying to scrape my data couldn't use up all my API allotment or impact other players' ability to access it.
With appropriate logging, this can also give you the ability to detect malicious use from a hacked client, analyze its patterns, and update your server to block or send false results to clients matching that pattern, if you're particularly concerned about targeted misuse like this.