私人文件夹锁定器






4.78/5 (21投票s)
文件夹加密/解密,带独立功能
引言
我发现自己又在寻找一个应用程序,找不到一个能满足我需求的良好/免费的应用程序。我决定像往常一样自己编写代码。该代码可以加密和解密一个文件夹及其内部找到的所有文件,包括子文件夹。它还有一个独立的功能,可以将所有文件封装成一个名为 locker.exe 的自解压/解密应用程序。
背景
本文将重点介绍文件的加密/解密、独立应用程序的资源管理以及运行时编译此类独立应用程序的代码。
使用代码
首先,我将重点介绍加密和解密方法。文件加密是通过使用 EncryptFile
方法实现的。
private static void EncryptFile(string inputFile, byte[] key)
{
try
{
string ext = Path.GetExtension(inputFile);
string outputFile = inputFile.Replace(ext, "_enc" + ext);
//Prepare the file for encryption by getting it into a stream
string cryptFile = outputFile;
FileStream fsCrypt = new FileStream(cryptFile, FileMode.Create);
//Setup the Encryption Standard using Write mode
RijndaelManaged rijndaelCrypto = new RijndaelManaged();
CryptoStream cs = new CryptoStream(fsCrypt,
rijndaelCrypto.CreateEncryptor(key, key), CryptoStreamMode.Write);
//Write the encrypted file stream
FileStream fsIn = new FileStream(inputFile, FileMode.Open);
int data;
while ((data = fsIn.ReadByte()) != -1)
{
cs.WriteByte((byte)data);
}
//Close all the Writers
fsIn.Close();
cs.Close();
fsCrypt.Close();
//Delete the original file
File.Delete(inputFile);
//Rename the encrypted file to that of the original
File.Copy(outputFile, inputFile);
File.Delete(outputFile);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
V1 版本使用了 MD5CryptoServiceProvider
类。此类存在一些问题,如果您不编码密码密钥,就会收到错误。通过使用 UTF8
和 MD5CryptoServiceProvider
,我们确保密钥始终是 128 位或 16 字节的密钥。这是 CryptoStream
对象所必需的。
由于 MD5 很容易被破解,V2 版本改用了 PasswordDerivedBytes
类(感谢 RobTeixeira)。此类不仅需要私钥和公钥,还需要一个 salt,以确保每个加密的密码都是唯一的。包含的 EncryptionUtils
类有两个生成随机 Salt 的方法,但在 PrivateFolderLocker
中未使用。而是使用文件名来生成 Salt。
public static byte[] EncryptPassword(string clearText, string password, string salt)
{
byte[] saltBytes = Encoding.Unicode.GetBytes(salt);
byte[] clearBytes = System.Text.Encoding.Unicode.GetBytes(clearText);
PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, saltBytes);
byte[] encryptedData = EncryptPW(clearBytes, pdb.GetBytes(32), pdb.GetBytes(16));
//return Convert.ToBase64String(encryptedData); //For returning string instead
return encryptedData;
}
文件解密是再次通过使用 DecryptFile
方法实现的。
private static void DecryptFile(string inputFile, byte[] key)
{
string ext = Path.GetExtension(inputFile);
string outputFile = inputFile.Replace(ext, "_enc" + ext);
//Prepare the file for decryption by getting it into a stream
FileStream fsCrypt = new FileStream(inputFile, FileMode.Open);
//Setup the Decryption Standard using Read mode
RijndaelManaged rijndaelCrypto = new RijndaelManaged();
CryptoStream cs = new CryptoStream(fsCrypt,
rijndaelCrypto.CreateDecryptor(key, key), CryptoStreamMode.Read);
//Write the decrypted file stream
FileStream fsOut = new FileStream(outputFile, FileMode.Create);
try
{
int data;
while ((data = cs.ReadByte()) != -1)
{ fsOut.WriteByte((byte)data); }
//Close all the Writers
fsOut.Close();
cs.Close();
fsCrypt.Close();
//Delete the original file
File.Delete(inputFile);
//Rename the encrypted file to that of the original
File.Copy(outputFile, inputFile);
File.Delete(outputFile);
}
catch (Exception ex)
{
throw ex;
}
finally
{
fsOut = null;
cs = null;
fsCrypt = null;
}
}
EncryptFile
和 DecryptFile
方法之间的主要区别在于 CryptoStream
对象的模式。加密使用 CryptoStreamMode.Write
,解密使用 CryptoStreamMode.Read
。
由于加密和解密单个文件用途不大,因此编写了一个递归方法来简化此过程。
private static void GetAllFilesInDir(DirectoryInfo dir, string searchPattern)
{
// list the files
try
{
foreach (FileInfo f in dir.GetFiles(searchPattern))
{
//Console.WriteLine("File {0}", f.FullName);
files.Add(f);
}
}
catch
{
Console.WriteLine("Directory {0} \n could not be accessed!!!!", dir.FullName);
return; // We already got an error trying to access dir so don't try to access it again
}
// process each directory
// If I have been able to see the files in the directory I should also be able
// to look at its directories so I don't think I should place this in a try catch block
foreach (DirectoryInfo d in dir.GetDirectories())
{
folders.Add(d);
GetAllFilesInDir(d, searchPattern);
}
}
此实用程序的 V3 版本将构建一个 XML 数据库来存储文件和目录,以便提取时能够记住文件夹结构。作为 XML 数据库的附加功能,Salt 字段将存储一个更强的 Salt 以获得更好的安全性。
我使用了列表来存储找到的文件和文件夹。
static List<FileInfo> files = new List<FileInfo>();
static List<DirectoryInfo> folders = new List<DirectoryInfo>();
为了使用这种递归方法,我创建了单独的方法来加密和解密文件夹。
public static bool EncryptedFolder(string folderDirectory, string pword,bool compress)
{
bool status = false;
string fileLocation = "";
string salt = "";
byte[] encPW = null;
try
{
status = Directory.Exists(folderDirectory);
if (status)
{
DirectoryInfo di = new DirectoryInfo(folderDirectory);
//Clear Folder and File list
folders = new List<DirectoryInfo>();
files = new List<FileInfo>();
//Build new Folder and File list
GetAllFilesInDir(di, "*");
foreach (FileInfo fi in files)
{
fileLocation = fi.FullName;
if (compress)
{
fileLocation += ".zip";
}
if (compress)
{
//Compress the file using 7Zip's LZMA compression
CompressFileLZMA(fi.FullName, fileLocation);
//Delete the original file
if (File.Exists(fi.FullName))
{
File.Delete(fi.FullName);
}
}
//Build the Encrypted Password with a unique salt based on the file's info
string fileData = string.Format("{0}", fi.Name.Substring(0, fi.Name.IndexOf(".")));
salt = Convert.ToBase64String(GetBytes(fileData));
encPW = EncryptPassword(pword, "!PrivateLocker-2013", salt);
string strPW = Convert.ToBase64String(encPW);
EncryptFile(fileLocation, encPW);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
status = false;
}
return status;
}
V2 版本通过添加 7Zip LZMA 压缩扩展了原始方法。这确保了在使用独立选项时,Locker.exe 的体积尽可能小。
既然我们能够加密和解密整个文件夹,我想为什么每次想解密文件时都必须随身携带应用程序呢?我决定创建一种方法,将加密的文件嵌入到一个独立的 exe 中。
为此,我必须将加密的文件放入应用程序中并将其嵌入为资源。这需要一些运行时代码编译。为此,我必须使用 CodeDomProvider
。
public static bool CompileExecutable(string sourceName, string resourceDirectory)
{
FileInfo sourceFile = new FileInfo(sourceName);
CodeDomProvider provider = null;
bool compileOk = false;
// Select the code provider based on the input file extension.
if (sourceFile.Extension.ToUpper(CultureInfo.InvariantCulture) == ".CS")
{
provider = CodeDomProvider.CreateProvider("CSharp");
}
else if (sourceFile.Extension.ToUpper(CultureInfo.InvariantCulture) == ".VB")
{
provider = CodeDomProvider.CreateProvider("VisualBasic");
}
else
{
Console.WriteLine("Source file must have a .cs or .vb extension");
}
if (provider != null)
{
// Format the executable file name.
// Build the output assembly path using the current directory
String exeName = String.Format(@"{0}\Locker.exe", resourceDirectory);
CompilerParameters cp = new CompilerParameters();
// Generate an executable instead of
// a class library.
cp.GenerateExecutable = true;
// Specify the assembly file name to generate.
cp.OutputAssembly = exeName;
// Save the assembly as a physical file.
cp.GenerateInMemory = false;
// Set whether to treat all warnings as errors.
cp.TreatWarningsAsErrors = false;
//Add the Resources
resourceFiles.Clear();
GetAllFilesInDir(new DirectoryInfo(resourceDirectory), "*");
foreach (FileInfo file in resourceFiles)
{
if (file.Name != "standalone.cs")
{
cp.EmbeddedResources.Add(file.FullName);
}
if (file.Name == "Lzma.dll")
{
cp.ReferencedAssemblies.Add(file.FullName);
}
if (file.Name == "key2.ico")
{
//Add Icon
cp.CompilerOptions = string.Format("/win32icon:{0}", file.FullName);
}
}
// Invoke compilation of the source file.
CompilerResults cr = provider.CompileAssemblyFromFile(cp,
sourceName);
if(cr.Errors.Count > 0)
{
// Display compilation errors.
Console.WriteLine("Errors building {0} into {1}",
sourceName, cr.PathToAssembly);
foreach(CompilerError ce in cr.Errors)
{
Console.WriteLine(" {0}", ce.ToString());
Console.WriteLine();
}
}
else
{
// Display a successful compilation message.
Console.WriteLine("Source {0} built into {1} successfully.",
sourceName, cr.PathToAssembly);
}
// Return the results of the compilation.
if (cr.Errors.Count > 0)
{
compileOk = false;
}
else
{
compileOk = true;
}
if (resourceFiles != null)
{
foreach (FileInfo file in resourceFiles)
{
try
{
File.Delete(file.FullName);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
return compileOk;
}
该应用程序的 V2 版本对 CompileExecutable
方法进行了一些有趣的修改。由于独立 exe 现在使用 LZMA.dll,我必须将其包含在资源中并在运行时提取。
我创建了一个小控制台应用程序(standalone.cs),它使用之前编写的解密方法,然后创建了一种动态提取嵌入资源文件的方法。
private static void ExtractResources(string dir)
{
try
{
string fInfo = "";
Assembly asm = Assembly.GetExecutingAssembly();
Stream fstr = null;
//Create The output Directory if it Doesn't Exist
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
//Loop thru all the resources and Extract them
foreach (string resourceName in asm.GetManifestResourceNames())
{
fInfo = dir + @"\" + resourceName.Replace(asm.GetName().Name + ".Resources.", "");
fstr = asm.GetManifestResourceStream(resourceName);
if(fInfo.Contains("Lzma"))
{
fInfo = System.Environment.CurrentDirectory + "\\" +
resourceName.Replace(asm.GetName().Name + ".Resources.", "");
}
if (fstr != null && !fInfo.Contains("key2.ico"))
{
SaveStreamToFile(fInfo, fstr);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
因此,我将此 ExtractResources
方法称为提取我嵌入到主应用程序中的 standalone.cs 文件。在 V2 版本中,会提取 LZMA.dll 以使用 7Zip 解压缩方法。
然后,将 standalone.cs 文件的路径传递给提取后的 CompileExecutable
方法,并将选择用于加密的文件夹作为资源路径传递。CompileExecutable
方法然后编译 standalone.cs 文件,并遍历选择用于加密的所有文件。它使用 CompilerParameters
对象 cp
将这些文件添加为嵌入资源。cp.EmbeddedResources.Add(fileName)
从而将加密的文件嵌入到编译后的 exe 中。
此过程创建一个名为 Locker.exe 的 exe 文件,它本质上由 standalone.cs 文件中的代码组成。
那么…… standalone.cs 文件中包含什么?嗯,它是一个相当直接的控制台应用程序,它向用户请求密码。然后它使用 ExtractResources
方法提取其所有嵌入式资源。然后它使用 DecryptFile
方法解密提取的文件。
关注点
在编写此应用程序的过程中,我了解到资源管理很糟糕,并且不像人们想象的那么直接。我不得不使用“一点黑魔法”(即反射)来查看独立应用程序中嵌入的所有资源。我还了解到,将资源放在 Resource.resx 文件中意味着您无法取回文件的扩展名。这使得我直接将资源嵌入到应用程序中,而不是嵌入到资源文件中。
历史
- V1:这是该应用程序的 V1 版本,我敢肯定一些 CodeProject 的专家可以对其进行一些改进。仍然存在一个问题,即独立应用程序不会重新创建子目录。这可以通过一个 hack 来修复,即在编译结束时添加一个 XML 文件列表。在提取过程中,我们可以使用此 XML 列表将资源提取到正确的子目录。通过使用在递归搜索文件过程中创建的文件夹列表,创建此 XML 文件应该足够容易。
- V2:此版本的
PrivateFolderLocker
使用了更好的加密标准。它实现了PasswordDeriveBytes
类,并为每个文件生成一个 Salt。