I did a little test and found some issues.
Will update you later.
Here is the update.
First of all, I am just coding with the 4.0.x spine unity runtime, things maybe different for latest version.
Because I don't want to keep patching the runtime code to integrate encryption in the future. I tried to do the implementation as generic as possible. It should be no problem adding them to the runtime code base. The modification to the original runtime code base just just 3 lines excluding bug fixes. Two files are added.
EncryptionPanel.cs
- This is the UI to test and config encryption.
In practice, you can actually write more code to automatically encrypt every spine skel.bytes files during import when encryption setting is on in the project saving player more mouse clicks. I just leave the current progress this way as I don't really need it.
SpineEncryption.cs
The class SpineEncryption is used to decode bytes from encrypted file.
To modify the runtime to use this class, all we need to do is to insert a line of code before passing the bytes down any parsing methods. It will automatically do the stuff inside.
var bytes = SpineEncryption.GetBytes(skeletonJSON.bytes);
Below is the three locations that need to inject the code.
The GetBytes() method will try to detect if its encrypted or not using the existing SkeletonBinary.GetVersionString() so both encrypted and non-encrypted file will be parsed correctly.
While writing this part of the code, I found that the GetVersionStringOld3X() is written incorrectly, it doesn't throw Exception like the GetVersionString() do and could try to read thousand of bytes for the version string. Not sure if newer version of runtime has it fixed. Anyway, I changed it slightly to this to make them consistent so that encryption code can catch any exception correctly:
public string GetVersionStringOld3X () {
// Hash.
int byteCount = ReadInt(true);
if (byteCount > 1) input.Position += byteCount - 1;
// Version.
byteCount = ReadInt(true);
if (byteCount > 1 && byteCount < 32) // check max length!
{
byteCount
---
;
var buffer = new byte[byteCount];
ReadFully(buffer, 0, byteCount);
return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount);
}
//return null; // throw as new implementation!
throw new ArgumentException("Stream does not contain a valid binary Skeleton Data.\n");
}
I suggest to further check if final string only contains valid chars. e.g. numbers, '.' , 'beta', 'alpha', etc
There is still a chance the byteCount value read from encrypted file is just slip through the length check.
Possible Improvements
The current implementation relies on EncryptionPanel to auto restore the latest setting. e.g. whether encryption is enabled or not and also the value of saved encryption Key and IV for the editor.
The code is:
private void OnEnable()
{
key = EditorPrefs.GetString("spine.encryption.key");
iv = EditorPrefs.GetString("spine.encryption.iv");
var enableEncryption = EditorPrefs.GetBool("spine.enableEncryption");
SetEncryptionEnabled(enableEncryption);
}
void SetEncryptionEnabled( bool enabeld)
{
if (enabeld)
{
SpineEncryption.UseDefaultEncryption(key, iv);
}
else
{
SpineEncryption.UseNoEncryption();
}
EditorPrefs.SetBool("spine.enableEncryption", enabeld);
}
If EncryptionPanel is closed and project code get recompiled due to code change, it does not restore the setting to enable encryption. This would lead to error parsing encrypted files. I am ok with the EncryptionPanel always being opened.
If you want it to work properly without the panel staying open, all you need to do is to run the same code like above somewhere when recompilation is detected.
Another improvement I realize after I finished the thing is that it is better to save the setting to a file instead of using EditorPrefs so that the setting can be pushed to project git and be shared among peoples. But I am a one man team so it doesn't bother me and I just leave it this way. 8)
Optionally, you can code it to scan for skel.bytes file in the project folder and check or auto-encrypt them in one go. That way user don't need to encrypt the files one by one by hand. Doing it during import may not be better because there could be exported files in the project already.
Optionally 2, in theory, the encryption core can also be used for texture files too. This is not my goal so I just leave it.
How to use?
After integrating the code to the runtime, in editor, we can open the EncryptionPanel from
Menu > Windows > Spine > Encryption Panel
There you see Generate button that can generate a new Key/IV pair for encryption / decryption.
Key consistency is to check whether the key you see in the panel is actually the key in use in the runtime.
To encrypt/decrypt a file, select any file containing ".skel.bytes" and press the Encrypt button.
You can also use the "Test" button to check if a file is encrypted or not. Note that it cannot decrypt old encrypted file if key/IV is changed. That means once you decided to use a key/IV pair, save a backup somewhere and don't lost it.
The panel is for editor only. In game, you need to actively call
SpineEncryption.UseDefaultEncryption( key, iv);
to tell the runtime to enable encryption.
You can also implement your own version of encryption using IEncryptionModule and assign it to
SpineEncryption.ActiveModule
About encryption
AES in unity mono is buggy so I used Rijndael instead. It should not be a problem as the goal here is just to prevent direct extraction of the animation files. It only add a thin layer to security. The current code is not mean to protect from professional hacking. Hacker can still decompile your game and find your key / iv and decrypt the file.
Enjoy
P.S.
I only tested it within Unity Editor. Use at your own risk.