从用户ID算法生成独特随机的好友码

4
我正在寻找一种方法,通过顺序用户ID生成一个随机的、唯一的9位好友码(Friend Code)给用户。这样做的想法是为了避免人们通过逐个搜索好友码来枚举用户。如果有1000个可能的代码和100个注册用户,则搜索随机代码应该有10%的几率找到一个用户。
一种可能的方法是随机生成代码,检查代码是否已经被使用,如果已经被使用则重新生成。我正在寻找一种方法(主要是出于好奇心),通过算法生成好友码,并保证第一次尝试时唯一。
具体来说,给定一个数字范围(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}");

用户ID 1:054,677,867 用户ID 99999999:237,969,637 用户ID 123456:822,632,399

不幸的是,这对于大范围来说是不合适的 - 这个程序需要8GB的RAM才能运行,如果使用10或12位数字的好友代码,预先生成列表在内存或数据库中都是不可行的。我正在寻找一个不需要这个预生成步骤的解决方案。

我对使用种子随机数生成器或位操作技巧来实现此目的的解决方案感兴趣,如果可能的话。上述函数是可逆的(通过搜索列表的值),但解决方案不需要可逆。


最简单的方法是随机生成一个代码,检查该代码是否已被使用,如果已被使用,则再次尝试。不,这并不是最简单的方法。从概念上讲,只需使用块密码来加密您的整数标识符即可更简单:https://dev59.com/HEfSa4cB1Zd3GeqPA-WX - Dai
这看起来就是我想要的,只是我不确定如何将块密码适应于一系列数字而不是2^n个数字。 - Neo
输出只能在1到999,999,999范围内,Guid格式不正确。 - Neo
1
@Barns OP正在询问如何编写一个地图样式的函数,该函数始终正确地将一个整数映射到另一个整数(关于列表索引的内容是一个干扰因素,在我看来)。他们试图发明自己的加密系统(块大小为4个字节)-正如我们都知道的那样(通常是通过个人经验!),这是一个愚蠢的任务。 - Dai
我正在寻找一种方法,可以从顺序用户ID中生成一个随机且唯一的9位好友代码。如果是从顺序用户ID生成的,那就不是随机的。 - Enigmativity
显示剩余9条评论
2个回答

6

快速数学课!

你正在考虑开发一种将一个整数值(原始的“秘密”UserId值)映射到另一个值(加密的“公共”值)并再次映射回来的方法。这正是块密码所做的事情(除了每个“块”通常是16字节大,而不是单个字符或整数值)。因此,换句话说,你想创建自己的加密系统。

(请注意,即使你考虑将用户ID 123转换为string,例如YouTube视频ID,如"dQw4w9WgXcQ"),它仍然是一个整数:因为计算机中存储的每个标量值,包括字符串,都可以表示为整数 - 因此在1990年代晚期出现了“非法素数”问题)。

而且,任何本科级别的密码学课程最重要的经验教训就是永远不要创建自己的加密系统

既然安全不是最重要的问题...

只有防止递增整数ID值泄露(例如,让您的访问者和用户看不到您实际拥有多少数据库记录),那么使用Hashids库:https://hashids.org/

在你的代码中,构建一个单一的Hashids对象(我会使用一个公共的static readonly字段或属性 - 或者更好的方式:一个可注入的单例服务),并使用.Encode方法将任何整数int/Int32值转换为string值。
要将string值转换回原始的int/Int32,请使用.Decode方法。
顺便说一句,我不喜欢这个库被称为“Hashids”,因为哈希应该是单向函数-因为值仍然可以通过使用秘密的“盐”值(为什么不叫“键”?)进行反转,它在我看来并不是一个真正的哈希。

如果安全真的很重要...

那么您需要将每个整数值视为块密码中的离散块(而不是流密码,因为每个值都需要独立地加密和解密)。

出于实用性考虑,您需要使用一个带有小块大小的对称块密码。不幸的是,许多具有小块大小的块密码并不是很好(TripleDES 的块大小为 64 位 - 但今天已经不安全了),所以让我们使用 AES。

AES使用128位(16字节)的分块大小-这与两个Int64整数连接在一起相同。假设您对16字节值使用base64url编码,那么输出将为22个字符长(Base64每个字符使用6位)。如果您对这种长度的字符串感到满意,那么就可以了。从128位值生成的最短URL安全字符串是21(几乎没有任何改进),因为Base-73是在URL传输系统中可安全使用的最大值(在处理纯文本时,永远不要自动假设Unicode被任何地方支持)。
虽然可以使AES适应生成更小的输出块大小,但在这种情况下不起作用 ,因为使用诸如CTR模式之类的技术意味着生成的输出需要包括额外的状态信息(IV、计数器等),这将占用与所获得的空间相同的空间
以下是代码:

非常重要的注意事项:

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 );
}

1
一个简单的方法可以生成一长串数字,只要你正确地获取常数。
ulong Next(ulong current)
{
    unchecked
    {
        return (999_999_937L * current + 383_565_383L) % 999_999_999L;
    }
};

从记忆中,如果函数中的值选择正确,这种函数可以产生999_999_999位数的序列。
我的测试代码显示,这种方法可以生成500_499个不重复的数字。
我的计算机可以在不到9毫秒的时间内生成整个序列,因此这是一种相当快的算法。
这个序列的前十个元素(用前导0填充)是:
383565383、602511613、027845340、657154301、639998680、703647183、757439993、422285770、201847617、869013116。

5_960_464 * 当前数 + 383_565_383L 在重复之前产生了长度为 1_000_998 的序列。


1
但是你又如何决定那些数字的返回呢? - Dai
@Dai - 你是什么意思?我不明白你在问什么? - Enigmativity
OP 问如何将其转换为另一个并再次转换回来(即编码和解码)。因此,如果我从您的“Next”函数获得输出值,我该如何获取派生自原始“current”值的值? - Dai
@Dai - 用蛮力法很容易。我可以在9毫秒内遍历所有的50万个数字。找到输入将是微不足道的。 - Enigmativity
1
它不会是每个会话一次,而是每个请求一次。在云和共享托管场景中,独占整个CPU线程9毫秒将导致系统管理员给您写一封强烈措辞的电子邮件。这段代码所做的基本上就像挖掘比特币一样 - 但没有从中获得任何收益。 - Dai
显示剩余3条评论

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