[翻译]小心你复制的内容:使用零宽字符将用户名不可见的插入文本中

翻译源:Be careful what you copy: Invisibly inserting usernames into text with Zero-Width Characters

不想读?试试这个 demo

零宽字符都是不可见的「非打印」字符,大多数应用程序中都不会显示这些字符。比如,我在这句话中添加了十个零宽字符​​​​​​​​​​,你能分辨出来吗?(提示:将句子粘贴到 Diff Checker 来查看这些字符的位置。这些字符可以被用于做某些用户的「指纹」字符。)

它当然可以,只是你不知道。

为什么?

好吧,开始的原因并不是那么让人兴奋的。几年前我是一个电竞团队的成员。这个团队有一个私人留言板,用于发布重要的公告。最终这些公告会出现在网络中的其他地方,来假冒我们团队。当然更重要的是,确保留言板冗余以便共享机密信息和策略。

网络的安全性似乎非常让人紧张,因此理论上来说,任一登录用户就可以简单地拷贝和发布公告到任何地方。我创建了一个脚本来允许团队在每个公告中带有含用户名的隐形指纹。

我看到 Zach Aysan 最近的一篇文章中大家对于零宽字符很感兴趣,所以我想用一个交互式的 demo 来公开这个方法。这个代码示例已经更新为了最新的 JavaScript,但是整体的逻辑是相同的。

怎么做?

确切的步骤和逻辑详述如下,但是简单的来说:用户名会被转成二进制,二进制会被转为一系列零宽字符来表示每个二进制数。零宽字符将被插入到文本当中。如果所述文本被发布到其他地方,则可以提出宽度字符串,并在该过程中反向的找出复制他的人的用户名。

将文本带上指纹

1: 得到已登录用户的用户名并将其转换为二进制

在这里我们将用户名的每个字符转换为同等的二进制码:

const zeroPad = num => ‘00000000’.slice(String(num).length) + num;
const textToBinary = username => (
  username.split('').map(char =>
    zeroPad(char.charCodeAt(0).toString(2))).join(' ')
);

2: 得到用户名转换而来的二进制码然后转换为零宽字符

遍历二进制字符串,并将每个 1 转换成零宽字符空间,将 0 转换为零宽非连接字符。一旦我们转换了字母,我们在移动到下一个字符前插入一个零宽链接字符。

const binaryToZeroWidth = binary => (
  binary.split('').map((binaryNum) => {
    const num = parseInt(binaryNum, 10);
    if (num === 1) {
      return '​'; // zero-width space
    } else if (num === 0) {
      return '‌'; // zero-width non-joiner
    }
    return '‍'; // zero-width joiner
  }).join('') // zero-width no-break space
);

3: 在机密文本中插入零宽「用户名」

这步只是将零宽字符块插入到机密文本中。

从指纹文本中提取出用户名

将逻辑反过来。

1: 从机密文本中提取零宽「用户名」
从字符串中删除预期的机密文本,只留下零宽字符。

2: 把零宽「用户名」转回二进制

在这里我们根据之前添加的零宽不间断空格来分割字符串。这将为我们提供等效于用户名字母的二进制编码的零宽字符,我们遍历零宽字符并返回 1 或 0 以重新创建二进制字符串。如果我们没有找到相应的 0 或者 1,我们必须命中零宽连接符,从而完成了一个字符的二进制的转换。然后我们可以在字符串中附加一个空格并移动到下一个字符中。

const zeroWidthToBinary = string => (
  string.split('').map((char) => { // zero-width no-break space
    if (char === '​') { // zero-width space
      return '1';
    } else if (char === '‌') {  // zero-width non-joiner
      return '0';
    }
    return ' '; // add single space
  }).join('')
);

3: 将二进制用户名转换成文本

最后,我们解析二进制字符串并将每个 1 和 0 序列转换成相应的字符。

const binaryToText = string => (
  string.split(' ').map(num =>
    String.fromCharCode(parseInt(num, 2))).join('')
);

总结

公司会比任何时候做的都多来避免信息泄露和阻止泄密者,这个技巧只是众多可以使用的技巧之一。根据你的工作,了解复制文本的相关的风险可能至关重要。很少有应用会尝试渲染零宽字符。比如,你希望你的终端渲染他们(我的终端并没有)。

回到留言板方案,这个计划按照预期工作了,在脚本部署后不久就发布了新的声明。几个小时内,文本已经在其他地方被共享了,并附有零宽字符串。罪魁祸首的用户名并正确识别并且封号。一个成功的项目!

当然,这种方法有一些注意事项。比如,用户知道脚本,理论上他们可以插入自己的零宽字符串并陷害其他人。更好的解决方案是插入一个不公开的唯一用户 ID 而不是用户名。

要尝试一下?请查看演示或者查看源代码

如果您觉得文章不错,可以通过赞助支持我

标签: 知识, 语法, 翻译

添加新评论