在 .NET 中创建一个具有密码学安全性的随机 GUID

38

我想在.NET中创建一个加密安全的GUID(v4)。

.NET的Guid.NewGuid()函数不是加密安全的,但是.NET提供了System.Security.Cryptography.RNGCryptoServiceProvider类。

我希望能够将一个随机数函数作为委托传递给Guid.NewGuid(甚至传递一些提供生成器接口的类),但是默认实现似乎不支持这种方式。

我能否通过同时使用System.GUIDSystem.Security.Cryptography.RNGCryptoServiceProvider来创建一个加密安全的GUID呢?


3
至少在Windows 10上,.NET NewGuid使用Ole32.CoCreateGuid,该函数使用Rpcrt4.UuidCreate,后者使用BCryptPrimitives.ProcessPrng(https://download.microsoft.com/download/1/c/9/1c9813b8-089c-4fef-b2ad-ad80e79403ba/Whitepaper%20-%20The%20Windows%2010%20random%20number%20generation%20infrastructure.pdf)。因此,我认为在最近的Windows版本中,Guid.NewGuid确实创建了加密安全的GUID。 - Simon Mourier
6个回答

58

是的,你可以使用Guid通过字节数组创建一个新的Guid,而RNGCryptoServiceProvider则可以生成一个随机的字节数组,因此你可以使用生成的字节数组来创建一个新的Guid:

public Guid CreateCryptographicallySecureGuid() 
{
    using (var provider = new RNGCryptoServiceProvider()) 
    {
        var bytes = new byte[16];
        provider.GetBytes(bytes);

        return new Guid(bytes);
    }
}

2
等等,这真的这么简单吗?我以为有一个GUID标准——前几个字节定义了使用的版本,而最后几个字节定义了其他东西?我以为GUID不仅仅是随机字节,因此才问的。 - user677526
2
如果您想要GUID避免冲突,那么加密安全的GUID和标准化的GUID之间是有区别的。如果是这样,请使用Guid.NewGuid;否则,请使用此方法。任何标准化的GUID都不会是加密安全的,因为它们生成时有规则,正如您所说,其强度将非常低(如果您知道如何生成16字节中的8个字节,则只需要暴力破解这8个字节...)。 - Gusman
1
规则是为了避免机器之间的碰撞,如果我没记错的话,我认为在同一台机器上这些 GUID 之间发生碰撞的机会要少得多,与标准化的 GUID 相比,获得重复的机会是 2^128 中的一个,所以我们面对太阳爆炸的可能性更大,哈哈。 - Gusman
3
如果有价值的话,RNGCryptoServiceProvider 实现了 IDisposable 接口,因此应该在使用时使用 using 语句或 try/finally 代码块来包装它。未来阅读这篇问题的读者可能会发现这很有用。 - user677526
1
与 .NET Core 相关的跨平台考虑的相关文章。https://dev59.com/P1kT5IYBdhLWcg3wa-1z#38644970 - Damien Sawyer
显示剩余7条评论

17

以下是Brad M的答案:https://dev59.com/p1oU5IYBdhLWcg3wu4ni#54132397

如果有人感兴趣,这里是针对.NET Core 1.0(DNX)调整过的样例代码。

public Guid CreateCryptographicallySecureGuid()
{
    using (var provider = System.Security.Cryptography.RandomNumberGenerator.Create())
    {
        var bytes = new byte[16];
        provider.GetBytes(bytes);

        return new Guid(bytes);
    }
}

12

https://www.rfc-editor.org/rfc/rfc4122提到有一些位应该被修正,以表明此GUID是第4版(随机)的。以下代码已被修改以设置/取消这些位。

public Guid CreateCryptographicallySecureGuid()
{
    using (var provider = new RNGCryptoServiceProvider())
    {
        var bytes = new byte[16];
        provider.GetBytes(bytes);
        bytes[8] = (byte)(bytes[8] & 0xBF | 0x80);
        bytes[7] = (byte)(bytes[7] & 0x4F | 0x40);
        return new Guid(bytes);
    }
}

这很重要的原因是什么? - Tiago Freitas Leal
7
如果审计员或其他IT安全专家查看生成和用于安全身份号码的GUID,则需要知道所有这些号码是否属于难以猜测的那种类型。如果您对整个GUID进行随机化(而不是设置版本),则即使GUID实际上不是不安全的第1类或其他某种类型,也会创建出一些看起来像是不安全的GUID。因此,将版本设置为最适当的值将有助于避免某些混淆。我怀疑如果您忽略标准,任何代码都不会停止工作。但是为了清晰起见,最好遵守标准。 - rlamoni
1
@rlamoni,您的答案非常出色,让我走上了正确的轨道。然而,您使用的按位操作有些不对。这是我更新后的答案 - om-ha
@om-ha 我比较了你的位运算和rlamoni的,还有像https://www.freecodeformat.com/validate-uuid-guid.php和https://www.beautifyconverter.com/uuid-validator.php这样的网站也支持rlamoni的观点。 - Benrobot
@Benrobot,我们俩都不正确。这是因为字节序的问题。我在下面更新了我的答案链接 - om-ha
@rlamoni在LittleEndian架构/模式中考虑了版本位(第7个字节是版本字节),我的初始答案考虑了BigEndian架构/模式中的版本位(第6个字节是版本字节)。我的更新的答案两者都考虑到了。 - om-ha

12

如果您至少使用c#7.2和netcoreapp2.1(或System.Memory),则这是最快/最高效的方法。

public static Guid CreateCryptographicallySecureGuid()
{
    Span<byte> bytes = stackalloc byte[16];
    RandomNumberGenerator.Fill(bytes);
    return new Guid(bytes);
}

我创建了一个基准测试,将其与已被接受的答案进行比较。我修改它以使用 RandomNumberGenerator 的静态实现,因为 GetBytes() 是线程安全的。(尽管我看到的唯一保证是 RNGCryptoServiceProvider 拥有线程安全的实现...可能其他实现没有)

[MemoryDiagnoser]
public class Test
{
    private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();

    [Benchmark]
    public void Heap()
    {
        var bytes = new byte[16];
        _rng.GetBytes(bytes);
        new Guid(bytes);
    }

    [Benchmark]
    public void Fill()
    {
        Span<byte> bytes = stackalloc byte[16];
        RandomNumberGenerator.Fill(bytes);
        new Guid(bytes);
    }
}
| Method |     Mean |     Error |    StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
|------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
|   Heap | 129.4 ns | 0.3074 ns | 0.2725 ns |      0.0093 |           - |           - |                40 B |
|   Fill | 116.5 ns | 0.3440 ns | 0.2872 ns |           - |           - |           - |                   - |

11

2020 Modified Version

我发现@rlamoni的回答非常好。只需要在位运算中进行一些修改,才能正确反映GUID版本4的标识位,以考虑大端和小端体系结构。

具体来说,对我的回答进行了以下更正:

  1. 修改后的字节应为第7个和第9个字节。
  2. 按位与操作数已进行更正。

更新

正如用户Benrobot所指出的,Guid无效。我想这是因为他使用的设备Endianness与我的不同。

这促使我进一步改进我的答案。除了我在原始答案中指出的两个更正之外,这里还有一些我答案的改进点:

  1. 考虑到当前计算机体系结构的Endianness
  2. 添加了自说明的常量和变量标识符,而不是使用常量字面值。
  3. 使用简单的using语句,不需要括号。
  4. 添加了一些注释和所需的using指令
using System;
using System.Security.Cryptography;

/// Generates a cryptographically secure random Guid.
///
/// Characteristics
///     - Variant: RFC 4122
///     - Version: 4
/// RFC
///     https://www.rfc-editor.org/rfc/rfc4122#section-4.1.3
/// Stackoverflow
///     https://dev59.com/p1oU5IYBdhLWcg3wu4ni#59437504
static Guid CreateCryptographicallySecureRandomRFC4122Guid()
{
    using var cryptoProvider = new RNGCryptoServiceProvider();

    // byte indices
    int versionByteIndex = BitConverter.IsLittleEndian ? 7 : 6;
    const int variantByteIndex = 8;

    // version mask & shift for `Version 4`
    const int versionMask = 0x0F;
    const int versionShift = 0x40;

    // variant mask & shift for `RFC 4122`
    const int variantMask = 0x3F;
    const int variantShift = 0x80;
            
    // get bytes of cryptographically-strong random values
    var bytes = new byte[16];
    cryptoProvider.GetBytes(bytes);

    // Set version bits -- 6th or 7th byte according to Endianness, big or little Endian respectively
    bytes[versionByteIndex] = (byte)(bytes[versionByteIndex] & versionMask | versionShift);

    // Set variant bits -- 9th byte
    bytes[variantByteIndex] = (byte)(bytes[variantByteIndex] & variantMask | variantShift);

    // Initialize Guid from the modified random bytes
    return new Guid(bytes);
}

在线验证器

用于检查生成的 GUID 的有效性:

参考资料

  • RFC4122 版本的章节
  • GuidOne,一款被低估的GUID库。
  • 这篇GUID指南。
  • Cryptosys的Uuid.cs C#

1
我简直不敢相信我的字节序问题已经受到质疑了:)。是的,您的更新答案对我也有效。感谢您的更新。 - Benrobot
这种方式生成的GUID是否保证唯一性?即,如果两个不同的服务器生成GUID,它们永远不可能相同,就像常规的GUID一样吗? - A X
@AX 是一个有效的问题,GUID 碰撞非常罕见,大约是 10^38 中的1个。在这里检查答案:https://dev59.com/jHVC5IYBdhLWcg3wykUtGUID v1 保证随机性。然而,GUID v4(本答案)保证了随机性和不可预测性。 - om-ha
10的38次方,比宇宙中的星星数量还要高得多。 - om-ha
@om-ha 谢谢 - 但是为了明确起见,这是可能的。由于您没有在网络卡的MAC地址前添加前缀,因此不能保证其唯一性,因此在使用之前仍需要检查其是否唯一(例如通过查找进行)? - A X
显示剩余3条评论

