The app is done. It has all the basic features like creating an archive, adding/deleting/extracting files and so forth. The blog post will have a ZIP with the complete project. This might actually be useful to mankind.
The data I'm getting looks nice and encrypted. I'm not a mathematician but I believe I'm doing everything right, and again, it sure looks good and all my tests check out.
Code: Select all
// -----------------------------------------------------------------------------
//
// Class: EncryptedArchive
// Author: Mark Wing
// Date: 05/11/2013
// Purpose: C# Encrypted Archive class for adding, deleting and
// extracting files.
//
// Notice: Copyright (c) 2013 by Mark Wing.
// You are free to use and distribute this code.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Security.Cryptography;
using System.Data;
namespace FileEncryptionUtilities
{
public class EncryptedArchive
{
// Hard-coded salt so the same password always generates the same key.
public byte[] Salt = { 12, 33, 82, 18, 76, 55, 90, 34,
66, 20, 99, 52, 68, 21, 87, 100, 77 };
public byte[] Key = null;
public int KeySize = 32; // In bytes.
public byte[] InitVector = null;
public int IVSize = 16;
public string Password = "";
public string Notes = "";
public string RepositoryFile = "";
public List<string> Errors = new List<string>();
private FileStream _stream = null;
// -----------------------------------------------------------------------------
// Open the repository for reading.
// -----------------------------------------------------------------------------
public bool OpenForReading()
{
bool result = false;
try
{
// Open the repository for reading only and share it for reading.
_stream = new FileStream(RepositoryFile, FileMode.Open, FileAccess.Read, FileShare.Read);
result = true;
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return result;
}
// -----------------------------------------------------------------------------
// Open the repository for writing.
// -----------------------------------------------------------------------------
public bool OpenForWriting()
{
bool result = false;
try
{
// Open the repository for reading only and share it for reading.
_stream = new FileStream(RepositoryFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
_stream.Seek(0, SeekOrigin.End); // Seek to end of file for append.
result = true;
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return result;
}
// -----------------------------------------------------------------------------
// Close the open repository.
// -----------------------------------------------------------------------------
public void Close()
{
try
{
if (_stream != null)
{
_stream.Close();
_stream.Dispose();
_stream = null;
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
}
// -----------------------------------------------------------------------------
// Load an input file into a buffer.
// -----------------------------------------------------------------------------
public byte[] LoadInputFile(string fileName)
{
try
{
// First, try to open the input file and load it into memory.
byte[] newBuffer = null;
FileStream inputStream = new FileStream(fileName, FileMode.Open,
FileAccess.Read, FileShare.ReadWrite);
int inputLen = (int)inputStream.Length;
newBuffer = new byte[inputLen];
inputStream.Read(newBuffer, 0, inputLen);
inputStream.Close();
inputStream.Dispose();
return newBuffer;
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Encrypt a file and return a pile of bytes.
// -----------------------------------------------------------------------------
public byte[] EncryptFile(string fileName, RepositoryFile repoFile, byte[] newBuffer)
{
try
{
// Setup the key for this file.
Key = Convert.FromBase64String(repoFile.Properties["Key"]);
InitVector = Convert.FromBase64String(repoFile.Properties["IV"]);
// Do the actual encryption.
return EncryptBuffer(newBuffer, false);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return newBuffer;
}
// -----------------------------------------------------------------------------
// Encrypt/decrypt a buffer full of bytes.
// -----------------------------------------------------------------------------
public byte[] EncryptBuffer(byte[] inBuffer, bool decrypt)
{
try
{
// Use AES 256.
Aes encryptor = Aes.Create();
encryptor.Key = Key;
encryptor.IV = InitVector;
// Create the crypto stream.
MemoryStream memStream = new MemoryStream();
CryptoStream crypStream = null;
if (decrypt)
{
crypStream = new CryptoStream(memStream,
encryptor.CreateDecryptor(),
CryptoStreamMode.Write);
}
else
{
crypStream = new CryptoStream(memStream,
encryptor.CreateEncryptor(),
CryptoStreamMode.Write);
}
// Encrypt/decrypt the buffer.
crypStream.Write(inBuffer, 0, inBuffer.Length);
crypStream.Close();
crypStream.Dispose();
byte[] cryptoBytes = memStream.ToArray();
// Do some cleanup.
memStream.Close();
memStream.Dispose();
return cryptoBytes;
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Encrypt/decrypt a buffer full of bytes with a password.
// -----------------------------------------------------------------------------
public byte[] EncryptBufferWithPassword(byte[] inBuffer, bool decrypt)
{
try
{
DeriveKey();
// Encrypt the buffer with the password derived key.
return EncryptBuffer(inBuffer, decrypt);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Compute the hash of a file buffer.
// -----------------------------------------------------------------------------
public byte[] ComputeHash(byte[] buffer)
{
try
{
MD5 calc = MD5.Create();
return calc.ComputeHash(buffer);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Create a new salt buffer.
// -----------------------------------------------------------------------------
public byte[] GimmeSomeSalt()
{
try
{
// Create a random salt.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] saltBytes = new byte[KeySize];
rng.GetBytes(saltBytes);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Derive a key from password. Use hard-coded salt so we get the same key
// every time.
// -----------------------------------------------------------------------------
public void DeriveKey()
{
try
{
// Create the password derived key.
PasswordDeriveBytes bytes = new PasswordDeriveBytes(Password, Salt);
Key = bytes.GetBytes(KeySize);
InitVector = bytes.GetBytes(IVSize);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
}
// -----------------------------------------------------------------------------
// Add a file to the repository.
// -----------------------------------------------------------------------------
public bool AddFileToRepository(string fileToAdd)
{
bool result = false;
try
{
// Build a file object.
RepositoryFile info = BuildFileInfo(fileToAdd);
if (info != null)
{
// Load the file.
byte[] fileBuffer = LoadInputFile(fileToAdd);
if (fileBuffer == null)
return false;
// Compute the file's hash.
info.Properties.Add("Hash", Convert.ToBase64String(ComputeHash(fileBuffer)));
// Add any extra properties here -->
// Convert to document for storage in the archive.
XmlDocument fileDoc = info.ToXml();
// Now encrypt the XML document.
byte[] docBuffer = EncryptBufferWithPassword(Encoding.Unicode.GetBytes(fileDoc.InnerXml), false);
// Open the archive.
if (OpenForWriting())
{
BinaryWriter writer = new BinaryWriter(_stream);
// Write the encrypted file buffer.
byte[] fileEncBuffer = EncryptFile(fileToAdd, info, fileBuffer);
if (fileEncBuffer != null)
{
// Write the buffer sizes.
writer.Write(docBuffer.Length);
writer.Write(fileDoc.InnerXml.Length);
writer.Write(fileEncBuffer.Length);
writer.Write(int.Parse(info.Properties["Size"]));
// Write the buffers.
writer.Write(docBuffer);
writer.Write(fileEncBuffer);
}
// Close the archive.
Close();
}
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return result;
}
// -----------------------------------------------------------------------------
// Build a file object based on a Windows file.
// -----------------------------------------------------------------------------
public RepositoryFile BuildFileInfo(string fileName)
{
RepositoryFile repoFile = null;
try
{
// Make sure file exists.
if (File.Exists(fileName))
{
// Get the basic info.
FileInfo info = new FileInfo(fileName);
// Create file object and fill it.
repoFile = new RepositoryFile();
// Get all the basic file info.
repoFile.Properties.Add("Name", info.Name);
repoFile.Properties.Add("Size", info.Length.ToString());
repoFile.Properties.Add("DateCreated", info.CreationTime.ToString());
repoFile.Properties.Add("DateAccessed", info.LastAccessTime.ToString());
repoFile.Properties.Add("DateModified", info.LastWriteTime.ToString());
repoFile.Properties.Add("Extension", info.Extension.Substring(1, info.Extension.Length - 1));
// Generate a key for this file.
repoFile.GenerateKey(KeySize);
// Generate a GUID too.
repoFile.GenerateGuid();
// Calculate folder name by stripping off drive.
string[] dirSplit = info.DirectoryName.Split(':');
if (dirSplit.Length > 1)
{
repoFile.Properties.Add("Folder", dirSplit[1]);
}
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return repoFile;
}
// -----------------------------------------------------------------------------
// Return a list of all the files in the repository.
// -----------------------------------------------------------------------------
public List<RepositoryFile> ListFiles()
{
try
{
ContainerListCommand lister = new ContainerListCommand();
return (List<RepositoryFile>) IterateThroughRepository(lister, "");
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Load meta data for file at current file seek offset.
// -----------------------------------------------------------------------------
private RepositoryFile LoadFileMetaData()
{
try
{
BinaryReader reader = new BinaryReader(_stream);
RepositoryFile newFile = new RepositoryFile();
// Read the size values for meta and file data.
int docEncSize = reader.ReadInt32();
int docDecSize = reader.ReadInt32();
int fileEncSize = reader.ReadInt32();
int fileDecSize = reader.ReadInt32();
// Load the meta data.
byte[] docBuffer = reader.ReadBytes(docEncSize);
byte[] decDocBuffer = EncryptBufferWithPassword(docBuffer, true);
string docString = Encoding.Unicode.GetString(decDocBuffer);
XmlDocument doc = new XmlDocument();
doc.LoadXml(docString);
// Build a file obect from XML document.
newFile.FromXml(doc);
// Add the file property if it's not there.
if (!newFile.Properties.ContainsKey("EncryptedSize"))
{
newFile.Properties.Add("EncryptedSize", fileEncSize.ToString());
}
return newFile;
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Convert file list to data table for easy binding to grid controls and such.
// -----------------------------------------------------------------------------
public DataTable ConvertListToTable(List<RepositoryFile> fileList)
{
DataTable tbl = new DataTable();
try
{
tbl.Columns.Add("Name");
tbl.Columns.Add("Folder");
tbl.Columns.Add("Size", System.Type.GetType("System.Int32"));
tbl.Columns.Add("Date", System.Type.GetType("System.DateTime"));
tbl.Columns.Add("Hash");
tbl.Columns.Add("GUID");
foreach (RepositoryFile fileEntry in fileList)
{
DataRow row = tbl.NewRow();
row["Name"] = fileEntry.Properties["Name"];
row["Folder"] = fileEntry.Properties["Folder"];
row["Size"] = int.Parse(fileEntry.Properties["Size"]);
row["Date"] = DateTime.Parse(fileEntry.Properties["DateCreated"]);
row["Hash"] = fileEntry.Properties["Hash"];
row["GUID"] = fileEntry.Properties["GUID"];
tbl.Rows.Add(row);
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return tbl;
}
// -----------------------------------------------------------------------------
// Extract a single file by ID.
// -----------------------------------------------------------------------------
public bool ExtractFile(string fileId, string outputFolder, bool keepArchiveFolder)
{
bool retVal = false;
try
{
// Seek to the GUID.
RepositoryFile repoFile = SeekToFile(fileId);
if (repoFile != null)
{
byte[] encryptedBuffer = LoadFileFromRepository(repoFile);
if (encryptedBuffer != null)
{
// Now decrypt it.
Key = Convert.FromBase64String(repoFile.Properties["Key"]);
InitVector = Convert.FromBase64String(repoFile.Properties["IV"]);
byte[] unencryptedBuffer = EncryptBuffer(encryptedBuffer, true);
if (unencryptedBuffer != null)
{
// Compute the hash of the unencrypted file.
string hashVal = Convert.ToBase64String(ComputeHash(unencryptedBuffer));
// Make sure the hashes match.
if (hashVal != repoFile.Properties["Hash"])
return false;
// Build the output file name.
string fileName = "";
string folder = "";
if (keepArchiveFolder)
{
fileName = string.Format("{0}{1}\\{2}",
outputFolder,
repoFile.Properties["Folder"],
repoFile.Properties["Name"]);
folder = string.Format("{0}{1}",
outputFolder,
repoFile.Properties["Folder"]);
}
else
{
fileName = string.Format("{0}\\{1}",
outputFolder,
repoFile.Properties["Name"]);
folder = outputFolder;
}
// Create the output folder.
try
{
Directory.CreateDirectory(folder);
}
catch (Exception)
{
}
// Write the unencrypted file.
FileStream outStream = new FileStream(fileName, FileMode.Create, FileAccess.Write);
BinaryWriter writer = new BinaryWriter(outStream);
writer.Write(unencryptedBuffer);
outStream.Close();
writer.Dispose();
outStream.Dispose();
}
}
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return retVal;
}
// -----------------------------------------------------------------------------
// Load an encrypted file from the repository if we already have its info.
// -----------------------------------------------------------------------------
private byte[] LoadFileFromRepository(RepositoryFile repoFile)
{
try
{
if (OpenForReading())
{
_stream.Seek(long.Parse(repoFile.Properties["FileOffset"]), SeekOrigin.Begin);
BinaryReader reader = new BinaryReader(_stream);
byte[] buffer = reader.ReadBytes(int.Parse(repoFile.Properties["EncryptedSize"]));
reader.Dispose();
Close();
return buffer;
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Allow for bulk processing on the repository.
// -----------------------------------------------------------------------------
public Object IterateThroughRepository(IFileContainerCommand command, string param1)
{
try
{
if (OpenForReading())
{
command.Param1(param1);
if (command.Begin(ref _stream))
{
RepositoryFile fileEntry = null;
// Keep reading files until we run out of space.
do
{
// Load this file's meta data.
fileEntry = LoadFileMetaData();
if (fileEntry != null)
{
// Jot down the offset of this file.
if (fileEntry.Properties.ContainsKey("FileOffset"))
{
fileEntry.Properties.Remove("FileOffset");
}
fileEntry.Properties.Add("FileOffset", _stream.Position.ToString());
if (!command.ProcessFile(fileEntry))
break;
// Seek past file data.
_stream.Seek(long.Parse(fileEntry.Properties["EncryptedSize"]),
SeekOrigin.Current);
}
} while (fileEntry != null);
// Close the file.
Close();
return command.End();
}
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Seek to file by its unique ID.
// -----------------------------------------------------------------------------
public RepositoryFile SeekToFile(string fileId)
{
try
{
ContainerSeekToFile seeker = new ContainerSeekToFile();
return (RepositoryFile)IterateThroughRepository(seeker, fileId);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return null;
}
// -----------------------------------------------------------------------------
// Seek to file by its unique ID.
// -----------------------------------------------------------------------------
public void ExtractMetaData(string outputXmlFile)
{
try
{
ExtractAllMetaData extractor = new ExtractAllMetaData();
XmlDocument doc = (XmlDocument)IterateThroughRepository(extractor, "");
doc.Save(outputXmlFile);
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
}
// -----------------------------------------------------------------------------
// Delete files from archive given a list of GUIDs.
// -----------------------------------------------------------------------------
public bool DeleteFilesFromRepository(List<string> fileIds)
{
bool retVal = false;
try
{
List<RepositoryFile> allFiles = ListFiles();
if (allFiles != null && fileIds.Count > 0)
{
// Create and open temp archive.
string tempFileName = RepositoryFile + ".copy_of";
FileStream tempArchive = new FileStream(tempFileName,
FileMode.Create, FileAccess.Write, FileShare.Read);
BinaryWriter writer = new BinaryWriter(tempArchive);
// Open the main archive.
if (OpenForReading())
{
BinaryReader reader = new BinaryReader(_stream);
// Loop through all files in archive.
foreach (RepositoryFile repoFile in allFiles)
{
// See if it's not in delete list.
if (!fileIds.Contains(repoFile.Properties["GUID"]))
{
// Re-encrypt the meta data.
XmlDocument fileDoc = repoFile.ToXml();
byte[] docBuffer = EncryptBufferWithPassword(Encoding.Unicode.GetBytes(fileDoc.InnerXml), false);
// Copy it to the new archive.
writer.Write(docBuffer.Length);
writer.Write(fileDoc.InnerXml.Length);
writer.Write(int.Parse(repoFile.Properties["EncryptedSize"]));
writer.Write(int.Parse(repoFile.Properties["Size"]));
// Load the encrypted file as-is.
_stream.Seek(long.Parse(repoFile.Properties["FileOffset"]), SeekOrigin.Begin);
byte[] fileEncBuffer = reader.ReadBytes(int.Parse(repoFile.Properties["EncryptedSize"]));
// Write the buffers.
writer.Write(docBuffer);
writer.Write(fileEncBuffer);
}
}
reader.Dispose();
}
// Close both archives.
Close();
tempArchive.Close();
// Now swap the files out.
File.Delete(RepositoryFile);
File.Move(tempFileName, RepositoryFile);
retVal = true;
}
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
return retVal;
}
}
// -----------------------------------------------------------------------------
// Repository file class.
// -----------------------------------------------------------------------------
public class RepositoryFile
{
// Name/value pair list of file properties.
public Dictionary<string, string> Properties = new Dictionary<string, string>();
// -----------------------------------------------------------------------------
// Populate from an XML document.
// -----------------------------------------------------------------------------
public void FromXml(XmlDocument doc)
{
try
{
foreach (XmlNode topNode in doc.DocumentElement.ChildNodes)
{
Properties.Add(topNode.Name, topNode.InnerText);
}
}
catch (Exception)
{
}
}
// -----------------------------------------------------------------------------
// Convert the file properties to XML document.
// -----------------------------------------------------------------------------
public XmlDocument ToXml()
{
XmlDocument doc = null;
try
{
doc = new XmlDocument();
doc.LoadXml("<root></root>");
foreach (KeyValuePair<string, string> fileProp in Properties)
{
// Append a new node in the form of <property>value</property>
XmlNode node = doc.CreateElement(fileProp.Key);
node.InnerText = fileProp.Value;
doc.DocumentElement.AppendChild(node);
}
}
catch (Exception)
{
}
return doc;
}
// -----------------------------------------------------------------------------
// Generate a separate key and IV for every file in the repository.
// -----------------------------------------------------------------------------
public void GenerateKey(int keySizeBytes)
{
try
{
Aes encryptor = Aes.Create();
encryptor.KeySize = keySizeBytes * 8;
encryptor.GenerateKey();
encryptor.GenerateIV();
Properties["Key"] = Convert.ToBase64String(encryptor.Key);
Properties["IV"] = Convert.ToBase64String(encryptor.IV);
}
catch (Exception)
{
}
}
// -----------------------------------------------------------------------------
// Generate a GUID for this file, since we're allowing duplicate names.
// -----------------------------------------------------------------------------
public void GenerateGuid()
{
try
{
Properties["GUID"] = Guid.NewGuid().ToString();
}
catch (Exception)
{
}
}
}
}