65.9K
CodeProject 正在变化。 阅读更多。
Home

私人文件夹锁定器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (21投票s)

2012年12月19日

CPOL

5分钟阅读

viewsIcon

77320

downloadIcon

10322

文件夹加密/解密,带独立功能

引言

我发现自己又在寻找一个应用程序,找不到一个能满足我需求的良好/免费的应用程序。我决定像往常一样自己编写代码。该代码可以加密和解密一个文件夹及其内部找到的所有文件,包括子文件夹。它还有一个独立的功能,可以将所有文件封装成一个名为 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 类。此类存在一些问题,如果您不编码密码密钥,就会收到错误。通过使用 UTF8MD5CryptoServiceProvider,我们确保密钥始终是 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;
    }
} 

EncryptFileDecryptFile 方法之间的主要区别在于 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。
© . All rights reserved.