6

om-ha的回答非常出色。但是我想将其优化为.NET 5.0,该版本不需要RNG加密提供程序。我修改后的.NET 5.0版本如下:

using System;
using System.Security.Cryptography;

/// Generates a cryptographically secure random Guid.
///
/// Characteristics
///     - GUID Variant: RFC 4122
///     - GUID Version: 4
///     - .NET 5
/// RFC
///     https://tools.ietf.org/html/rfc4122#section-4.1.3
/// Stackoverflow
///     https://dev59.com/p1oU5IYBdhLWcg3wu4ni#59437504
static Guid CreateCryptographicallySecureRandomRFC4122Guid()
{
    // Byte indices
    int versionByteIndex = BitConverter.IsLittleEndian ? 7 : 6;
    const int variantByteIndex = 8;

    // Version mask & shift for `Version 4`
    const int versionMask = 0x0F;
    const int versionShift = 0x40;

    // Variant mask & shift for `RFC 4122`
    const int variantMask = 0x3F;
    const int variantShift = 0x80;

    // Get bytes of cryptographically-strong random values
    var bytes = new byte[16];

    RandomNumberGenerator.Fill(bytes);

    // Set version bits -- 6th or 7th byte according to Endianness, big or little Endian respectively
    bytes[versionByteIndex] = (byte)(bytes[versionByteIndex] & versionMask | versionShift);

    // Set variant bits -- 8th byte
    bytes[variantByteIndex] = (byte)(bytes[variantByteIndex] & variantMask | variantShift);

    // Initialize Guid from the modified random bytes
    return new Guid(bytes);
}

这里的编辑使用RandomNumberGenerator (文档),Fill方法执行以下操作:

用加密强度的随机字节填充一个范围


感谢您对我的回答进行改进! - om-ha

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