从零构建文本隐形水印:一种基于Unicode零宽度字符的隐写实践
1. 什么是文本隐形水印?
你可能见过图片上的半透明logo,但文本也能藏水印——而且完全看不见。文本隐形水印是一种通过修改文本的底层编码结构,嵌入隐藏信息的技术。就像用隐形墨水在纸上写字,平时看不出痕迹,但用特殊方法就能显现。
这种技术主要依赖Unicode中的零宽度字符——这些字符在显示时不会占据任何视觉空间。比如:
U+200B(零宽度空格)U+200C(零宽度非连接符)U+200D(零宽度连接符)
实际场景中,你可以在合同、小说甚至代码里嵌入作者信息或版权标识。即使文本被复制粘贴多次,水印依然存在。去年有个案例:某科技公司发现内部代码泄露,通过提取文本中的零宽度字符水印,最终锁定了泄露员工。
2. 零宽度字符的工作原理
2.1 Unicode的隐藏彩蛋
Unicode标准中有一组特殊字符,它们没有宽度、不可见,但能被计算机识别。比如在字符串"Hello"中插入U+200B,显示仍是"Hello",但实际存储的是"Hello"(这里用模拟零宽度字符)。
这些字符的二进制表示如下:
| 字符名称 | Unicode编码 | 二进制表示 |
|---|---|---|
| 零宽度空格 | U+200B | 11100000 10000000 10001011 |
| 零宽度非连接符 | U+200C | 11100000 10000000 10001100 |
| 零宽度连接符 | U+200D | 11100000 10000000 10001101 |
2.2 编码规则设计
假设我们要嵌入二进制水印"1011":
- 将原文每个字符后插入一个零宽度字符
- 用
U+200B代表"0",U+200D代表"1" - 按水印位循环插入:H[1]e[0]l[1]l[1]o
提取时只需扫描零宽度字符序列,还原二进制流。我实测发现,普通文本编辑器(如VS Code)会保留这些字符,但微信、钉钉等IM工具可能会过滤掉它们——这是需要注意的兼容性问题。
3. 实战:Python实现水印嵌入
3.1 基础版本代码
def embed_watermark(text, watermark): zero_width_map = {'0': '\u200b', '1': '\u200d'} watermarked = [] for i, char in enumerate(text): watermark_bit = watermark[i % len(watermark)] watermarked.append(char + zero_width_map[watermark_bit]) return ''.join(watermarked) # 使用示例 original = "重要机密文件" watermark = "110101" # 版权标识的二进制形式 hidden_text = embed_watermark(original, watermark) print(f"看似普通的文本:{hidden_text}") print(f"实际长度:{len(hidden_text)} vs 原始长度:{len(original)}")这段代码会在每个字符后插入水印位。用hexdump查看输出,能看到交替出现的E2 80 8B和E2 80 8D(零宽度字符的UTF-8编码)。
3.2 增强鲁棒性
单纯插入字符容易被批量删除,我们可以:
- 随机化插入位置:不是每个字符后都插入
- 添加校验码:在水印末尾加CRC校验
- 加密水印:先用AES加密再嵌入
改进后的嵌入逻辑:
import random from Crypto.Cipher import AES def robust_embed(text, watermark, key): # AES加密水印 cipher = AES.new(key, AES.MODE_ECB) encrypted = cipher.encrypt(watermark.ljust(16).encode()) # 转换为二进制串 binary_wm = ''.join(f'{byte:08b}' for byte in encrypted) # 随机选择插入位置 positions = sorted(random.sample(range(len(text)), len(binary_wm))) # 插入水印 result = list(text) for pos, bit in zip(positions, binary_wm): result.insert(pos + 1, '\u200b' if bit == '0' else '\u200d') return ''.join(result)4. 水印提取与检测
4.1 基础提取器
def extract_watermark(watermarked_text): bits = [] for char in watermarked_text: if char == '\u200b': bits.append('0') elif char == '\u200d': bits.append('1') return ''.join(bits) # 高级版:处理加密水印 def decrypt_watermark(watermarked_text, key): binary_str = extract_watermark(watermarked_text) bytes_data = [int(binary_str[i:i+8], 2) for i in range(0, len(binary_str), 8)] cipher = AES.new(key, AES.MODE_ECB) return cipher.decrypt(bytes(bytes_data)).decode().strip()4.2 对抗文本编辑
文本可能被修改,导致水印损坏。我们可以:
- 冗余编码:重复嵌入3次水印,取多数表决
- 位置散列:用HMAC算法确定插入位置
- 错误校正:添加Reed-Solomon编码
from hashlib import sha256 def hmac_position(text, watermark, secret): hmac = sha256((text + secret).encode()).hexdigest() positions = [] for i in range(len(watermark)): pos = int(hmac[i*2:i*2+2], 16) % len(text) positions.append(pos) return sorted(positions)5. 应用场景与局限性
5.1 典型使用案例
- 代码版权保护:在开源代码中嵌入开发者信息
- 文档溯源:政府公文添加部门标识
- 聊天审计:IM自动在消息中插入发送者ID
- 防抄袭检测:论文预印本嵌入指纹
去年某开源项目发现有人去除License后二次销售,通过提取代码中的零宽度字符水印,成功证明了原始著作权。
5.2 技术限制
编辑器兼容性:
- 保留水印:VS Code、Sublime Text
- 可能丢失:Word、WPS(粘贴时)
- 完全过滤:大多数网页文本框
攻击手段:
- 重新输入文本(OCR无法提取)
- 转PDF再转文本
- 使用
strings命令提取可见字符
容量限制:每100字符约可嵌入20-30位水印信息
我在实际项目中发现,结合自然语言水印(同义词替换)和零宽度字符,能显著提高抗攻击能力。例如同时用"快速"和"迅速"来编码二进制位,即使零宽度字符被过滤,仍有部分信息保留。
