I'm developing a game using Unity engine. Currently, I'm on my very first journey to creating a secure and universal game data saving/loading. My game is able to succesfully save its data (game progress) and metadata (custom savable types, encapsulating the data, and necessary for succesfull data deserialization) into two files, but when it comes to loading the data, a weird error occurs upon decoding. It appears a really weird one to me because I googled similar error topics but wasn't able to find a satisfying answer.
The error and its stacktrace are:
CryptographicException: Bad PKCS7 padding. Invalid length 0.
Mono.Security.Cryptography.SymmetricTransform.ThrowBadPaddingException (System.Security.Cryptography.PaddingMode padding, System.Int32 length, System.Int32 position) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
Mono.Security.Cryptography.SymmetricTransform.FinalDecrypt (System.Byte[] inputBuffer, System.Int32 inputOffset, System.Int32 inputCount) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
Mono.Security.Cryptography.SymmetricTransform.TransformFinalBlock (System.Byte[] inputBuffer, System.Int32 inputOffset, System.Int32 inputCount) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.Security.Cryptography.CryptoStream.FlushFinalBlock () (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.Security.Cryptography.CryptoStream.Dispose (System.Boolean disposing) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.IO.Stream.Close () (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.IO.StreamReader.Dispose (System.Boolean disposing) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.IO.TextReader.Dispose () (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
AuxMath.Decode (System.String input, System.Security.Cryptography.Aes decoder, System.Text.Encoding encoding) (at Assets/Scripts/Misc/AuxMath.cs:191)
SavingSystem.TryLoadMetadata (System.Security.Cryptography.Aes decoder, System.Text.Encoding encoding) (at Assets/Scripts/Saving System/SavingSystem.cs:164)
Rethrow as Exception: Metadata loading failed!
SavingSystem.TryLoadMetadata (System.Security.Cryptography.Aes decoder, System.Text.Encoding encoding) (at Assets/Scripts/Saving System/SavingSystem.cs:180)
SavingSystem.Load () (at Assets/Scripts/Saving System/SavingSystem.cs:82)
SavingSystem.Awake () (at Assets/Scripts/Saving System/SavingSystem.cs:43)
My saving/loading.
private void Save()
{
Aes encoder = Aes.Create();
encoder.Key = _keyContainer.Key;
PrepareSavableData();
SaveGameData(encoder, Encoding.UTF8);
SaveMetadata(encoder, Encoding.UTF8);
SavegameCompleted?.Invoke(this, EventArgs.Empty);
}
private bool Load()
{
Aes decoder = Aes.Create();
decoder.Key = _keyContainer.Key;
if (TryLoadMetadata(decoder, Encoding.UTF8) && TryLoadGameData(decoder, Encoding.UTF8))
{
return true;
}
return false;
}
The key for encryption is created randomly using default Aes settings and stored inside a KeyContainer ScriptableObject.
Here is the actual saving.
private void PrepareSavableData()
{
foreach (var entity in _registeredEntities)
{
_storedStates[entity.ID] = entity.GetState();
}
}
private void SaveMetadata(Aes encoder, Encoding encoding)
{
using FileStream fileStream = new(MetadataPath, FileMode.Create, FileAccess.Write);
using StreamWriter writer = new(fileStream, encoding);
List<string> knownTypesNames = new(_knownSavableDataCustomTypes.Count);
foreach (var type in _knownSavableDataCustomTypes)
{
knownTypesNames.Add(type.ToString());
}
string data = AuxMath.SerializeObjectToString(knownTypesNames, encoding);
string encodedData = AuxMath.Encode(data, encoder, encoding);
writer.Write(encodedData);
writer.Close();
}
private bool TryLoadMetadata(Aes decoder, Encoding encoding)
{
if (File.Exists(MetadataPath))
{
try
{
using FileStream fileStream = new(MetadataPath, FileMode.Open, FileAccess.Read);
using StreamReader reader = new(fileStream, encoding);
string encodedData = reader.ReadToEnd();
string decodedData = AuxMath.Decode(encodedData, decoder, encoding);
var knownTypesNames = AuxMath.DeserializeStringToObject<List<string>>(decodedData, encoding, _knownSavableDataCustomTypes);
HashSet<Type> knownTypes = new(knownTypesNames.Count);
foreach (var typeName in knownTypesNames)
{
knownTypes.Add(Type.GetType(typeName));
}
_knownSavableDataCustomTypes.UnionWith(knownTypes);
return true;
}
catch (Exception e)
{
throw new Exception("Metadata loading failed!", e);
}
}
return false;
}
private void SaveGameData(Aes encoder, Encoding encoding)
{
using FileStream fileStream = new(SavegamePath, FileMode.Create, FileAccess.Write);
using StreamWriter writer = new(fileStream, encoding);
string data = AuxMath.SerializeObjectToString(_storedStates, encoding);
string encodedData = AuxMath.Encode(data, encoder, encoding);
writer.Write(encodedData);
writer.Close();
}
private bool TryLoadGameData(Aes decoder, Encoding encoding)
{
if (File.Exists(SavegamePath))
{
try
{
using FileStream fileStream = new(SavegamePath, FileMode.Open, FileAccess.Read);
using StreamReader reader = new(fileStream, encoding);
string encodedData = reader.ReadToEnd();
string decodedData = AuxMath.Decode(encodedData, decoder, encoding);
_storedStates = AuxMath.DeserializeStringToObject<Dictionary<string, IEnumerable<object>>>(decodedData, encoding, _knownSavableDataCustomTypes);
return true;
}
catch (Exception e)
{
throw new Exception("Game data loading failed!", e);
}
}
return false;
}
I'm using DataContractSerializer to convert custom object types with a valuable game data to XML string representation in preparation for encoding/decoding.
public static string SerializeObjectToString(object obj, Encoding encoding)
{
if (obj is null)
{
throw new ArgumentNullException($"{nameof(obj)}", "Cannot serialize a null object!");
}
using MemoryStream memoryStream = new();
using StreamReader reader = new(memoryStream, encoding);
DataContractSerializer serializer = new(obj.GetType());
serializer.WriteObject(memoryStream, obj);
memoryStream.Position = 0;
return reader.ReadToEnd();
}
public static T DeserializeStringToObject<T>(string objectAsXml, Encoding encoding, IEnumerable<Type> knownTypes)
{
if (string.IsNullOrEmpty(objectAsXml))
{
throw new ArgumentNullException($"{nameof(objectAsXml)}", "Data is empty!");
}
if (knownTypes is null)
{
throw new ArgumentException("Known types are not supplied! Deserialization will fail!", $"{nameof(knownTypes)}");
}
using MemoryStream memoryStream = new();
byte[] xmlAsBytes = encoding.GetBytes(objectAsXml);
DataContractSerializer deserializer = new(typeof(T), knownTypes);
memoryStream.Write(xmlAsBytes, 0, xmlAsBytes.Length);
memoryStream.Position = 0;
if (deserializer.ReadObject(memoryStream) is T value)
{
return value;
}
else
{
throw new Exception("Passed data is invalid or corrupted and cannot be restored!");
}
}
Finally, encoding and decoding. Encryption algorithm gets a new initialization vector on every encoding. It gets written unencryptedly directly into the stream, before the encrypted stream writes the secured data. Upon decryption it is necessary to read 16 bytes first from the stream, as they represent the decryption initialization vector.
public static string Encode(string input, Aes encoder, Encoding encoding)
{
if (string.IsNullOrEmpty(input))
{
throw new ArgumentNullException($"{nameof(input)}", "Attempted to encode an empty input!");
}
if (encoder is null)
{
throw new ArgumentNullException($"{nameof(encoder)}", "Encoder is not set!");
}
encoder.GenerateIV();
using MemoryStream memoryStream = new();
using CryptoStream encodingStream = new(memoryStream, encoder.CreateEncryptor(), CryptoStreamMode.Write);
using StreamWriter encodedWriter = new(encodingStream, encoding);
memoryStream.Write(encoder.IV);
encodedWriter.Write(input);
memoryStream.Position = 0;
encodedWriter.Close();
return encoding.GetString(memoryStream.ToArray());
}
public static string Decode(string input, Aes decoder, Encoding encoding)
{
if (string.IsNullOrEmpty(input))
{
throw new ArgumentNullException($"{nameof(input)}", "Attempted to decode an empty input!");
}
if (decoder is null)
{
throw new ArgumentNullException($"{nameof(decoder)}", "Decoder is not set!");
}
using MemoryStream memoryStream = new();
memoryStream.Write(encoding.GetBytes(input));
byte[] iv = new byte[decoder.IV.Length];
memoryStream.Read(iv, 0, decoder.IV.Length);
decoder.IV = iv;
using CryptoStream decodingStream = new(memoryStream, decoder.CreateDecryptor(), CryptoStreamMode.Read);
using StreamReader decodedReader = new(decodingStream, encoding);
return decodedReader.ReadToEnd();
}
The following code both works perfectly and is a lot shorter:
public static byte[] Encrypt(byte[] input, KeyContainer container)
{
byte[] iv = container.GetNewIV();
using SymmetricAlgorithm algorithm = Aes.Create();
using ICryptoTransform encryptor = algorithm.CreateEncryptor(container.GetKey(), iv);
List<byte> output = new(iv.Length + input.Length + algorithm.BlockSize / 8);
byte[] encryptedInput = encryptor.TransformFinalBlock(input, 0, input.Length);
output.AddRange(iv);
output.AddRange(encryptedInput);
algorithm.Clear();
return output.ToArray();
}
public static byte[] Decrypt(byte[] input, KeyContainer container)
{
byte[] iv = input[..KeyContainer.IVLength];
byte[] encryptedInput = input[KeyContainer.IVLength..];
using SymmetricAlgorithm algorithm = Aes.Create();
using ICryptoTransform decryptor = algorithm.CreateDecryptor(container.GetKey(), iv);
byte[] output = decryptor.TransformFinalBlock(encryptedInput, 0, encryptedInput.Length);
algorithm.Clear();
return output;
}
Instead of referring to an Aes instance directly a more generalized SymmetricAlgorithm must be used because AES is one of the implementations of a symmetric encryption.
Instead of trying to perform cryptographic transformations implicitly by using ICryptoTransform as a backing resource inside a stream, one must be performed by calling TransformFinalBlock() method.
After all the crypto activity is done, any memory containing valuable data must be overridden by calling the Clear() method on a symmetric algorithm instance. This way hackers cannot recower any data to attempt and actually succesfully decrypt the result.
Related
Currently I creating a save\load system for my game. Its purpose is to be able to convert a Dictionary<string, object> to an encrypted string, and write it to disk file, and vice versa.
Saving functionality works just fine as follows:
public void Save()
{
PrepareSavableData();
using StreamWriter writer = new(SavegamePath);
string rawData = AuxMath.SerializeObjectToString(_storedStates);
string encodedData = AuxMath.Encode(rawData, PublicKey, PrivateKey);
writer.Write(encodedData);
writer.Close();
SavegameCompleted?.Invoke(this, EventArgs.Empty);
}
private void PrepareSavableData()
{
foreach (var entity in _registeredEntities)
{
_storedStates[entity.ID] = entity.GetState();
}
}
I fetch the data from every registered SavableEntitiy as follows:
public object GetState()
{
List<object> states = new(_savables.Count);
foreach (var savable in _savables)
{
states.Add(savable.CaptureState());
}
return states;
}
In the Save() above I use DES algorithm to encrypt data that is previously serialized to a string. No problems with that as well - I tested it thoroughly.
public static string SerializeObjectToString(object obj)
{
using MemoryStream memoryStream = new();
using StreamReader reader = new(memoryStream, Encoding.UTF8);
DataContractSerializer serializer = new(obj.GetType());
serializer.WriteObject(memoryStream, obj);
memoryStream.Position = 0;
return reader.ReadToEnd();
}
public static string Encode(string input, string publicKey, string privateKey)
{
try
{
byte[] inputAsBytes = Encoding.UTF8.GetBytes(input);
byte[] publicKeyAsBytes = Encoding.UTF8.GetBytes(publicKey);
byte[] privateKeyAsBytes = Encoding.UTF8.GetBytes(privateKey);
using DESCryptoServiceProvider cryptoServiceProvider = new();
using MemoryStream memoryStream = new();
using CryptoStream cryptoStream = new(memoryStream,
cryptoServiceProvider.CreateEncryptor(publicKeyAsBytes, privateKeyAsBytes),
CryptoStreamMode.Write);
cryptoStream.Write(inputAsBytes, 0, inputAsBytes.Length);
cryptoStream.FlushFinalBlock();
return Convert.ToBase64String(memoryStream.ToArray());
}
catch (Exception e)
{
return e.Message;
}
}
So fetching the savable data, serializing it, encoding, and writing to a disc file works without a hitch. At the game startup save file loading is performed, decoding, and deserialization, so that later any SavableEntity which registers itself can restore its previous state if one exists in the state dictionary. Reading the save file and decoding it works just fine.
private bool Load()
{
if (File.Exists(SavegamePath))
{
try
{
using StreamReader reader = new(SavegamePath);
string encodedData = reader.ReadToEnd();
string decodedData = AuxMath.Decode(encodedData, PublicKey, PrivateKey);
_storedStates = AuxMath.DeserializeStringToObject<Dictionary<string, object>>(decodedData, _knownSavableTypes);
SavegameLoadSuccesful?.Invoke(this, EventArgs.Empty);
return true;
}
catch (Exception e)
{
SavegameLoadFailed?.Invoke(this, new SavegameLoadFailedEventArgs(e.Message));
return false;
}
}
return false;
}
public static string Decode(string encodedInput, string publicKey, string privateKey)
{
try
{
byte[] encodedInputAsBytes = Convert.FromBase64String(encodedInput);
byte[] publicKeyAsBytes = Encoding.UTF8.GetBytes(publicKey);
byte[] privateKeyAsBytes = Encoding.UTF8.GetBytes(privateKey);
using DESCryptoServiceProvider cryptoServiceProvider = new();
using MemoryStream memoryStream = new();
using CryptoStream cryptoStream = new(memoryStream,
cryptoServiceProvider.CreateDecryptor(publicKeyAsBytes, privateKeyAsBytes),
CryptoStreamMode.Write);
cryptoStream.Write(encodedInputAsBytes, 0, encodedInputAsBytes.Length);
cryptoStream.FlushFinalBlock();
return Encoding.UTF8.GetString(memoryStream.ToArray());
}
catch (Exception e)
{
return e.Message;
}
}
But, when it finally comes to the data deserialization phase from a successfully loaded and decoded file, an error occurs inside exactly this deserialization method right inside the if statement. DataContractSerializer cannot properly deserialize a states dictionary from a memory stream.
public static T DeserializeStringToObject<T>(string objectAsXml, IEnumerable<Type> knownTypes)
{
using MemoryStream memoryStream = new();
using StreamWriter writer = new(memoryStream, Encoding.UTF8);
DataContractSerializer deserializer = new(typeof(T), knownTypes);
writer.Write(objectAsXml);
memoryStream.Position = 0;
if (deserializer.ReadObject(memoryStream) is T value)
{
return value;
}
else
{
throw new Exception("Passed data is invalid or corrupted and cannot be properly restored!");
}
}
Currently this error occurs:
XmlException: Unexpected end of file. Following elements are not closed: Value, KeyValueOfstringanyType, ArrayOfKeyValueOfstringanyType. Line 1, position 197.
It may be something trivial, but I wasn't able to crack it on my own so far. Please, help!
Best regards, Vyacheslav.
I'm studying encryption in C# and I'm having trouble. I have some Rijndael encryption code and it's working perfectly with strings. But now I'm studying serialization and the BinaryWriter writes the data of classes without any protection. I'm using this code to test; is there a way to "encrypt the class", or something similar?
To clarify the question, here is my code:
FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Create);
using (BinaryWriter sw = new BinaryWriter(file))
{
byte[] byt = ConverteObjectEmByte(myVarClass);
sw.Write(byt);
}
And this is how I read it:
MyClass newMyVarClass;
FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Open);
using (BinaryReader sr = new BinaryReader(file))
{
// 218 is the size of the byte array that I've tested (byt)
myNewVarClass = (MyClass)ConverteByteEmObject(sr.ReadBytes(218));
}
Thanks!
Rather than converting to byte[] as an intermediate step when passing to different stream objects you can chain multiple streams together, passing the output from one to the input of another.
This approach makes sense here, as you are chaining together
Binary Serialization => Encryption => Writing to File.
With this in mind, you can change ConvertObjectEmByte to something like:
public static void WriteObjectToStream(Stream outputStream, Object obj)
{
if (object.ReferenceEquals(null, obj))
{
return;
}
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outputStream, obj);
}
and similarly, ConvertByteEmObject can become:
public static object ReadObjectFromStream(Stream inputStream)
{
BinaryFormatter binForm = new BinaryFormatter();
object obj = binForm.Deserialize(inputStream);
return obj;
}
To add in the encryption/decryption, we can write functions that create CryptoStream objects that we can chain with these binary serialization functions. My example functions below look a bit different from the Encrypt/Decrypt functions in the article you linked to because the IV (Initialization Vector) is now generated randomly and written to the stream (and read from the stream on the other end). It's important that the IV is unique for each chunk of data you encrypt for security, and you should also use a random number generator intended for cryptographic purposes like RNGCryptoServiceProvider, rather than a pseudo-random number generator like Random.
public static CryptoStream CreateEncryptionStream(byte[] key, Stream outputStream)
{
byte[] iv = new byte[ivSize];
using (var rng = new RNGCryptoServiceProvider())
{
// Using a cryptographic random number generator
rng.GetNonZeroBytes(iv);
}
// Write IV to the start of the stream
outputStream.Write(iv, 0, iv.Length);
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream encryptor = new CryptoStream(
outputStream,
rijndael.CreateEncryptor(key, iv),
CryptoStreamMode.Write);
return encryptor;
}
public static CryptoStream CreateDecryptionStream(byte[] key, Stream inputStream)
{
byte[] iv = new byte[ivSize];
if (inputStream.Read(iv, 0, iv.Length) != iv.Length)
{
throw new ApplicationException("Failed to read IV from stream.");
}
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream decryptor = new CryptoStream(
inputStream,
rijndael.CreateDecryptor(key, iv),
CryptoStreamMode.Read);
return decryptor;
}
Finally, we can glue it together:
byte[] key = Convert.FromBase64String(cryptoKey);
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Create))
using (CryptoStream cryptoStream = CreateEncryptionStream(key, file))
{
WriteObjectToStream(cryptoStream, myVarClass);
}
MyClass newMyVarClass;
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Open))
using (CryptoStream cryptoStream = CreateDecryptionStream(key, file))
{
newMyVarClass = (MyClass)ReadObjectFromStream(cryptoStream);
}
Note that we pass the file stream object to CreateEncryptionStream (and CreateDecryptionStream), and then pass the cryptoStream object to WriteObjectToStream (and ReadObjectfromStream). You'll also notice that the streams are scoped inside using blocks, so that they'll automatically be cleaned up when we're finished with them.
Here's the full test program:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
namespace CryptoStreams
{
class Program
{
[Serializable]
public class MyClass
{
public string TestValue
{
get;
set;
}
public int SomeInt
{
get;
set;
}
}
public static void WriteObjectToStream(Stream outputStream, Object obj)
{
if (object.ReferenceEquals(null, obj))
{
return;
}
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outputStream, obj);
}
public static object ReadObjectFromStream(Stream inputStream)
{
BinaryFormatter binForm = new BinaryFormatter();
object obj = binForm.Deserialize(inputStream);
return obj;
}
private const string cryptoKey =
"Q3JpcHRvZ3JhZmlhcyBjb20gUmluamRhZWwgLyBBRVM=";
private const int keySize = 256;
private const int ivSize = 16; // block size is 128-bit
public static CryptoStream CreateEncryptionStream(byte[] key, Stream outputStream)
{
byte[] iv = new byte[ivSize];
using (var rng = new RNGCryptoServiceProvider())
{
// Using a cryptographic random number generator
rng.GetNonZeroBytes(iv);
}
// Write IV to the start of the stream
outputStream.Write(iv, 0, iv.Length);
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream encryptor = new CryptoStream(
outputStream,
rijndael.CreateEncryptor(key, iv),
CryptoStreamMode.Write);
return encryptor;
}
public static CryptoStream CreateDecryptionStream(byte[] key, Stream inputStream)
{
byte[] iv = new byte[ivSize];
if (inputStream.Read(iv, 0, iv.Length) != iv.Length)
{
throw new ApplicationException("Failed to read IV from stream.");
}
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream decryptor = new CryptoStream(
inputStream,
rijndael.CreateDecryptor(key, iv),
CryptoStreamMode.Read);
return decryptor;
}
static void Main(string[] args)
{
MyClass myVarClass = new MyClass
{
SomeInt = 1234,
TestValue = "Hello"
};
byte[] key = Convert.FromBase64String(cryptoKey);
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Create))
{
using (CryptoStream cryptoStream = CreateEncryptionStream(key, file))
{
WriteObjectToStream(cryptoStream, myVarClass);
}
}
MyClass newMyVarClass;
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Open))
using (CryptoStream cryptoStream = CreateDecryptionStream(key, file))
{
newMyVarClass = (MyClass)ReadObjectFromStream(cryptoStream);
}
Console.WriteLine("newMyVarClass.SomeInt: {0}; newMyVarClass.TestValue: {1}",
newMyVarClass.SomeInt,
newMyVarClass.TestValue);
}
}
}
I'm not sure whether the .Net Library had changed or just the code is wrong. I can't directly run the code written by softwariness.
Since that, I changed the code based on the answer so that it can be used correctly. Here's an example.
public class CryptoSerialization
{
public static void WriteObjectToStream(Stream outputStream, object obj)
{
if (obj is null) throw new ArgumentNullException("obj can't be null");
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outputStream, obj);
}
public static object ReadObjectFromStream(Stream inputStream)
{
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(inputStream);
}
public static CryptoStream CreateEncryptionStream(Stream outputStream, byte[] Key, byte[] IV)
{
Rijndael rijndael = new RijndaelManaged();
return new CryptoStream(outputStream, rijndael.CreateEncryptor(Key, IV), CryptoStreamMode.Write);
}
public static CryptoStream CreateDecryptionStream(Stream inputStream, byte[] Key, byte[] IV)
{
Rijndael rijndael = new RijndaelManaged();
return new CryptoStream(inputStream, rijndael.CreateDecryptor(Key, IV), CryptoStreamMode.Read);
}
public static void EncryptObjectToFile(object obj, string path, byte[] Key, byte[] IV)
{
using FileStream file = new FileStream(path, FileMode.Create);
using (CryptoStream cryptoStream = CreateEncryptionStream(file, Key, IV))
{
WriteObjectToStream(cryptoStream, obj);
}
}
public static object DecryptObjectFromFile(string path, byte[] Key, byte[] IV)
{
using FileStream file = new FileStream(path, FileMode.Open);
using (CryptoStream cryptoStream = CreateDecryptionStream(file, Key, IV))
{
return ReadObjectFromStream(cryptoStream);
}
}
}
[Serializable]
public class Student
{
public string Name;
public int Age;
}
static async Task Main(string[] args)
{
// the original string "[This is an example key string!]";
// I don't know if the length of the string has to be 32, but when I tried 64, it went wrong.
string cryptoKey = "W1RoaXMgaXMgYW4gZXhhbXBsZSBrZXkgc3RyaW5nIV0=";
byte[] Key = Convert.FromBase64String(cryptoKey);
byte[] IV = new byte[16];
using (RNGCryptoServiceProvider rngcsp = new RNGCryptoServiceProvider())
{
rngcsp.GetBytes(IV);
}
//same as
//Rijndael rijndael = new RijndaelManaged();
//rijndael.GenerateIV();
//byte[] iv = rijndael.IV;
List<Student> students = new List<Student>() { new Student { Name = "John", Age = 10 }, new Student { Name = "Marry", Age = 15 } };
CryptoSerialization.EncryptObjectToFile(students, Environment.CurrentDirectory + #"\testCrypto.dat", Key, IV);
List<Student> newStudents = (List<Student>)CryptoSerialization.DecryptObjectFromFile(Environment.CurrentDirectory + #"\testCrypto.dat", Key, IV);
newStudents.ForEach((stu) =>
{
Console.WriteLine(stu.Name + ", " + stu.Age);
});
Console.ReadKey();
}
I'm studying encryption in C# and I'm having trouble. I have some Rijndael encryption code and it's working perfectly with strings. But now I'm studying serialization and the BinaryWriter writes the data of classes without any protection. I'm using this code to test; is there a way to "encrypt the class", or something similar?
To clarify the question, here is my code:
FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Create);
using (BinaryWriter sw = new BinaryWriter(file))
{
byte[] byt = ConverteObjectEmByte(myVarClass);
sw.Write(byt);
}
And this is how I read it:
MyClass newMyVarClass;
FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Open);
using (BinaryReader sr = new BinaryReader(file))
{
// 218 is the size of the byte array that I've tested (byt)
myNewVarClass = (MyClass)ConverteByteEmObject(sr.ReadBytes(218));
}
Thanks!
Rather than converting to byte[] as an intermediate step when passing to different stream objects you can chain multiple streams together, passing the output from one to the input of another.
This approach makes sense here, as you are chaining together
Binary Serialization => Encryption => Writing to File.
With this in mind, you can change ConvertObjectEmByte to something like:
public static void WriteObjectToStream(Stream outputStream, Object obj)
{
if (object.ReferenceEquals(null, obj))
{
return;
}
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outputStream, obj);
}
and similarly, ConvertByteEmObject can become:
public static object ReadObjectFromStream(Stream inputStream)
{
BinaryFormatter binForm = new BinaryFormatter();
object obj = binForm.Deserialize(inputStream);
return obj;
}
To add in the encryption/decryption, we can write functions that create CryptoStream objects that we can chain with these binary serialization functions. My example functions below look a bit different from the Encrypt/Decrypt functions in the article you linked to because the IV (Initialization Vector) is now generated randomly and written to the stream (and read from the stream on the other end). It's important that the IV is unique for each chunk of data you encrypt for security, and you should also use a random number generator intended for cryptographic purposes like RNGCryptoServiceProvider, rather than a pseudo-random number generator like Random.
public static CryptoStream CreateEncryptionStream(byte[] key, Stream outputStream)
{
byte[] iv = new byte[ivSize];
using (var rng = new RNGCryptoServiceProvider())
{
// Using a cryptographic random number generator
rng.GetNonZeroBytes(iv);
}
// Write IV to the start of the stream
outputStream.Write(iv, 0, iv.Length);
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream encryptor = new CryptoStream(
outputStream,
rijndael.CreateEncryptor(key, iv),
CryptoStreamMode.Write);
return encryptor;
}
public static CryptoStream CreateDecryptionStream(byte[] key, Stream inputStream)
{
byte[] iv = new byte[ivSize];
if (inputStream.Read(iv, 0, iv.Length) != iv.Length)
{
throw new ApplicationException("Failed to read IV from stream.");
}
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream decryptor = new CryptoStream(
inputStream,
rijndael.CreateDecryptor(key, iv),
CryptoStreamMode.Read);
return decryptor;
}
Finally, we can glue it together:
byte[] key = Convert.FromBase64String(cryptoKey);
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Create))
using (CryptoStream cryptoStream = CreateEncryptionStream(key, file))
{
WriteObjectToStream(cryptoStream, myVarClass);
}
MyClass newMyVarClass;
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Open))
using (CryptoStream cryptoStream = CreateDecryptionStream(key, file))
{
newMyVarClass = (MyClass)ReadObjectFromStream(cryptoStream);
}
Note that we pass the file stream object to CreateEncryptionStream (and CreateDecryptionStream), and then pass the cryptoStream object to WriteObjectToStream (and ReadObjectfromStream). You'll also notice that the streams are scoped inside using blocks, so that they'll automatically be cleaned up when we're finished with them.
Here's the full test program:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
namespace CryptoStreams
{
class Program
{
[Serializable]
public class MyClass
{
public string TestValue
{
get;
set;
}
public int SomeInt
{
get;
set;
}
}
public static void WriteObjectToStream(Stream outputStream, Object obj)
{
if (object.ReferenceEquals(null, obj))
{
return;
}
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outputStream, obj);
}
public static object ReadObjectFromStream(Stream inputStream)
{
BinaryFormatter binForm = new BinaryFormatter();
object obj = binForm.Deserialize(inputStream);
return obj;
}
private const string cryptoKey =
"Q3JpcHRvZ3JhZmlhcyBjb20gUmluamRhZWwgLyBBRVM=";
private const int keySize = 256;
private const int ivSize = 16; // block size is 128-bit
public static CryptoStream CreateEncryptionStream(byte[] key, Stream outputStream)
{
byte[] iv = new byte[ivSize];
using (var rng = new RNGCryptoServiceProvider())
{
// Using a cryptographic random number generator
rng.GetNonZeroBytes(iv);
}
// Write IV to the start of the stream
outputStream.Write(iv, 0, iv.Length);
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream encryptor = new CryptoStream(
outputStream,
rijndael.CreateEncryptor(key, iv),
CryptoStreamMode.Write);
return encryptor;
}
public static CryptoStream CreateDecryptionStream(byte[] key, Stream inputStream)
{
byte[] iv = new byte[ivSize];
if (inputStream.Read(iv, 0, iv.Length) != iv.Length)
{
throw new ApplicationException("Failed to read IV from stream.");
}
Rijndael rijndael = new RijndaelManaged();
rijndael.KeySize = keySize;
CryptoStream decryptor = new CryptoStream(
inputStream,
rijndael.CreateDecryptor(key, iv),
CryptoStreamMode.Read);
return decryptor;
}
static void Main(string[] args)
{
MyClass myVarClass = new MyClass
{
SomeInt = 1234,
TestValue = "Hello"
};
byte[] key = Convert.FromBase64String(cryptoKey);
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Create))
{
using (CryptoStream cryptoStream = CreateEncryptionStream(key, file))
{
WriteObjectToStream(cryptoStream, myVarClass);
}
}
MyClass newMyVarClass;
using (FileStream file = new FileStream(Environment.CurrentDirectory + #"\class.dat", FileMode.Open))
using (CryptoStream cryptoStream = CreateDecryptionStream(key, file))
{
newMyVarClass = (MyClass)ReadObjectFromStream(cryptoStream);
}
Console.WriteLine("newMyVarClass.SomeInt: {0}; newMyVarClass.TestValue: {1}",
newMyVarClass.SomeInt,
newMyVarClass.TestValue);
}
}
}
I'm not sure whether the .Net Library had changed or just the code is wrong. I can't directly run the code written by softwariness.
Since that, I changed the code based on the answer so that it can be used correctly. Here's an example.
public class CryptoSerialization
{
public static void WriteObjectToStream(Stream outputStream, object obj)
{
if (obj is null) throw new ArgumentNullException("obj can't be null");
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outputStream, obj);
}
public static object ReadObjectFromStream(Stream inputStream)
{
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(inputStream);
}
public static CryptoStream CreateEncryptionStream(Stream outputStream, byte[] Key, byte[] IV)
{
Rijndael rijndael = new RijndaelManaged();
return new CryptoStream(outputStream, rijndael.CreateEncryptor(Key, IV), CryptoStreamMode.Write);
}
public static CryptoStream CreateDecryptionStream(Stream inputStream, byte[] Key, byte[] IV)
{
Rijndael rijndael = new RijndaelManaged();
return new CryptoStream(inputStream, rijndael.CreateDecryptor(Key, IV), CryptoStreamMode.Read);
}
public static void EncryptObjectToFile(object obj, string path, byte[] Key, byte[] IV)
{
using FileStream file = new FileStream(path, FileMode.Create);
using (CryptoStream cryptoStream = CreateEncryptionStream(file, Key, IV))
{
WriteObjectToStream(cryptoStream, obj);
}
}
public static object DecryptObjectFromFile(string path, byte[] Key, byte[] IV)
{
using FileStream file = new FileStream(path, FileMode.Open);
using (CryptoStream cryptoStream = CreateDecryptionStream(file, Key, IV))
{
return ReadObjectFromStream(cryptoStream);
}
}
}
[Serializable]
public class Student
{
public string Name;
public int Age;
}
static async Task Main(string[] args)
{
// the original string "[This is an example key string!]";
// I don't know if the length of the string has to be 32, but when I tried 64, it went wrong.
string cryptoKey = "W1RoaXMgaXMgYW4gZXhhbXBsZSBrZXkgc3RyaW5nIV0=";
byte[] Key = Convert.FromBase64String(cryptoKey);
byte[] IV = new byte[16];
using (RNGCryptoServiceProvider rngcsp = new RNGCryptoServiceProvider())
{
rngcsp.GetBytes(IV);
}
//same as
//Rijndael rijndael = new RijndaelManaged();
//rijndael.GenerateIV();
//byte[] iv = rijndael.IV;
List<Student> students = new List<Student>() { new Student { Name = "John", Age = 10 }, new Student { Name = "Marry", Age = 15 } };
CryptoSerialization.EncryptObjectToFile(students, Environment.CurrentDirectory + #"\testCrypto.dat", Key, IV);
List<Student> newStudents = (List<Student>)CryptoSerialization.DecryptObjectFromFile(Environment.CurrentDirectory + #"\testCrypto.dat", Key, IV);
newStudents.ForEach((stu) =>
{
Console.WriteLine(stu.Name + ", " + stu.Age);
});
Console.ReadKey();
}
This is a method, for encrypting a text and returning the cipher text:
public static string Encrypt(this string clearText, CryptologyMethod method)
{
ICryptoTransform cryptoTransform = null;
switch (method)
{
case CryptologyMethod.TripleDes:
cryptoTransform = new TripleDESCryptoServiceProvider().CreateEncryptor(Config.TripleDesKey, Config.TripleDesIV);
break;
case CryptologyMethod.AES:
cryptoTransform = new AesCryptoServiceProvider().CreateEncryptor(Config.AesKey, Config.AesIV);
break;
}
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Write))
{
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
streamWriter.Write(clearText);
cryptoStream.FlushFinalBlock();
return memoryStream.Text();
}
}
}
}
public enum CryptologyMethod
{
TripleDes,
AES
}
Text() is an extension method, to read the content of a stream:
public static string Text(this Stream stream)
{
return new StreamReader(stream).ReadToEnd();
}
Also, Config.TripleDesKey and other properties return array of bytes read from configuration file.
The problem is that when I use this extension method, the result is always an empty string:
string cipherText = "some clear text".Encrypt(CryptologyMethod.TripleDes);
// cipherText is empty
I think I've had that issue before, the solution was pretty obvious once I figured it out and completely opaque before then.
The problem was that ReadToEnd is relative, as in "from current location to end". The problem being that "current position" already WAS "end" because I'd just finished writing to the end of the stream. So I would fix your problem like this:
public static string Text(this Stream stream)
{
stream.Position = 0
return new StreamReader(stream).ReadToEnd();
}
Worked for me with a similar issue, hope it helps.
Scenario: One symmetric key, each user has his own IV, the documents are stored in a NVARCHAR(MAX) field. When I try to to decrypt the file, I get:
The input data is not a complete block.
// Create symmetric key
public static byte[] CreateKey()
{
AesCryptoServiceProvider aesCrypto = (AesCryptoServiceProvider)AesCryptoServiceProvider.Create();
byte[] key = aesCrypto.Key;
return key;
}
//Get key (stored in a database)
public static Byte[] GetAppKey()
{
return db.Encryptors.Where(x => x.EncryptorID == 1).Single().EncryptionKey.ToArray();
}
// Get application IV (stored in database)
public static Byte[] GetAppIV()
{
return db.Encryptors.Where(x => x.EncryptorID == 1).Single().IV.ToArray();
}
// Encrypt document (this will be stored in a VARBINARY(MAX) field
public static byte[] EncryptBinaryToBytes(Binary document, byte[] iv)
{
byte[] key = GetAppKey();
byte[] encrypted;
using (AesCryptoServiceProvider aesCsp = new AesCryptoServiceProvider())
{
aesCsp.Key = key;
aesCsp.IV = iv;
ICryptoTransform encryptor = aesCsp.CreateEncryptor(aesCsp.Key, aesCsp.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(document);
}
encrypted = msEncrypt.ToArray();
}
}
}
// return the encrypted document
return encrypted;
}
// Decrypt document
public static byte[] DecryptBytesToBytes(byte[] document, byte[] iv)
{
byte[] key = GetAppKey();
using (AesCryptoServiceProvider aesCsp = new AesCryptoServiceProvider())
{
aesCsp.Key = key;
aesCsp.IV = iv;
ICryptoTransform decryptor = aesCsp.CreateDecryptor(aesCsp.Key, aesCsp.IV);
using (MemoryStream msDecrypt = new MemoryStream())
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write))
{
using (StreamWriter swDecrypt = new StreamWriter(csDecrypt))
{
swDecrypt.Write(document);
}
byte[] decrypted = msDecrypt.ToArray();
// return the unencrypted document
return decrypted;
}
}
}
}
Thanks in advance.
To store the document
byte[] fileByte = fluUploadFile.FileBytes;
Binary document = new Binary(fileByte);
byte[] appIv = AES.GetAppIV();
byte[] encryptedDocument = AES.EncryptBinaryToBytes(document, appIv);
byte[] decryptedDocument = AES.DecryptBytesToBytes(encryptedDocument, appIv);
Document d = new Document()
{
OriginalName = originalName,
DocSize = fileSize,
BinaryDocument = encryptedDocument,
UploadedName = uploadedFileName,
MimeType = MIMEType,
DocExtension = extension
};
db.Documents.InsertOnSubmit(d);
db.SubmitChanges();
It's really important that you change the data type of the database field to VARBINARY(MAX), that way you avoid issues with character encodings and byte combinations that cannot be interpreted as legal characters.
Also, I think the problem is that you are not closing the streams before calling ToArray() method on the MemoryStream in both encrypt and decrypt routines. It's very important to call Close() in the CryptoStream so that FlushFinalBlock() is called and the encryption process writes the final block to the stream.
Try moving the call to MemoryStream.ToArray() to the outer using block, that is, outside the using block of CryptoStream, so that Dispose() is called on the CryptoStream and call MemoryStream.Close() before that.
Another problem with your code is that you are wrapping the CryptoStream with a StreamWriter, which writes the text representation of the object you pass into the Write method. You should instead write directly to the CryptoStream to avoid any byte to string conversions.