如何在C#中创建文件夹的哈希值?

27

我需要为一个包含一些文件的文件夹创建哈希值。我已经对每个文件完成了此任务,但我正在寻找一种方法来为文件夹中的所有文件创建一个哈希值。有什么想法吗?

(当然,我可以为每个文件创建哈希值并将其连接到一些大哈希值,但这不是我喜欢的方式)

7个回答

39

这个程序会对所有文件(相对路径)和内容进行哈希处理,并正确处理文件排序。

而且速度很快——对于一个4MB的目录,只需要30毫秒。

using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Linq;

...

public static string CreateMd5ForFolder(string path)
{
    // assuming you want to include nested folders
    var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories)
                         .OrderBy(p => p).ToList();

    MD5 md5 = MD5.Create();

    for(int i = 0; i < files.Count; i++)
    {
        string file = files[i];
        
        // hash path
        string relativePath = file.Substring(path.Length + 1);
        byte[] pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLower());
        md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0);
        
        // hash contents
        byte[] contentBytes = File.ReadAllBytes(file);
        if (i == files.Count - 1)
            md5.TransformFinalBlock(contentBytes, 0, contentBytes.Length);
        else
            md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0);
    }
    
    return BitConverter.ToString(md5.Hash).Replace("-", "").ToLower();
}

2
如果您将此部署到强制执行 FIPS 合规性的本地安全策略服务器上,请注意 FIPS 合规性。 - SkeetJon
1
@SkeetJon 技术对于任何加密算法都是相同的,因此您可以将 SHA 替换为 FIPS 设备。 - Ed Bayiates
4
搜索模式要小心,应该使用"*"而不是"*.*"。 - Jan
1
File.ReadAllBytes可能会导致该方法对内存的占用非常大。 - Ronnie Overby
在您的方法中,如果文件系统以不同的顺序返回相同的文件,则会获得不同的哈希值。 - iRumba
iRumba - 它通过路径对文件进行排序(OrderBy...)。 - Dunc

20

Dunc的回答效果不错,但是它不能处理空目录。下面的代码返回MD5值“d41d8cd98f00b204e9800998ecf8427e”(一个长度为0的字符流的MD5值)用于空目录。

public static string CreateDirectoryMd5(string srcPath)
{
    var filePaths = Directory.GetFiles(srcPath, "*", SearchOption.AllDirectories).OrderBy(p => p).ToArray();

    using (var md5 = MD5.Create())
    {
        foreach (var filePath in filePaths)
        {
            // hash path
            byte[] pathBytes = Encoding.UTF8.GetBytes(filePath);
            md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0);

            // hash contents
            byte[] contentBytes = File.ReadAllBytes(filePath);

            md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0);
        }

        //Handles empty filePaths case
        md5.TransformFinalBlock(new byte[0], 0, 0);

        return BitConverter.ToString(md5.Hash).Replace("-", "").ToLower();
    }
}

2
如果您使用此版本,则希望将filePath截断为相对路径,以创建pathBytes - Stanley.Goldman

9
这里有一个使用流式处理来避免内存和延迟问题的解决方案。
默认情况下,文件路径包含在哈希中,这将考虑到文件中的数据以及文件系统条目本身,从而避免哈希冲突。此帖子标记为“安全”,因此这应该很重要。
最后,此解决方案使您控制哈希算法,哪些文件被哈希以及以什么顺序进行哈希。
public static class HashAlgorithmExtensions
{
    public static async Task<byte[]> ComputeHashAsync(this HashAlgorithm alg, IEnumerable<FileInfo> files, bool includePaths = true)
    {
        using (var cs = new CryptoStream(Stream.Null, alg, CryptoStreamMode.Write))
        {
            foreach (var file in files)
            {
                if (includePaths)
                {
                    var pathBytes = Encoding.UTF8.GetBytes(file.FullName);
                    cs.Write(pathBytes, 0, pathBytes.Length);
                }

                using (var fs = file.OpenRead())
                    await fs.CopyToAsync(cs);
            }

            cs.FlushFinalBlock();
        }

        return alg.Hash;
    }
}

一个将文件夹中所有文件进行哈希的示例:

