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