using System;
using System.IO;
using System.Security.Cryptography;
using UnityEngine;
namespace Framework.Storage {
/// XML serializer that offers some basic tamper detection
/// Type of the structure that will be serialized
public class TamperDetectingSerializer {
///
/// Sequence of random characters that are used to salt the file's hash code
///
public string Salt;
/// Delegate to a method that reads the hash code from the structure
public Func ReadHashCodeDelegate;
/// Delegate to a method that assigns the hash code to the structure
public Action AssignHashCodeDelegate;
/// Initialized a new tamper-detecting serializer
///
/// Delegate for the method that reads the SHA-256 sum from the structure
///
///
/// Delegate for a method that assigns the SHA-256 to the structure
///
public TamperDetectingSerializer(
Func readHashCodeDelegate,
Action assignHashCodeDelegate
) {
this.ReadHashCodeDelegate = readHashCodeDelegate;
this.AssignHashCodeDelegate = assignHashCodeDelegate;
this.Salt = Application.productName;
}
/// Loads the state of an object from a file
/// Instance into which the state will be loaded
/// File from which the state will be loaded
/// True if the file was genuine, false if it was tampered with
public bool Load(T instance, string path) {
SimpleXmlSerializer.Load(instance, path);
string savedSha256 = this.ReadHashCodeDelegate(instance);
string computedSha256 = getSha256(instance);
return (savedSha256 == computedSha256);
}
/// Loads the state of an object from a file
/// Instance into which the state will be loaded
/// File from which the state will be loaded
/// Expected name of the root element
/// True if the file was genuine, false if it was tampered with
public bool Load(T instance, string path, string rootElementName) {
SimpleXmlSerializer.Load(instance, path, rootElementName);
string savedSha256 = this.ReadHashCodeDelegate(instance);
string computedSha256 = getSha256(instance);
return (savedSha256 == computedSha256);
}
/// Loads the state of an object from a file
/// Instance into which the state will be loaded
/// Stream from which the state will be loaded
/// True if the file was genuine, false if it was tampered with
public bool Load(T instance, Stream stream) {
SimpleXmlSerializer.Load(instance, stream);
string savedSha256 = this.ReadHashCodeDelegate(instance);
string computedSha256 = getSha256(instance);
return (savedSha256 == computedSha256);
}
/// Loads the state of an object from a file
/// Instance into which the state will be loaded
/// Stream from which the state will be loaded
/// Expected name of the root element
/// True if the file was genuine, false if it was tampered with
public bool Load(T instance, Stream stream, string rootElementName) {
SimpleXmlSerializer.Load(instance, stream, rootElementName);
string savedSha256 = this.ReadHashCodeDelegate(instance);
string computedSha256 = getSha256(instance);
return (savedSha256 == computedSha256);
}
/// Saves the state of an object into a file
/// Instance whose state will be saved
/// File into which the state will be saved
public void Save(T instance, string path) {
string computedSha256 = getSha256(instance);
this.AssignHashCodeDelegate(instance, computedSha256);
SimpleXmlSerializer.Save(instance, path);
}
/// Saves the state of an object into a file
/// Instance whose state will be saved
/// File into which the state will be saved
/// Name that will be given to the root element
public void Save(T instance, string path, string rootElementName) {
string computedSha256 = getSha256(instance);
this.AssignHashCodeDelegate(instance, computedSha256);
SimpleXmlSerializer.Save(instance, path, rootElementName);
}
/// Saves the state of an object into a file
/// Instance whose state will be saved
/// File into which the state will be saved
public void Save(T instance, Stream stream) {
string computedSha256 = getSha256(instance);
this.AssignHashCodeDelegate(instance, computedSha256);
SimpleXmlSerializer.Save(instance, stream);
}
/// Saves the state of an object into a file
/// Instance whose state will be saved
/// File into which the state will be saved
/// Name that will be given to the root element
public void Save(T instance, Stream stream, string rootElementName) {
string computedSha256 = getSha256(instance);
this.AssignHashCodeDelegate(instance, computedSha256);
SimpleXmlSerializer.Save(instance, stream, rootElementName);
}
/// Returns the SHA-256 sum of a structure serialized to XML
///
/// Structure whose SHA-256 when serialized to XML will be returned
///
///
/// Name of the root element in the XML file, when null is passed,
/// defaults to the type name of the structure.
///
/// The SHA-256 sum of the structure as a string
private string getSha256(T instance, string rootElementName = null) {
byte[] hashCode;
using(var memoryStream = new MemoryStream()) {
// Fill the hash code with a default so the SHA-256 is reproducible
// before saving and after loading the file (assuming the salt is the same)
if(string.IsNullOrEmpty(this.Salt)) {
this.AssignHashCodeDelegate(instance, this.Salt);
} else {
this.AssignHashCodeDelegate(instance, string.Empty);
}
// Make sure the salt is removed again and doesn't linger in memory.
// Just a tiny measure to make it a little bit harder to tamper with.
try {
// Now serialize the structure into an in-memory XML stream of which
// we can compute the SHA-256 sum
if(rootElementName == null) {
SimpleXmlSerializer.Save(instance, memoryStream);
} else {
SimpleXmlSerializer.Save(instance, memoryStream, rootElementName);
}
// Compute the hash code of the XML stream with salt in it
{
var sha256 = new SHA256Managed();
//sha256.Initialize();
hashCode = sha256.ComputeHash(memoryStream.GetBuffer(), 0, (int) memoryStream.Length);
}
}
finally {
this.AssignHashCodeDelegate(instance, string.Empty);
Array.Clear(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}
return BitConverter.ToString(hashCode);
}
}
} // namespace Framework.Storage