async Task<byte[]> HashFolder(DirectoryInfo folder, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
    using(var alg = MD5.Create())
        return await alg.ComputeHashAsync(folder.EnumerateFiles(searchPattern, searchOption));
}

2
此外,我建议对文件名进行排序,并使用 Path.GetRelativePath(folder, file.FullName),这样即使文件位于不同的文件夹位置,哈希值也是稳定的。 - Bjorg
同意需要在哈希之前对“文件”进行排序。但是,“Path.GetRelativePath”仅在netcore中找到。在文件的完整路径上进行排序应该可以解决问题。对于具有许多条目的目录,应特别注意以避免排序过于昂贵。有方法可以在不对整个序列进行排序的情况下生成确定性文件枚举。 - Ronnie Overby
请注意,FileStream.CopyToAsync() 是在.NET Framework 4.5中引入的。 - ClownCoder

7
创建文件的tarball,对tarball进行哈希处理。
> tar cf hashes *.abc
> md5sum hashes

或者对单个文件进行哈希处理并将输出导入哈希命令。
> md5sum *.abc | md5sum

编辑:以上两种方法都不会对文件进行排序,因此可能会因shell扩展星号的方式而在每次调用时返回不同的哈希值。


这是唯一一个考虑到所有元信息的答案,例如日期、访问权限、UID、GUID等。 - itsafire

4

如果您已经获得所有文件的哈希值,只需按字母顺序排序这些哈希值,将它们连接起来并再次进行哈希以创建超级哈希。


1
问题在于,您可以更改文件名并仍然获得相同的哈希值 - 这可能是您想要的,也可能不是。 - Straff

1

将文件名和文件内容连接成一个大字符串并进行哈希,或者为了提高性能,分块进行哈希。

当然,您需要考虑一些事情:

  • 您需要按名称对文件进行排序,以便在文件顺序更改的情况下不会得到两个不同的哈希值。
  • 使用此方法,您只考虑文件名和内容。如果文件名不重要,则可以先按内容排序,然后进行哈希。如果更多属性(ctime/mtime/hidden/archived..)很重要,请将它们包含在待哈希的字符串中。

感谢您的回复。该字符串可能非常大,因此我需要将其分成块,只是在考虑如何正确地执行此操作。 - Igor Pistolyaka
我记得C#哈希函数有一个可以输入块的功能,最后你可以要求获取最终哈希值,但不确定这些函数/类是什么。使用它们,您可以按照自己喜欢的方式在内存中对输入进行排序,然后循环文件并将块加载到几百KB中,并将其提供给哈希器,这样您就不需要太多内存,但仍需要一些时间进行哈希,这是无法摆脱的。 - aularon
这可能会导致与内存相关的问题。 - Ronnie Overby

-1

快速而简单的文件夹哈希,不会深入到子目录或读取二进制数据。它基于文件和子文件夹名称。

Public Function GetFolderHash(ByVal sFolder As String) As String
    Dim oFiles As List(Of String) = IO.Directory.GetFiles(sFolder).OrderBy(Function(x) x.Count).ToList()
    Dim oFolders As List(Of String) = IO.Directory.GetDirectories(sFolder).OrderBy(Function(x) x.Count).ToList()
    oFiles.AddRange(oFolders)

    If oFiles.Count = 0 Then
        Return ""
    End If

    Dim oDM5 As System.Security.Cryptography.MD5 = System.Security.Cryptography.MD5.Create()
    For i As Integer = 0 To oFiles.Count - 1
        Dim sFile As String = oFiles(i)
        Dim sRelativePath As String = sFile.Substring(sFolder.Length + 1)
        Dim oPathBytes As Byte() = System.Text.Encoding.UTF8.GetBytes(sRelativePath.ToLower())

        If i = oFiles.Count - 1 Then
            oDM5.TransformFinalBlock(oPathBytes, 0, oPathBytes.Length)
        Else
            oDM5.TransformBlock(oPathBytes, 0, oPathBytes.Length, oPathBytes, 0)
        End If
    Next

    Return BitConverter.ToString(oDM5.Hash).Replace("-", "").ToLower()
End Function

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接