Neo*_*Neo 3 c# random algorithm math guid
我正在寻找一种方法来从一个连续的用户 ID 中为用户生成一个随机的、唯一的 9 位朋友代码。这背后的想法是,人们无法通过一一搜索好友代码来枚举用户。如果有 1000 个可能的代码和 100 个注册用户,搜索随机代码应该有 10% 的机会找到用户。
一种可能的方法是随机生成一个代码,检查该代码是否已被使用,如果是,则再试一次。我正在寻找一种方法(主要是出于好奇),其中朋友代码是通过算法生成的,并且第一次尝试时保证该用户 ID 是唯一的。
具体来说,给定一个数字范围(1 到 999,999,999),在这个数字上运行该函数应该返回同一范围内的另一个数字,该数字与输入数字成对且唯一。只有当范围发生变化和/或随机性的输入种子发生变化时,这种配对才会有所不同。
理想情况下,个人不应该在不知道种子和算法的情况下(或者拥有非常大的样本池和大量时间 - 这不需要加密安全)轻松地从朋友 ID 逆向工程用户 ID,所以简单地从最大范围中减去用户 ID 不是一个有效的解决方案。
下面是一些 C# 代码,它通过生成整个数字范围、打乱列表,然后通过将用户 ID 视为列表索引来检索朋友 ID 来完成我所追求的目标:
int start = 1; // Starting number (inclusive)
int end = 999999999; // End number (inclusive)
Random random = new Random(23094823); // Random with a given seed
var friendCodeList = new List<int>();
friendCodeList.AddRange(Enumerable.Range(start, end + 1)); // Populate list
int n = friendCodeList.Count;
// Shuffle the list, this should be the same for a given start, end and seed
while (n > 1)
{
n--;
int k = random.Next(n + 1);
int value = friendCodeList[k];
friendCodeList[k] = friendCodeList[n];
friendCodeList[n] = value;
}
// Retrieve friend codes from the list
var userId = 1;
Console.WriteLine($"User ID {userId}: {friendCodeList[userId]:000,000,000}");
userId = 99999999;
Console.WriteLine($"User ID {userId}: {friendCodeList[userId]:000,000,000}");
userId = 123456;
Console.WriteLine($"User ID {userId}: {friendCodeList[userId]:000,000,000}");
Run Code Online (Sandbox Code Playgroud)
User ID 1: 054,677,867
User ID 99999999: 237,969,637
User ID 123456: 822,632,399
不幸的是,这不适合大范围 - 该程序需要 8GB 的 RAM 才能运行,使用 10 或 12 位朋友代码在内存或数据库中预生成列表是不可行的。我正在寻找不需要此预生成步骤的解决方案。
如果可能的话,我对使用种子随机数生成器或按位技巧来实现这一目标的解决方案感兴趣。上述函数是可逆的(通过搜索列表的值),但解决方案不需要。
您正在考虑开发一种将一个整数值(原始“秘密”UserId值)映射到另一个((加密的)“公共”值)并再次映射回来的方法。这正是块密码所做的(除了每个“块”通常是 16 个字节,而不是单个字符或整数值)。换句话说,您想创建自己的密码系统。
(请注意,即使您正在考虑将 UserId 123 转换为一个string而不是整数,例如 YouTube Video Id like "dQw4w9WgXcQ") - 它仍然是一个整数:因为存储在计算机中的每个标量值,包括字符串,都可以表示作为整数 -因此是 1990 年代后期的“非法素数”问题)。
任何本科级别的密码学计算机科学课程最大、最重要的收获就是永远不要创建自己的密码系统!。
有了这个...
...并且您只关心防止披露递增的整数 Id 值(例如,您的访问者和用户看不到您真正拥有多少数据库记录),然后使用 Hashids 库:https ://hashids.org/
在您的代码中,构造一个Hashids对象(我会使用公共static readonly字段或属性 - 或者更好:单例可注入服务)并使用该.Encode方法将任何整数int/Int32值转换为string值。
要将string值转换回原始int/ Int32,请使用该.Decode方法。
顺便说一句,当散列旨在成为单向函数时,我不喜欢如何将库称为“Hashids”——因为这些值仍然是可逆的——尽管使用了一个秘密的“盐”值(为什么不是称为“密钥”?)它不是真正的哈希,imo。
然后您需要将每个整数值视为分组密码中的一个离散块(不是流密码,因为每个值都需要自己独立加密和解密)。
出于实用性的目的,您需要使用具有小块大小的对称块密码。不幸的是,许多具有小块大小的块密码并不是很好(TripleDES 的块大小为 64 位 - 但今天它很弱),所以让我们坚持使用 AES。
AES 具有 128 位(16 字节)的块大小 - 这Int64与彼此连接的两个整数相同。假设您base64url对 16 字节值使用编码,那么您的输出将是 22 个字符长(因为 Base64 每个字符使用 6 位)。如果您对这种长度的琴弦感到满意,那么您就大功告成了。您可以从 128 位值生成的最短 URL 安全字符串是 21(几乎没有任何改进),因为 Base-73 是您可以在所有现代 URL 传输系统(永远不会自动假设在处理纯文本时任何地方都支持 Unicode)。
可以调整 AES 以生成更小的输出块大小,但在这种情况下它不起作用,因为使用 CTR 模式等技术意味着生成的输出需要包含额外的状态信息(IV、计数器等),这些信息将结束-up 占用的空间与获得的相同。
这是代码:
非常重要的注意事项:
private static readonly Byte[] _key = new Byte[] { }. // Must be 128, 192 or 256 bits (16, 24, or 32 bytes) in length.
private static readonly Byte[] _iv = new Byte[8]; // You could use the default all-zeroes.
// Note that this method works with Int32 arguments.
private static Byte[] ProcessBlock( Byte[] inputBlock, Boolean encrypt )
{
Byte[] outputBlock;
using( Aes aes = Aes.Create() )
{
aes.Key = _key;
aes.IV = _iv;
using( ICryptoTransform xform = encrypt ? aes.CreateEncryptor() : aes.CreateDecryptor() )
{
outputBlock = xform.TransformFinalBlock( inputBlock, 0, inputBlock.Length );
}
}
}
public static Byte[] EncryptInteger( Int64 value )
{
Byte[] inputBlock = new Byte[16];
inputBlock[0] = (Byte)(value >> 0 & 0xFF);
inputBlock[1] = (Byte)(value >> 8 & 0xFF);
inputBlock[2] = (Byte)(value >> 16 & 0xFF);
inputBlock[3] = (Byte)(value >> 24 & 0xFF);
inputBlock[4] = (Byte)(value >> 32 & 0xFF);
inputBlock[5] = (Byte)(value >> 40 & 0xFF);
inputBlock[6] = (Byte)(value >> 48 & 0xFF);
inputBlock[7] = (Byte)(value >> 56 & 0xFF);
return ProcessBlock( inputBlock, encrypt: true );
}
public static Int64 DecryptInteger( Byte[] block )
{
Byte[] outputBlock = ProcessInteger( value, encrypt: false );
return
(Int64)outputBlock[0] << 0 |
(Int64)outputBlock[1] << 8 |
(Int64)outputBlock[2] << 16 |
(Int64)outputBlock[3] << 24 |
(Int64)outputBlock[4] << 32 |
(Int64)outputBlock[5] << 40 |
(Int64)outputBlock[6] << 48 |
(Int64)outputBlock[7] << 56;
};
public static String EncryptIntegerToString( Int64 value ) => Convert.ToBase64String( EncryptInteger( value ) ).Replace( '+', '-' ).Replace( '/', '_' );
public static Int64 DecryptIntegerFromString( String base64Url )
{
if( String.IsNullOrWhiteSpace( base64Url ) ) throw new ArgumentException( message: "Invalid string.", paramName: nameof(base64Url) );
// Convert Base64Url to Base64:
String base64 = base64Url.Replace( '-', '+' ).Replace( '_', '/' );
Byte[] block = Convert.FromBase64String( base64 );
return DecryptInteger( block );
}
Run Code Online (Sandbox Code Playgroud)