如何在 C# 中生成可证明公平的掷骰子?

How to generate provably fair dice rolls in C#?

我研究了可证明公平的随机数,并发现了这个网站:https://dicesites.com/provably-fair

首先,什么 class 应该用于服务器端哈希?散列算法太多了,比如SHA512、SHA256、SHA384Cng,我不明白它们之间的区别。

其次,将使用什么方法将未散列种子转换为散列种子,以及将使用什么方法在散列创建期间考虑用户提供的种子字符串。另外,随机数是否简单地添加在用户提供的字符串的末尾以防止重复的哈希值?

第三,我不明白为什么散列的服务器种子最初是SHA256散列,但后来用于计算HMAC SHA512散列。

最后,如何将最终生成的哈希值的前 5 个字符转换为卷号?

我没有找到任何使用服务器种子和客户端种子的随机数生成器的例子,只有像 System.Security.Cryptography.RandomNumberGenerator.

这样的例子

您 link 访问的页面描述了该过程,但我会尝试更详细地介绍并提供 C# 示例。

首先发生了两次散列。一个通用散列来证明服务器在您赌博时没有更改服务器密钥,这个散列不是秘密的并且在游戏开始时提供给玩家。还有一个键控哈希(称为 HMAC)来实际生成骰子掷骰并使用服务器密钥、用户提供的数据和一个向上计数的数字的组合。

这是发生的过程:

  1. 服务器为播放会话生成密钥并将计数器设置为 0。
  2. 密钥上使用SHA256生成哈希值,这个哈希值提供给玩家。此哈希值不用于任何数学运算以生成骰子掷骰,它仅用于玩家验证。
  3. 玩家请求掷骰子并提供用于生成数字的短语。
  4. 服务器使用SHA512-HMAC,密钥为密钥然后用户提供的字符串加上“-”加上在步骤1中设置的计数器的编号来生成散列。
  5. 服务器将计数器加 1,这是因为每次都使用相同的服务器密钥,如果使用相同的用户字符串,它只会一遍又一遍地生成相同的数字。
  6. 服务器获取生成的哈希值的前 21 位,将其转换为 int,然后检查 int 是否大于 999999,如果是,则一直重复直到它找到一个不超过 999999 的数字。
  7. 它从第 6 步中获取数字并对其执行 number%(10000)/100.0 以获得浮点数。
  8. 那个浮点数返回给用户。
  9. 从第 3 步开始重复新卷或继续第 10 步。
  10. 玩家发出游戏结束的信号。服务器 returns 用户的密钥并在第 1 步重新启动。

用户一旦从第 10 步获得密钥,就可以使用 SHA256 对其进行哈希处理,并检查他获得的哈希值是否与他在游戏会话开始时被告知的哈希值相同。然后他可以重新执行服务器现在执行的所有步骤,因为他拥有密钥并验证服务器没有伪造任何骰子。

如何在代码中执行此操作:

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

namespace SandboxConsole
{
    public class Result
    {
        public Result(string hmacMessage, float roll)
        {
            HmacMessage = hmacMessage;
            Roll = roll;
        }

        public string HmacMessage { get; }
        public float Roll { get; }
    }

    class FairDiceRollServer
    {
        private byte[] _serverKey;
        private ulong _nonce;

        public byte[] StartSession()
        {
            if (_serverKey != null)
                throw new InvalidOperationException("You must call EndSession before starting a new session");

            //Generate a new server key.
            using (var rng = RandomNumberGenerator.Create())
            {
                _serverKey = new byte[128];
                rng.GetBytes(_serverKey);
            }
            _nonce = 0;
            //Hash the server key and return it to the player.
            using (var sha = SHA256.Create())
            {
                return sha.ComputeHash(_serverKey);
            }
        }

        public Result RollDice(string userKey)
        {
            if(_serverKey == null)
                throw new InvalidOperationException("You must call StartSession first");
            if(_nonce == ulong.MaxValue)
                throw new InvalidOperationException("Ran out of Nonce values, you must start a new session.");

            using (var hmac = new HMACSHA256(_serverKey))
            {
                float? roll = null;
                string message = null;
                while (roll == null)
                {
                    message = userKey + "-" + _nonce;
                    _nonce++;

                    var data = Encoding.UTF8.GetBytes(message);
                    var hash = hmac.ComputeHash(data);
                    roll = GetNumberFromByteArray(hash);
                }
                return new Result(message, roll.Value);
            }
        }

        private float? GetNumberFromByteArray(byte[] hash)
        {
            var hashString = string.Join("", hash.Select(x => x.ToString("X2")));
            const int chars = 5;
            for (int i = 0; i <= hashString.Length - chars; i += chars)
            {
                var substring = hashString.Substring(i, chars);
                var number = int.Parse(substring, System.Globalization.NumberStyles.HexNumber);
                if(number > 999999)
                    continue;
                return (number % 10000) / 100.0f;
            }
            return null;
        }

        public byte[] EndSession()
        {
            var key = _serverKey;
            _serverKey = null;
            return key;
        }
    }
}

使用示例

using System;
using System.Linq;

namespace SandboxConsole
{
    class Program
    {
        private int _test;
        static void Main(string[] args)
        {
            var server = new FairDiceRollServer();
            var hash = server.StartSession();
            Console.WriteLine(string.Join("", hash.Select(x => x.ToString("X2"))));
            for (int i = 0; i < 10; i++)
            {
                var roll = server.RollDice("My Key");
                Console.WriteLine("Message: {0} Result: {1}", roll.HmacMessage, roll.Roll);
            }
            var key= server.EndSession();
            Console.WriteLine(string.Join("", key.Select(x => x.ToString("X2"))));
            Console.ReadLine();
        }

    }
}

使用有关所用算法的已发布信息、RollDice 返回的信息和从 EndSession 返回给用户的密钥,用户可以重新创建所有骰子并证明服务器真正随机生成(由于用户在滚动中提供的数据,服务器不允许选择)而不是一些保证会导致丢失的伪造的预选密钥。