零宽字符文本水印原理

2,517 阅读3分钟

最近看到word-wrap源码里面有一个正则,有点迷惑:

if (options.cut !== true) {
  regexString += '([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)';
}

零宽空格

如果要搞清楚这句正则是干什么的,就需要了解到零宽空格了。

零宽空格(zero-width space, ZWSP)是一种不可打印的Unicode字符,用于可能需要换行处。 zh.wikipedia.org/wiki/%E9%9B…

复制这个om,作如下动作: image.png
image.png

那这个和加密有什么关系呢?

水印原理

我们不难查到,零宽空格会延伸到零宽连字。

零宽连字和零宽不连字

零宽连字(zero-width joiner,ZWJ)是一个控制字符,放在某些需要复杂排版语言(如阿拉伯语印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。零宽连字符的Unicode码位是U+200D (HTML: ‍ ‍)。 zh.wikipedia.org/wiki/%E9%9B…

具体例子可以看到wiki上的例子。

零宽不连字 (英文:zero-width non-joiner,缩写:ZWNJ)是一个不打印字符,放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。Unicode中的零宽不连字字符映射为U+200C zero width non-joiner,HTML字符值引用为: ‌ zh.wikipedia.org/wiki/%E9%9B…

文本水印

我们常见的有图片水印,软件水印,视频水印等。那文本上的水印要是什么样子呢?

  1. 文本中间穿插文案,比如这是一段[xxx.com]示例文案,说明这句话[xxx.com]的出处
  2. 文本中通过某些不可见的字符,来携带水印。用户看不见,但是复制粘贴会携带上。

第一种就不说了,我们来看看第二种。上面说了两种领宽字符,恰好满足了不可见,且会被复制的特点。那要怎么用这两个字符呢?

原理

我们约定如下:

  1. ZWJ -> 0
  2. ZWSP -> 1
  3. ZWNJ -> 无

有一段文本:这是一段示例文案,说明这句话的出处,水印为xxx.com。伪代码:

let zwj = '‍'
let zwsp = '&zwsp;'
let zwnj = '‌'
let dict = {
  0: zwj,
  1: zwsp,
}
let source = '这是一段示例文案,说明这句话的出处'
let logo = 'xxx.com'
let watermask = log
// 追加
`source${watermask}`

如果这里我们把水印换成1011,我们可以很快的实现:

let logo = '1011'
let watermask = zwsp + zwj + zwsp + zwsp

`source${watermask}` // 这是一段示例文案,说明这句话的出处&zwsp;‍&zwsp;&zwsp;

如果要换成更丰富的水印字符,我们需要有一个作为无的标识,引入了zwnj如下:

let logo = 'ab' // 或者更复杂
let watermask = logo.map(char => tsf(char)).join(zwnj)
// 每个字符转为二进制码
function tsf(char) {
	let b = char.charCodeAt().toString(2) // a => 1100001
  // 转为零宽字符标识
	return b.map(itm => {
  	return dict[itm]
  })
}

解密的话,上面逆向即可。

总结

整个流程就是

应用场景

  1. 博客文章署名
  2. 小说网站等

注意

这种加密方式虽然一般情况是无感知的,但**如果在有些编辑器中粘贴,是可以看到零宽字符的。**比如开头的截图,浏览器的控制台是可以看到这个字符的。