Copy this link when reproducing:
http://www.casperlee.com/en/y/blog/48
One of the most important and special features of this personal document management system is to support handling documents of unlimited sizes. In this part of the article, let's find out how.
First of all, let's prepare some utility functions.
In order to encrypt documents, we need several encoding / decoding functions. Add a new class CEncoding.cs in the library project, and put the following source code in.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace com.casperlee.Library
{
public static class CEncoding
{
private static byte[] bIV = { 0x12, 0x34, 0x56, 0x78,
0x90, 0xAB, 0xCD, 0xEF };
private static int MAX_PW_LEN_DES = 8;
private static int MAX_PW_LEN_3DES = 24;
private static char DEFAULT_CHAR_DES = ' ';
private static string DEFAULT_KEY_3DES = "1BE94GDX16U5XR2B4B4P73F1";
private static string Generate3DesKey(string aKey)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < MAX_PW_LEN_3DES; i++)
{
if (i < aKey.Length)
{
sb.Append(aKey[i]);
}
else
{
sb.Append(DEFAULT_KEY_3DES[i]);
}
}
return sb.ToString();
}
public static string EncodeDes(string aString, string aKey)
{
aKey = aKey.PadRight(MAX_PW_LEN_DES, DEFAULT_CHAR_DES);
aKey = aKey.Substring(0, MAX_PW_LEN_DES);
byte[] rgbKey = Encoding.UTF8.GetBytes(aKey);
byte[] rgbIV = bIV;
byte[] inputByteArray = Encoding.UTF8.GetBytes(aString);
DESCryptoServiceProvider dCSP = new DESCryptoServiceProvider();
MemoryStream mStream = new MemoryStream();
CryptoStream cStream = new CryptoStream(
mStream,
dCSP.CreateEncryptor(rgbKey, rgbIV),
CryptoStreamMode.Write);
cStream.Write(inputByteArray, 0, inputByteArray.Length);
cStream.FlushFinalBlock();
return Convert.ToBase64String(mStream.ToArray());
}
public static string DecodeDes(string aString, string aKey)
{
aKey = aKey.PadRight(MAX_PW_LEN_DES, DEFAULT_CHAR_DES);
aKey = aKey.Substring(0, MAX_PW_LEN_DES);
byte[] rgbKey = Encoding.UTF8.GetBytes(aKey);
byte[] rgbIV = bIV;
byte[] inputByteArray = Convert.FromBase64String(aString);
DESCryptoServiceProvider DCSP = new DESCryptoServiceProvider();
MemoryStream mStream = new MemoryStream();
CryptoStream cStream = new CryptoStream(
mStream,
DCSP.CreateDecryptor(rgbKey, rgbIV),
CryptoStreamMode.Write);
cStream.Write(inputByteArray, 0, inputByteArray.Length);
cStream.FlushFinalBlock();
return Encoding.UTF8.GetString(mStream.ToArray());
}
public static byte[] Encode3Des(byte[] aValue, string aKey)
{
TripleDESCryptoServiceProvider provider = new TripleDESCryptoServiceProvider();
ICryptoTransform ct;
MemoryStream ms;
CryptoStream cs;
aKey = Generate3DesKey(aKey);
byte[] keyBytes = Encoding.UTF8.GetBytes(aKey);
provider.Key = keyBytes;
provider.IV = bIV;
provider.Mode = System.Security.Cryptography.CipherMode.ECB;
provider.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
ct = provider.CreateEncryptor(provider.Key, provider.IV);
using (ms = new MemoryStream())
{
using (cs = new CryptoStream(ms, ct, CryptoStreamMode.Write))
{
cs.Write(aValue, 0, aValue.Length);
cs.FlushFinalBlock();
cs.Close();
return ms.ToArray();
}
}
}
public static string Encode3Des(string aValue, string aKey)
{
byte[] oldBytes = Encoding.GetEncoding("GB2312").GetBytes(aValue);
byte[] newBytes = Encode3Des(oldBytes, aKey);
return Convert.ToBase64String(newBytes);
}
public static byte[] Decode3Des(byte[] aValue, string aKey)
{
TripleDESCryptoServiceProvider provider = new TripleDESCryptoServiceProvider();
ICryptoTransform ct;
MemoryStream ms;
CryptoStream cs;
aKey = Generate3DesKey(aKey);
byte[] keyBytes = Encoding.UTF8.GetBytes(aKey);
provider.Key = keyBytes;
provider.IV = bIV;
provider.Mode = System.Security.Cryptography.CipherMode.ECB;
provider.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
ct = provider.CreateDecryptor(provider.Key, provider.IV);
using (ms = new MemoryStream())
{
using (cs = new CryptoStream(ms, ct, CryptoStreamMode.Write))
{
cs.Write(aValue, 0, aValue.Length);
cs.FlushFinalBlock();
cs.Close();
return ms.ToArray();
}
}
}
public static string Decode3Des(string aValue, string aKey)
{
byte[] oldBytes = Convert.FromBase64String(aValue);
byte[] newBytes = Decode3Des(oldBytes, aKey);
return Encoding.GetEncoding("GB2312").GetString(newBytes);
}
public static string GetMD5String(string aData)
{
string retStr;
MD5CryptoServiceProvider m5 = new MD5CryptoServiceProvider();
byte[] inputBye;
byte[] outputBye;
inputBye = Encoding.GetEncoding("GB2312").GetBytes(aData);
outputBye = m5.ComputeHash(inputBye);
retStr = System.BitConverter.ToString(outputBye);
retStr = retStr.Replace("-", "");
return retStr;
}
}
}
There are 3 groups of encoding / decoding functions in this class:
1. EncodeDes / DecodeDes
2. Encode3Des / Decode3Des
3. GetMD5String
The EncodeDes / DecodeDes function only supports a password with at most 8 characters. I want a function which supports a password with more characters, so I chose the Encode3Des / Decode3Des function which supports a password with at most 24 characters.
Hint: although the Encode3Des / Decode3Des function supports a password with at most 24 characters (192 bits), there is a rule: the first 8 characters must not be the same as the second 8 characters, and the second 8 characters must not be the same as the last 8 characters. Please see https://msdn.microsoft.com/en-us/library/system.security.cryptography.tripledes.isweakkey(v=vs.110).aspx for detail information.
I want to encrypt the file, but I don't want to save the original password which offered by the user to somewhere in the system physically. Instead, I'll use GetMD5String to get the MD5 hash code of the original password, and then save this code into the database. Next time when the user offers the password, I compare its MD5 hash code with the one in the database to figure out if the password is correct or not.
System.Exception only has a string property Message to show the short description of the exception. But I want my exception class to have an extra integer property ExceptionCode, so I decided to extend System.Exception and make my own exception class. Add a new class CException.cs in the library project, and put the following source code in.
using System;
namespace com.casperlee.Library
{
public class CException: SystemException
{
public const int EXP_WILL_NOT_HAPPEN = 1;
public CException(int aCode): base (aCode.ToString())
{
this.exceptionCode = aCode;
}
public CException(int aCode, string aMessage): base (aMessage)
{
this.exceptionCode = aCode;
}
private int exceptionCode;
public int ExceptionCode
{
get { return exceptionCode; }
}
}
}
Now that we've got all the required utility functions, let's start to implement the key class in the file handling process: CFileHandler (CFileHandler.cs in the Bll project).
To support huge files, we need to divide the file into several pieces and import / save them one by one. And we also need a way to link all these pieces together. My idea is to add a file header to each piece, and each header contains a NextFile pointer which refers to the next file it linked. So we need a structure for the file header, and here it is:
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct FileHeader
{
public Int16 StruVersion;
public Int16 FileCount;
public Int16 FileIndex;
public Int16 EncryptMethod;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string NextFile;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)]
public byte[] Reserved;
}
Hint:
1. Because we want to save this structure directly to files and we want the total length of the file header is fixed, each field must be of a fixed length, so we use "Int16" or "Int32" instead of "short" or "int", and we also specify the length when using a "string" type.
2. The structure must use four-byte alignment. The structure shown as below is not a good one.
public struct AStru
{
public Byte Field1;
public Byte Field2;
public Byte Field3;
public Int32 Field4;
}
Instead, we need to add an extra Byte field just below the Field3:
public struct AStru
{
public Byte Field1;
public Byte Field2;
public Byte Field3;
public Byte ExtraField;
public Int32 Field4;
}
As you can see, there is a "StruVersion" field in the structure FileHeader. It is used for backward compatibility. Using the "StruVersion" field along with the last field "Reserved", we can easily and perfectly deal with the situation where we need to add more fields to this structure in the future.
For instance, we may need to add a new field to this structure in the future.
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct FileHeader
{
public Int16 StruVersion;
public Int16 FileCount;
public Int16 FileIndex;
public Int16 EncryptMethod;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string NextFile;
public Int32 NewField;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 508)]
public byte[] Reserved;
}
Hint: when modifying this structure in the future, we need to ensure the total length is the same as before, so that our latest code is compatible to the old structure.
With the StruVersion field, we can easily distinguish between the old structure and the new structure, say the StruVersion of the old structure is 1 and the version of the new one is 2, we can write some code like the below:
if (fileHeader.StruVersion >= 2)
{
...
}
else
{
...
}
To show the progress of the handling process, we need to define an event for the CFileHandler class.
public enum WorkingStateEnum { wsNone, wsBegin, wsInprogress, wsEnd, wsError };
public class FileHandleEventArgs : EventArgs
{
private WorkingStateEnum workingState;
public WorkingStateEnum WorkingState { get { return workingState; } }
private int handled;
public int Handled { get { return handled; } }
private int total;
public int Total { get { return total; } }
public FileHandleEventArgs(WorkingStateEnum aState, int aHandled,
int aTotal)
{
this.workingState = aState;
this.handled = aHandled;
this.total = aTotal;
}
}
public delegate void FileHandleEventDelegate(object sender, FileHandleEventArgs e);
public event FileHandleEventDelegate FileHandleEvent;
protected void OnFileHandleEvent(FileHandleEventArgs e)
{
if (FileHandleEvent != null)
{
FileHandleEvent(this, e);
}
}
public void RaiseFileHandleEvent(WorkingStateEnum aState, int aHandled,
int aTotal)
{
FileHandleEventArgs e = new FileHandleEventArgs(aState, aHandled, aTotal);
OnFileHandleEvent(e);
}
Since we've got the file header and the event definition, we can implement the main functions of this class now. The source code is pretty much straightforward, I'll just post it here instead of saying too much.
public void EncryptFile(string aFile, string anEncryptedFile, string aPassword)
{
if (!File.Exists(aFile))
{
throw new CException(CException.EXP_WILL_NOT_HAPPEN, "The specified file does not exist!");
}
using (FileStream fsInput = new FileStream(aFile, FileMode.Open))
{
long fileLen = fsInput.Length;
int fileCount = (int)Math.Ceiling((double)fileLen / (double)BufferSize);
RaiseFileHandleEvent(WorkingStateEnum.wsBegin, 0, fileCount);
string nextFile = anEncryptedFile;
int[] randomSizes = RandomFileSizes(fileLen, fileCount);
for (int i = 0; i < fileCount; i++)
{
using (FileStream fsOutput = new FileStream(nextFile, FileMode.Create))
{
BinaryWriter bw = new BinaryWriter(fsOutput);
nextFile = string.Empty;
if (i != fileCount - 1)
{
string currentFolder = Path.GetDirectoryName(anEncryptedFile);
nextFile = GetNextFileName(currentFolder);
}
WriteFileHeader(bw, fileCount, i + 1, nextFile);
WriteFileContent(bw, fsInput, aPassword, randomSizes[i]);
bw.Flush();
RaiseFileHandleEvent(WorkingStateEnum.wsInprogress, i + 1, fileCount);
}
}
RaiseFileHandleEvent(WorkingStateEnum.wsEnd, 0, fileCount);
}
}
public void DecryptFile(string anEncryptedFile, string aFile, string aPassword)
{
if (!File.Exists(anEncryptedFile))
{
throw new CException(CException.EXP_WILL_NOT_HAPPEN, "The specified file does not exist!");
}
int fileCount = 0;
using (FileStream fsOutput = new FileStream(aFile, FileMode.Create))
{
string nextFile = anEncryptedFile;
while (true)
{
using (FileStream fsInput = new FileStream(nextFile, FileMode.Open))
{
BinaryReader br = new BinaryReader(fsInput);
FileHeader header = ReadFileHeader(br);
if (header.FileIndex == 1)
{
fileCount = header.FileCount;
RaiseFileHandleEvent(WorkingStateEnum.wsBegin, 0, fileCount);
}
int struLen = Marshal.SizeOf(typeof(FileHeader));
byte[] buffer = new byte[fsInput.Length - struLen];
br.Read(buffer, 0, buffer.Length);
byte[] newBytes = CEncoding.Decode3Des(buffer, aPassword);
fsOutput.Write(newBytes, 0, newBytes.Length);
RaiseFileHandleEvent(WorkingStateEnum.wsInprogress, header.FileIndex, fileCount);
if (string.IsNullOrEmpty(header.NextFile))
{
break;
}
nextFile = Path.Combine(Path.GetDirectoryName(anEncryptedFile), header.NextFile);
}
}
}
RaiseFileHandleEvent(WorkingStateEnum.wsEnd, fileCount, fileCount);
}
As for other auxiliary properties or functions, please see the complete CFileHandler.cs posted below for information.
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace com.casperlee.Library
{
public class CFileHandler
{
private static CFileHandler instance;
private CFileHandler()
{
this.BufferSize = 20 * 1024 * 1024;
}
public static CFileHandler Instance
{
get
{
if (instance == null)
{
instance = new CFileHandler();
}
return instance;
}
}
public enum WorkingStateEnum { wsNone, wsBegin, wsInprogress, wsEnd, wsError };
public class FileHandleEventArgs : EventArgs
{
private WorkingStateEnum workingState;
public WorkingStateEnum WorkingState { get { return workingState; } }
private int handled;
public int Handled { get { return handled; } }
private int total;
public int Total { get { return total; } }
public FileHandleEventArgs(WorkingStateEnum aState, int aHandled,
int aTotal)
{
this.workingState = aState;
this.handled = aHandled;
this.total = aTotal;
}
}
public delegate void FileHandleEventDelegate(object sender, FileHandleEventArgs e);
public event FileHandleEventDelegate FileHandleEvent;
protected void OnFileHandleEvent(FileHandleEventArgs e)
{
if (FileHandleEvent != null)
{
FileHandleEvent(this, e);
}
}
public void RaiseFileHandleEvent(WorkingStateEnum aState, int aHandled,
int aTotal)
{
FileHandleEventArgs e = new FileHandleEventArgs(aState, aHandled, aTotal);
OnFileHandleEvent(e);
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct FileHeader
{
public Int16 StruVersion;
public Int16 FileCount;
public Int16 FileIndex;
public Int16 EncryptMethod;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string NextFile;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)]
public byte[] Reserved;
}
public int BufferSize { get; set; }
private string GetNextFileName(string aFolder)
{
string nextFile = Path.Combine(aFolder, Guid.NewGuid().ToString());
while (File.Exists(nextFile))
{
nextFile += "_1";
}
return nextFile;
}
private void WriteFileHeader(BinaryWriter aWriter, int aFileCount, int aFileIndex, string aNextFile)
{
int struLen = Marshal.SizeOf(typeof(FileHeader));
IntPtr pHeader = Marshal.AllocHGlobal(struLen);
try
{
byte[] bHeader = new byte[struLen];
FileHeader header = new FileHeader();
header.StruVersion = 1;
header.EncryptMethod = 1;
header.FileCount = (short)aFileCount;
header.FileIndex = (short)(aFileIndex);
header.NextFile = Path.GetFileName(aNextFile);
Marshal.StructureToPtr(header, pHeader, true);
Marshal.Copy(pHeader, bHeader, 0, struLen);
aWriter.Write(bHeader);
}
finally
{
Marshal.FreeHGlobal(pHeader);
}
}
private FileHeader ReadFileHeader(BinaryReader aReader)
{
int struLen = Marshal.SizeOf(typeof(FileHeader));
IntPtr pHeader = Marshal.AllocHGlobal(struLen);
try
{
byte[] bHeader = new byte[struLen];
aReader.Read(bHeader, 0, struLen);
Marshal.Copy(bHeader, 0, pHeader, struLen);
FileHeader header = (FileHeader)Marshal.PtrToStructure(pHeader, typeof(FileHeader));
return header;
}
finally
{
Marshal.FreeHGlobal(pHeader);
}
}
private void WriteFileContent(BinaryWriter aWriter, FileStream aFileStream,
string aPassword, int aBufferSize)
{
byte[] buffer = new byte[aBufferSize];
int readLen = aFileStream.Read(buffer, 0, aBufferSize);
byte[] newBytes = buffer;
if (readLen != aBufferSize)
{
newBytes = new byte[readLen];
for (int j = 0; j < readLen; j++)
{
newBytes[j] = buffer[j];
}
}
newBytes = CEncoding.Encode3Des(newBytes, aPassword);
aWriter.Write(newBytes);
}
private int[] RandomFileSizes(long aTotalSize, int aFileCount)
{
int[] result = new int[aFileCount];
double average = (double)aTotalSize / (double)aFileCount;
long fixedSize = (long)(average * 0.8);
long leftSize = aTotalSize - fixedSize * aFileCount;
Random r = new Random();
double dTotal = 0;
for (int i = 0; i < aFileCount; i++)
{
result[i] = r.Next(aFileCount) + 1;
dTotal += result[i];
}
long lTotal = 0;
for (int i = 0; i < aFileCount - 1; i++)
{
result[i] = (int)(fixedSize + leftSize * result[i] / dTotal);
lTotal += result[i];
}
result[aFileCount - 1] = (int)(aTotalSize - lTotal);
return result;
}
public void EncryptFile(string aFile, string anEncryptedFile, string aPassword)
{
if (!File.Exists(aFile))
{
throw new CException(CException.EXP_WILL_NOT_HAPPEN, "The specified file does not exist!");
}
using (FileStream fsInput = new FileStream(aFile, FileMode.Open))
{
long fileLen = fsInput.Length;
int fileCount = (int)Math.Ceiling((double)fileLen / (double)BufferSize);
RaiseFileHandleEvent(WorkingStateEnum.wsBegin, 0, fileCount);
string nextFile = anEncryptedFile;
int[] randomSizes = RandomFileSizes(fileLen, fileCount);
for (int i = 0; i < fileCount; i++)
{
using (FileStream fsOutput = new FileStream(nextFile, FileMode.Create))
{
BinaryWriter bw = new BinaryWriter(fsOutput);
nextFile = string.Empty;
if (i != fileCount - 1)
{
string currentFolder = Path.GetDirectoryName(anEncryptedFile);
nextFile = GetNextFileName(currentFolder);
}
WriteFileHeader(bw, fileCount, i + 1, nextFile);
WriteFileContent(bw, fsInput, aPassword, randomSizes[i]);
bw.Flush();
RaiseFileHandleEvent(WorkingStateEnum.wsInprogress, i + 1, fileCount);
}
}
RaiseFileHandleEvent(WorkingStateEnum.wsEnd, 0, fileCount);
}
}
public void DecryptFile(string anEncryptedFile, string aFile, string aPassword)
{
if (!File.Exists(anEncryptedFile))
{
throw new CException(CException.EXP_WILL_NOT_HAPPEN, "The specified file does not exist!");
}
int fileCount = 0;
using (FileStream fsOutput = new FileStream(aFile, FileMode.Create))
{
string nextFile = anEncryptedFile;
while (true)
{
using (FileStream fsInput = new FileStream(nextFile, FileMode.Open))
{
BinaryReader br = new BinaryReader(fsInput);
FileHeader header = ReadFileHeader(br);
if (header.FileIndex == 1)
{
fileCount = header.FileCount;
RaiseFileHandleEvent(WorkingStateEnum.wsBegin, 0, fileCount);
}
int struLen = Marshal.SizeOf(typeof(FileHeader));
byte[] buffer = new byte[fsInput.Length - struLen];
br.Read(buffer, 0, buffer.Length);
byte[] newBytes = CEncoding.Decode3Des(buffer, aPassword);
fsOutput.Write(newBytes, 0, newBytes.Length);
RaiseFileHandleEvent(WorkingStateEnum.wsInprogress, header.FileIndex, fileCount);
if (string.IsNullOrEmpty(header.NextFile))
{
break;
}
nextFile = Path.Combine(Path.GetDirectoryName(anEncryptedFile), header.NextFile);
}
}
}
RaiseFileHandleEvent(WorkingStateEnum.wsEnd, fileCount, fileCount);
}
}
}